feat: pattern support args and multiple variants; add zed extension for highlight
All checks were successful
build-dsl-smoke / Build judge (push) Successful in 13s
build-dsl-smoke / debug / clang / linux (push) Successful in 6s
build-dsl-smoke / debug / gcc / linux (push) Successful in 8s
build-dsl-smoke / release / clang / linux (push) Successful in 9s
build-dsl-smoke / release / gcc / linux (push) Successful in 7s
build-dsl-smoke / sanitized / clang / linux (push) Successful in 8s
build-dsl-smoke / sanitized / gcc / linux (push) Successful in 7s
build-dsl-smoke / debug / clang / windows (push) Successful in 16s
build-dsl-smoke / debug-valgrind / gcc / linux (push) Successful in 14s
build-dsl-smoke / debug / msvc / windows (push) Successful in 18s
build-dsl-smoke / release / clang / windows (push) Successful in 17s
build-dsl-smoke / release / msvc / windows (push) Successful in 17s
build-dsl-smoke / SUMMARY (push) Successful in 5s
All checks were successful
build-dsl-smoke / Build judge (push) Successful in 13s
build-dsl-smoke / debug / clang / linux (push) Successful in 6s
build-dsl-smoke / debug / gcc / linux (push) Successful in 8s
build-dsl-smoke / release / clang / linux (push) Successful in 9s
build-dsl-smoke / release / gcc / linux (push) Successful in 7s
build-dsl-smoke / sanitized / clang / linux (push) Successful in 8s
build-dsl-smoke / sanitized / gcc / linux (push) Successful in 7s
build-dsl-smoke / debug / clang / windows (push) Successful in 16s
build-dsl-smoke / debug-valgrind / gcc / linux (push) Successful in 14s
build-dsl-smoke / debug / msvc / windows (push) Successful in 18s
build-dsl-smoke / release / clang / windows (push) Successful in 17s
build-dsl-smoke / release / msvc / windows (push) Successful in 17s
build-dsl-smoke / SUMMARY (push) Successful in 5s
This commit is contained in:
@@ -16,47 +16,85 @@ func expandPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
|
||||
return expandGlobPattern(pattern)
|
||||
}
|
||||
|
||||
type patternCase struct {
|
||||
name string
|
||||
inputPath string
|
||||
outputPath string
|
||||
dir string
|
||||
}
|
||||
|
||||
func expandGlobPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
|
||||
inputFiles, err := filepath.Glob(pattern.InputGlob)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid input glob %q: %w", pattern.InputGlob, err)
|
||||
inputIsGlob := strings.Contains(pattern.InputGlob, "*")
|
||||
outputIsGlob := strings.Contains(pattern.OutputGlob, "*")
|
||||
|
||||
if pattern.InputGlob == "" && pattern.OutputGlob == "" {
|
||||
return nil, fmt.Errorf("pattern needs at least one of input/output/dirs")
|
||||
}
|
||||
if len(inputFiles) == 0 {
|
||||
return nil, fmt.Errorf("no files matched input glob %q", pattern.InputGlob)
|
||||
if !inputIsGlob && !outputIsGlob {
|
||||
return nil, fmt.Errorf("pattern needs at least one glob field (input or output must contain *)")
|
||||
}
|
||||
|
||||
inputPrefix, inputSuffix := splitGlob(pattern.InputGlob)
|
||||
outputPrefix, outputSuffix := splitGlob(pattern.OutputGlob)
|
||||
var cases []patternCase
|
||||
|
||||
var tests []*dsl.Test
|
||||
for _, inputPath := range inputFiles {
|
||||
wildcard := extractWildcard(inputPath, inputPrefix, inputSuffix)
|
||||
outputPath := outputPrefix + wildcard + outputSuffix
|
||||
|
||||
inputContent, err := os.ReadFile(inputPath)
|
||||
switch {
|
||||
case inputIsGlob && outputIsGlob:
|
||||
inputFiles, err := filepath.Glob(pattern.InputGlob)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read input %q: %w", inputPath, err)
|
||||
return nil, fmt.Errorf("invalid input glob %q: %w", pattern.InputGlob, err)
|
||||
}
|
||||
outputContent, err := os.ReadFile(outputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read output %q: %w", outputPath, 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)
|
||||
outputPath := outputPrefix + wildcard + outputSuffix
|
||||
cases = append(cases, patternCase{
|
||||
name: wildcard,
|
||||
inputPath: inputPath,
|
||||
outputPath: outputPath,
|
||||
})
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("pattern:%s", wildcard)
|
||||
stdin := string(inputContent)
|
||||
expected := string(outputContent)
|
||||
case inputIsGlob && !outputIsGlob:
|
||||
inputFiles, err := filepath.Glob(pattern.InputGlob)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid input glob %q: %w", pattern.InputGlob, 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{
|
||||
name: wildcard,
|
||||
inputPath: inputPath,
|
||||
outputPath: pattern.OutputGlob,
|
||||
})
|
||||
}
|
||||
|
||||
tests = append(tests, &dsl.Test{
|
||||
Name: name,
|
||||
Stdin: &stdin,
|
||||
Env: map[string]string{},
|
||||
InFiles: map[string]string{},
|
||||
OutFiles: map[string]string{},
|
||||
Stdout: dsl.ExactMatcher{Value: expected},
|
||||
Stderr: dsl.NoMatcher{},
|
||||
})
|
||||
case !inputIsGlob && outputIsGlob:
|
||||
outputFiles, err := filepath.Glob(pattern.OutputGlob)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid output glob %q: %w", pattern.OutputGlob, 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{
|
||||
name: wildcard,
|
||||
inputPath: pattern.InputGlob,
|
||||
outputPath: outputPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
return tests, nil
|
||||
|
||||
return buildTests(cases, pattern.Args)
|
||||
}
|
||||
|
||||
func expandDirPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
|
||||
@@ -68,42 +106,96 @@ func expandDirPattern(pattern *dsl.Pattern) ([]*dsl.Test, error) {
|
||||
return nil, fmt.Errorf("no directories matched %q", pattern.DirsGlob)
|
||||
}
|
||||
|
||||
var tests []*dsl.Test
|
||||
var cases []patternCase
|
||||
for _, dir := range dirs {
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil || !info.IsDir() {
|
||||
continue
|
||||
}
|
||||
cases = append(cases, patternCase{
|
||||
name: filepath.Base(dir),
|
||||
inputPath: filepath.Join(dir, pattern.InputFile),
|
||||
outputPath: filepath.Join(dir, pattern.OutputFile),
|
||||
dir: dir,
|
||||
})
|
||||
}
|
||||
return buildTests(cases, pattern.Args)
|
||||
}
|
||||
|
||||
inputPath := filepath.Join(dir, pattern.InputFile)
|
||||
outputPath := filepath.Join(dir, pattern.OutputFile)
|
||||
func buildTests(cases []patternCase, argTemplate []string) ([]*dsl.Test, error) {
|
||||
useInputAsFile := argsContain(argTemplate, "{input_path}")
|
||||
useOutputAsFile := argsContain(argTemplate, "{output_path}")
|
||||
|
||||
inputContent, err := os.ReadFile(inputPath)
|
||||
var tests []*dsl.Test
|
||||
for _, c := range cases {
|
||||
inputContent, err := os.ReadFile(c.inputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %q: %w", inputPath, err)
|
||||
return nil, fmt.Errorf("read input %q: %w", c.inputPath, err)
|
||||
}
|
||||
outputContent, err := os.ReadFile(outputPath)
|
||||
outputContent, err := os.ReadFile(c.outputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %q: %w", outputPath, err)
|
||||
return nil, fmt.Errorf("read output %q: %w", c.outputPath, err)
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("pattern:%s", filepath.Base(dir))
|
||||
stdin := string(inputContent)
|
||||
expected := string(outputContent)
|
||||
|
||||
tests = append(tests, &dsl.Test{
|
||||
Name: name,
|
||||
Stdin: &stdin,
|
||||
t := &dsl.Test{
|
||||
Name: fmt.Sprintf("pattern:%s", c.name),
|
||||
Env: map[string]string{},
|
||||
InFiles: map[string]string{},
|
||||
OutFiles: map[string]string{},
|
||||
Stdout: dsl.ExactMatcher{Value: expected},
|
||||
Stderr: dsl.NoMatcher{},
|
||||
})
|
||||
}
|
||||
|
||||
inputName := filepath.Base(c.inputPath)
|
||||
outputName := filepath.Base(c.outputPath)
|
||||
|
||||
if useInputAsFile {
|
||||
t.InFiles[inputName] = string(inputContent)
|
||||
} else {
|
||||
s := string(inputContent)
|
||||
t.Stdin = &s
|
||||
}
|
||||
|
||||
if useOutputAsFile {
|
||||
t.OutFiles[outputName] = string(outputContent)
|
||||
t.Stdout = dsl.NoMatcher{}
|
||||
} else {
|
||||
t.Stdout = dsl.ExactMatcher{Value: string(outputContent)}
|
||||
}
|
||||
|
||||
if len(argTemplate) > 0 {
|
||||
t.Args = substituteArgs(argTemplate, map[string]string{
|
||||
"{input_path}": inputName,
|
||||
"{output_path}": outputName,
|
||||
"{name}": c.name,
|
||||
"{dir}": c.dir,
|
||||
})
|
||||
}
|
||||
|
||||
tests = append(tests, t)
|
||||
}
|
||||
return tests, nil
|
||||
}
|
||||
|
||||
func argsContain(args []string, placeholder string) bool {
|
||||
for _, a := range args {
|
||||
if strings.Contains(a, placeholder) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func substituteArgs(template []string, vars map[string]string) []string {
|
||||
out := make([]string, len(template))
|
||||
for i, a := range template {
|
||||
for k, v := range vars {
|
||||
a = strings.ReplaceAll(a, k, v)
|
||||
}
|
||||
out[i] = a
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func splitGlob(pattern string) (prefix, suffix string) {
|
||||
before, after, found := strings.Cut(pattern, "*")
|
||||
if !found {
|
||||
|
||||
240
runner/expander_test.go
Normal file
240
runner/expander_test.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Mond1c/judge/dsl"
|
||||
)
|
||||
|
||||
func writeFile(t *testing.T, dir, name, content string) {
|
||||
t.Helper()
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandGlobPairedStdioMode(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writeFile(t, dir, "tests/01.in", "1 2 3\n")
|
||||
writeFile(t, dir, "tests/01.ans", "6\n")
|
||||
writeFile(t, dir, "tests/02.in", "10 20\n")
|
||||
writeFile(t, dir, "tests/02.ans", "30\n")
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
defer os.Chdir(cwd)
|
||||
os.Chdir(dir)
|
||||
|
||||
tests, err := expandPattern(&dsl.Pattern{
|
||||
InputGlob: "tests/*.in",
|
||||
OutputGlob: "tests/*.ans",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(tests) != 2 {
|
||||
t.Fatalf("expected 2 tests, got %d", len(tests))
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if tc.Stdin == nil {
|
||||
t.Errorf("%s: stdin should be set in stdio mode", tc.Name)
|
||||
}
|
||||
if len(tc.Args) != 0 {
|
||||
t.Errorf("%s: args should be empty without template", tc.Name)
|
||||
}
|
||||
if _, ok := tc.Stdout.(dsl.ExactMatcher); !ok {
|
||||
t.Errorf("%s: stdout should be ExactMatcher, got %T", tc.Name, tc.Stdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandGlobWithSharedOutput(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writeFile(t, dir, "tests/01.in", "1\n")
|
||||
writeFile(t, dir, "tests/02.in", "2\n")
|
||||
writeFile(t, dir, "tests/03.in", "3\n")
|
||||
writeFile(t, dir, "expected.ans", "ok\n")
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
defer os.Chdir(cwd)
|
||||
os.Chdir(dir)
|
||||
|
||||
tests, err := expandPattern(&dsl.Pattern{
|
||||
InputGlob: "tests/*.in",
|
||||
OutputGlob: "expected.ans",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expand: %v", err)
|
||||
}
|
||||
if len(tests) != 3 {
|
||||
t.Fatalf("expected 3 tests, got %d", len(tests))
|
||||
}
|
||||
for _, tc := range tests {
|
||||
m, ok := tc.Stdout.(dsl.ExactMatcher)
|
||||
if !ok {
|
||||
t.Fatalf("%s: stdout should be ExactMatcher, got %T", tc.Name, tc.Stdout)
|
||||
}
|
||||
if m.Value != "ok\n" {
|
||||
t.Errorf("%s: shared output = %q, want %q", tc.Name, m.Value, "ok\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandGlobFileModeInputOnly(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writeFile(t, dir, "tests/01.in", "hello\n")
|
||||
writeFile(t, dir, "tests/01.ans", "HELLO\n")
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
defer os.Chdir(cwd)
|
||||
os.Chdir(dir)
|
||||
|
||||
tests, err := expandPattern(&dsl.Pattern{
|
||||
InputGlob: "tests/*.in",
|
||||
OutputGlob: "tests/*.ans",
|
||||
Args: []string{"{input_path}"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(tests) != 1 {
|
||||
t.Fatalf("expected 1 test, got %d", len(tests))
|
||||
}
|
||||
tc := tests[0]
|
||||
if tc.Stdin != nil {
|
||||
t.Errorf("stdin should be nil in file mode for input")
|
||||
}
|
||||
if content, ok := tc.InFiles["01.in"]; !ok || content != "hello\n" {
|
||||
t.Errorf("InFiles[01.in] = %q, want %q", content, "hello\n")
|
||||
}
|
||||
if _, ok := tc.Stdout.(dsl.ExactMatcher); !ok {
|
||||
t.Errorf("stdout should still be ExactMatcher when output not in file mode, got %T", tc.Stdout)
|
||||
}
|
||||
if len(tc.Args) != 1 || tc.Args[0] != "01.in" {
|
||||
t.Errorf("args = %v, want [01.in]", tc.Args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandGlobFileModeBoth(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writeFile(t, dir, "tests/01.in", "1 2 3\n")
|
||||
writeFile(t, dir, "tests/01.ans", "6\n")
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
defer os.Chdir(cwd)
|
||||
os.Chdir(dir)
|
||||
|
||||
tests, err := expandPattern(&dsl.Pattern{
|
||||
InputGlob: "tests/*.in",
|
||||
OutputGlob: "tests/*.ans",
|
||||
Args: []string{"{input_path}", "{output_path}"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tc := tests[0]
|
||||
if tc.Stdin != nil {
|
||||
t.Error("stdin should be nil")
|
||||
}
|
||||
if _, ok := tc.Stdout.(dsl.NoMatcher); !ok {
|
||||
t.Errorf("stdout should be NoMatcher when output is file, got %T", tc.Stdout)
|
||||
}
|
||||
if content := tc.OutFiles["01.ans"]; content != "6\n" {
|
||||
t.Errorf("OutFiles[01.ans] = %q, want %q", content, "6\n")
|
||||
}
|
||||
if len(tc.Args) != 2 || tc.Args[0] != "01.in" || tc.Args[1] != "01.ans" {
|
||||
t.Errorf("args = %v, want [01.in 01.ans]", tc.Args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandGlobArgsWithStaticAndPlaceholders(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writeFile(t, dir, "tests/01.in", "x\n")
|
||||
writeFile(t, dir, "tests/01.ans", "x\n")
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
defer os.Chdir(cwd)
|
||||
os.Chdir(dir)
|
||||
|
||||
tests, err := expandPattern(&dsl.Pattern{
|
||||
InputGlob: "tests/*.in",
|
||||
OutputGlob: "tests/*.ans",
|
||||
Args: []string{"--mode", "strict", "{input_path}"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(tests[0].Args) != 3 || tests[0].Args[0] != "--mode" || tests[0].Args[2] != "01.in" {
|
||||
t.Errorf("args = %v", tests[0].Args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandDirModeWithArgs(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writeFile(t, dir, "cases/a/input.txt", "one\n")
|
||||
writeFile(t, dir, "cases/a/expected.txt", "ONE\n")
|
||||
writeFile(t, dir, "cases/b/input.txt", "two\n")
|
||||
writeFile(t, dir, "cases/b/expected.txt", "TWO\n")
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
defer os.Chdir(cwd)
|
||||
os.Chdir(dir)
|
||||
|
||||
tests, err := expandPattern(&dsl.Pattern{
|
||||
DirsGlob: "cases/*",
|
||||
InputFile: "input.txt",
|
||||
OutputFile: "expected.txt",
|
||||
Args: []string{"{input_path}", "{output_path}"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(tests) != 2 {
|
||||
t.Fatalf("expected 2 tests, got %d", len(tests))
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if _, ok := tc.InFiles["input.txt"]; !ok {
|
||||
t.Errorf("%s: missing InFiles[input.txt]", tc.Name)
|
||||
}
|
||||
if _, ok := tc.OutFiles["expected.txt"]; !ok {
|
||||
t.Errorf("%s: missing OutFiles[expected.txt]", tc.Name)
|
||||
}
|
||||
if len(tc.Args) != 2 {
|
||||
t.Errorf("%s: args = %v", tc.Name, tc.Args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandPatternRejectsAllLiterals(t *testing.T) {
|
||||
_, err := expandPattern(&dsl.Pattern{
|
||||
InputGlob: "tests/a.in",
|
||||
OutputGlob: "tests/a.ans",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no glob fields")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "glob") {
|
||||
t.Errorf("error %q does not mention glob", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandPatternNoMatches(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cwd, _ := os.Getwd()
|
||||
defer os.Chdir(cwd)
|
||||
os.Chdir(dir)
|
||||
|
||||
_, err := expandPattern(&dsl.Pattern{
|
||||
InputGlob: "missing/*.in",
|
||||
OutputGlob: "missing/*.ans",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error on zero matches")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user