4 Commits
v0.2.5 ... main

Author SHA1 Message Date
8cf2584ce5 make max output settings
Some checks failed
go-test / go test (push) Successful in 12s
Release / Build & publish (push) Failing after 1m7s
2026-06-10 11:37:59 +03:00
b44cccf8d1 fix wrappers
All checks were successful
go-test / go test (push) Successful in 12s
Release / Build & publish (push) Successful in 31s
2026-06-10 10:30:25 +03:00
1fec40fc9e some crlf fixes
All checks were successful
go-test / go test (push) Successful in 12s
Release / Build & publish (push) Successful in 57s
2026-06-10 10:22:25 +03:00
2ebf193695 some fixes 2026-06-10 10:19:25 +03:00
8 changed files with 205 additions and 7 deletions

View File

@@ -19,6 +19,10 @@ type File struct {
Binary string
Sources string
// MaxOutput caps captured stdout/stderr per test (bytes). Zero means use the
// runner default. Raise it for suites whose expected output is large.
MaxOutput int64
NormalizeCRLF bool
TrimTrailingWS bool

View File

@@ -307,6 +307,17 @@ func (p *Parser) parseFile() (*File, error) {
}
f.MemoryLimit = n
case "max_output":
p.advance()
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
return nil, err
}
n, err := p.parseSize()
if err != nil {
return nil, err
}
f.MaxOutput = n
case "group":
g, err := p.parseGroup(f.Timeout, f.MemoryLimit)
if err != nil {

View File

@@ -37,6 +37,24 @@ group("g") {
}
}
func TestParseMaxOutput(t *testing.T) {
src := `
build "make"
max_output = 64M
group("g") {
weight = 1.0
test("t") { stdout = "ok\n" }
}
`
f, _, err := Parse(src)
if err != nil {
t.Fatalf("parse: %v", err)
}
if f.MaxOutput != 64*1024*1024 {
t.Errorf("MaxOutput = %d, want %d", f.MaxOutput, 64*1024*1024)
}
}
func TestParsePatternStageAllMode(t *testing.T) {
src := `
build "make"

View File

@@ -75,7 +75,7 @@
},
"block-keywords": {
"name": "variable.parameter.jdg",
"match": "\\b(?:weight|scoring|stdin|stdout|stderr|args|exitCode|input|dirs)\\b"
"match": "\\b(?:weight|scoring|stdin|stdout|stderr|args|args_file|exitCode|input|dirs|stage)\\b"
},
"matcher-keywords": {
"name": "support.function.jdg",

View File

@@ -48,6 +48,8 @@
"input"
"output"
"dirs"
"stage"
"args_file"
] @property
[

View File

@@ -86,6 +86,13 @@ func writeBuildText(w io.Writer, b *runner.BuildRun, multi bool) {
fmt.Fprintf(w, "│ %s\n", line)
}
}
if tr.Status != runner.StatusPass && strings.TrimSpace(tr.ActualStderr) != "" {
fmt.Fprintf(w, "│ ── stderr ──\n")
for _, line := range clampLines(tr.ActualStderr, 10) {
fmt.Fprintf(w, "│ %s\n", line)
}
}
}
fmt.Fprintf(w, "└─\n")
}
@@ -265,6 +272,18 @@ func extractTotalScore(data []byte) (float64, error) {
return header.TotalScore, nil
}
// clampLines returns up to max lines of s (trailing newline ignored), appending
// a truncation note when more lines were omitted.
func clampLines(s string, max int) []string {
s = strings.TrimRight(s, "\n")
lines := strings.Split(s, "\n")
if len(lines) <= max {
return lines
}
out := append([]string{}, lines[:max]...)
return append(out, fmt.Sprintf("... (%d more lines)", len(lines)-max))
}
func humanBytes(n int64) string {
const (
KiB = 1024

96
runner/normalize_test.go Normal file
View File

@@ -0,0 +1,96 @@
package runner
import (
"os"
"path/filepath"
"testing"
"github.com/Mond1c/judge/dsl"
)
// Under a wrapper, only args that name a staged file become absolute; numeric
// and flag args must be passed through verbatim (regression: a numeric arg "1"
// was being turned into "<tmpdir>/1", breaking programs that take non-path
// arguments).
func TestAbsoluteArgsOnlyRewritesStagedFiles(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "01.in"), []byte("x"), 0644); err != nil {
t.Fatal(err)
}
got := absoluteArgs(dir, []string{"$.txt", "1", "1", "3", "01.in", "--flag"})
want := []string{"$.txt", "1", "1", "3", filepath.Join(dir, "01.in"), "--flag"}
for i := range want {
if got[i] != want[i] {
t.Errorf("arg[%d] = %q, want %q", i, got[i], want[i])
}
}
}
// A staged/expected value loaded from disk on Windows can carry CRLF. With
// normalize_crlf the runner must normalize the EXPECTED side too, otherwise it
// can never match the LF-normalized actual output.
func TestNormalizeExpectationsCRLF(t *testing.T) {
r := &Runner{file: &dsl.File{NormalizeCRLF: true}}
test := &dsl.Test{
Stdout: dsl.ExactMatcher{Value: "1\r\n2\r\n"},
Stderr: dsl.NoMatcher{},
OutFiles: map[string]string{"out.txt": "a\r\nb\r\n"},
}
r.normalizeExpectations(test)
m, ok := test.Stdout.(dsl.ExactMatcher)
if !ok {
t.Fatalf("stdout matcher type changed: %T", test.Stdout)
}
if m.Value != "1\n2\n" {
t.Errorf("expected stdout normalized to %q, got %q", "1\n2\n", m.Value)
}
if got := test.OutFiles["out.txt"]; got != "a\nb\n" {
t.Errorf("expected outfile normalized to %q, got %q", "a\nb\n", got)
}
// And the normalized expected must match normalized actual end-to-end.
if fails := test.Stdout.Match("stdout", normalizeOutput("1\r\n2\r\n", r.file)); len(fails) != 0 {
t.Errorf("normalized expected should match normalized actual, got %v", fails)
}
}
func TestNormalizeExpectationsTrimTrailingWS(t *testing.T) {
r := &Runner{file: &dsl.File{TrimTrailingWS: true}}
test := &dsl.Test{
Stdout: dsl.AnyOrderMatcher{Lines: []string{"foo ", "bar\t"}},
Stderr: dsl.NoMatcher{},
}
r.normalizeExpectations(test)
m := test.Stdout.(dsl.AnyOrderMatcher)
if m.Lines[0] != "foo" || m.Lines[1] != "bar" {
t.Errorf("anyOrder lines not trimmed: %q", m.Lines)
}
}
// Regex matchers must never be rewritten: a literal \r in the pattern is
// meaningful and normalization would corrupt it.
func TestNormalizeExpectationsLeavesRegexAlone(t *testing.T) {
r := &Runner{file: &dsl.File{NormalizeCRLF: true}}
test := &dsl.Test{
Stdout: dsl.RegexMatcher{Pattern: "a\r\nb"},
Stderr: dsl.NoMatcher{},
}
r.normalizeExpectations(test)
if m := test.Stdout.(dsl.RegexMatcher); m.Pattern != "a\r\nb" {
t.Errorf("regex pattern was modified: %q", m.Pattern)
}
}
func TestNormalizeExpectationsNoopWhenDisabled(t *testing.T) {
r := &Runner{file: &dsl.File{}}
want := "1\r\n2\r\n"
test := &dsl.Test{Stdout: dsl.ExactMatcher{Value: want}, Stderr: dsl.NoMatcher{}}
r.normalizeExpectations(test)
if m := test.Stdout.(dsl.ExactMatcher); m.Value != want {
t.Errorf("value changed with normalization disabled: %q", m.Value)
}
}

View File

@@ -407,6 +407,8 @@ func (r *Runner) runGroup(g *dsl.Group) *GroupResult {
t.Wrapper = g.Wrapper
}
r.normalizeExpectations(t)
tr := r.runTest(t)
gr.Tests = append(gr.Tests, tr)
if tr.Status == StatusPass {
@@ -486,8 +488,12 @@ func (r *Runner) runTest(t *dsl.Test) *TestResult {
cmd.Stdin = strings.NewReader(*t.Stdin)
}
stdout := &cappedBuffer{limit: MaxOutputBytes}
stderr := &cappedBuffer{limit: MaxOutputBytes}
outLimit := MaxOutputBytes
if r.file.MaxOutput > 0 {
outLimit = int(r.file.MaxOutput)
}
stdout := &cappedBuffer{limit: outLimit}
stderr := &cappedBuffer{limit: outLimit}
cmd.Stdout = stdout
cmd.Stderr = stderr
@@ -587,15 +593,23 @@ func (r *Runner) runTest(t *dsl.Test) *TestResult {
return tr
}
// absoluteArgs rewrites only those arguments that name a file staged in the
// test's working directory to an absolute path. This helps wrappers (gdb,
// valgrind) that may resolve paths from a different directory, without
// corrupting non-path arguments — a numeric arg like "1" must stay "1", not
// become "<tmpdir>/1". The process runs with Dir=dir anyway, so relative paths
// already resolve there; this is purely a safety net for genuine file args.
func absoluteArgs(dir string, args []string) []string {
out := make([]string, len(args))
for i, a := range args {
if !filepath.IsAbs(a) {
if a != "" && !filepath.IsAbs(a) {
if _, err := os.Stat(filepath.Join(dir, a)); err == nil {
out[i] = filepath.Join(dir, a)
} else {
out[i] = a
continue
}
}
out[i] = a
}
return out
}
@@ -609,6 +623,40 @@ func buildExecCmd(ctx context.Context, wrapper, binary string, args []string) *e
return exec.CommandContext(ctx, full[0], full[1:]...)
}
// normalizeExpectations applies the suite's CRLF/whitespace normalization to a
// test's *expected* values, mirroring what normalizeOutput does to the actual
// output. Without this, an expected value loaded from disk on Windows (e.g. an
// answer file checked out with CRLF) would never match LF-normalized actual
// output even with normalize_crlf enabled.
func (r *Runner) normalizeExpectations(t *dsl.Test) {
if !r.file.NormalizeCRLF && !r.file.TrimTrailingWS {
return
}
t.Stdout = r.normalizeMatcher(t.Stdout)
t.Stderr = r.normalizeMatcher(t.Stderr)
for k, v := range t.OutFiles {
t.OutFiles[k] = normalizeOutput(v, r.file)
}
}
// normalizeMatcher normalizes the expected payload of the matchers that compare
// whole-output text. Regex and numeric matchers are left untouched: their
// patterns are authored deliberately and must not be rewritten.
func (r *Runner) normalizeMatcher(m dsl.Matcher) dsl.Matcher {
switch x := m.(type) {
case dsl.ExactMatcher:
return dsl.ExactMatcher{Value: normalizeOutput(x.Value, r.file)}
case dsl.AnyOrderMatcher:
lines := make([]string, len(x.Lines))
for i, l := range x.Lines {
lines[i] = normalizeOutput(l, r.file)
}
return dsl.AnyOrderMatcher{Lines: lines}
default:
return m
}
}
func normalizeOutput(s string, f *dsl.File) string {
if f.NormalizeCRLF {
s = strings.ReplaceAll(s, "\r\n", "\n")