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 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) { _, 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") } }