Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c023831222 | |||
| 7f14f8c236 | |||
| 5e0effc6fe | |||
| c85c65ed49 |
@@ -2,14 +2,6 @@ name: build-dsl-smoke
|
||||
run-name: "Structured build DSL smoke test"
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'dsl/**'
|
||||
- 'runner/**'
|
||||
- 'reporter/**'
|
||||
- 'cmd/cli/**'
|
||||
- 'example/c-sum-v2/**'
|
||||
- '.gitea/workflows/build-dsl-smoke.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
|
||||
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
|
||||
@@ -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=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=amd64 go build -ldflags "-s -w" -o "dist/judge-darwin-amd64"
|
||||
|
||||
- name: Build VS Code extension
|
||||
shell: bash
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,5 @@
|
||||
example/c-sum/solution
|
||||
example/solution/solution
|
||||
.DS_Store
|
||||
*/.DS_Store
|
||||
.claude
|
||||
|
||||
45
dsl/ast.go
45
dsl/ast.go
@@ -1,6 +1,8 @@
|
||||
package dsl
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
Build string
|
||||
@@ -75,41 +77,20 @@ type Test struct {
|
||||
OutFiles map[string]string
|
||||
}
|
||||
|
||||
type Matcher interface {
|
||||
matcherNode()
|
||||
func (t *Test) SetInputFile(inputName string, inputContent []byte) {
|
||||
t.InFiles[inputName] = string(inputContent)
|
||||
}
|
||||
|
||||
type ExactMatcher struct {
|
||||
Value string
|
||||
func (t *Test) SetStdin(inputContent []byte) {
|
||||
s := string(inputContent)
|
||||
t.Stdin = &s
|
||||
}
|
||||
|
||||
func (ExactMatcher) matcherNode() {}
|
||||
|
||||
type ContainsMatcher struct {
|
||||
Substr string
|
||||
func (t *Test) SetOutputFile(outputName string, outputContent []byte) {
|
||||
t.OutFiles[outputName] = string(outputContent)
|
||||
t.Stdout = NoMatcher{}
|
||||
}
|
||||
|
||||
func (ContainsMatcher) matcherNode() {}
|
||||
|
||||
type RegexMatcher struct {
|
||||
Pattern string
|
||||
func (t *Test) SetStdout(outputContent []byte) {
|
||||
t.Stdout = ExactMatcher{Value: string(outputContent)}
|
||||
}
|
||||
|
||||
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
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")
|
||||
}
|
||||
}
|
||||
22
dsl/build.go
22
dsl/build.go
@@ -1,5 +1,10 @@
|
||||
package dsl
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
)
|
||||
|
||||
type BuildProfile int
|
||||
|
||||
const (
|
||||
@@ -105,9 +110,7 @@ func (dst *BuildConfig) MergeFrom(src *BuildConfig) {
|
||||
if dst.Defines == nil {
|
||||
dst.Defines = map[string]string{}
|
||||
}
|
||||
for k, v := range src.Defines {
|
||||
dst.Defines[k] = v
|
||||
}
|
||||
maps.Copy(dst.Defines, src.Defines)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,24 +135,15 @@ func (b *BuildConfig) Resolve(defaults *BuildConfig, os string) BuildConfig {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if len(b.Compilers) > 0 && !contains(b.Compilers, compiler) {
|
||||
if len(b.Compilers) > 0 && !slices.Contains(b.Compilers, compiler) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func contains(xs []string, x string) bool {
|
||||
for _, v := range xs {
|
||||
if v == x {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ToolchainSpec struct {
|
||||
Name string
|
||||
Platforms []string
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -371,8 +371,6 @@ func (l *Lexer) readNumberOrDuration(line, col int) (Token, error) {
|
||||
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 {
|
||||
ch, ok := l.peek()
|
||||
if !ok {
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package runner
|
||||
package dsl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -7,15 +7,23 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Mond1c/judge/dsl"
|
||||
)
|
||||
|
||||
func applyMatcher(label string, m dsl.Matcher, actual string) []string {
|
||||
switch m := m.(type) {
|
||||
case dsl.NoMatcher:
|
||||
return nil
|
||||
case dsl.ExactMatcher:
|
||||
// TODO: maybe move to ast.go
|
||||
type Matcher interface {
|
||||
matcherNode()
|
||||
|
||||
Match(label, 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",
|
||||
@@ -23,7 +31,15 @@ func applyMatcher(label string, m dsl.Matcher, actual string) []string {
|
||||
)}
|
||||
}
|
||||
return nil
|
||||
case dsl.ContainsMatcher:
|
||||
}
|
||||
|
||||
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",
|
||||
@@ -31,11 +47,20 @@ func applyMatcher(label string, m dsl.Matcher, actual string) []string {
|
||||
)}
|
||||
}
|
||||
return nil
|
||||
case dsl.RegexMatcher:
|
||||
}
|
||||
|
||||
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",
|
||||
@@ -43,21 +68,29 @@ func applyMatcher(label string, m dsl.Matcher, actual string) []string {
|
||||
)}
|
||||
}
|
||||
return nil
|
||||
|
||||
case dsl.NumericEpsMatcher:
|
||||
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 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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
type AnyOrderMatcher struct {
|
||||
Lines []string
|
||||
}
|
||||
|
||||
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)
|
||||
expectedLines := make([]string, len(m.Lines))
|
||||
copy(expectedLines, m.Lines)
|
||||
@@ -125,10 +159,10 @@ func matchAnyOrder(label string, m dsl.AnyOrderMatcher, actual string) []string
|
||||
return errs
|
||||
}
|
||||
|
||||
func splitLines(s string) []string {
|
||||
s = strings.TrimRight(s, "\n")
|
||||
if s == "" {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Split(s, "\n")
|
||||
type NoMatcher struct{}
|
||||
|
||||
func (NoMatcher) matcherNode() {}
|
||||
|
||||
func (NoMatcher) Match(label, actual string) []string {
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package runner
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -161,7 +162,7 @@ func compileMSVC(cfg dsl.BuildConfig, tc Toolchain, outputPath string) []string
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -195,12 +196,3 @@ func sortedKeys(m map[string]string) []string {
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func containsString(xs []string, x string) bool {
|
||||
for _, v := range xs {
|
||||
if v == x {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ func TestCompileGNUDefinesOrderDeterministic(t *testing.T) {
|
||||
}
|
||||
tc := Toolchain{Class: CompilerGNU, Binary: "gcc"}
|
||||
argv1, _ := Compile(cfg, tc, "s")
|
||||
for i := 0; i < 20; i++ {
|
||||
for range 20 {
|
||||
argv2, _ := Compile(cfg, tc, "s")
|
||||
if !reflect.DeepEqual(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"}
|
||||
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) {
|
||||
t.Errorf("argv =\n %v\nwant\n %v", argv, want)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,18 @@ type patternCase struct {
|
||||
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) {
|
||||
inputIsGlob := strings.Contains(pattern.InputGlob, "*")
|
||||
outputIsGlob := strings.Contains(pattern.OutputGlob, "*")
|
||||
@@ -36,16 +48,13 @@ func expandGlobPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
|
||||
|
||||
var cases []patternCase
|
||||
|
||||
// TODO: i know that this is copypaste, but i do not want make clousers or ifs inside cycle for now
|
||||
switch {
|
||||
case inputIsGlob && outputIsGlob:
|
||||
inputFiles, err := filepath.Glob(pattern.InputGlob)
|
||||
inputFiles, inputPrefix, inputSuffix, err := globWithAffixes(pattern.InputGlob)
|
||||
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)
|
||||
for _, inputPath := range inputFiles {
|
||||
wildcard := extractWildcard(inputPath, inputPrefix, inputSuffix)
|
||||
@@ -58,14 +67,10 @@ func expandGlobPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
|
||||
}
|
||||
|
||||
case inputIsGlob && !outputIsGlob:
|
||||
inputFiles, err := filepath.Glob(pattern.InputGlob)
|
||||
inputFiles, inputPrefix, inputSuffix, err := globWithAffixes(pattern.InputGlob)
|
||||
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 {
|
||||
wildcard := extractWildcard(inputPath, inputPrefix, inputSuffix)
|
||||
cases = append(cases, patternCase{
|
||||
@@ -76,14 +81,10 @@ func expandGlobPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
|
||||
}
|
||||
|
||||
case !inputIsGlob && outputIsGlob:
|
||||
outputFiles, err := filepath.Glob(pattern.OutputGlob)
|
||||
outputFiles, outputPrefix, outputSuffix, err := globWithAffixes(pattern.OutputGlob)
|
||||
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 {
|
||||
wildcard := extractWildcard(outputPath, outputPrefix, outputSuffix)
|
||||
cases = append(cases, patternCase{
|
||||
@@ -122,12 +123,7 @@ func expandDirPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
|
||||
return buildTests(cases, pattern.Args)
|
||||
}
|
||||
|
||||
func buildTests(cases []patternCase, argTemplate []string) ([]*dsl.Test, error) {
|
||||
useInputAsFile := argsContain(argTemplate, "{input_path}")
|
||||
useOutputAsFile := argsContain(argTemplate, "{output_path}")
|
||||
|
||||
var tests []*dsl.Test
|
||||
for _, c := range cases {
|
||||
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)
|
||||
@@ -149,17 +145,15 @@ func buildTests(cases []patternCase, argTemplate []string) ([]*dsl.Test, error)
|
||||
outputName := filepath.Base(c.outputPath)
|
||||
|
||||
if useInputAsFile {
|
||||
t.InFiles[inputName] = string(inputContent)
|
||||
t.SetInputFile(inputName, inputContent)
|
||||
} else {
|
||||
s := string(inputContent)
|
||||
t.Stdin = &s
|
||||
t.SetStdin(inputContent)
|
||||
}
|
||||
|
||||
if useOutputAsFile {
|
||||
t.OutFiles[outputName] = string(outputContent)
|
||||
t.Stdout = dsl.NoMatcher{}
|
||||
t.SetOutputFile(outputName, outputContent)
|
||||
} else {
|
||||
t.Stdout = dsl.ExactMatcher{Value: string(outputContent)}
|
||||
t.SetStdout(outputContent)
|
||||
}
|
||||
|
||||
if len(argTemplate) > 0 {
|
||||
@@ -171,6 +165,19 @@ func buildTests(cases []patternCase, argTemplate []string) ([]*dsl.Test, error)
|
||||
})
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func buildTests(cases []patternCase, argTemplate []string) ([]*dsl.Test, error) {
|
||||
useInputAsFile := argsContain(argTemplate, "{input_path}")
|
||||
useOutputAsFile := argsContain(argTemplate, "{output_path}")
|
||||
|
||||
var tests []*dsl.Test
|
||||
for _, c := range cases {
|
||||
t, err := buildTest(&c, argTemplate, useInputAsFile, useOutputAsFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tests = append(tests, t)
|
||||
}
|
||||
return tests, nil
|
||||
|
||||
@@ -102,9 +102,9 @@ func createScopeCgroup() (string, error) {
|
||||
return "", fmt.Errorf("read /proc/self/cgroup: %w", err)
|
||||
}
|
||||
var rel string
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") {
|
||||
if strings.HasPrefix(line, "0::") {
|
||||
rel = strings.TrimPrefix(line, "0::")
|
||||
for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") {
|
||||
if after, ok := strings.CutPrefix(line, "0::"); ok {
|
||||
rel = after
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -200,7 +200,7 @@ func (l *linuxLimiter) collect() limitStats {
|
||||
}
|
||||
}
|
||||
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)
|
||||
if len(fields) != 2 {
|
||||
continue
|
||||
@@ -217,7 +217,7 @@ func (l *linuxLimiter) cleanup() {
|
||||
if l.cgPath == "" {
|
||||
return
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
for range 10 {
|
||||
err := os.Remove(l.cgPath)
|
||||
if err == nil || os.IsNotExist(err) {
|
||||
l.cgPath = ""
|
||||
|
||||
@@ -1,6 +1,44 @@
|
||||
package runner
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStatusString(t *testing.T) {
|
||||
cases := []struct {
|
||||
s Status
|
||||
want string
|
||||
}{
|
||||
{StatusPass, "PASS"},
|
||||
{StatusFail, "FAIL"},
|
||||
{StatusTLE, "TLE"},
|
||||
{StatusMLE, "MLE"},
|
||||
{StatusBuildError, "BUILD_ERROR"},
|
||||
{StatusRuntimeError, "RE"},
|
||||
{Status(999), "UNKNOWN"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := c.s.String(); got != c.want {
|
||||
t.Errorf("Status(%d).String() = %q, want %q", c.s, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddFailureAppends(t *testing.T) {
|
||||
r := &TestResult{}
|
||||
r.addFailure("first %s", "msg")
|
||||
r.addFailure("second %d", 2)
|
||||
if len(r.Failures) != 2 {
|
||||
t.Fatalf("Failures len = %d, want 2", len(r.Failures))
|
||||
}
|
||||
if r.Failures[0] != "first msg" {
|
||||
t.Errorf("Failures[0] = %q", r.Failures[0])
|
||||
}
|
||||
if !strings.Contains(r.Failures[1], "second 2") {
|
||||
t.Errorf("Failures[1] = %q", r.Failures[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateScoreEmpty(t *testing.T) {
|
||||
r := &SuiteResult{}
|
||||
|
||||
@@ -558,10 +558,10 @@ func (r *Runner) runTest(t *dsl.Test) *TestResult {
|
||||
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)
|
||||
}
|
||||
for _, f := range applyMatcher("stderr", t.Stderr, tr.ActualStderr) {
|
||||
for _, f := range t.Stderr.Match("stderr", tr.ActualStderr) {
|
||||
tr.addFailure("%s", f)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user