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 [flags] judge aggregate Flags: --json output as JSON instead of text --wrapper exec wrapper (e.g. "valgrind --error-exitcode=99") --binary name of executable produced by build (overrides .jdg) --build 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 and \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 ") } 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 }