Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8cf2584ce5 | |||
| b44cccf8d1 | |||
| 1fec40fc9e | |||
| 2ebf193695 | |||
| 1ef3973a15 |
@@ -24,6 +24,8 @@ Flags:
|
|||||||
--build <name> run only the named structured build (use with matrix CI)
|
--build <name> run only the named structured build (use with matrix CI)
|
||||||
--list-builds print the names of structured builds in the suite as JSON
|
--list-builds print the names of structured builds in the suite as JSON
|
||||||
--list-matrix print the full (build, toolchain, platform) matrix as JSON
|
--list-matrix print the full (build, toolchain, platform) matrix as JSON
|
||||||
|
--test <name> run tests with specific group/name or glob
|
||||||
|
--group <name> run all tests from <name> group
|
||||||
--help show help
|
--help show help
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@@ -32,6 +34,9 @@ Example:
|
|||||||
judge lab1.jdg ./student-solution --build=sanitized
|
judge lab1.jdg ./student-solution --build=sanitized
|
||||||
judge --list-builds lab1.jdg
|
judge --list-builds lab1.jdg
|
||||||
judge aggregate reports/
|
judge aggregate reports/
|
||||||
|
judge suite.jdg . --test "basic/*"
|
||||||
|
judge suite.jdg . --test "*/timeout"
|
||||||
|
judge suite.jdg . --group "basic"
|
||||||
`
|
`
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
14
dsl/ast.go
14
dsl/ast.go
@@ -19,6 +19,10 @@ type File struct {
|
|||||||
Binary string
|
Binary string
|
||||||
Sources string
|
Sources string
|
||||||
|
|
||||||
|
// MaxOutput caps captured stdout/stderr per test (bytes). Zero means use the
|
||||||
|
// runner default. Raise it for suites whose expected output is large.
|
||||||
|
MaxOutput int64
|
||||||
|
|
||||||
NormalizeCRLF bool
|
NormalizeCRLF bool
|
||||||
TrimTrailingWS bool
|
TrimTrailingWS bool
|
||||||
|
|
||||||
@@ -53,6 +57,16 @@ type Pattern struct {
|
|||||||
InputFile string
|
InputFile string
|
||||||
OutputFile string
|
OutputFile string
|
||||||
|
|
||||||
|
// StageAll, valid only in dir mode, stages every file found in each matched
|
||||||
|
// directory into the test's working directory (basenames preserved), except
|
||||||
|
// the OutputFile and ArgsFile. Use it when the program discovers its own
|
||||||
|
// inputs in the working directory instead of receiving a single input file.
|
||||||
|
StageAll bool
|
||||||
|
// ArgsFile, valid only in dir mode, reads the program arguments from this
|
||||||
|
// file inside each matched directory (whitespace-separated). It takes
|
||||||
|
// precedence over Args. Placeholders are still substituted.
|
||||||
|
ArgsFile string
|
||||||
|
|
||||||
Args []string
|
Args []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -307,6 +307,17 @@ func (p *Parser) parseFile() (*File, error) {
|
|||||||
}
|
}
|
||||||
f.MemoryLimit = n
|
f.MemoryLimit = n
|
||||||
|
|
||||||
|
case "max_output":
|
||||||
|
p.advance()
|
||||||
|
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n, err := p.parseSize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f.MaxOutput = n
|
||||||
|
|
||||||
case "group":
|
case "group":
|
||||||
g, err := p.parseGroup(f.Timeout, f.MemoryLimit)
|
g, err := p.parseGroup(f.Timeout, f.MemoryLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -807,6 +818,18 @@ func (p *Parser) parsePattern() (*Pattern, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pat.DirsGlob = val.Value
|
pat.DirsGlob = val.Value
|
||||||
|
case "stage":
|
||||||
|
b, err := p.parseBool()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pat.StageAll = b
|
||||||
|
case "args_file":
|
||||||
|
val, err := p.expect(TOKEN_STRING)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pat.ArgsFile = val.Value
|
||||||
case "args":
|
case "args":
|
||||||
xs, err := p.parseStringList()
|
xs, err := p.parseStringList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -37,6 +37,56 @@ group("g") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseMaxOutput(t *testing.T) {
|
||||||
|
src := `
|
||||||
|
build "make"
|
||||||
|
max_output = 64M
|
||||||
|
group("g") {
|
||||||
|
weight = 1.0
|
||||||
|
test("t") { stdout = "ok\n" }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
f, _, err := Parse(src)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse: %v", err)
|
||||||
|
}
|
||||||
|
if f.MaxOutput != 64*1024*1024 {
|
||||||
|
t.Errorf("MaxOutput = %d, want %d", f.MaxOutput, 64*1024*1024)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePatternStageAllMode(t *testing.T) {
|
||||||
|
src := `
|
||||||
|
build "make"
|
||||||
|
group("g") {
|
||||||
|
weight = 1.0
|
||||||
|
pattern {
|
||||||
|
dirs = "testdata/test_*"
|
||||||
|
stage = true
|
||||||
|
args_file = "args.txt"
|
||||||
|
output = "answer.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
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.StageAll {
|
||||||
|
t.Error("StageAll = false, want true")
|
||||||
|
}
|
||||||
|
if pat.ArgsFile != "args.txt" {
|
||||||
|
t.Errorf("ArgsFile = %q", pat.ArgsFile)
|
||||||
|
}
|
||||||
|
if pat.OutputFile != "answer.txt" {
|
||||||
|
t.Errorf("OutputFile = %q", pat.OutputFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParsePatternUnknownField(t *testing.T) {
|
func TestParsePatternUnknownField(t *testing.T) {
|
||||||
src := `
|
src := `
|
||||||
build "make"
|
build "make"
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
},
|
},
|
||||||
"block-keywords": {
|
"block-keywords": {
|
||||||
"name": "variable.parameter.jdg",
|
"name": "variable.parameter.jdg",
|
||||||
"match": "\\b(?:weight|scoring|stdin|stdout|stderr|args|exitCode|input|dirs)\\b"
|
"match": "\\b(?:weight|scoring|stdin|stdout|stderr|args|args_file|exitCode|input|dirs|stage)\\b"
|
||||||
},
|
},
|
||||||
"matcher-keywords": {
|
"matcher-keywords": {
|
||||||
"name": "support.function.jdg",
|
"name": "support.function.jdg",
|
||||||
|
|||||||
@@ -48,6 +48,8 @@
|
|||||||
"input"
|
"input"
|
||||||
"output"
|
"output"
|
||||||
"dirs"
|
"dirs"
|
||||||
|
"stage"
|
||||||
|
"args_file"
|
||||||
] @property
|
] @property
|
||||||
|
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -298,12 +298,16 @@ module.exports = grammar({
|
|||||||
$.pattern_input_field,
|
$.pattern_input_field,
|
||||||
$.pattern_output_field,
|
$.pattern_output_field,
|
||||||
$.pattern_dirs_field,
|
$.pattern_dirs_field,
|
||||||
|
$.pattern_stage_field,
|
||||||
|
$.pattern_args_file_field,
|
||||||
$.pattern_args_field,
|
$.pattern_args_field,
|
||||||
),
|
),
|
||||||
|
|
||||||
pattern_input_field: $ => seq('input', '=', field('value', $.string)),
|
pattern_input_field: $ => seq('input', '=', field('value', $.string)),
|
||||||
pattern_output_field: $ => seq('output', '=', field('value', $.string)),
|
pattern_output_field: $ => seq('output', '=', field('value', $.string)),
|
||||||
pattern_dirs_field: $ => seq('dirs', '=', field('value', $.string)),
|
pattern_dirs_field: $ => seq('dirs', '=', field('value', $.string)),
|
||||||
|
pattern_stage_field: $ => seq('stage', '=', field('value', $.bool)),
|
||||||
|
pattern_args_file_field: $ => seq('args_file', '=', field('value', $.string)),
|
||||||
pattern_args_field: $ => seq('args', '=', field('value', repeat1($.string))),
|
pattern_args_field: $ => seq('args', '=', field('value', repeat1($.string))),
|
||||||
|
|
||||||
bool: $ => choice('true', 'false'),
|
bool: $ => choice('true', 'false'),
|
||||||
|
|||||||
@@ -86,6 +86,13 @@ func writeBuildText(w io.Writer, b *runner.BuildRun, multi bool) {
|
|||||||
fmt.Fprintf(w, "│ %s\n", line)
|
fmt.Fprintf(w, "│ %s\n", line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tr.Status != runner.StatusPass && strings.TrimSpace(tr.ActualStderr) != "" {
|
||||||
|
fmt.Fprintf(w, "│ ── stderr ──\n")
|
||||||
|
for _, line := range clampLines(tr.ActualStderr, 10) {
|
||||||
|
fmt.Fprintf(w, "│ %s\n", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "└─\n")
|
fmt.Fprintf(w, "└─\n")
|
||||||
}
|
}
|
||||||
@@ -265,6 +272,18 @@ func extractTotalScore(data []byte) (float64, error) {
|
|||||||
return header.TotalScore, nil
|
return header.TotalScore, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clampLines returns up to max lines of s (trailing newline ignored), appending
|
||||||
|
// a truncation note when more lines were omitted.
|
||||||
|
func clampLines(s string, max int) []string {
|
||||||
|
s = strings.TrimRight(s, "\n")
|
||||||
|
lines := strings.Split(s, "\n")
|
||||||
|
if len(lines) <= max {
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
out := append([]string{}, lines[:max]...)
|
||||||
|
return append(out, fmt.Sprintf("... (%d more lines)", len(lines)-max))
|
||||||
|
}
|
||||||
|
|
||||||
func humanBytes(n int64) string {
|
func humanBytes(n int64) string {
|
||||||
const (
|
const (
|
||||||
KiB = 1024
|
KiB = 1024
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ func globWithAffixes(pattern string) ([]string, string, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func expandGlobPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
|
func expandGlobPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
|
||||||
|
if pattern.StageAll {
|
||||||
|
return nil, fmt.Errorf("pattern: `stage` is only valid with `dirs`")
|
||||||
|
}
|
||||||
|
if pattern.ArgsFile != "" {
|
||||||
|
return nil, fmt.Errorf("pattern: `args_file` is only valid with `dirs`")
|
||||||
|
}
|
||||||
|
|
||||||
inputIsGlob := strings.Contains(pattern.InputGlob, "*")
|
inputIsGlob := strings.Contains(pattern.InputGlob, "*")
|
||||||
outputIsGlob := strings.Contains(pattern.OutputGlob, "*")
|
outputIsGlob := strings.Contains(pattern.OutputGlob, "*")
|
||||||
|
|
||||||
@@ -107,6 +114,22 @@ func expandDirPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
|
|||||||
return nil, fmt.Errorf("no directories matched %q", pattern.DirsGlob)
|
return nil, fmt.Errorf("no directories matched %q", pattern.DirsGlob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pattern.StageAll {
|
||||||
|
var tests []*dsl.Test
|
||||||
|
for _, dir := range dirs {
|
||||||
|
info, err := os.Stat(dir)
|
||||||
|
if err != nil || !info.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t, err := buildStagedDirTest(dir, pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tests = append(tests, t)
|
||||||
|
}
|
||||||
|
return tests, nil
|
||||||
|
}
|
||||||
|
|
||||||
var cases []patternCase
|
var cases []patternCase
|
||||||
for _, dir := range dirs {
|
for _, dir := range dirs {
|
||||||
info, err := os.Stat(dir)
|
info, err := os.Stat(dir)
|
||||||
@@ -123,6 +146,72 @@ func expandDirPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
|
|||||||
return buildTests(cases, pattern.Args)
|
return buildTests(cases, pattern.Args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildStagedDirTest builds a single test from a directory, staging every
|
||||||
|
// regular file in it (except the output and args files) into the test's
|
||||||
|
// working directory. Program arguments come from ArgsFile (if set) or the
|
||||||
|
// static Args template; the expected stdout is the content of OutputFile.
|
||||||
|
func buildStagedDirTest(dir string, pattern *dsl.Pattern) (*dsl.Test, error) {
|
||||||
|
base := filepath.Base(dir)
|
||||||
|
zero := 0
|
||||||
|
t := &dsl.Test{
|
||||||
|
Name: fmt.Sprintf("pattern:%s", base),
|
||||||
|
Env: map[string]string{},
|
||||||
|
InFiles: map[string]string{},
|
||||||
|
OutFiles: map[string]string{},
|
||||||
|
ExitCode: &zero,
|
||||||
|
Stdout: dsl.NoMatcher{},
|
||||||
|
Stderr: dsl.NoMatcher{},
|
||||||
|
}
|
||||||
|
|
||||||
|
skip := map[string]bool{}
|
||||||
|
if pattern.OutputFile != "" {
|
||||||
|
skip[pattern.OutputFile] = true
|
||||||
|
}
|
||||||
|
if pattern.ArgsFile != "" {
|
||||||
|
skip[pattern.ArgsFile] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read dir %q: %w", dir, err)
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() || skip[e.Name()] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
content, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read staged file %q: %w", e.Name(), err)
|
||||||
|
}
|
||||||
|
t.SetInputFile(e.Name(), content)
|
||||||
|
}
|
||||||
|
|
||||||
|
argTemplate := pattern.Args
|
||||||
|
if pattern.ArgsFile != "" {
|
||||||
|
raw, err := os.ReadFile(filepath.Join(dir, pattern.ArgsFile))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read args file %q: %w", pattern.ArgsFile, err)
|
||||||
|
}
|
||||||
|
argTemplate = strings.Fields(string(raw))
|
||||||
|
}
|
||||||
|
if len(argTemplate) > 0 {
|
||||||
|
t.Args = substituteArgs(argTemplate, map[string]string{
|
||||||
|
"{name}": base,
|
||||||
|
"{dir}": dir,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if pattern.OutputFile != "" {
|
||||||
|
outputContent, err := os.ReadFile(filepath.Join(dir, pattern.OutputFile))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read output %q: %w", pattern.OutputFile, err)
|
||||||
|
}
|
||||||
|
t.SetStdout(outputContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
func buildTest(c *patternCase, argTemplate []string, useInputAsFile, useOutputAsFile bool) (*dsl.Test, error) {
|
func buildTest(c *patternCase, argTemplate []string, useInputAsFile, useOutputAsFile bool) (*dsl.Test, error) {
|
||||||
inputContent, err := os.ReadFile(c.inputPath)
|
inputContent, err := os.ReadFile(c.inputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -211,6 +211,100 @@ func TestExpandDirModeWithArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExpandDirModeStageAll(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
// Two test directories, each with several input files the program discovers
|
||||||
|
// itself, plus an args file and an expected-stdout file.
|
||||||
|
writeFile(t, dir, "cases/test_1/1.txt", "10\n")
|
||||||
|
writeFile(t, dir, "cases/test_1/2.txt", "20\n")
|
||||||
|
writeFile(t, dir, "cases/test_1/decoy.log", "ignore me\n")
|
||||||
|
writeFile(t, dir, "cases/test_1/args.txt", "$.txt 1 1 2\n")
|
||||||
|
writeFile(t, dir, "cases/test_1/answer.txt", "10\n20\n")
|
||||||
|
writeFile(t, dir, "cases/test_2/a.txt", "x\n")
|
||||||
|
writeFile(t, dir, "cases/test_2/args.txt", "a.txt\n")
|
||||||
|
writeFile(t, dir, "cases/test_2/answer.txt", "x\n")
|
||||||
|
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
defer os.Chdir(cwd)
|
||||||
|
os.Chdir(dir)
|
||||||
|
|
||||||
|
tests, err := expandPattern(&dsl.Pattern{
|
||||||
|
DirsGlob: "cases/*",
|
||||||
|
StageAll: true,
|
||||||
|
ArgsFile: "args.txt",
|
||||||
|
OutputFile: "answer.txt",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(tests) != 2 {
|
||||||
|
t.Fatalf("expected 2 tests, got %d", len(tests))
|
||||||
|
}
|
||||||
|
|
||||||
|
var tc1 *dsl.Test
|
||||||
|
for _, tc := range tests {
|
||||||
|
if tc.Name == "pattern:test_1" {
|
||||||
|
tc1 = tc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tc1 == nil {
|
||||||
|
t.Fatalf("test_1 not found among %v", tests)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All input files staged, including the decoy; answer/args excluded.
|
||||||
|
for _, name := range []string{"1.txt", "2.txt", "decoy.log"} {
|
||||||
|
if _, ok := tc1.InFiles[name]; !ok {
|
||||||
|
t.Errorf("staged InFiles missing %q (have %v)", name, tc1.InFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := tc1.InFiles["answer.txt"]; ok {
|
||||||
|
t.Error("answer.txt must not be staged")
|
||||||
|
}
|
||||||
|
if _, ok := tc1.InFiles["args.txt"]; ok {
|
||||||
|
t.Error("args.txt must not be staged")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Args read from args.txt, whitespace-split.
|
||||||
|
want := []string{"$.txt", "1", "1", "2"}
|
||||||
|
if strings.Join(tc1.Args, " ") != strings.Join(want, " ") {
|
||||||
|
t.Errorf("args = %v, want %v", tc1.Args, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expected stdout is the answer file content.
|
||||||
|
m, ok := tc1.Stdout.(dsl.ExactMatcher)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("stdout should be ExactMatcher, got %T", tc1.Stdout)
|
||||||
|
}
|
||||||
|
if m.Value != "10\n20\n" {
|
||||||
|
t.Errorf("stdout = %q, want %q", m.Value, "10\n20\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A successful functional test must exit zero.
|
||||||
|
if tc1.ExitCode == nil || *tc1.ExitCode != 0 {
|
||||||
|
t.Errorf("ExitCode = %v, want 0", tc1.ExitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandStageAllRejectedInGlobMode(t *testing.T) {
|
||||||
|
_, err := expandPattern(&dsl.Pattern{
|
||||||
|
InputGlob: "tests/*.in",
|
||||||
|
StageAll: true,
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "stage") {
|
||||||
|
t.Fatalf("expected stage-only-with-dirs error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandArgsFileRejectedInGlobMode(t *testing.T) {
|
||||||
|
_, err := expandPattern(&dsl.Pattern{
|
||||||
|
InputGlob: "tests/*.in",
|
||||||
|
ArgsFile: "args.txt",
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "args_file") {
|
||||||
|
t.Fatalf("expected args_file-only-with-dirs error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExpandPatternRejectsAllLiterals(t *testing.T) {
|
func TestExpandPatternRejectsAllLiterals(t *testing.T) {
|
||||||
_, err := expandPattern(&dsl.Pattern{
|
_, err := expandPattern(&dsl.Pattern{
|
||||||
InputGlob: "tests/a.in",
|
InputGlob: "tests/a.in",
|
||||||
|
|||||||
96
runner/normalize_test.go
Normal file
96
runner/normalize_test.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Mond1c/judge/dsl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Under a wrapper, only args that name a staged file become absolute; numeric
|
||||||
|
// and flag args must be passed through verbatim (regression: a numeric arg "1"
|
||||||
|
// was being turned into "<tmpdir>/1", breaking programs that take non-path
|
||||||
|
// arguments).
|
||||||
|
func TestAbsoluteArgsOnlyRewritesStagedFiles(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "01.in"), []byte("x"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := absoluteArgs(dir, []string{"$.txt", "1", "1", "3", "01.in", "--flag"})
|
||||||
|
want := []string{"$.txt", "1", "1", "3", filepath.Join(dir, "01.in"), "--flag"}
|
||||||
|
for i := range want {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
t.Errorf("arg[%d] = %q, want %q", i, got[i], want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A staged/expected value loaded from disk on Windows can carry CRLF. With
|
||||||
|
// normalize_crlf the runner must normalize the EXPECTED side too, otherwise it
|
||||||
|
// can never match the LF-normalized actual output.
|
||||||
|
func TestNormalizeExpectationsCRLF(t *testing.T) {
|
||||||
|
r := &Runner{file: &dsl.File{NormalizeCRLF: true}}
|
||||||
|
|
||||||
|
test := &dsl.Test{
|
||||||
|
Stdout: dsl.ExactMatcher{Value: "1\r\n2\r\n"},
|
||||||
|
Stderr: dsl.NoMatcher{},
|
||||||
|
OutFiles: map[string]string{"out.txt": "a\r\nb\r\n"},
|
||||||
|
}
|
||||||
|
r.normalizeExpectations(test)
|
||||||
|
|
||||||
|
m, ok := test.Stdout.(dsl.ExactMatcher)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("stdout matcher type changed: %T", test.Stdout)
|
||||||
|
}
|
||||||
|
if m.Value != "1\n2\n" {
|
||||||
|
t.Errorf("expected stdout normalized to %q, got %q", "1\n2\n", m.Value)
|
||||||
|
}
|
||||||
|
if got := test.OutFiles["out.txt"]; got != "a\nb\n" {
|
||||||
|
t.Errorf("expected outfile normalized to %q, got %q", "a\nb\n", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// And the normalized expected must match normalized actual end-to-end.
|
||||||
|
if fails := test.Stdout.Match("stdout", normalizeOutput("1\r\n2\r\n", r.file)); len(fails) != 0 {
|
||||||
|
t.Errorf("normalized expected should match normalized actual, got %v", fails)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeExpectationsTrimTrailingWS(t *testing.T) {
|
||||||
|
r := &Runner{file: &dsl.File{TrimTrailingWS: true}}
|
||||||
|
test := &dsl.Test{
|
||||||
|
Stdout: dsl.AnyOrderMatcher{Lines: []string{"foo ", "bar\t"}},
|
||||||
|
Stderr: dsl.NoMatcher{},
|
||||||
|
}
|
||||||
|
r.normalizeExpectations(test)
|
||||||
|
|
||||||
|
m := test.Stdout.(dsl.AnyOrderMatcher)
|
||||||
|
if m.Lines[0] != "foo" || m.Lines[1] != "bar" {
|
||||||
|
t.Errorf("anyOrder lines not trimmed: %q", m.Lines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regex matchers must never be rewritten: a literal \r in the pattern is
|
||||||
|
// meaningful and normalization would corrupt it.
|
||||||
|
func TestNormalizeExpectationsLeavesRegexAlone(t *testing.T) {
|
||||||
|
r := &Runner{file: &dsl.File{NormalizeCRLF: true}}
|
||||||
|
test := &dsl.Test{
|
||||||
|
Stdout: dsl.RegexMatcher{Pattern: "a\r\nb"},
|
||||||
|
Stderr: dsl.NoMatcher{},
|
||||||
|
}
|
||||||
|
r.normalizeExpectations(test)
|
||||||
|
if m := test.Stdout.(dsl.RegexMatcher); m.Pattern != "a\r\nb" {
|
||||||
|
t.Errorf("regex pattern was modified: %q", m.Pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeExpectationsNoopWhenDisabled(t *testing.T) {
|
||||||
|
r := &Runner{file: &dsl.File{}}
|
||||||
|
want := "1\r\n2\r\n"
|
||||||
|
test := &dsl.Test{Stdout: dsl.ExactMatcher{Value: want}, Stderr: dsl.NoMatcher{}}
|
||||||
|
r.normalizeExpectations(test)
|
||||||
|
if m := test.Stdout.(dsl.ExactMatcher); m.Value != want {
|
||||||
|
t.Errorf("value changed with normalization disabled: %q", m.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -407,6 +407,8 @@ func (r *Runner) runGroup(g *dsl.Group) *GroupResult {
|
|||||||
t.Wrapper = g.Wrapper
|
t.Wrapper = g.Wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.normalizeExpectations(t)
|
||||||
|
|
||||||
tr := r.runTest(t)
|
tr := r.runTest(t)
|
||||||
gr.Tests = append(gr.Tests, tr)
|
gr.Tests = append(gr.Tests, tr)
|
||||||
if tr.Status == StatusPass {
|
if tr.Status == StatusPass {
|
||||||
@@ -486,8 +488,12 @@ func (r *Runner) runTest(t *dsl.Test) *TestResult {
|
|||||||
cmd.Stdin = strings.NewReader(*t.Stdin)
|
cmd.Stdin = strings.NewReader(*t.Stdin)
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout := &cappedBuffer{limit: MaxOutputBytes}
|
outLimit := MaxOutputBytes
|
||||||
stderr := &cappedBuffer{limit: MaxOutputBytes}
|
if r.file.MaxOutput > 0 {
|
||||||
|
outLimit = int(r.file.MaxOutput)
|
||||||
|
}
|
||||||
|
stdout := &cappedBuffer{limit: outLimit}
|
||||||
|
stderr := &cappedBuffer{limit: outLimit}
|
||||||
cmd.Stdout = stdout
|
cmd.Stdout = stdout
|
||||||
cmd.Stderr = stderr
|
cmd.Stderr = stderr
|
||||||
|
|
||||||
@@ -587,15 +593,23 @@ func (r *Runner) runTest(t *dsl.Test) *TestResult {
|
|||||||
return tr
|
return tr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// absoluteArgs rewrites only those arguments that name a file staged in the
|
||||||
|
// test's working directory to an absolute path. This helps wrappers (gdb,
|
||||||
|
// valgrind) that may resolve paths from a different directory, without
|
||||||
|
// corrupting non-path arguments — a numeric arg like "1" must stay "1", not
|
||||||
|
// become "<tmpdir>/1". The process runs with Dir=dir anyway, so relative paths
|
||||||
|
// already resolve there; this is purely a safety net for genuine file args.
|
||||||
func absoluteArgs(dir string, args []string) []string {
|
func absoluteArgs(dir string, args []string) []string {
|
||||||
out := make([]string, len(args))
|
out := make([]string, len(args))
|
||||||
for i, a := range args {
|
for i, a := range args {
|
||||||
if !filepath.IsAbs(a) {
|
if a != "" && !filepath.IsAbs(a) {
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, a)); err == nil {
|
||||||
out[i] = filepath.Join(dir, a)
|
out[i] = filepath.Join(dir, a)
|
||||||
} else {
|
continue
|
||||||
out[i] = a
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
out[i] = a
|
||||||
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,6 +623,40 @@ func buildExecCmd(ctx context.Context, wrapper, binary string, args []string) *e
|
|||||||
return exec.CommandContext(ctx, full[0], full[1:]...)
|
return exec.CommandContext(ctx, full[0], full[1:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizeExpectations applies the suite's CRLF/whitespace normalization to a
|
||||||
|
// test's *expected* values, mirroring what normalizeOutput does to the actual
|
||||||
|
// output. Without this, an expected value loaded from disk on Windows (e.g. an
|
||||||
|
// answer file checked out with CRLF) would never match LF-normalized actual
|
||||||
|
// output even with normalize_crlf enabled.
|
||||||
|
func (r *Runner) normalizeExpectations(t *dsl.Test) {
|
||||||
|
if !r.file.NormalizeCRLF && !r.file.TrimTrailingWS {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Stdout = r.normalizeMatcher(t.Stdout)
|
||||||
|
t.Stderr = r.normalizeMatcher(t.Stderr)
|
||||||
|
for k, v := range t.OutFiles {
|
||||||
|
t.OutFiles[k] = normalizeOutput(v, r.file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeMatcher normalizes the expected payload of the matchers that compare
|
||||||
|
// whole-output text. Regex and numeric matchers are left untouched: their
|
||||||
|
// patterns are authored deliberately and must not be rewritten.
|
||||||
|
func (r *Runner) normalizeMatcher(m dsl.Matcher) dsl.Matcher {
|
||||||
|
switch x := m.(type) {
|
||||||
|
case dsl.ExactMatcher:
|
||||||
|
return dsl.ExactMatcher{Value: normalizeOutput(x.Value, r.file)}
|
||||||
|
case dsl.AnyOrderMatcher:
|
||||||
|
lines := make([]string, len(x.Lines))
|
||||||
|
for i, l := range x.Lines {
|
||||||
|
lines[i] = normalizeOutput(l, r.file)
|
||||||
|
}
|
||||||
|
return dsl.AnyOrderMatcher{Lines: lines}
|
||||||
|
default:
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeOutput(s string, f *dsl.File) string {
|
func normalizeOutput(s string, f *dsl.File) string {
|
||||||
if f.NormalizeCRLF {
|
if f.NormalizeCRLF {
|
||||||
s = strings.ReplaceAll(s, "\r\n", "\n")
|
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user