Files
judge/cmd/cli/main.go
Mikhail Kornilovich 128a64a609
All checks were successful
build-dsl-smoke / Discover matrix (push) Successful in 8s
build-dsl-smoke / Build judge (push) Successful in 11s
build-dsl-smoke / ${{ matrix.cell.build }} / ${{ matrix.cell.toolchain }} / ${{ matrix.cell.platform }} (push) Successful in 5s
memory-limit / Build judge (pull_request) Successful in 10s
build-dsl-smoke / SUMMARY (push) Successful in 3s
memory-limit / Linux / gcc (pull_request) Successful in 9s
memory-limit / Linux / clang (pull_request) Successful in 13s
memory-limit / Windows / clang (pull_request) Successful in 16s
memory-limit / Windows / msvc (pull_request) Successful in 17s
add new build system
2026-04-11 01:51:38 +03:00

264 lines
5.9 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/Mond1c/judge/dsl"
"github.com/Mond1c/judge/reporter"
"github.com/Mond1c/judge/runner"
)
const usage = `judge — CI/CD testing system for student solutions
Usage:
judge <tests.jdg> <solution-dir> [flags]
judge aggregate <reports-dir>
Flags:
--json output as JSON instead of text
--wrapper <cmd> exec wrapper (e.g. "valgrind --error-exitcode=99")
--binary <name> name of executable produced by build (overrides .jdg)
--build <name> run only the named structured build (use with matrix CI)
--list-builds print the names of structured builds in the suite as JSON
--list-matrix print the full (build, toolchain, platform) matrix as JSON
--help show help
Example:
judge lab1.jdg ./student-solution
judge lab1.jdg ./student-solution --json
judge lab1.jdg ./student-solution --build=sanitized
judge --list-builds lab1.jdg
judge aggregate reports/
`
func main() {
args := os.Args[1:]
if len(args) == 0 || hasFlag(args, "--help") || hasFlag(args, "-h") {
fmt.Print(usage)
os.Exit(0)
}
if len(args) >= 1 && args[0] == "aggregate" {
runAggregate(args[1:])
return
}
if hasFlag(args, "--list-builds") {
runListBuilds(args)
return
}
if hasFlag(args, "--list-matrix") {
runListMatrix(args)
return
}
jsonOutput := hasFlag(args, "--json")
wrapper := flagValue(args, "--wrapper")
binary := flagValue(args, "--binary")
targetBuild := flagValue(args, "--build")
positional := positionalArgs(args)
if len(positional) < 2 {
fmt.Fprintf(os.Stderr, "error: need <tests.jdg> and <solution-dir>\n\n%s", usage)
os.Exit(1)
}
testFile := positional[0]
solutionDir := positional[1]
f := parseSuite(testFile)
if _, err := os.Stat(solutionDir); err != nil {
fatalf("solution dir %q not found: %v", solutionDir, err)
}
r := runner.New(f, runner.Config{
WorkDir: solutionDir,
BinaryName: binary,
Wrapper: wrapper,
TargetBuild: targetBuild,
})
result := r.Run()
if jsonOutput {
if err := reporter.JSON(os.Stdout, result); err != nil {
fatalf("json output error: %v", err)
}
} else {
reporter.Text(os.Stdout, result)
}
if result.TotalScore < 0.9999 {
os.Exit(1)
}
}
func parseSuite(path string) *dsl.File {
src, err := os.ReadFile(path)
if err != nil {
fatalf("cannot read %q: %v", path, err)
}
f, warns, err := dsl.Parse(string(src))
if err != nil {
fatalf("parse error in %q:\n %v", path, err)
}
for _, w := range warns {
fmt.Fprintf(os.Stderr, "warning: %s\n", w)
}
return f
}
// runListBuilds prints a JSON array of build names for CI matrix discovery.
// A legacy suite without structured builds reports ["default"] so workflows
// can always iterate `fromJSON` and have exactly one cell.
func runListBuilds(args []string) {
positional := positionalArgs(args)
if len(positional) < 1 {
fatalf("--list-builds requires the path to a .jdg file")
}
f := parseSuite(positional[0])
var names []string
if len(f.Builds) == 0 {
names = []string{"default"}
} else {
for _, b := range f.Builds {
names = append(names, b.Name)
}
}
enc := json.NewEncoder(os.Stdout)
if err := enc.Encode(names); err != nil {
fatalf("encode list-builds: %v", err)
}
}
type matrixEntry struct {
Build string `json:"build"`
Toolchain string `json:"toolchain"`
Platform string `json:"platform"`
Wrapper string `json:"wrapper,omitempty"`
}
func runListMatrix(args []string) {
positional := positionalArgs(args)
if len(positional) < 1 {
fatalf("--list-matrix requires the path to a .jdg file")
}
f := parseSuite(positional[0])
var entries []matrixEntry
if len(f.Builds) == 0 {
if len(f.Toolchains) == 0 {
entries = append(entries, matrixEntry{Build: "default", Toolchain: "default", Platform: "linux"})
} else {
for _, tc := range f.Toolchains {
for _, platform := range tc.Platforms {
entries = append(entries, matrixEntry{Build: "default", Toolchain: tc.Name, Platform: platform})
}
}
}
} else if len(f.Toolchains) == 0 {
fatalf("suite has structured builds but no `toolchains { ... }` block; add one to use --list-matrix")
} else {
for _, b := range f.Builds {
for _, tc := range f.Toolchains {
for _, platform := range tc.Platforms {
eff := b.Resolve(f.BuildDefaults, platform)
if !eff.AppliesTo(platform, tc.Name) {
continue
}
entries = append(entries, matrixEntry{
Build: b.Name,
Toolchain: tc.Name,
Platform: platform,
Wrapper: eff.Wrapper,
})
}
}
}
}
enc := json.NewEncoder(os.Stdout)
if err := enc.Encode(entries); err != nil {
fatalf("encode list-matrix: %v", err)
}
}
func runAggregate(args []string) {
if len(args) < 1 {
fatalf("usage: judge aggregate <reports-dir>")
}
if err := reporter.Aggregate(os.Stdout, args[0]); err != nil {
fatalf("%v", err)
}
}
func fatalf(msg string, args ...any) {
fmt.Fprintf(os.Stderr, "error: "+msg+"\n", args...)
os.Exit(1)
}
func hasFlag(args []string, name string) bool {
for _, a := range args {
if a == name {
return true
}
}
return false
}
func flagValue(args []string, name string) string {
prefix := name + "="
for i, a := range args {
if a == name && i+1 < len(args) {
return args[i+1]
}
if strings.HasPrefix(a, prefix) {
return a[len(prefix):]
}
}
return ""
}
func positionalArgs(args []string) []string {
known := map[string]bool{
"--json": true,
"--help": true,
"-h": true,
"--list-builds": true,
"--list-matrix": true,
}
withValue := map[string]bool{
"--wrapper": true,
"--binary": true,
"--build": true,
}
var out []string
skip := false
for _, a := range args {
if skip {
skip = false
continue
}
if known[a] {
continue
}
if withValue[a] {
skip = true
continue
}
if strings.HasPrefix(a, "--wrapper=") || strings.HasPrefix(a, "--binary=") || strings.HasPrefix(a, "--build=") {
continue
}
out = append(out, a)
}
return out
}