From cb70adbf098ab6c7d70089b3a76c3928712c64be Mon Sep 17 00:00:00 2001 From: Mikhail Kornilovich Date: Sun, 5 Apr 2026 22:26:07 +0300 Subject: [PATCH] fixes --- .gitea/workflows/judge.yml | 5 +-- cmd/cli/main.go | 53 ++++++++++--------------------- example/c-sum/sum.jdg | 6 ++-- runner/expander.go | 8 ++--- runner/result.go | 3 +- runner/runner.go | 64 +++++++++++--------------------------- 6 files changed, 46 insertions(+), 93 deletions(-) diff --git a/.gitea/workflows/judge.yml b/.gitea/workflows/judge.yml index aa720f5..cbbc94e 100644 --- a/.gitea/workflows/judge.yml +++ b/.gitea/workflows/judge.yml @@ -176,10 +176,11 @@ jobs: echo "" >> SUMMARY.md echo "| Configuration | Score |" >> SUMMARY.md echo "|---|---|" >> SUMMARY.md + shopt -s nullglob for f in reports/*/*.json; do cfg=$(basename "$(dirname "$f")" | sed 's/^report_//') - score=$(grep -o '"TotalScore":[^,}]*' "$f" | head -1 | cut -d: -f2) - echo "| $cfg | $score |" >> SUMMARY.md + score=$(grep -o '"total_score":[^,}]*' "$f" | head -1 | cut -d: -f2) + echo "| $cfg | ${score:- -} |" >> SUMMARY.md done cat SUMMARY.md diff --git a/cmd/cli/main.go b/cmd/cli/main.go index d3ee1fd..c332957 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -1,9 +1,9 @@ package main import ( + "flag" "fmt" "os" - "strings" "github.com/Mond1c/judge/dsl" "github.com/Mond1c/judge/reporter" @@ -28,23 +28,25 @@ Example: ` func main() { - args := os.Args[1:] + fs := flag.NewFlagSet("judge", flag.ContinueOnError) + fs.SetOutput(os.Stderr) + fs.Usage = func() { fmt.Fprint(os.Stderr, usage) } - if len(args) == 0 || contains(args, "--help") || contains(args, "-h") { - fmt.Print(usage) - os.Exit(0) + jsonOutput := fs.Bool("json", false, "output as JSON") + wrapper := fs.String("wrapper", "", "exec wrapper command") + binary := fs.String("binary", "", "binary name override") + + if err := fs.Parse(os.Args[1:]); err != nil { + os.Exit(2) } - if len(args) < 2 { + if fs.NArg() < 2 { fmt.Fprintf(os.Stderr, "error: need and \n\n%s", usage) os.Exit(1) } - testFile := args[0] - solutionDir := args[1] - jsonOutput := contains(args, "--json") - wrapper := flagValue(args, "--wrapper") - binary := flagValue(args, "--binary") + testFile := fs.Arg(0) + solutionDir := fs.Arg(1) src, err := os.ReadFile(testFile) if err != nil { @@ -65,12 +67,12 @@ func main() { r := runner.New(f, runner.Config{ WorkDir: solutionDir, - BinaryName: binary, - Wrapper: wrapper, + BinaryName: *binary, + Wrapper: *wrapper, }) result := r.Run() - if jsonOutput { + if *jsonOutput { if err := reporter.JSON(os.Stdout, result); err != nil { fatalf("json output error: %v", err) } @@ -87,26 +89,3 @@ func fatalf(msg string, args ...any) { fmt.Fprintf(os.Stderr, "error: "+msg+"\n", args...) os.Exit(1) } - -// flagValue returns the value of --name or --name=value, else "". -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 contains(slice []string, s string) bool { - for _, v := range slice { - if v == s { - return true - } - } - return false -} diff --git a/example/c-sum/sum.jdg b/example/c-sum/sum.jdg index 95494e9..f339817 100644 --- a/example/c-sum/sum.jdg +++ b/example/c-sum/sum.jdg @@ -1,13 +1,15 @@ // Cross-platform C solution test suite. -// $CC is supplied by CI matrix (gcc / clang / cl). +// $CC is supplied by CI matrix (gcc / clang / msvc). // // Run locally: // CC=gcc judge sum.jdg . // CC=clang judge sum.jdg . // -// Under MSVC on CI we use build_windows (cl's CLI is different). +// On Windows, build_windows branches on CC because MSVC's cl has a +// different CLI from clang. Executed via `cmd /C`, so %VAR% is cmd syntax. build "$CC -O2 -std=c11 -Wall -Wextra solution.c -o solution" +build_windows "if /I \"%CC%\"==\"msvc\" (cl /nologo /O2 /W3 solution.c /Fe:solution.exe) else (%CC% -O2 -std=c11 -Wall -Wextra solution.c -o solution.exe)" binary = "solution" timeout 5s diff --git a/runner/expander.go b/runner/expander.go index 2de28eb..528ab32 100644 --- a/runner/expander.go +++ b/runner/expander.go @@ -9,7 +9,7 @@ import ( "github.com/Mond1c/judge/dsl" ) -func expandPattern(pattern *dsl.Pattern, groupTimeout interface{ IsZero() bool }) ([]*dsl.Test, error) { +func expandPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) { if pattern.IsDirMode() { return expandDirPattern(pattern) } @@ -105,11 +105,11 @@ func expandDirPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) { } func splitGlob(pattern string) (prefix, suffix string) { - idx := strings.Index(pattern, "*") - if idx < 0 { + before, after, found := strings.Cut(pattern, "*") + if !found { return pattern, "" } - return pattern[:idx], pattern[idx+1:] + return before, after } func extractWildcard(path, prefix, suffix string) string { diff --git a/runner/result.go b/runner/result.go index 5fc70b8..8bd8f49 100644 --- a/runner/result.go +++ b/runner/result.go @@ -44,9 +44,8 @@ type TestResult struct { ActualCode int } -func (r *TestResult) fail(msg string, args ...any) { +func (r *TestResult) addFailure(msg string, args ...any) { r.Failures = append(r.Failures, fmt.Sprintf(msg, args...)) - r.Status = StatusFail } type GroupResult struct { diff --git a/runner/runner.go b/runner/runner.go index ddd6cbf..9dc6324 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -15,14 +15,12 @@ import ( "github.com/Mond1c/judge/dsl" ) -// MaxOutputBytes caps stdout/stderr captured from the solution process. -// Prevents runaway student programs from OOM-ing the judge host. -const MaxOutputBytes = 16 * 1024 * 1024 // 16 MiB +const MaxOutputBytes = 16 * 1024 * 1024 type Config struct { WorkDir string BinaryName string - Wrapper string // CLI override, wins over DSL wrapper + Wrapper string } type Runner struct { @@ -47,7 +45,6 @@ func New(f *dsl.File, cfg Config) *Runner { return &Runner{file: f, cfg: cfg, binary: resolveBinary(absWork, name)} } -// resolveBinary picks /name, falling back to /name.exe on Windows. func resolveBinary(workDir, name string) string { primary := filepath.Join(workDir, name) if runtime.GOOS == "windows" && !strings.HasSuffix(strings.ToLower(name), ".exe") { @@ -90,7 +87,6 @@ func (r *Runner) Run() *SuiteResult { return result } - // After build, re-resolve binary (the exe may have been produced just now). r.binary = resolveBinary(r.cfg.WorkDir, filepath.Base(r.binary)) for _, g := range r.file.Groups { @@ -102,7 +98,6 @@ func (r *Runner) Run() *SuiteResult { return result } -// buildCommand picks the most specific build command for this OS. func (r *Runner) buildCommand() string { switch runtime.GOOS { case "windows": @@ -148,8 +143,6 @@ func (r *Runner) build() (string, error) { return out.String(), nil } -// shellCommand runs a command string through the platform's shell so env vars -// like $CC expand naturally from the CI matrix. func shellCommand(ctx context.Context, cmdline string) *exec.Cmd { if runtime.GOOS == "windows" { return exec.CommandContext(ctx, "cmd", "/C", cmdline) @@ -166,7 +159,7 @@ func (r *Runner) runGroup(g *dsl.Group) *GroupResult { tests := g.Tests if g.Pattern != nil { - expanded, err := expandPattern(g.Pattern, &zeroChecker{}) + expanded, err := expandPattern(g.Pattern) if err != nil { gr.Tests = append(gr.Tests, &TestResult{ Name: "pattern_expand", @@ -177,16 +170,6 @@ func (r *Runner) runGroup(g *dsl.Group) *GroupResult { gr.Score = 0 return gr } - for _, t := range expanded { - if t.Timeout == 0 { - t.Timeout = g.Timeout - } - for k, v := range g.Env { - if _, ok := t.Env[k]; !ok { - t.Env[k] = v - } - } - } tests = append(tests, expanded...) } @@ -234,7 +217,7 @@ func (r *Runner) runTest(t *dsl.Test) *TestResult { tmpDir, err := os.MkdirTemp("", "judge-test-*") if err != nil { tr.Status = StatusRuntimeError - tr.Failures = append(tr.Failures, fmt.Sprintf("failed to create temp dir: %v", err)) + tr.addFailure("failed to create temp dir: %v", err) return tr } defer os.RemoveAll(tmpDir) @@ -242,11 +225,13 @@ func (r *Runner) runTest(t *dsl.Test) *TestResult { for name, content := range t.InFiles { path := filepath.Join(tmpDir, name) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { - tr.fail("mkdir for file %q: %v", name, err) + tr.Status = StatusRuntimeError + tr.addFailure("mkdir for file %q: %v", name, err) return tr } if err := os.WriteFile(path, []byte(content), 0644); err != nil { - tr.fail("write input file %q: %v", name, err) + tr.Status = StatusRuntimeError + tr.addFailure("write input file %q: %v", name, err) return tr } } @@ -259,7 +244,6 @@ func (r *Runner) runTest(t *dsl.Test) *TestResult { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - // Wrapper precedence: CLI flag > test > group (already copied into test). wrapper := r.cfg.Wrapper if wrapper == "" { wrapper = t.Wrapper @@ -269,8 +253,6 @@ func (r *Runner) runTest(t *dsl.Test) *TestResult { cmd.Dir = tmpDir cmd.Env = os.Environ() - // Force C locale so numeric formatting (decimal separator, etc.) is stable - // across runners. Student code can still override via test env. cmd.Env = append(cmd.Env, "LC_ALL=C", "LANG=C") for k, v := range t.Env { cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v)) @@ -293,12 +275,12 @@ func (r *Runner) runTest(t *dsl.Test) *TestResult { tr.ActualStderr = normalizeOutput(stderr.String(), r.file) if stdout.truncated || stderr.truncated { - tr.fail("output truncated at %d bytes (possible runaway output)", MaxOutputBytes) + tr.addFailure("output truncated at %d bytes (possible runaway output)", MaxOutputBytes) } if ctx.Err() == context.DeadlineExceeded { tr.Status = StatusTLE - tr.Failures = append(tr.Failures, fmt.Sprintf("time limit exceeded (%v)", timeout)) + tr.addFailure("time limit exceeded (%v)", timeout) return tr } @@ -308,46 +290,43 @@ func (r *Runner) runTest(t *dsl.Test) *TestResult { actualCode = exitErr.ExitCode() } else { tr.Status = StatusRuntimeError - tr.fail("runtime error: %v", runErr) + tr.addFailure("runtime error: %v", runErr) return tr } } tr.ActualCode = actualCode if t.ExitCode != nil && actualCode != *t.ExitCode { - tr.fail("exit code: expected %d, got %d", *t.ExitCode, actualCode) + tr.addFailure("exit code: expected %d, got %d", *t.ExitCode, actualCode) } for _, f := range applyMatcher("stdout", t.Stdout, tr.ActualStdout) { - tr.fail("%s", f) + tr.addFailure("%s", f) } for _, f := range applyMatcher("stderr", t.Stderr, tr.ActualStderr) { - tr.fail("%s", f) + tr.addFailure("%s", f) } for name, expected := range t.OutFiles { path := filepath.Join(tmpDir, name) content, err := os.ReadFile(path) if err != nil { - tr.fail("output file %q not found: %v", name, err) + tr.addFailure("output file %q not found: %v", name, err) continue } actual := normalizeOutput(string(content), r.file) for _, f := range applyMatcher(fmt.Sprintf("file(%s)", name), dsl.ExactMatcher{Value: expected}, actual) { - tr.fail("%s", f) + tr.addFailure("%s", f) } } - if len(tr.Failures) > 0 { + if tr.Status == StatusPass && len(tr.Failures) > 0 { tr.Status = StatusFail } return tr } -// buildExecCmd creates an exec.Cmd for the solution, optionally prefixed with -// a wrapper (gdb, valgrind, qemu, ...). Wrapper is a shell string so users can -// pass flags like "valgrind --error-exitcode=99 --leak-check=full". func buildExecCmd(ctx context.Context, wrapper, binary string, args []string) *exec.Cmd { if wrapper == "" { return exec.CommandContext(ctx, binary, args...) @@ -358,7 +337,6 @@ func buildExecCmd(ctx context.Context, wrapper, binary string, args []string) *e return exec.CommandContext(ctx, full[0], full[1:]...) } -// normalizeOutput applies the file-level CRLF / trailing-WS rules. func normalizeOutput(s string, f *dsl.File) string { if f.NormalizeCRLF { s = strings.ReplaceAll(s, "\r\n", "\n") @@ -374,8 +352,6 @@ func normalizeOutput(s string, f *dsl.File) string { return s } -// cappedBuffer stops writing once limit bytes are captured, but keeps draining -// the source so the child process doesn't block on a full pipe. type cappedBuffer struct { buf bytes.Buffer limit int @@ -386,7 +362,7 @@ func (c *cappedBuffer) Write(p []byte) (int, error) { room := c.limit - c.buf.Len() if room <= 0 { c.truncated = true - return len(p), nil // pretend we wrote, to keep the pipe flowing + return len(p), nil } if len(p) > room { c.buf.Write(p[:room]) @@ -399,7 +375,3 @@ func (c *cappedBuffer) Write(p []byte) (int, error) { func (c *cappedBuffer) String() string { return c.buf.String() } var _ io.Writer = (*cappedBuffer)(nil) - -type zeroChecker struct{} - -func (z *zeroChecker) IsZero() bool { return true }