test: expand dsl/runner coverage and add go-test CI workflow
All checks were successful
go-test / go test (push) Successful in 56s

- 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%.
This commit is contained in:
2026-04-16 00:59:09 +03:00
parent 5e0effc6fe
commit 7f14f8c236
11 changed files with 1914 additions and 1 deletions

View File

@@ -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

75
dsl/ast_test.go Normal file
View File

@@ -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")
}
}

379
dsl/build_parser_test.go Normal file
View File

@@ -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)
}
}

74
dsl/build_string_test.go Normal file
View File

@@ -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)
}
}

175
dsl/lexer_test.go Normal file
View File

@@ -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")
}
}

187
dsl/matcher_test.go Normal file
View File

@@ -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)
}
}

220
dsl/merge_test.go Normal file
View File

@@ -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
}

299
dsl/parser_errors_test.go Normal file
View File

@@ -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)
}
})
}
}

237
dsl/parser_features_test.go Normal file
View File

@@ -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")
}
}

193
dsl/parser_misc_test.go Normal file
View File

@@ -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)
}
}

View File

@@ -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{}