test: expand dsl/runner coverage and add go-test CI workflow
All checks were successful
go-test / go test (push) Successful in 56s
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:
36
.gitea/workflows/go-test.yml
Normal file
36
.gitea/workflows/go-test.yml
Normal 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
75
dsl/ast_test.go
Normal 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
379
dsl/build_parser_test.go
Normal 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
74
dsl/build_string_test.go
Normal 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
175
dsl/lexer_test.go
Normal 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
187
dsl/matcher_test.go
Normal 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
220
dsl/merge_test.go
Normal 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
299
dsl/parser_errors_test.go
Normal 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
237
dsl/parser_features_test.go
Normal 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
193
dsl/parser_misc_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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{}
|
||||||
|
|||||||
Reference in New Issue
Block a user