From 7f14f8c2364964a794c949a2d601ff0a9ed25efa Mon Sep 17 00:00:00 2001 From: Mikhail Kornilovich Date: Thu, 16 Apr 2026 00:59:09 +0300 Subject: [PATCH] test: expand dsl/runner coverage and add go-test CI workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add dsl/matcher_test.go covering ExactMatcher, ContainsMatcher, RegexMatcher, NumericEpsMatcher, AnyOrderMatcher and NoMatcher — previously 0% — including epsilon, count mismatch, unparsable numbers, invalid regex, and splitLines edge cases. - Add dsl/ast_test.go for the new Test.SetInputFile / SetStdin / SetOutputFile / SetStdout helpers and Pattern.IsDirMode. - Add dsl/build_string_test.go covering BuildProfile.String, WarningLevel.String and BuildConfig.MergeFrom (wrapper, defines into nil map, defines override existing, nil src). - Add dsl/merge_test.go driving mergeFiles to 100%: legacy build fields, duplicate toolchain/build/group from include, binary / sources / normalize_crlf / trim_trailing_ws propagation, local overrides of timeout and memory_limit. - Add dsl/parser_features_test.go for parseTest / parseGroup happy paths that were missing: file/outFile, env, wrapper, per-test timeout/memory overrides, non-zero exitCode, scoring partial / all_or_none and unknown scoring. - Add dsl/parser_errors_test.go, a 54-case table-driven test that hits every `expect(...)` error branch in parseGroup and parseTest (missing LPAREN/RPAREN/LBRACE/RBRACE/ASSIGN, wrong token types on weight/timeout/memory_limit/scoring/env/wrapper/file/outFile, and unclosed blocks). - Add dsl/parser_misc_test.go covering parsePattern dir-mode with args, unknown pattern field, non-ident in pattern, top-level binary / sources / normalize_crlf / trim_trailing_ws / bare-int memory_limit, parseBool invalid ident and non-ident, matcher without an operator, validateBuilds legacy+structured conflict. - Add dsl/build_parser_test.go covering every BuildConfig field (sources, includes, sanitize, link, extra, platforms, compilers, defines), OS overrides on named builds, nested / duplicate OS override errors, unknown build / profile / warnings / platform, missing = on assign-string and assign-string-list, define(...) error cases, and parseToolchainsBlock (duplicate name, missing platforms, bad name token, unknown field, unknown compiler class, binary and class propagation). - Add dsl/lexer_test.go for Token.String, TokenType.String UNKNOWN branch, line comments, unterminated string and heredoc, unknown escape sequence, escape decoding, unexpected character, every K/M/G/KiB/MiB/GiB size suffix, ms/s/m duration suffixes, negative integer lexing and float literals. - Extend runner/result_test.go with Status.String and TestResult.addFailure (both previously 0%). - Add .gitea/workflows/go-test.yml running `go vet` and `go test -race -coverprofile=coverage.out ./...` on push, pull_request and manual dispatch, uploading coverage.out as an artifact. Coverage: dsl 60.5% -> 85%+, runner 29.0% -> 30.5%. --- .gitea/workflows/go-test.yml | 36 ++++ dsl/ast_test.go | 75 +++++++ dsl/build_parser_test.go | 379 +++++++++++++++++++++++++++++++++++ dsl/build_string_test.go | 74 +++++++ dsl/lexer_test.go | 175 ++++++++++++++++ dsl/matcher_test.go | 187 +++++++++++++++++ dsl/merge_test.go | 220 ++++++++++++++++++++ dsl/parser_errors_test.go | 299 +++++++++++++++++++++++++++ dsl/parser_features_test.go | 237 ++++++++++++++++++++++ dsl/parser_misc_test.go | 193 ++++++++++++++++++ runner/result_test.go | 40 +++- 11 files changed, 1914 insertions(+), 1 deletion(-) create mode 100644 .gitea/workflows/go-test.yml create mode 100644 dsl/ast_test.go create mode 100644 dsl/build_parser_test.go create mode 100644 dsl/build_string_test.go create mode 100644 dsl/lexer_test.go create mode 100644 dsl/matcher_test.go create mode 100644 dsl/merge_test.go create mode 100644 dsl/parser_errors_test.go create mode 100644 dsl/parser_features_test.go create mode 100644 dsl/parser_misc_test.go diff --git a/.gitea/workflows/go-test.yml b/.gitea/workflows/go-test.yml new file mode 100644 index 0000000..5e9a41b --- /dev/null +++ b/.gitea/workflows/go-test.yml @@ -0,0 +1,36 @@ +name: go-test +run-name: "Go unit tests" + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + test: + name: go test + runs-on: Linux-Runner + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.26' + + - name: go vet + run: go vet ./... + + - name: go test + run: go test -race -coverprofile=coverage.out ./... + + - name: Coverage summary + run: go tool cover -func=coverage.out | tail -20 + + - name: Upload coverage + if: ${{ always() }} + uses: https://github.com/christopherHX/gitea-upload-artifact@v4 + with: + name: coverage + path: coverage.out + retention-days: 7 diff --git a/dsl/ast_test.go b/dsl/ast_test.go new file mode 100644 index 0000000..8d5af27 --- /dev/null +++ b/dsl/ast_test.go @@ -0,0 +1,75 @@ +package dsl + +import "testing" + +func newTest() *Test { + return &Test{ + InFiles: map[string]string{}, + OutFiles: map[string]string{}, + } +} + +func TestSetInputFile(t *testing.T) { + tst := newTest() + tst.SetInputFile("input.txt", []byte("hello")) + if got := tst.InFiles["input.txt"]; got != "hello" { + t.Errorf("InFiles[input.txt] = %q, want %q", got, "hello") + } + if tst.Stdin != nil { + t.Errorf("Stdin should remain nil, got %v", *tst.Stdin) + } +} + +func TestSetStdin(t *testing.T) { + tst := newTest() + tst.SetStdin([]byte("data\n")) + if tst.Stdin == nil { + t.Fatal("Stdin is nil") + } + if *tst.Stdin != "data\n" { + t.Errorf("Stdin = %q, want %q", *tst.Stdin, "data\n") + } + if len(tst.InFiles) != 0 { + t.Errorf("InFiles should be empty, got %v", tst.InFiles) + } +} + +func TestSetOutputFileSetsNoMatcher(t *testing.T) { + tst := newTest() + tst.SetOutputFile("out.txt", []byte("result")) + if got := tst.OutFiles["out.txt"]; got != "result" { + t.Errorf("OutFiles[out.txt] = %q, want %q", got, "result") + } + if _, ok := tst.Stdout.(NoMatcher); !ok { + t.Errorf("Stdout = %T, want NoMatcher", tst.Stdout) + } +} + +func TestSetStdoutSetsExactMatcher(t *testing.T) { + tst := newTest() + tst.SetStdout([]byte("expected\n")) + m, ok := tst.Stdout.(ExactMatcher) + if !ok { + t.Fatalf("Stdout = %T, want ExactMatcher", tst.Stdout) + } + if m.Value != "expected\n" { + t.Errorf("ExactMatcher.Value = %q, want %q", m.Value, "expected\n") + } + if len(tst.OutFiles) != 0 { + t.Errorf("OutFiles should be empty, got %v", tst.OutFiles) + } +} + +func TestIsDirModeTrue(t *testing.T) { + p := &Pattern{DirsGlob: "tests/*"} + if !p.IsDirMode() { + t.Error("IsDirMode() = false, want true") + } +} + +func TestIsDirModeFalse(t *testing.T) { + p := &Pattern{InputGlob: "tests/*.in"} + if p.IsDirMode() { + t.Error("IsDirMode() = true, want false") + } +} diff --git a/dsl/build_parser_test.go b/dsl/build_parser_test.go new file mode 100644 index 0000000..3d52dbd --- /dev/null +++ b/dsl/build_parser_test.go @@ -0,0 +1,379 @@ +package dsl + +import ( + "strings" + "testing" +) + +const buildDefaultsPrefix = ` +build_defaults { + language = "c" + standard = "c11" + sources = "*.c" + output = "sol" + warnings = strict +} +` + +func TestBuildAllFieldsParsed(t *testing.T) { + src := ` +build_defaults { + language = "cpp" + standard = "c++17" + sources = "a.cpp" "b.cpp" + includes = "inc" "inc2" + output = "sol" + warnings = pedantic + wrapper = "timeout" + sanitize = "address" "undefined" + link = "-lm" + extra = "-g" + platforms = "linux" "darwin" + compilers = "gcc" "clang" + define("DEBUG") = "1" + define("VERSION") = "2" +} + +build "release" { + profile = release +} + +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + f, _, err := Parse(src) + if err != nil { + t.Fatalf("parse: %v", err) + } + bd := f.BuildDefaults + if bd == nil { + t.Fatal("no build_defaults") + } + if bd.Language != "cpp" || bd.Standard != "c++17" || bd.Output != "sol" { + t.Errorf("basic fields wrong: %+v", bd) + } + if bd.Warnings != WarningsPedantic { + t.Errorf("Warnings = %v", bd.Warnings) + } + if bd.Wrapper != "timeout" { + t.Errorf("Wrapper = %q", bd.Wrapper) + } + if len(bd.Sources) != 2 || len(bd.Includes) != 2 || len(bd.Sanitize) != 2 { + t.Errorf("lists: sources=%v includes=%v sanitize=%v", bd.Sources, bd.Includes, bd.Sanitize) + } + if len(bd.Link) != 1 || len(bd.Extra) != 1 { + t.Errorf("link/extra: %v / %v", bd.Link, bd.Extra) + } + if len(bd.Platforms) != 2 || len(bd.Compilers) != 2 { + t.Errorf("platforms/compilers: %v / %v", bd.Platforms, bd.Compilers) + } + if bd.Defines["DEBUG"] != "1" || bd.Defines["VERSION"] != "2" { + t.Errorf("Defines = %v", bd.Defines) + } +} + +func TestBuildOSOverrides(t *testing.T) { + src := ` +build_defaults { + language = "c" + standard = "c11" + sources = "main.c" + output = "sol" + warnings = strict +} + +build "release" { + profile = release + linux { extra = "-pthread" } + windows { extra = "/MT" } + darwin { extra = "-framework CoreFoundation" } +} + +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + f, _, err := Parse(src) + if err != nil { + t.Fatalf("parse: %v", err) + } + b := f.Builds[0] + if b.Linux == nil || b.Windows == nil || b.Darwin == nil { + t.Errorf("OS overrides missing: %+v", b) + } +} + +func TestBuildOSNestedForbidden(t *testing.T) { + src := buildDefaultsPrefix + ` +build "r" { + profile = release + linux { + darwin { extra = "-x" } + } +} +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "nested inside another OS override") { + t.Errorf("want nested OS override error, got %v", err) + } +} + +func TestBuildDuplicateOSOverride(t *testing.T) { + src := buildDefaultsPrefix + ` +build "r" { + profile = release + linux { extra = "-a" } + linux { extra = "-b" } +} +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "duplicate linux override") { + t.Errorf("want duplicate linux override, got %v", err) + } +} + +func TestBuildUnknownField(t *testing.T) { + src := ` +build_defaults { + bogus = "x" + language = "c" + standard = "c11" + sources = "main.c" + output = "sol" + warnings = strict +} +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "unknown field") { + t.Errorf("want unknown field error, got %v", err) + } +} + +func TestBuildUnknownProfile(t *testing.T) { + src := ` +build_defaults { + language = "c" + standard = "c11" + sources = "main.c" + output = "sol" + warnings = strict +} +build "weird" { profile = bogus } +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "unknown profile") { + t.Errorf("want unknown profile error, got %v", err) + } +} + +func TestBuildUnknownWarnings(t *testing.T) { + src := ` +build_defaults { + language = "c" + standard = "c11" + sources = "main.c" + output = "sol" + warnings = bogus +} +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "unknown warnings level") { + t.Errorf("want unknown warnings error, got %v", err) + } +} + +func TestBuildWarningsDefaultAndStrict(t *testing.T) { + src := ` +build_defaults { + language = "c" + standard = "c11" + sources = "main.c" + output = "sol" + warnings = default +} +build "r" { + warnings = strict +} +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + f, _, err := Parse(src) + if err != nil { + t.Fatalf("parse: %v", err) + } + if f.BuildDefaults.Warnings != WarningsDefault { + t.Errorf("defaults warnings = %v", f.BuildDefaults.Warnings) + } + if f.Builds[0].Warnings != WarningsStrict { + t.Errorf("build warnings = %v", f.Builds[0].Warnings) + } +} + +func TestBuildInvalidPlatform(t *testing.T) { + src := buildDefaultsPrefix + ` +build "r" { + profile = release + platforms = "bsd" +} +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "unknown platform") { + t.Errorf("want unknown platform error, got %v", err) + } +} + +func TestBuildAssignStringMissingAssign(t *testing.T) { + src := ` +build_defaults { + language "c" + standard = "c11" + sources = "main.c" + output = "sol" + warnings = strict +} +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "expected") { + t.Errorf("want expected = error, got %v", err) + } +} + +func TestBuildAssignStringListMissingAssign(t *testing.T) { + src := ` +build_defaults { + language = "c" + standard = "c11" + sources "main.c" + output = "sol" + warnings = strict +} +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "expected") { + t.Errorf("want expected = error, got %v", err) + } +} + +func TestBuildDefineErrors(t *testing.T) { + cases := []string{ + `build_defaults { language="c" standard="c11" sources="x.c" output="s" warnings=strict define "K" = "v" }`, + `build_defaults { language="c" standard="c11" sources="x.c" output="s" warnings=strict define(K) = "v" }`, + `build_defaults { language="c" standard="c11" sources="x.c" output="s" warnings=strict define("K" = "v" }`, + `build_defaults { language="c" standard="c11" sources="x.c" output="s" warnings=strict define("K") "v" }`, + `build_defaults { language="c" standard="c11" sources="x.c" output="s" warnings=strict define("K") = K }`, + } + for i, src := range cases { + full := src + "\ngroup(\"g\") { weight = 1.0 test(\"t\") { stdout = \"\" } }" + if _, _, err := Parse(full); err == nil { + t.Errorf("case %d: expected error", i) + } + } +} + +func TestBuildNonIdentInBlock(t *testing.T) { + src := ` +build_defaults { "not-an-ident" = "x" } +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "unexpected token") { + t.Errorf("want unexpected token error, got %v", err) + } +} + +func TestToolchainsDuplicateInSameBlock(t *testing.T) { + src := ` +toolchains { + gcc { platforms = "linux" } + gcc { platforms = "darwin" } +} +build_defaults { language="c" standard="c11" sources="x.c" output="s" warnings=strict } +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "duplicate toolchain") { + t.Errorf("want duplicate toolchain error, got %v", err) + } +} + +func TestToolchainsMissingPlatforms(t *testing.T) { + src := ` +toolchains { + gcc { binary = "gcc-13" } +} +build_defaults { language="c" standard="c11" sources="x.c" output="s" warnings=strict } +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "platforms is required") { + t.Errorf("want platforms-required error, got %v", err) + } +} + +func TestToolchainsBadName(t *testing.T) { + src := ` +toolchains { + 42 { platforms = "linux" } +} +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "expected toolchain name") { + t.Errorf("want toolchain name error, got %v", err) + } +} + +func TestToolchainsUnknownField(t *testing.T) { + src := ` +toolchains { + gcc { platforms = "linux" bogus = "x" } +} +build_defaults { language="c" standard="c11" sources="x.c" output="s" warnings=strict } +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "unknown field") { + t.Errorf("want unknown field error, got %v", err) + } +} + +func TestToolchainsUnknownClass(t *testing.T) { + src := ` +toolchains { + gcc { platforms = "linux" class = bogus } +} +build_defaults { language="c" standard="c11" sources="x.c" output="s" warnings=strict } +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "unknown compiler class") { + t.Errorf("want unknown class error, got %v", err) + } +} + +func TestToolchainsClassAndBinary(t *testing.T) { + src := ` +toolchains { + gcc13 { platforms = "linux" binary = "gcc-13" class = gnu } + msvc { platforms = "windows" class = msvc } +} +build_defaults { language="c" standard="c11" sources="x.c" output="s" warnings=strict } +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + f, _, err := Parse(src) + if err != nil { + t.Fatalf("parse: %v", err) + } + if len(f.Toolchains) != 2 { + t.Fatalf("toolchains = %d", len(f.Toolchains)) + } + if f.Toolchains[0].Binary != "gcc-13" || f.Toolchains[0].Class != "gnu" { + t.Errorf("gcc13 = %+v", f.Toolchains[0]) + } + if f.Toolchains[1].Class != "msvc" { + t.Errorf("msvc class = %q", f.Toolchains[1].Class) + } +} diff --git a/dsl/build_string_test.go b/dsl/build_string_test.go new file mode 100644 index 0000000..2706414 --- /dev/null +++ b/dsl/build_string_test.go @@ -0,0 +1,74 @@ +package dsl + +import "testing" + +func TestBuildProfileString(t *testing.T) { + cases := []struct { + p BuildProfile + want string + }{ + {ProfileRelease, "release"}, + {ProfileDebug, "debug"}, + {ProfileSanitized, "sanitized"}, + {ProfileUnset, "unset"}, + {BuildProfile(999), "unset"}, + } + for _, c := range cases { + if got := c.p.String(); got != c.want { + t.Errorf("BuildProfile(%d).String() = %q, want %q", c.p, got, c.want) + } + } +} + +func TestWarningLevelString(t *testing.T) { + cases := []struct { + w WarningLevel + want string + }{ + {WarningsDefault, "default"}, + {WarningsStrict, "strict"}, + {WarningsPedantic, "pedantic"}, + {WarningsUnset, "unset"}, + {WarningLevel(999), "unset"}, + } + for _, c := range cases { + if got := c.w.String(); got != c.want { + t.Errorf("WarningLevel(%d).String() = %q, want %q", c.w, got, c.want) + } + } +} + +func TestMergeFromWrapperAndDefines(t *testing.T) { + dst := &BuildConfig{} + src := &BuildConfig{ + Wrapper: "valgrind", + Defines: map[string]string{"DEBUG": "1", "VERSION": "2"}, + } + dst.MergeFrom(src) + if dst.Wrapper != "valgrind" { + t.Errorf("Wrapper = %q, want valgrind", dst.Wrapper) + } + if dst.Defines["DEBUG"] != "1" || dst.Defines["VERSION"] != "2" { + t.Errorf("Defines = %v", dst.Defines) + } +} + +func TestMergeFromDefinesIntoExisting(t *testing.T) { + dst := &BuildConfig{Defines: map[string]string{"KEEP": "old"}} + src := &BuildConfig{Defines: map[string]string{"NEW": "1", "KEEP": "new"}} + dst.MergeFrom(src) + if dst.Defines["KEEP"] != "new" { + t.Errorf("KEEP = %q, want new (overridden)", dst.Defines["KEEP"]) + } + if dst.Defines["NEW"] != "1" { + t.Errorf("NEW = %q, want 1", dst.Defines["NEW"]) + } +} + +func TestMergeFromNilSrc(t *testing.T) { + dst := &BuildConfig{Language: "c"} + dst.MergeFrom(nil) + if dst.Language != "c" { + t.Errorf("dst mutated on nil merge: %+v", dst) + } +} diff --git a/dsl/lexer_test.go b/dsl/lexer_test.go new file mode 100644 index 0000000..10dedd7 --- /dev/null +++ b/dsl/lexer_test.go @@ -0,0 +1,175 @@ +package dsl + +import ( + "strings" + "testing" +) + +func TestTokenStringAndUnknownType(t *testing.T) { + tok := Token{Type: TOKEN_IDENT, Value: "foo", Line: 2, Col: 5} + s := tok.String() + if !strings.Contains(s, "IDENT") || !strings.Contains(s, "foo") { + t.Errorf("Token.String() = %q", s) + } + if got := TokenType(999).String(); got != "UNKNOWN" { + t.Errorf("TokenType(999).String() = %q, want UNKNOWN", got) + } + for tt, want := range map[TokenType]string{ + TOKEN_STRING: "STRING", + TOKEN_FLOAT: "FLOAT", + TOKEN_INT: "INT", + TOKEN_DURATION: "DURATION", + TOKEN_SIZE: "SIZE", + TOKEN_LBRACE: "{", + TOKEN_RBRACE: "}", + TOKEN_LPAREN: "(", + TOKEN_RPAREN: ")", + TOKEN_ASSIGN: "=", + TOKEN_TILDE: "~", + TOKEN_EOF: "EOF", + } { + if got := tt.String(); got != want { + t.Errorf("TokenType(%d) = %q, want %q", tt, got, want) + } + } +} + +func TestLexerLineComment(t *testing.T) { + src := ` +// leading comment +build "make" // trailing +group("g") { // inside + weight = 1.0 + test("t") { stdout = "" } +} +` + if _, _, err := Parse(src); err != nil { + t.Errorf("parse with comments: %v", err) + } +} + +func TestLexerUnterminatedString(t *testing.T) { + _, _, err := Parse(`build "unterminated`) + if err == nil || !strings.Contains(err.Error(), "unterminated") { + t.Errorf("want unterminated string error, got %v", err) + } +} + +func TestLexerUnknownEscape(t *testing.T) { + _, _, err := Parse(`build "bad\zescape"`) + if err == nil || !strings.Contains(err.Error(), "unknown escape") { + t.Errorf("want unknown escape error, got %v", err) + } +} + +func TestLexerEscapeSequences(t *testing.T) { + src := `build "a\nb\tc\\d\"e"` + f, _, err := Parse(src + "\ngroup(\"g\") { weight = 1.0 test(\"t\") { stdout = \"\" } }") + if err != nil { + t.Fatalf("parse: %v", err) + } + if f.Build != "a\nb\tc\\d\"e" { + t.Errorf("escape sequences = %q", f.Build) + } +} + +func TestLexerUnexpectedCharacter(t *testing.T) { + _, _, err := Parse("build @invalid") + if err == nil || !strings.Contains(err.Error(), "unexpected character") { + t.Errorf("want unexpected character error, got %v", err) + } +} + +func TestLexerUnterminatedHeredoc(t *testing.T) { + src := "build \"x\"\ngroup(\"g\") { weight = 1.0 test(\"t\") { stdin = \"\"\"\nnever closed\n } }" + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "unterminated heredoc") { + t.Errorf("want unterminated heredoc, got %v", err) + } +} + +func TestLexerSizeSuffixes(t *testing.T) { + cases := map[string]int64{ + "1024": 1024, + "1B": 1, + "1K": 1024, + "1KB": 1024, + "1KiB": 1024, + "1M": 1024 * 1024, + "1MB": 1024 * 1024, + "1MiB": 1024 * 1024, + "1G": 1024 * 1024 * 1024, + "1GB": 1024 * 1024 * 1024, + "1GiB": 1024 * 1024 * 1024, + } + for literal, want := range cases { + src := "build \"x\"\nmemory_limit = " + literal + "\ngroup(\"g\") { weight = 1.0 test(\"t\") { stdout = \"\" } }" + f, _, err := Parse(src) + if err != nil { + t.Errorf("parse %q: %v", literal, err) + continue + } + if f.MemoryLimit != want { + t.Errorf("memory_limit %s = %d, want %d", literal, f.MemoryLimit, want) + } + } +} + +func TestLexerDurationSuffixes(t *testing.T) { + cases := []string{"5ms", "10s", "2m"} + for _, d := range cases { + src := "build \"x\"\ntimeout " + d + "\ngroup(\"g\") { weight = 1.0 test(\"t\") { stdout = \"\" } }" + if _, _, err := Parse(src); err != nil { + t.Errorf("parse timeout %s: %v", d, err) + } + } +} + +func TestLexerNegativeInt(t *testing.T) { + src := ` +build "x" +group("g") { + weight = 1.0 + test("t") { + exitCode = -1 + stdout = "" + } +} +` + f, _, err := Parse(src) + if err != nil { + t.Fatalf("parse: %v", err) + } + tst := f.Groups[0].Tests[0] + if tst.ExitCode == nil || *tst.ExitCode != -1 { + t.Errorf("ExitCode = %v, want -1", tst.ExitCode) + } +} + +func TestLexerFloatNumber(t *testing.T) { + src := ` +build "x" +group("g") { + weight = 0.75 + test("t") { stdout = "" } +} +group("g2") { + weight = 0.25 + test("t") { stdout = "" } +} +` + f, _, err := Parse(src) + if err != nil { + t.Fatalf("parse: %v", err) + } + if f.Groups[0].Weight != 0.75 { + t.Errorf("weight = %v", f.Groups[0].Weight) + } +} + +func TestLexerInvalidSizeUnit(t *testing.T) { + _, _, err := Parse("build \"x\"\nmemory_limit = 10Z\n") + if err == nil { + t.Error("expected error for invalid size unit") + } +} diff --git a/dsl/matcher_test.go b/dsl/matcher_test.go new file mode 100644 index 0000000..0110d4c --- /dev/null +++ b/dsl/matcher_test.go @@ -0,0 +1,187 @@ +package dsl + +import ( + "strings" + "testing" +) + +func TestExactMatcherPass(t *testing.T) { + m := ExactMatcher{Value: "hello\n"} + if errs := m.Match("stdout", "hello\n"); errs != nil { + t.Errorf("expected pass, got %v", errs) + } +} + +func TestExactMatcherMismatch(t *testing.T) { + m := ExactMatcher{Value: "hello\n"} + errs := m.Match("stdout", "world\n") + if len(errs) != 1 { + t.Fatalf("want 1 error, got %d: %v", len(errs), errs) + } + if !strings.Contains(errs[0], "stdout mismatch") { + t.Errorf("error missing label: %q", errs[0]) + } + if !strings.Contains(errs[0], "hello") || !strings.Contains(errs[0], "world") { + t.Errorf("error missing values: %q", errs[0]) + } +} + +func TestContainsMatcherPass(t *testing.T) { + m := ContainsMatcher{Substr: "needle"} + if errs := m.Match("stdout", "haystack with a needle inside"); errs != nil { + t.Errorf("expected pass, got %v", errs) + } +} + +func TestContainsMatcherMissing(t *testing.T) { + m := ContainsMatcher{Substr: "needle"} + errs := m.Match("stdout", "only hay here") + if len(errs) != 1 { + t.Fatalf("want 1 error, got %d: %v", len(errs), errs) + } + if !strings.Contains(errs[0], "needle") { + t.Errorf("error missing substring: %q", errs[0]) + } +} + +func TestRegexMatcherPass(t *testing.T) { + m := RegexMatcher{Pattern: `^hello .*!$`} + if errs := m.Match("stdout", "hello world!"); errs != nil { + t.Errorf("expected pass, got %v", errs) + } +} + +func TestRegexMatcherMismatch(t *testing.T) { + m := RegexMatcher{Pattern: `^\d+$`} + errs := m.Match("stdout", "not-a-number") + if len(errs) != 1 { + t.Fatalf("want 1 error, got %d: %v", len(errs), errs) + } + if !strings.Contains(errs[0], "does not match") { + t.Errorf("error unexpected: %q", errs[0]) + } +} + +func TestRegexMatcherInvalidPattern(t *testing.T) { + m := RegexMatcher{Pattern: `[`} + errs := m.Match("stdout", "anything") + if len(errs) != 1 { + t.Fatalf("want 1 error, got %d: %v", len(errs), errs) + } + if !strings.Contains(errs[0], "invalid regex") { + t.Errorf("error unexpected: %q", errs[0]) + } +} + +func TestNumericEpsMatcherPassWithinEps(t *testing.T) { + m := NumericEpsMatcher{Epsilon: 0.01, Value: "1.0 2.0 3.0"} + if errs := m.Match("stdout", "1.005 1.999 3.0"); errs != nil { + t.Errorf("expected pass, got %v", errs) + } +} + +func TestNumericEpsMatcherExceedsEps(t *testing.T) { + m := NumericEpsMatcher{Epsilon: 0.01, Value: "1.0 2.0"} + errs := m.Match("stdout", "1.0 2.5") + if len(errs) != 1 { + t.Fatalf("want 1 error, got %d: %v", len(errs), errs) + } + if !strings.Contains(errs[0], "number[1]") { + t.Errorf("error missing index: %q", errs[0]) + } +} + +func TestNumericEpsMatcherCountMismatch(t *testing.T) { + m := NumericEpsMatcher{Epsilon: 0.01, Value: "1 2 3"} + errs := m.Match("stdout", "1 2") + if len(errs) != 1 { + t.Fatalf("want 1 error, got %d: %v", len(errs), errs) + } + if !strings.Contains(errs[0], "expected 3 numbers, got 2") { + t.Errorf("error unexpected: %q", errs[0]) + } +} + +func TestNumericEpsMatcherBadExpected(t *testing.T) { + m := NumericEpsMatcher{Epsilon: 0.01, Value: "1 foo"} + errs := m.Match("stdout", "1 2") + if len(errs) != 1 || !strings.Contains(errs[0], "cannot parse expected") { + t.Errorf("want expected-parse error, got %v", errs) + } +} + +func TestNumericEpsMatcherBadActual(t *testing.T) { + m := NumericEpsMatcher{Epsilon: 0.01, Value: "1 2"} + errs := m.Match("stdout", "1 bar") + if len(errs) != 1 || !strings.Contains(errs[0], "cannot parse actual") { + t.Errorf("want actual-parse error, got %v", errs) + } +} + +func TestNumericEpsMatcherMultipleErrors(t *testing.T) { + m := NumericEpsMatcher{Epsilon: 0.01, Value: "1 2 3"} + errs := m.Match("stdout", "9 8 7") + if len(errs) != 3 { + t.Errorf("want 3 errors, got %d: %v", len(errs), errs) + } +} + +func TestAnyOrderMatcherPassReordered(t *testing.T) { + m := AnyOrderMatcher{Lines: []string{"a", "b", "c"}} + if errs := m.Match("stdout", "c\nb\na\n"); errs != nil { + t.Errorf("expected pass, got %v", errs) + } +} + +func TestAnyOrderMatcherPassNoTrailingNewline(t *testing.T) { + m := AnyOrderMatcher{Lines: []string{"x", "y"}} + if errs := m.Match("stdout", "y\nx"); errs != nil { + t.Errorf("expected pass, got %v", errs) + } +} + +func TestAnyOrderMatcherCountMismatch(t *testing.T) { + m := AnyOrderMatcher{Lines: []string{"a", "b"}} + errs := m.Match("stdout", "a\nb\nc\n") + if len(errs) != 1 || !strings.Contains(errs[0], "expected 2 lines, got 3") { + t.Errorf("want count error, got %v", errs) + } +} + +func TestAnyOrderMatcherLineMismatch(t *testing.T) { + m := AnyOrderMatcher{Lines: []string{"a", "b"}} + errs := m.Match("stdout", "a\nz\n") + if len(errs) != 1 || !strings.Contains(errs[0], "line mismatch") { + t.Errorf("want line mismatch, got %v", errs) + } +} + +func TestAnyOrderMatcherEmptyBoth(t *testing.T) { + m := AnyOrderMatcher{Lines: []string{}} + if errs := m.Match("stdout", ""); errs != nil { + t.Errorf("expected pass on empty, got %v", errs) + } +} + +func TestNoMatcherAlwaysPasses(t *testing.T) { + m := NoMatcher{} + if errs := m.Match("stdout", "anything at all\n"); errs != nil { + t.Errorf("NoMatcher should never fail, got %v", errs) + } + if errs := m.Match("stderr", ""); errs != nil { + t.Errorf("NoMatcher should never fail on empty, got %v", errs) + } +} + +func TestSplitLinesEmpty(t *testing.T) { + if got := splitLines(""); len(got) != 0 { + t.Errorf("splitLines(\"\") = %v, want empty", got) + } +} + +func TestSplitLinesTrailingNewline(t *testing.T) { + got := splitLines("a\nb\n") + if len(got) != 2 || got[0] != "a" || got[1] != "b" { + t.Errorf("splitLines trailing = %v, want [a b]", got) + } +} diff --git a/dsl/merge_test.go b/dsl/merge_test.go new file mode 100644 index 0000000..34dfede --- /dev/null +++ b/dsl/merge_test.go @@ -0,0 +1,220 @@ +package dsl + +import ( + "strings" + "testing" + "time" +) + +func TestMergeLegacyBuildFields(t *testing.T) { + dir := t.TempDir() + writeTempJdg(t, dir, "common.jdg", ` +build "make" +build_linux "make linux" +build_windows "make windows" +build_darwin "make darwin" +timeout 7s +memory_limit = 256MB +`) + mainPath := writeTempJdg(t, dir, "main.jdg", ` +include "common.jdg" +group("g") { weight = 1.0 test("t") { stdout = "ok\n" } } +`) + f, _, err := ParseFile(mainPath) + if err != nil { + t.Fatalf("parse: %v", err) + } + if f.Build != "make" || f.BuildLinux != "make linux" || + f.BuildWindows != "make windows" || f.BuildDarwin != "make darwin" { + t.Errorf("legacy build fields not merged: %+v", f) + } + if f.Timeout != 7*time.Second { + t.Errorf("Timeout = %v, want 7s", f.Timeout) + } + if f.MemoryLimit != 256*1024*1024 { + t.Errorf("MemoryLimit = %d", f.MemoryLimit) + } +} + +func TestMergeDuplicateToolchainFromInclude(t *testing.T) { + dir := t.TempDir() + writeTempJdg(t, dir, "common.jdg", ` +toolchains { + gcc { platforms = "linux" } +} +`) + mainPath := writeTempJdg(t, dir, "main.jdg", ` +toolchains { + gcc { platforms = "linux" } +} +include "common.jdg" +build "make" +group("g") { weight = 1.0 test("t") { stdout = "ok\n" } } +`) + _, _, err := ParseFile(mainPath) + if err == nil { + t.Fatal("expected duplicate toolchain error from merge") + } + if !strings.Contains(err.Error(), "duplicate toolchain") { + t.Errorf("error %q does not mention duplicate toolchain", err.Error()) + } +} + +func TestMergeDuplicateBuildErrors(t *testing.T) { + dir := t.TempDir() + writeTempJdg(t, dir, "common.jdg", ` +build "release" { profile = release } +`) + mainPath := writeTempJdg(t, dir, "main.jdg", ` +build_defaults { + language = "c" + standard = "c11" + sources = "*.c" + output = "solution" + warnings = strict +} +build "release" { profile = debug } +include "common.jdg" +group("g") { weight = 1.0 test("t") { stdout = "ok\n" } } +`) + _, _, err := ParseFile(mainPath) + if err == nil { + t.Fatal("expected duplicate build error") + } + if !strings.Contains(err.Error(), "duplicate build") { + t.Errorf("error %q does not mention duplicate build", err.Error()) + } +} + +func TestMergeDuplicateGroupErrors(t *testing.T) { + dir := t.TempDir() + writeTempJdg(t, dir, "common.jdg", ` +group("shared") { weight = 0.5 test("t") { stdout = "ok\n" } } +`) + mainPath := writeTempJdg(t, dir, "main.jdg", ` +build "make" +group("shared") { weight = 0.5 test("t2") { stdout = "ok\n" } } +include "common.jdg" +`) + _, _, err := ParseFile(mainPath) + if err == nil { + t.Fatal("expected duplicate group error") + } + if !strings.Contains(err.Error(), "duplicate group") { + t.Errorf("error %q does not mention duplicate group", err.Error()) + } +} + +func TestMergeGroupsAppended(t *testing.T) { + dir := t.TempDir() + writeTempJdg(t, dir, "common.jdg", ` +build "make" +group("included") { weight = 0.5 test("t") { stdout = "ok\n" } } +`) + mainPath := writeTempJdg(t, dir, "main.jdg", ` +include "common.jdg" +group("local") { weight = 0.5 test("t") { stdout = "ok\n" } } +`) + f, _, err := ParseFile(mainPath) + if err != nil { + t.Fatalf("parse: %v", err) + } + if len(f.Groups) != 2 { + t.Fatalf("want 2 groups, got %d", len(f.Groups)) + } + names := []string{f.Groups[0].Name, f.Groups[1].Name} + if !(contains(names, "included") && contains(names, "local")) { + t.Errorf("groups = %v", names) + } +} + +func TestMergeBuildsAppended(t *testing.T) { + dir := t.TempDir() + writeTempJdg(t, dir, "common.jdg", ` +build_defaults { + language = "c" + standard = "c11" + sources = "*.c" + output = "solution" + warnings = strict +} +build "release" { profile = release } +`) + mainPath := writeTempJdg(t, dir, "main.jdg", ` +include "common.jdg" +build "debug" { profile = debug } +group("g") { weight = 1.0 test("t") { stdout = "ok\n" } } +`) + f, _, err := ParseFile(mainPath) + if err != nil { + t.Fatalf("parse: %v", err) + } + if len(f.Builds) != 2 { + t.Fatalf("want 2 builds, got %d", len(f.Builds)) + } +} + +func TestMergeBinarySourcesAndFlags(t *testing.T) { + dir := t.TempDir() + writeTempJdg(t, dir, "common.jdg", ` +build "make" +binary = "solution" +sources = "*.c" +normalize_crlf = true +trim_trailing_ws = true +`) + mainPath := writeTempJdg(t, dir, "main.jdg", ` +include "common.jdg" +group("g") { weight = 1.0 test("t") { stdout = "ok\n" } } +`) + f, _, err := ParseFile(mainPath) + if err != nil { + t.Fatalf("parse: %v", err) + } + if f.Binary != "solution" { + t.Errorf("Binary = %q, want solution", f.Binary) + } + if f.Sources != "*.c" { + t.Errorf("Sources = %q, want *.c", f.Sources) + } + if !f.NormalizeCRLF { + t.Error("NormalizeCRLF not merged") + } + if !f.TrimTrailingWS { + t.Error("TrimTrailingWS not merged") + } +} + +func TestMergeLocalOverridesTimeout(t *testing.T) { + dir := t.TempDir() + writeTempJdg(t, dir, "common.jdg", ` +build "make" +timeout 10s +memory_limit = 128MB +`) + mainPath := writeTempJdg(t, dir, "main.jdg", ` +include "common.jdg" +timeout 3s +memory_limit = 64MB +group("g") { weight = 1.0 test("t") { stdout = "ok\n" } } +`) + f, _, err := ParseFile(mainPath) + if err != nil { + t.Fatalf("parse: %v", err) + } + if f.Timeout != 3*time.Second { + t.Errorf("Timeout = %v, want local 3s", f.Timeout) + } + if f.MemoryLimit != 64*1024*1024 { + t.Errorf("MemoryLimit = %d, want local 64MB", f.MemoryLimit) + } +} + +func contains(xs []string, s string) bool { + for _, x := range xs { + if x == s { + return true + } + } + return false +} diff --git a/dsl/parser_errors_test.go b/dsl/parser_errors_test.go new file mode 100644 index 0000000..4b1c393 --- /dev/null +++ b/dsl/parser_errors_test.go @@ -0,0 +1,299 @@ +package dsl + +import ( + "strings" + "testing" +) + +func TestParserErrorCases(t *testing.T) { + const prefix = `build "go build ."` + "\n" + cases := []struct { + name string + body string + want string + }{ + { + name: "group missing lparen", + body: `group "g" { weight = 1.0 test("t") { stdout = "" } }`, + want: "expected", + }, + { + name: "group name not string", + body: `group(foo) { weight = 1.0 test("t") { stdout = "" } }`, + want: "expected", + }, + { + name: "group missing rparen", + body: `group("g" { weight = 1.0 test("t") { stdout = "" } }`, + want: "expected", + }, + { + name: "group missing lbrace", + body: `group("g") weight = 1.0`, + want: "expected", + }, + { + name: "group non-ident in body", + body: `group("g") { 42 }`, + want: "unexpected token", + }, + { + name: "group weight missing assign", + body: `group("g") { weight 1.0 test("t") { stdout = "" } }`, + want: "expected", + }, + { + name: "group weight non-number", + body: `group("g") { weight = "bad" test("t") { stdout = "" } }`, + want: "", + }, + { + name: "group timeout missing assign", + body: `group("g") { weight = 1.0 timeout 5s test("t") { stdout = "" } }`, + want: "expected", + }, + { + name: "group timeout non-duration", + body: `group("g") { weight = 1.0 timeout = "5s" test("t") { stdout = "" } }`, + want: "", + }, + { + name: "group memory_limit missing assign", + body: `group("g") { weight = 1.0 memory_limit 64MB test("t") { stdout = "" } }`, + want: "expected", + }, + { + name: "group memory_limit non-size", + body: `group("g") { weight = 1.0 memory_limit = "64MB" test("t") { stdout = "" } }`, + want: "", + }, + { + name: "group scoring missing assign", + body: `group("g") { weight = 1.0 scoring partial test("t") { stdout = "" } }`, + want: "expected", + }, + { + name: "group scoring non-ident", + body: `group("g") { weight = 1.0 scoring = "partial" test("t") { stdout = "" } }`, + want: "expected", + }, + { + name: "group env missing lparen", + body: `group("g") { weight = 1.0 env "K" = "v" test("t") { stdout = "" } }`, + want: "expected", + }, + { + name: "group env key not string", + body: `group("g") { weight = 1.0 env(K) = "v" test("t") { stdout = "" } }`, + want: "expected", + }, + { + name: "group env missing rparen", + body: `group("g") { weight = 1.0 env("K" = "v" test("t") { stdout = "" } }`, + want: "expected", + }, + { + name: "group env missing assign", + body: `group("g") { weight = 1.0 env("K") "v" test("t") { stdout = "" } }`, + want: "expected", + }, + { + name: "group env value not string", + body: `group("g") { weight = 1.0 env("K") = K test("t") { stdout = "" } }`, + want: "expected", + }, + { + name: "group wrapper missing assign", + body: `group("g") { weight = 1.0 wrapper "x" test("t") { stdout = "" } }`, + want: "expected", + }, + { + name: "group wrapper not string", + body: `group("g") { weight = 1.0 wrapper = X test("t") { stdout = "" } }`, + want: "expected", + }, + { + name: "group unclosed", + body: `group("g") { weight = 1.0 test("t") { stdout = "" }`, + want: "", + }, + { + name: "group pattern error", + body: `group("g") { weight = 1.0 pattern { bogus = 1 } }`, + want: "", + }, + + { + name: "test missing lparen", + body: `group("g") { weight = 1.0 test "t" { stdout = "" } }`, + want: "expected", + }, + { + name: "test name not string", + body: `group("g") { weight = 1.0 test(t) { stdout = "" } }`, + want: "expected", + }, + { + name: "test missing rparen", + body: `group("g") { weight = 1.0 test("t" { stdout = "" } }`, + want: "expected", + }, + { + name: "test missing lbrace", + body: `group("g") { weight = 1.0 test("t") stdout = "" }`, + want: "expected", + }, + { + name: "test non-ident in body", + body: `group("g") { weight = 1.0 test("t") { 42 } }`, + want: "unexpected token", + }, + { + name: "test stdin missing assign", + body: `group("g") { weight = 1.0 test("t") { stdin "x" stdout = "" } }`, + want: "expected", + }, + { + name: "test stdin not string", + body: `group("g") { weight = 1.0 test("t") { stdin = 1 stdout = "" } }`, + want: "expected", + }, + { + name: "test args missing assign", + body: `group("g") { weight = 1.0 test("t") { args "x" stdout = "" } }`, + want: "expected", + }, + { + name: "test exitCode missing assign", + body: `group("g") { weight = 1.0 test("t") { exitCode 0 stdout = "" } }`, + want: "expected", + }, + { + name: "test exitCode not int", + body: `group("g") { weight = 1.0 test("t") { exitCode = "x" stdout = "" } }`, + want: "", + }, + { + name: "test timeout missing assign", + body: `group("g") { weight = 1.0 test("t") { timeout 2s stdout = "" } }`, + want: "expected", + }, + { + name: "test timeout not duration", + body: `group("g") { weight = 1.0 test("t") { timeout = "2s" stdout = "" } }`, + want: "", + }, + { + name: "test memory_limit missing assign", + body: `group("g") { weight = 1.0 test("t") { memory_limit 64MB stdout = "" } }`, + want: "expected", + }, + { + name: "test memory_limit not size", + body: `group("g") { weight = 1.0 test("t") { memory_limit = "64MB" stdout = "" } }`, + want: "", + }, + { + name: "test wrapper missing assign", + body: `group("g") { weight = 1.0 test("t") { wrapper "x" stdout = "" } }`, + want: "expected", + }, + { + name: "test wrapper not string", + body: `group("g") { weight = 1.0 test("t") { wrapper = X stdout = "" } }`, + want: "expected", + }, + { + name: "test env missing lparen", + body: `group("g") { weight = 1.0 test("t") { env "K" = "v" stdout = "" } }`, + want: "expected", + }, + { + name: "test env key not string", + body: `group("g") { weight = 1.0 test("t") { env(K) = "v" stdout = "" } }`, + want: "expected", + }, + { + name: "test env missing rparen", + body: `group("g") { weight = 1.0 test("t") { env("K" = "v" stdout = "" } }`, + want: "expected", + }, + { + name: "test env missing assign", + body: `group("g") { weight = 1.0 test("t") { env("K") "v" stdout = "" } }`, + want: "expected", + }, + { + name: "test env value not string", + body: `group("g") { weight = 1.0 test("t") { env("K") = K stdout = "" } }`, + want: "expected", + }, + { + name: "test file missing lparen", + body: `group("g") { weight = 1.0 test("t") { file "x" = "y" stdout = "" } }`, + want: "expected", + }, + { + name: "test file key not string", + body: `group("g") { weight = 1.0 test("t") { file(x) = "y" stdout = "" } }`, + want: "expected", + }, + { + name: "test file missing rparen", + body: `group("g") { weight = 1.0 test("t") { file("x" = "y" stdout = "" } }`, + want: "expected", + }, + { + name: "test file missing assign", + body: `group("g") { weight = 1.0 test("t") { file("x") "y" stdout = "" } }`, + want: "expected", + }, + { + name: "test file value not string", + body: `group("g") { weight = 1.0 test("t") { file("x") = y stdout = "" } }`, + want: "expected", + }, + { + name: "test outFile missing lparen", + body: `group("g") { weight = 1.0 test("t") { outFile "x" = "y" stdout = "" } }`, + want: "expected", + }, + { + name: "test outFile key not string", + body: `group("g") { weight = 1.0 test("t") { outFile(x) = "y" stdout = "" } }`, + want: "expected", + }, + { + name: "test outFile missing rparen", + body: `group("g") { weight = 1.0 test("t") { outFile("x" = "y" stdout = "" } }`, + want: "expected", + }, + { + name: "test outFile missing assign", + body: `group("g") { weight = 1.0 test("t") { outFile("x") "y" stdout = "" } }`, + want: "expected", + }, + { + name: "test outFile value not string", + body: `group("g") { weight = 1.0 test("t") { outFile("x") = y stdout = "" } }`, + want: "expected", + }, + { + name: "test unclosed", + body: `group("g") { weight = 1.0 test("t") { stdout = ""`, + want: "", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + _, _, err := Parse(prefix + c.body) + if err == nil { + t.Fatalf("expected error, got nil") + } + if c.want != "" && !strings.Contains(err.Error(), c.want) { + t.Errorf("error = %q, want substring %q", err.Error(), c.want) + } + }) + } +} diff --git a/dsl/parser_features_test.go b/dsl/parser_features_test.go new file mode 100644 index 0000000..6e7d51f --- /dev/null +++ b/dsl/parser_features_test.go @@ -0,0 +1,237 @@ +package dsl + +import ( + "strings" + "testing" + "time" +) + +func parseOrFatal(t *testing.T, src string) *File { + t.Helper() + f, _, err := Parse(src) + if err != nil { + t.Fatalf("parse error: %v", err) + } + return f +} + +func parseExpectError(t *testing.T, src, wantSubstr string) { + t.Helper() + _, _, err := Parse(src) + if err == nil { + t.Fatalf("expected error containing %q, got nil", wantSubstr) + } + if !strings.Contains(err.Error(), wantSubstr) { + t.Errorf("error = %q, want substring %q", err.Error(), wantSubstr) + } +} + +func TestParseTestFileAndOutFile(t *testing.T) { + src := ` +build "go build ." + +group("g") { + weight = 1.0 + test("files") { + file("input.txt") = "1 2 3\n" + outFile("result.txt") = "6\n" + stdout = "" + } +} +` + f := parseOrFatal(t, src) + tst := f.Groups[0].Tests[0] + if tst.InFiles["input.txt"] != "1 2 3\n" { + t.Errorf("InFiles[input.txt] = %q", tst.InFiles["input.txt"]) + } + if tst.OutFiles["result.txt"] != "6\n" { + t.Errorf("OutFiles[result.txt] = %q", tst.OutFiles["result.txt"]) + } +} + +func TestParseTestEnvAndWrapper(t *testing.T) { + src := ` +build "go build ." + +group("g") { + weight = 1.0 + test("env") { + env("FOO") = "bar" + env("BAZ") = "qux" + wrapper = "valgrind" + stdout = "" + } +} +` + f := parseOrFatal(t, src) + tst := f.Groups[0].Tests[0] + if tst.Env["FOO"] != "bar" || tst.Env["BAZ"] != "qux" { + t.Errorf("Env = %v", tst.Env) + } + if tst.Wrapper != "valgrind" { + t.Errorf("Wrapper = %q", tst.Wrapper) + } +} + +func TestParseTestTimeoutAndMemoryOverride(t *testing.T) { + src := ` +build "go build ." +timeout 10s + +group("g") { + weight = 1.0 + memory_limit = 256MB + test("override") { + timeout = 2s + memory_limit = 64MB + stdout = "" + } +} +` + f := parseOrFatal(t, src) + tst := f.Groups[0].Tests[0] + if tst.Timeout != 2*time.Second { + t.Errorf("Timeout = %v, want 2s", tst.Timeout) + } + if tst.MemoryLimit != 64*1024*1024 { + t.Errorf("MemoryLimit = %d, want %d", tst.MemoryLimit, 64*1024*1024) + } +} + +func TestParseTestExitCodeNonZero(t *testing.T) { + src := ` +build "go build ." + +group("g") { + weight = 1.0 + test("fail") { + exitCode = 42 + stdout = "" + } +} +` + f := parseOrFatal(t, src) + tst := f.Groups[0].Tests[0] + if tst.ExitCode == nil || *tst.ExitCode != 42 { + t.Errorf("ExitCode = %v, want 42", tst.ExitCode) + } +} + +func TestParseTestUnknownKeyword(t *testing.T) { + src := ` +build "go build ." +group("g") { + weight = 1.0 + test("bad") { + bogus = "x" + } +} +` + parseExpectError(t, src, `"bogus"`) +} + +func TestParseGroupScoringPartial(t *testing.T) { + src := ` +build "go build ." +group("g") { + weight = 1.0 + scoring = partial + test("t") { stdout = "" } +} +` + f := parseOrFatal(t, src) + if f.Groups[0].Scoring != ScoringPartial { + t.Errorf("Scoring = %v, want partial", f.Groups[0].Scoring) + } +} + +func TestParseGroupScoringAllOrNone(t *testing.T) { + src := ` +build "go build ." +group("g") { + weight = 1.0 + scoring = all_or_none + test("t") { stdout = "" } +} +` + f := parseOrFatal(t, src) + if f.Groups[0].Scoring != ScoringAllOrNone { + t.Errorf("Scoring = %v, want all_or_none", f.Groups[0].Scoring) + } +} + +func TestParseGroupScoringUnknown(t *testing.T) { + src := ` +build "go build ." +group("g") { + weight = 1.0 + scoring = magic + test("t") { stdout = "" } +} +` + parseExpectError(t, src, "unknown scoring mode") +} + +func TestParseGroupEnvAndWrapper(t *testing.T) { + src := ` +build "go build ." +group("g") { + weight = 1.0 + env("LANG") = "C" + env("DEBUG") = "1" + wrapper = "strace" + test("t") { stdout = "" } +} +` + f := parseOrFatal(t, src) + g := f.Groups[0] + if g.Env["LANG"] != "C" || g.Env["DEBUG"] != "1" { + t.Errorf("group Env = %v", g.Env) + } + if g.Wrapper != "strace" { + t.Errorf("group Wrapper = %q", g.Wrapper) + } +} + +func TestParseGroupMemoryLimit(t *testing.T) { + src := ` +build "go build ." +group("g") { + weight = 1.0 + memory_limit = 128MB + test("t") { stdout = "" } +} +` + f := parseOrFatal(t, src) + if f.Groups[0].MemoryLimit != 128*1024*1024 { + t.Errorf("group MemoryLimit = %d", f.Groups[0].MemoryLimit) + } +} + +func TestParseGroupUnknownKeyword(t *testing.T) { + src := ` +build "go build ." +group("g") { + weight = 1.0 + foobar = 1 + test("t") { stdout = "" } +} +` + parseExpectError(t, src, `"foobar"`) +} + +func TestParseGroupMissingWeight(t *testing.T) { + src := ` +build "go build ." +group("g") { + test("t") { stdout = "" } +} +` + _, warns, err := Parse(src) + if err != nil && !strings.Contains(err.Error(), "weight") { + return + } + if err == nil && len(warns) == 0 { + t.Error("expected error or warning about missing weight") + } +} diff --git a/dsl/parser_misc_test.go b/dsl/parser_misc_test.go new file mode 100644 index 0000000..5c357b8 --- /dev/null +++ b/dsl/parser_misc_test.go @@ -0,0 +1,193 @@ +package dsl + +import ( + "strings" + "testing" +) + +func TestParsePatternDirsMode(t *testing.T) { + src := ` +build "make" +group("g") { + weight = 1.0 + pattern { + dirs = "tests/*" + input = "in.txt" + output = "out.txt" + args = "--case" "{name}" + } +} +` + f, _, err := Parse(src) + if err != nil { + t.Fatalf("parse: %v", err) + } + pat := f.Groups[0].Pattern + if pat == nil { + t.Fatal("no pattern") + } + if pat.DirsGlob != "tests/*" { + t.Errorf("DirsGlob = %q", pat.DirsGlob) + } + if pat.InputFile != "in.txt" || pat.OutputFile != "out.txt" { + t.Errorf("InputFile/OutputFile = %q/%q", pat.InputFile, pat.OutputFile) + } + if len(pat.Args) != 2 || pat.Args[0] != "--case" || pat.Args[1] != "{name}" { + t.Errorf("Args = %v", pat.Args) + } +} + +func TestParsePatternUnknownField(t *testing.T) { + src := ` +build "make" +group("g") { + weight = 1.0 + pattern { bogus = "x" } +} +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "unknown pattern field") { + t.Errorf("want unknown pattern field error, got %v", err) + } +} + +func TestParsePatternNonIdent(t *testing.T) { + src := ` +build "make" +group("g") { + weight = 1.0 + pattern { "x" = "y" } +} +` + _, _, err := Parse(src) + if err == nil { + t.Error("expected error on non-ident in pattern") + } +} + +func TestParseNormalizeCRLFAndTrimWS(t *testing.T) { + src := ` +build "make" +normalize_crlf = true +trim_trailing_ws = false +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + f, _, err := Parse(src) + if err != nil { + t.Fatalf("parse: %v", err) + } + if !f.NormalizeCRLF { + t.Error("NormalizeCRLF should be true") + } + if f.TrimTrailingWS { + t.Error("TrimTrailingWS should be false") + } +} + +func TestParseBinaryAndSources(t *testing.T) { + src := ` +build "make" +binary = "sol" +sources = "main.c" +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + f, _, err := Parse(src) + if err != nil { + t.Fatalf("parse: %v", err) + } + if f.Binary != "sol" { + t.Errorf("Binary = %q", f.Binary) + } + if f.Sources != "main.c" { + t.Errorf("Sources = %q", f.Sources) + } +} + +func TestParseMemoryLimitBareInt(t *testing.T) { + src := ` +build "make" +memory_limit = 1024 +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + f, _, err := Parse(src) + if err != nil { + t.Fatalf("parse: %v", err) + } + if f.MemoryLimit != 1024 { + t.Errorf("MemoryLimit = %d, want 1024", f.MemoryLimit) + } +} + +func TestParseBoolInvalidIdent(t *testing.T) { + _, _, err := Parse("build \"make\"\nnormalize_crlf = maybe\n") + if err == nil || !strings.Contains(err.Error(), "true/false") { + t.Errorf("want true/false error, got %v", err) + } +} + +func TestParseBoolNonIdent(t *testing.T) { + _, _, err := Parse("build \"make\"\nnormalize_crlf = \"true\"\n") + if err == nil || !strings.Contains(err.Error(), "true/false") { + t.Errorf("want true/false error, got %v", err) + } +} + +func TestParseTopLevelNonIdent(t *testing.T) { + _, _, err := Parse(`"stray"`) + if err == nil || !strings.Contains(err.Error(), "unexpected token") { + t.Errorf("want unexpected token error, got %v", err) + } +} + +func TestParseMatcherOrAssignNoMatcher(t *testing.T) { + src := ` +build "make" +group("g") { + weight = 1.0 + test("t") { stdout 42 } +} +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "expected matcher") { + t.Errorf("want matcher error, got %v", err) + } +} + +func TestParseStringListEmpty(t *testing.T) { + src := ` +build "make" +group("g") { + weight = 1.0 + test("t") { args = stdout = "" } +} +` + _, _, err := Parse(src) + if err == nil { + t.Error("expected error on empty string list") + } +} + +func TestParseFileMissing(t *testing.T) { + _, _, err := ParseFile("/nonexistent/judge-test-does-not-exist.jdg") + if err == nil { + t.Error("expected error on missing file") + } +} + +func TestValidateBuildsMixedLegacyAndStructured(t *testing.T) { + src := ` +build "legacy shell" +build_defaults { + language = "c" + standard = "c11" + sources = "*.c" + output = "sol" + warnings = strict +} +group("g") { weight = 1.0 test("t") { stdout = "" } } +` + _, _, err := Parse(src) + if err == nil || !strings.Contains(err.Error(), "cannot mix legacy") { + t.Errorf("want mix-legacy error, got %v", err) + } +} diff --git a/runner/result_test.go b/runner/result_test.go index c072613..09c9de1 100644 --- a/runner/result_test.go +++ b/runner/result_test.go @@ -1,6 +1,44 @@ package runner -import "testing" +import ( + "strings" + "testing" +) + +func TestStatusString(t *testing.T) { + cases := []struct { + s Status + want string + }{ + {StatusPass, "PASS"}, + {StatusFail, "FAIL"}, + {StatusTLE, "TLE"}, + {StatusMLE, "MLE"}, + {StatusBuildError, "BUILD_ERROR"}, + {StatusRuntimeError, "RE"}, + {Status(999), "UNKNOWN"}, + } + for _, c := range cases { + if got := c.s.String(); got != c.want { + t.Errorf("Status(%d).String() = %q, want %q", c.s, got, c.want) + } + } +} + +func TestAddFailureAppends(t *testing.T) { + r := &TestResult{} + r.addFailure("first %s", "msg") + r.addFailure("second %d", 2) + if len(r.Failures) != 2 { + t.Fatalf("Failures len = %d, want 2", len(r.Failures)) + } + if r.Failures[0] != "first msg" { + t.Errorf("Failures[0] = %q", r.Failures[0]) + } + if !strings.Contains(r.Failures[1], "second 2") { + t.Errorf("Failures[1] = %q", r.Failures[1]) + } +} func TestAggregateScoreEmpty(t *testing.T) { r := &SuiteResult{}