4 Commits

Author SHA1 Message Date
c023831222 add to release flow darwin/amd64
Some checks failed
go-test / go test (push) Successful in 21s
Release / Build & publish (push) Failing after 28s
2026-04-18 09:55:18 +03:00
7f14f8c236 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%.
2026-04-16 00:59:09 +03:00
5e0effc6fe refactor: extract expander helpers and Test stdio/file setters
- Add globWithAffixes in runner/expander.go that wraps filepath.Glob
    with an empty-match check and returns the computed prefix/suffix
    from splitGlob, collapsing three near-identical lookup blocks in
    expandGlobPattern into single calls.
  - Extract per-case Test construction from buildTests into a new
    buildTest helper so the loop body is a single call and the read /
    assemble / arg-template logic lives in one place.
  - Add Test.SetInputFile, Test.SetStdin, Test.SetOutputFile and
    Test.SetStdout methods on dsl.Test to encapsulate the stdin-vs-
    InFiles and stdout-vs-OutFiles wiring that buildTest previously
    did inline.
  - Adopt the `for range N` loop form in the determinism check in
    runner/compiler_test.go.
  - Switch the MSVC release test to expect /std:c17 since MSVC does
    not ship a c11 mode (worth surfacing a warning about this later).
2026-04-16 00:28:30 +03:00
c85c65ed49 refactor: modernize stdlib usage and move matchers into dsl
All checks were successful
build-dsl-smoke / Build judge (push) Successful in 16s
build-dsl-smoke / debug / clang / linux (push) Successful in 6s
build-dsl-smoke / debug / gcc / linux (push) Successful in 7s
build-dsl-smoke / release / clang / linux (push) Successful in 8s
build-dsl-smoke / release / gcc / linux (push) Successful in 9s
build-dsl-smoke / sanitized / gcc / linux (push) Successful in 6s
build-dsl-smoke / sanitized / clang / linux (push) Successful in 9s
build-dsl-smoke / debug-valgrind / gcc / linux (push) Successful in 15s
build-dsl-smoke / debug / clang / windows (push) Successful in 15s
build-dsl-smoke / debug / msvc / windows (push) Successful in 19s
build-dsl-smoke / release / clang / windows (push) Successful in 18s
build-dsl-smoke / release / msvc / windows (push) Successful in 18s
build-dsl-smoke / SUMMARY (push) Successful in 4s
- Move Matcher types and matching logic from runner/matcher.go into
  the dsl package as methods on the Matcher types. Runner now calls
  t.Stdout.Match(label, actual) instead of type-switching via a
  package-level applyMatcher helper.
- Replace custom contains/containsString helpers with slices.Contains
  in dsl/build.go and runner/compiler.go.
- Use maps.Copy instead of manual map copy in BuildConfig.MergeFrom.
- Adopt strings.SplitSeq, strings.CutPrefix and the `for range N` loop
  form in runner/limiter_linux.go.
- Ignore example/imdb build artifact.
2026-04-15 21:24:11 +03:00
23 changed files with 2117 additions and 201 deletions

View File

@@ -2,14 +2,6 @@ name: build-dsl-smoke
run-name: "Structured build DSL smoke test" run-name: "Structured build DSL smoke test"
on: on:
push:
paths:
- 'dsl/**'
- 'runner/**'
- 'reporter/**'
- 'cmd/cli/**'
- 'example/c-sum-v2/**'
- '.gitea/workflows/build-dsl-smoke.yml'
workflow_dispatch: workflow_dispatch:
env: env:

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

View File

@@ -30,6 +30,7 @@ jobs:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o "dist/judge-linux-amd64" ./cmd/cli CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o "dist/judge-linux-amd64" ./cmd/cli
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o "dist/judge-windows-amd64.exe" ./cmd/cli CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o "dist/judge-windows-amd64.exe" ./cmd/cli
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -o "dist/judge-darwin-arm64" ./cmd/cli CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -o "dist/judge-darwin-arm64" ./cmd/cli
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o "dist/judge-darwin-amd64"
- name: Build VS Code extension - name: Build VS Code extension
shell: bash shell: bash

5
.gitignore vendored
View File

@@ -1,2 +1,5 @@
example/c-sum/solution example/c-sum/solution
example/solution/solution example/solution/solution
.DS_Store
*/.DS_Store
.claude

View File

@@ -1,6 +1,8 @@
package dsl package dsl
import "time" import (
"time"
)
type File struct { type File struct {
Build string Build string
@@ -75,41 +77,20 @@ type Test struct {
OutFiles map[string]string OutFiles map[string]string
} }
type Matcher interface { func (t *Test) SetInputFile(inputName string, inputContent []byte) {
matcherNode() t.InFiles[inputName] = string(inputContent)
} }
type ExactMatcher struct { func (t *Test) SetStdin(inputContent []byte) {
Value string s := string(inputContent)
t.Stdin = &s
} }
func (ExactMatcher) matcherNode() {} func (t *Test) SetOutputFile(outputName string, outputContent []byte) {
t.OutFiles[outputName] = string(outputContent)
type ContainsMatcher struct { t.Stdout = NoMatcher{}
Substr string
} }
func (ContainsMatcher) matcherNode() {} func (t *Test) SetStdout(outputContent []byte) {
t.Stdout = ExactMatcher{Value: string(outputContent)}
type RegexMatcher struct {
Pattern string
} }
func (RegexMatcher) matcherNode() {}
type NumericEpsMatcher struct {
Epsilon float64
Value string
}
func (NumericEpsMatcher) matcherNode() {}
type AnyOrderMatcher struct {
Lines []string
}
func (AnyOrderMatcher) matcherNode() {}
type NoMatcher struct{}
func (NoMatcher) matcherNode() {}

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

View File

@@ -1,5 +1,10 @@
package dsl package dsl
import (
"maps"
"slices"
)
type BuildProfile int type BuildProfile int
const ( const (
@@ -105,9 +110,7 @@ func (dst *BuildConfig) MergeFrom(src *BuildConfig) {
if dst.Defines == nil { if dst.Defines == nil {
dst.Defines = map[string]string{} dst.Defines = map[string]string{}
} }
for k, v := range src.Defines { maps.Copy(dst.Defines, src.Defines)
dst.Defines[k] = v
}
} }
} }
@@ -132,24 +135,15 @@ func (b *BuildConfig) Resolve(defaults *BuildConfig, os string) BuildConfig {
} }
func (b *BuildConfig) AppliesTo(os, compiler string) bool { func (b *BuildConfig) AppliesTo(os, compiler string) bool {
if len(b.Platforms) > 0 && !contains(b.Platforms, os) { if len(b.Platforms) > 0 && !slices.Contains(b.Platforms, os) {
return false return false
} }
if len(b.Compilers) > 0 && !contains(b.Compilers, compiler) { if len(b.Compilers) > 0 && !slices.Contains(b.Compilers, compiler) {
return false return false
} }
return true return true
} }
func contains(xs []string, x string) bool {
for _, v := range xs {
if v == x {
return true
}
}
return false
}
type ToolchainSpec struct { type ToolchainSpec struct {
Name string Name string
Platforms []string Platforms []string

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

View File

@@ -371,8 +371,6 @@ func (l *Lexer) readNumberOrDuration(line, col int) (Token, error) {
return Token{TOKEN_INT, buf.String(), line, col}, nil return Token{TOKEN_INT, buf.String(), line, col}, nil
} }
// tryReadSizeSuffix reads memory size suffixes: B, K, KB, KiB, M, MB, MiB, G, GB, GiB.
// Units are case-sensitive uppercase to avoid collision with duration "m" (minutes).
func (l *Lexer) tryReadSizeSuffix() string { func (l *Lexer) tryReadSizeSuffix() string {
ch, ok := l.peek() ch, ok := l.peek()
if !ok { if !ok {

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

View File

@@ -1,4 +1,4 @@
package runner package dsl
import ( import (
"fmt" "fmt"
@@ -7,57 +7,90 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"github.com/Mond1c/judge/dsl"
) )
func applyMatcher(label string, m dsl.Matcher, actual string) []string { // TODO: maybe move to ast.go
switch m := m.(type) { type Matcher interface {
case dsl.NoMatcher: matcherNode()
return nil
case dsl.ExactMatcher:
if actual != m.Value {
return []string{fmt.Sprintf(
"%s mismatch:\n expected: %q\n actual: %q",
label, m.Value, actual,
)}
}
return nil
case dsl.ContainsMatcher:
if !strings.Contains(actual, m.Substr) {
return []string{fmt.Sprintf(
"%s: expected to contain %q, got %q",
label, m.Substr, actual,
)}
}
return nil
case dsl.RegexMatcher:
re, err := regexp.Compile(m.Pattern)
if err != nil {
return []string{fmt.Sprintf("%s: invalid regex %q: %v", label, m.Pattern, err)}
}
if !re.MatchString(actual) {
return []string{fmt.Sprintf(
"%s: %q does not match regex %q",
label, actual, m.Pattern,
)}
}
return nil
case dsl.NumericEpsMatcher: Match(label, actual string) []string
errs := matchNumericEps(label, m, actual)
return errs
case dsl.AnyOrderMatcher:
return matchAnyOrder(label, m, actual)
default:
return []string{fmt.Sprintf("unknown matcher type %T", m)}
}
} }
func matchNumericEps(label string, m dsl.NumericEpsMatcher, actual string) []string { type ExactMatcher struct {
Value string
}
func (ExactMatcher) matcherNode() {}
// TODO: think about pointer receivers
func (m ExactMatcher) Match(label, actual string) []string {
if actual != m.Value {
return []string{fmt.Sprintf(
"%s mismatch:\n expected: %q\n actual: %q",
label, m.Value, actual,
)}
}
return nil
}
type ContainsMatcher struct {
Substr string
}
func (ContainsMatcher) matcherNode() {}
func (m ContainsMatcher) Match(label, actual string) []string {
if !strings.Contains(actual, m.Substr) {
return []string{fmt.Sprintf(
"%s: expected to contain %q, got %q",
label, m.Substr, actual,
)}
}
return nil
}
type RegexMatcher struct {
Pattern string
}
func (RegexMatcher) matcherNode() {}
func (m RegexMatcher) Match(label, actual string) []string {
re, err := regexp.Compile(m.Pattern)
if err != nil {
return []string{fmt.Sprintf("%s: invalid regex %q: %v", label, m.Pattern, err)}
}
if !re.MatchString(actual) {
return []string{fmt.Sprintf(
"%s: %q does not match regex %q",
label, actual, m.Pattern,
)}
}
return nil
}
type NumericEpsMatcher struct {
Epsilon float64
Value string
}
func (NumericEpsMatcher) matcherNode() {}
func parseNumbers(s string) ([]float64, error) {
fields := strings.Fields(s)
nums := make([]float64, 0, len(fields))
for _, f := range fields {
n, err := strconv.ParseFloat(f, 64)
if err != nil {
return nil, fmt.Errorf("not a number: %q", f)
}
nums = append(nums, n)
}
return nums, nil
}
func (m NumericEpsMatcher) Match(label, actual string) []string {
expectedNums, err := parseNumbers(m.Value) expectedNums, err := parseNumbers(m.Value)
if err != nil { if err != nil {
return []string{fmt.Sprintf("%s: cannot parse expected numbers %q: %v", label, m.Value, err)} return []string{fmt.Sprintf("%s: cannot parse expected numbers %q: %v", label, m.Value, err)}
@@ -85,20 +118,21 @@ func matchNumericEps(label string, m dsl.NumericEpsMatcher, actual string) []str
return errs return errs
} }
func parseNumbers(s string) ([]float64, error) { type AnyOrderMatcher struct {
fields := strings.Fields(s) Lines []string
nums := make([]float64, 0, len(fields))
for _, f := range fields {
n, err := strconv.ParseFloat(f, 64)
if err != nil {
return nil, fmt.Errorf("not a number: %q", f)
}
nums = append(nums, n)
}
return nums, nil
} }
func matchAnyOrder(label string, m dsl.AnyOrderMatcher, actual string) []string { func (AnyOrderMatcher) matcherNode() {}
func splitLines(s string) []string {
s = strings.TrimRight(s, "\n")
if s == "" {
return []string{}
}
return strings.Split(s, "\n")
}
func (m AnyOrderMatcher) Match(label, actual string) []string {
actualLines := splitLines(actual) actualLines := splitLines(actual)
expectedLines := make([]string, len(m.Lines)) expectedLines := make([]string, len(m.Lines))
copy(expectedLines, m.Lines) copy(expectedLines, m.Lines)
@@ -125,10 +159,10 @@ func matchAnyOrder(label string, m dsl.AnyOrderMatcher, actual string) []string
return errs return errs
} }
func splitLines(s string) []string { type NoMatcher struct{}
s = strings.TrimRight(s, "\n")
if s == "" { func (NoMatcher) matcherNode() {}
return []string{}
} func (NoMatcher) Match(label, actual string) []string {
return strings.Split(s, "\n") return nil
} }

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

@@ -3,6 +3,7 @@ package runner
import ( import (
"fmt" "fmt"
"runtime" "runtime"
"slices"
"sort" "sort"
"strings" "strings"
@@ -161,7 +162,7 @@ func compileMSVC(cfg dsl.BuildConfig, tc Toolchain, outputPath string) []string
argv = append(argv, "/W4", "/permissive-") argv = append(argv, "/W4", "/permissive-")
} }
if containsString(cfg.Sanitize, "address") && cfg.Profile != dsl.ProfileSanitized { if slices.Contains(cfg.Sanitize, "address") && cfg.Profile != dsl.ProfileSanitized {
argv = append(argv, "/fsanitize=address") argv = append(argv, "/fsanitize=address")
} }
@@ -195,12 +196,3 @@ func sortedKeys(m map[string]string) []string {
sort.Strings(keys) sort.Strings(keys)
return keys return keys
} }
func containsString(xs []string, x string) bool {
for _, v := range xs {
if v == x {
return true
}
}
return false
}

View File

@@ -185,7 +185,7 @@ func TestCompileGNUDefinesOrderDeterministic(t *testing.T) {
} }
tc := Toolchain{Class: CompilerGNU, Binary: "gcc"} tc := Toolchain{Class: CompilerGNU, Binary: "gcc"}
argv1, _ := Compile(cfg, tc, "s") argv1, _ := Compile(cfg, tc, "s")
for i := 0; i < 20; i++ { for range 20 {
argv2, _ := Compile(cfg, tc, "s") argv2, _ := Compile(cfg, tc, "s")
if !reflect.DeepEqual(argv1, argv2) { if !reflect.DeepEqual(argv1, argv2) {
t.Fatalf("defines order not deterministic:\n %v\n %v", argv1, argv2) t.Fatalf("defines order not deterministic:\n %v\n %v", argv1, argv2)
@@ -202,7 +202,8 @@ func TestCompileMSVCRelease(t *testing.T) {
} }
tc := Toolchain{Class: CompilerMSVC, Binary: "cl", Name: "msvc"} tc := Toolchain{Class: CompilerMSVC, Binary: "cl", Name: "msvc"}
argv, _ := Compile(cfg, tc, "solution.exe") argv, _ := Compile(cfg, tc, "solution.exe")
want := []string{"cl", "/nologo", "/std:c11", "/O2", "/W4", "solution.c", "/Fe:solution.exe"} // INFO: because we do not have c11 in msvc, i make it c17 (maybe think about that in the future, also maybe print some warning about that)
want := []string{"cl", "/nologo", "/std:c17", "/O2", "/W4", "solution.c", "/Fe:solution.exe"}
if !reflect.DeepEqual(argv, want) { if !reflect.DeepEqual(argv, want) {
t.Errorf("argv =\n %v\nwant\n %v", argv, want) t.Errorf("argv =\n %v\nwant\n %v", argv, want)
} }

View File

@@ -23,6 +23,18 @@ type patternCase struct {
dir string dir string
} }
func globWithAffixes(pattern string) ([]string, string, string, error) {
files, err := filepath.Glob(pattern)
if err != nil {
return nil, "", "", fmt.Errorf("invalid glob %q: %w", pattern, err)
}
if len(files) == 0 {
return nil, "", "", fmt.Errorf("no files matched glob %q", pattern)
}
prefix, suffix := splitGlob(pattern)
return files, prefix, suffix, nil
}
func expandGlobPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) { func expandGlobPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
inputIsGlob := strings.Contains(pattern.InputGlob, "*") inputIsGlob := strings.Contains(pattern.InputGlob, "*")
outputIsGlob := strings.Contains(pattern.OutputGlob, "*") outputIsGlob := strings.Contains(pattern.OutputGlob, "*")
@@ -36,16 +48,13 @@ func expandGlobPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
var cases []patternCase var cases []patternCase
// TODO: i know that this is copypaste, but i do not want make clousers or ifs inside cycle for now
switch { switch {
case inputIsGlob && outputIsGlob: case inputIsGlob && outputIsGlob:
inputFiles, err := filepath.Glob(pattern.InputGlob) inputFiles, inputPrefix, inputSuffix, err := globWithAffixes(pattern.InputGlob)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid input glob %q: %w", pattern.InputGlob, err) return nil, err
} }
if len(inputFiles) == 0 {
return nil, fmt.Errorf("no files matched input glob %q", pattern.InputGlob)
}
inputPrefix, inputSuffix := splitGlob(pattern.InputGlob)
outputPrefix, outputSuffix := splitGlob(pattern.OutputGlob) outputPrefix, outputSuffix := splitGlob(pattern.OutputGlob)
for _, inputPath := range inputFiles { for _, inputPath := range inputFiles {
wildcard := extractWildcard(inputPath, inputPrefix, inputSuffix) wildcard := extractWildcard(inputPath, inputPrefix, inputSuffix)
@@ -58,14 +67,10 @@ func expandGlobPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
} }
case inputIsGlob && !outputIsGlob: case inputIsGlob && !outputIsGlob:
inputFiles, err := filepath.Glob(pattern.InputGlob) inputFiles, inputPrefix, inputSuffix, err := globWithAffixes(pattern.InputGlob)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid input glob %q: %w", pattern.InputGlob, err) return nil, err
} }
if len(inputFiles) == 0 {
return nil, fmt.Errorf("no files matched input glob %q", pattern.InputGlob)
}
inputPrefix, inputSuffix := splitGlob(pattern.InputGlob)
for _, inputPath := range inputFiles { for _, inputPath := range inputFiles {
wildcard := extractWildcard(inputPath, inputPrefix, inputSuffix) wildcard := extractWildcard(inputPath, inputPrefix, inputSuffix)
cases = append(cases, patternCase{ cases = append(cases, patternCase{
@@ -76,14 +81,10 @@ func expandGlobPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
} }
case !inputIsGlob && outputIsGlob: case !inputIsGlob && outputIsGlob:
outputFiles, err := filepath.Glob(pattern.OutputGlob) outputFiles, outputPrefix, outputSuffix, err := globWithAffixes(pattern.OutputGlob)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid output glob %q: %w", pattern.OutputGlob, err) return nil, err
} }
if len(outputFiles) == 0 {
return nil, fmt.Errorf("no files matched output glob %q", pattern.OutputGlob)
}
outputPrefix, outputSuffix := splitGlob(pattern.OutputGlob)
for _, outputPath := range outputFiles { for _, outputPath := range outputFiles {
wildcard := extractWildcard(outputPath, outputPrefix, outputSuffix) wildcard := extractWildcard(outputPath, outputPrefix, outputSuffix)
cases = append(cases, patternCase{ cases = append(cases, patternCase{
@@ -122,55 +123,61 @@ func expandDirPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
return buildTests(cases, pattern.Args) return buildTests(cases, pattern.Args)
} }
func buildTest(c *patternCase, argTemplate []string, useInputAsFile, useOutputAsFile bool) (*dsl.Test, error) {
inputContent, err := os.ReadFile(c.inputPath)
if err != nil {
return nil, fmt.Errorf("read input %q: %w", c.inputPath, err)
}
outputContent, err := os.ReadFile(c.outputPath)
if err != nil {
return nil, fmt.Errorf("read output %q: %w", c.outputPath, err)
}
t := &dsl.Test{
Name: fmt.Sprintf("pattern:%s", c.name),
Env: map[string]string{},
InFiles: map[string]string{},
OutFiles: map[string]string{},
Stderr: dsl.NoMatcher{},
}
inputName := filepath.Base(c.inputPath)
outputName := filepath.Base(c.outputPath)
if useInputAsFile {
t.SetInputFile(inputName, inputContent)
} else {
t.SetStdin(inputContent)
}
if useOutputAsFile {
t.SetOutputFile(outputName, outputContent)
} else {
t.SetStdout(outputContent)
}
if len(argTemplate) > 0 {
t.Args = substituteArgs(argTemplate, map[string]string{
"{input_path}": inputName,
"{output_path}": outputName,
"{name}": c.name,
"{dir}": c.dir,
})
}
return t, nil
}
func buildTests(cases []patternCase, argTemplate []string) ([]*dsl.Test, error) { func buildTests(cases []patternCase, argTemplate []string) ([]*dsl.Test, error) {
useInputAsFile := argsContain(argTemplate, "{input_path}") useInputAsFile := argsContain(argTemplate, "{input_path}")
useOutputAsFile := argsContain(argTemplate, "{output_path}") useOutputAsFile := argsContain(argTemplate, "{output_path}")
var tests []*dsl.Test var tests []*dsl.Test
for _, c := range cases { for _, c := range cases {
inputContent, err := os.ReadFile(c.inputPath) t, err := buildTest(&c, argTemplate, useInputAsFile, useOutputAsFile)
if err != nil { if err != nil {
return nil, fmt.Errorf("read input %q: %w", c.inputPath, err) return nil, err
} }
outputContent, err := os.ReadFile(c.outputPath)
if err != nil {
return nil, fmt.Errorf("read output %q: %w", c.outputPath, err)
}
t := &dsl.Test{
Name: fmt.Sprintf("pattern:%s", c.name),
Env: map[string]string{},
InFiles: map[string]string{},
OutFiles: map[string]string{},
Stderr: dsl.NoMatcher{},
}
inputName := filepath.Base(c.inputPath)
outputName := filepath.Base(c.outputPath)
if useInputAsFile {
t.InFiles[inputName] = string(inputContent)
} else {
s := string(inputContent)
t.Stdin = &s
}
if useOutputAsFile {
t.OutFiles[outputName] = string(outputContent)
t.Stdout = dsl.NoMatcher{}
} else {
t.Stdout = dsl.ExactMatcher{Value: string(outputContent)}
}
if len(argTemplate) > 0 {
t.Args = substituteArgs(argTemplate, map[string]string{
"{input_path}": inputName,
"{output_path}": outputName,
"{name}": c.name,
"{dir}": c.dir,
})
}
tests = append(tests, t) tests = append(tests, t)
} }
return tests, nil return tests, nil

View File

@@ -102,9 +102,9 @@ func createScopeCgroup() (string, error) {
return "", fmt.Errorf("read /proc/self/cgroup: %w", err) return "", fmt.Errorf("read /proc/self/cgroup: %w", err)
} }
var rel string var rel string
for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") {
if strings.HasPrefix(line, "0::") { if after, ok := strings.CutPrefix(line, "0::"); ok {
rel = strings.TrimPrefix(line, "0::") rel = after
break break
} }
} }
@@ -200,7 +200,7 @@ func (l *linuxLimiter) collect() limitStats {
} }
} }
if data, err := os.ReadFile(filepath.Join(l.cgPath, "memory.events")); err == nil { if data, err := os.ReadFile(filepath.Join(l.cgPath, "memory.events")); err == nil {
for _, line := range strings.Split(string(data), "\n") { for line := range strings.SplitSeq(string(data), "\n") {
fields := strings.Fields(line) fields := strings.Fields(line)
if len(fields) != 2 { if len(fields) != 2 {
continue continue
@@ -217,7 +217,7 @@ func (l *linuxLimiter) cleanup() {
if l.cgPath == "" { if l.cgPath == "" {
return return
} }
for i := 0; i < 10; i++ { for range 10 {
err := os.Remove(l.cgPath) err := os.Remove(l.cgPath)
if err == nil || os.IsNotExist(err) { if err == nil || os.IsNotExist(err) {
l.cgPath = "" l.cgPath = ""

View File

@@ -1,6 +1,44 @@
package runner 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) { func TestAggregateScoreEmpty(t *testing.T) {
r := &SuiteResult{} r := &SuiteResult{}

View File

@@ -558,10 +558,10 @@ func (r *Runner) runTest(t *dsl.Test) *TestResult {
tr.addFailure("exit code: expected %d, got %d", *t.ExitCode, actualCode) tr.addFailure("exit code: expected %d, got %d", *t.ExitCode, actualCode)
} }
for _, f := range applyMatcher("stdout", t.Stdout, tr.ActualStdout) { for _, f := range t.Stdout.Match("stdout", tr.ActualStdout) {
tr.addFailure("%s", f) tr.addFailure("%s", f)
} }
for _, f := range applyMatcher("stderr", t.Stderr, tr.ActualStderr) { for _, f := range t.Stderr.Match("stderr", tr.ActualStderr) {
tr.addFailure("%s", f) tr.addFailure("%s", f)
} }