1. New build system
All checks were successful
build-dsl-smoke / Build judge (push) Successful in 12s
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 8s
build-dsl-smoke / release / gcc / linux (push) Successful in 6s
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 13s
build-dsl-smoke / debug-valgrind / gcc / linux (push) Successful in 14s
build-dsl-smoke / release / clang / windows (push) Successful in 16s
build-dsl-smoke / debug / msvc / windows (push) Successful in 18s
build-dsl-smoke / release / msvc / windows (push) Successful in 17s
build-dsl-smoke / SUMMARY (push) Successful in 4s
Release / Build & publish (push) Successful in 48s
All checks were successful
build-dsl-smoke / Build judge (push) Successful in 12s
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 8s
build-dsl-smoke / release / gcc / linux (push) Successful in 6s
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 13s
build-dsl-smoke / debug-valgrind / gcc / linux (push) Successful in 14s
build-dsl-smoke / release / clang / windows (push) Successful in 16s
build-dsl-smoke / debug / msvc / windows (push) Successful in 18s
build-dsl-smoke / release / msvc / windows (push) Successful in 17s
build-dsl-smoke / SUMMARY (push) Successful in 4s
Release / Build & publish (push) Successful in 48s
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
198
runner/compiler.go
Normal file
198
runner/compiler.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/Mond1c/judge/dsl"
|
||||
)
|
||||
|
||||
type CompilerClass int
|
||||
|
||||
const (
|
||||
CompilerUnknown CompilerClass = iota
|
||||
CompilerGNU
|
||||
CompilerMSVC
|
||||
)
|
||||
|
||||
type Toolchain struct {
|
||||
Class CompilerClass
|
||||
Binary string
|
||||
Name string
|
||||
}
|
||||
|
||||
func ResolveToolchain(ccEnv string) Toolchain {
|
||||
if ccEnv == "" {
|
||||
ccEnv = defaultCC()
|
||||
}
|
||||
lower := strings.ToLower(ccEnv)
|
||||
switch lower {
|
||||
case "gcc", "g++":
|
||||
return Toolchain{Class: CompilerGNU, Binary: ccEnv, Name: "gcc"}
|
||||
case "clang", "clang++":
|
||||
return Toolchain{Class: CompilerGNU, Binary: ccEnv, Name: "clang"}
|
||||
case "cl", "cl.exe", "msvc":
|
||||
return Toolchain{Class: CompilerMSVC, Binary: "cl", Name: "msvc"}
|
||||
case "clang-cl", "clang-cl.exe":
|
||||
return Toolchain{Class: CompilerMSVC, Binary: "clang-cl", Name: "clang-cl"}
|
||||
case "cc":
|
||||
return Toolchain{Class: CompilerGNU, Binary: "cc", Name: "cc"}
|
||||
default:
|
||||
return Toolchain{Class: CompilerGNU, Binary: ccEnv, Name: lower}
|
||||
}
|
||||
}
|
||||
|
||||
func ResolveToolchainSpec(spec *dsl.ToolchainSpec) Toolchain {
|
||||
inferred := ResolveToolchain(spec.Name)
|
||||
binary := spec.Binary
|
||||
if binary == "" {
|
||||
binary = inferred.Binary
|
||||
}
|
||||
var class CompilerClass
|
||||
switch spec.Class {
|
||||
case "gnu":
|
||||
class = CompilerGNU
|
||||
case "msvc":
|
||||
class = CompilerMSVC
|
||||
default:
|
||||
class = inferred.Class
|
||||
}
|
||||
return Toolchain{Class: class, Binary: binary, Name: spec.Name}
|
||||
}
|
||||
|
||||
func defaultCC() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "cl"
|
||||
}
|
||||
return "cc"
|
||||
}
|
||||
|
||||
func Compile(cfg dsl.BuildConfig, tc Toolchain, outputPath string) ([]string, error) {
|
||||
switch tc.Class {
|
||||
case CompilerGNU:
|
||||
return compileGNU(cfg, tc, outputPath), nil
|
||||
case CompilerMSVC:
|
||||
return compileMSVC(cfg, tc, outputPath), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown compiler class for toolchain %q", tc.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func compileGNU(cfg dsl.BuildConfig, tc Toolchain, outputPath string) []string {
|
||||
argv := []string{tc.Binary}
|
||||
|
||||
if cfg.Standard != "" {
|
||||
argv = append(argv, "-std="+cfg.Standard)
|
||||
}
|
||||
|
||||
switch cfg.Profile {
|
||||
case dsl.ProfileRelease:
|
||||
argv = append(argv, "-O2")
|
||||
case dsl.ProfileDebug:
|
||||
argv = append(argv, "-O0", "-g")
|
||||
case dsl.ProfileSanitized:
|
||||
argv = append(argv, "-O1", "-g", "-fno-omit-frame-pointer")
|
||||
}
|
||||
|
||||
switch cfg.Warnings {
|
||||
case dsl.WarningsStrict:
|
||||
argv = append(argv, "-Wall", "-Wextra")
|
||||
case dsl.WarningsPedantic:
|
||||
argv = append(argv, "-Wall", "-Wextra", "-Wpedantic")
|
||||
}
|
||||
|
||||
if len(cfg.Sanitize) > 0 {
|
||||
argv = append(argv, "-fsanitize="+strings.Join(cfg.Sanitize, ","))
|
||||
}
|
||||
|
||||
for _, inc := range cfg.Includes {
|
||||
argv = append(argv, "-I"+inc)
|
||||
}
|
||||
|
||||
for _, k := range sortedKeys(cfg.Defines) {
|
||||
v := cfg.Defines[k]
|
||||
if v == "" {
|
||||
argv = append(argv, "-D"+k)
|
||||
} else {
|
||||
argv = append(argv, fmt.Sprintf("-D%s=%s", k, v))
|
||||
}
|
||||
}
|
||||
|
||||
argv = append(argv, cfg.Extra...)
|
||||
argv = append(argv, cfg.Sources...)
|
||||
argv = append(argv, "-o", outputPath)
|
||||
|
||||
for _, lib := range cfg.Link {
|
||||
argv = append(argv, "-l"+lib)
|
||||
}
|
||||
return argv
|
||||
}
|
||||
|
||||
func compileMSVC(cfg dsl.BuildConfig, tc Toolchain, outputPath string) []string {
|
||||
argv := []string{tc.Binary, "/nologo"}
|
||||
|
||||
if cfg.Standard != "" {
|
||||
argv = append(argv, "/std:"+cfg.Standard)
|
||||
}
|
||||
|
||||
switch cfg.Profile {
|
||||
case dsl.ProfileRelease:
|
||||
argv = append(argv, "/O2")
|
||||
case dsl.ProfileDebug:
|
||||
argv = append(argv, "/Od", "/Zi")
|
||||
case dsl.ProfileSanitized:
|
||||
argv = append(argv, "/Od", "/Zi", "/fsanitize=address")
|
||||
}
|
||||
|
||||
switch cfg.Warnings {
|
||||
case dsl.WarningsStrict:
|
||||
argv = append(argv, "/W4")
|
||||
case dsl.WarningsPedantic:
|
||||
argv = append(argv, "/W4", "/permissive-")
|
||||
}
|
||||
|
||||
if containsString(cfg.Sanitize, "address") && cfg.Profile != dsl.ProfileSanitized {
|
||||
argv = append(argv, "/fsanitize=address")
|
||||
}
|
||||
|
||||
for _, inc := range cfg.Includes {
|
||||
argv = append(argv, "/I"+inc)
|
||||
}
|
||||
|
||||
for _, k := range sortedKeys(cfg.Defines) {
|
||||
v := cfg.Defines[k]
|
||||
if v == "" {
|
||||
argv = append(argv, "/D"+k)
|
||||
} else {
|
||||
argv = append(argv, fmt.Sprintf("/D%s=%s", k, v))
|
||||
}
|
||||
}
|
||||
|
||||
argv = append(argv, cfg.Extra...)
|
||||
argv = append(argv, cfg.Sources...)
|
||||
argv = append(argv, "/Fe:"+outputPath)
|
||||
return argv
|
||||
}
|
||||
|
||||
func sortedKeys(m map[string]string) []string {
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func containsString(xs []string, x string) bool {
|
||||
for _, v := range xs {
|
||||
if v == x {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
293
runner/compiler_test.go
Normal file
293
runner/compiler_test.go
Normal file
@@ -0,0 +1,293 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Mond1c/judge/dsl"
|
||||
)
|
||||
|
||||
func TestResolveToolchainKnown(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
wantClass CompilerClass
|
||||
wantName string
|
||||
}{
|
||||
{"gcc", CompilerGNU, "gcc"},
|
||||
{"g++", CompilerGNU, "gcc"},
|
||||
{"clang", CompilerGNU, "clang"},
|
||||
{"clang++", CompilerGNU, "clang"},
|
||||
{"cl", CompilerMSVC, "msvc"},
|
||||
{"cl.exe", CompilerMSVC, "msvc"},
|
||||
{"msvc", CompilerMSVC, "msvc"},
|
||||
{"clang-cl", CompilerMSVC, "clang-cl"},
|
||||
{"cc", CompilerGNU, "cc"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.in, func(t *testing.T) {
|
||||
tc := ResolveToolchain(c.in)
|
||||
if tc.Class != c.wantClass {
|
||||
t.Errorf("class: got %v, want %v", tc.Class, c.wantClass)
|
||||
}
|
||||
if tc.Name != c.wantName {
|
||||
t.Errorf("name: got %q, want %q", tc.Name, c.wantName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveToolchainSpec(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
spec dsl.ToolchainSpec
|
||||
wantClass CompilerClass
|
||||
wantBin string
|
||||
wantName string
|
||||
}{
|
||||
{
|
||||
"gcc inferred",
|
||||
dsl.ToolchainSpec{Name: "gcc", Platforms: []string{"linux"}},
|
||||
CompilerGNU, "gcc", "gcc",
|
||||
},
|
||||
{
|
||||
"msvc inferred",
|
||||
dsl.ToolchainSpec{Name: "msvc", Platforms: []string{"windows"}},
|
||||
CompilerMSVC, "cl", "msvc",
|
||||
},
|
||||
{
|
||||
"nvcc with explicit class",
|
||||
dsl.ToolchainSpec{Name: "nvcc", Platforms: []string{"linux"}, Class: "gnu"},
|
||||
CompilerGNU, "nvcc", "nvcc",
|
||||
},
|
||||
{
|
||||
"custom binary override",
|
||||
dsl.ToolchainSpec{Name: "clang", Platforms: []string{"linux", "windows"}, Binary: "clang-17"},
|
||||
CompilerGNU, "clang-17", "clang",
|
||||
},
|
||||
{
|
||||
"unknown name, explicit class",
|
||||
dsl.ToolchainSpec{Name: "hipcc", Platforms: []string{"linux"}, Class: "gnu"},
|
||||
CompilerGNU, "hipcc", "hipcc",
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := ResolveToolchainSpec(&c.spec)
|
||||
if got.Class != c.wantClass {
|
||||
t.Errorf("class: got %v, want %v", got.Class, c.wantClass)
|
||||
}
|
||||
if got.Binary != c.wantBin {
|
||||
t.Errorf("binary: got %q, want %q", got.Binary, c.wantBin)
|
||||
}
|
||||
if got.Name != c.wantName {
|
||||
t.Errorf("name: got %q, want %q", got.Name, c.wantName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveToolchainUnknownFallsBackToGNU(t *testing.T) {
|
||||
tc := ResolveToolchain("gcc-13")
|
||||
if tc.Class != CompilerGNU {
|
||||
t.Errorf("unknown compiler should fall back to GNU, got %v", tc.Class)
|
||||
}
|
||||
if tc.Binary != "gcc-13" {
|
||||
t.Errorf("binary should preserve the original string, got %q", tc.Binary)
|
||||
}
|
||||
if tc.Name != "gcc-13" {
|
||||
t.Errorf("name should be the lowercased original, got %q", tc.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileGNURelease(t *testing.T) {
|
||||
cfg := dsl.BuildConfig{
|
||||
Language: "c",
|
||||
Standard: "c11",
|
||||
Sources: []string{"solution.c"},
|
||||
Profile: dsl.ProfileRelease,
|
||||
Warnings: dsl.WarningsStrict,
|
||||
}
|
||||
tc := Toolchain{Class: CompilerGNU, Binary: "gcc", Name: "gcc"}
|
||||
argv, err := Compile(cfg, tc, "solution")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{"gcc", "-std=c11", "-O2", "-Wall", "-Wextra", "solution.c", "-o", "solution"}
|
||||
if !reflect.DeepEqual(argv, want) {
|
||||
t.Errorf("argv =\n %v\nwant\n %v", argv, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileGNUDebug(t *testing.T) {
|
||||
cfg := dsl.BuildConfig{
|
||||
Standard: "c11",
|
||||
Sources: []string{"a.c", "b.c"},
|
||||
Profile: dsl.ProfileDebug,
|
||||
}
|
||||
tc := Toolchain{Class: CompilerGNU, Binary: "gcc"}
|
||||
argv, _ := Compile(cfg, tc, "out")
|
||||
if !containsSubsequence(argv, []string{"-O0", "-g"}) {
|
||||
t.Errorf("debug flags missing: %v", argv)
|
||||
}
|
||||
if !containsSubsequence(argv, []string{"a.c", "b.c", "-o", "out"}) {
|
||||
t.Errorf("sources and output order wrong: %v", argv)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileGNUSanitized(t *testing.T) {
|
||||
cfg := dsl.BuildConfig{
|
||||
Standard: "c11",
|
||||
Sources: []string{"s.c"},
|
||||
Profile: dsl.ProfileSanitized,
|
||||
Sanitize: []string{"address", "undefined"},
|
||||
}
|
||||
tc := Toolchain{Class: CompilerGNU, Binary: "clang"}
|
||||
argv, _ := Compile(cfg, tc, "s")
|
||||
joined := strings.Join(argv, " ")
|
||||
if !strings.Contains(joined, "-fsanitize=address,undefined") {
|
||||
t.Errorf("sanitize flag missing: %v", argv)
|
||||
}
|
||||
if !strings.Contains(joined, "-O1") {
|
||||
t.Errorf("-O1 missing for sanitized profile: %v", argv)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileGNUIncludesAndDefinesAndLink(t *testing.T) {
|
||||
cfg := dsl.BuildConfig{
|
||||
Sources: []string{"m.c"},
|
||||
Includes: []string{"include", "vendor/inc"},
|
||||
Defines: map[string]string{"FOO": "1", "BAR": ""},
|
||||
Link: []string{"m", "pthread"},
|
||||
Extra: []string{"-fno-strict-aliasing"},
|
||||
}
|
||||
tc := Toolchain{Class: CompilerGNU, Binary: "gcc"}
|
||||
argv, _ := Compile(cfg, tc, "out")
|
||||
|
||||
joined := strings.Join(argv, " ")
|
||||
for _, want := range []string{"-Iinclude", "-Ivendor/inc", "-DFOO=1", "-DBAR", "-lm", "-lpthread", "-fno-strict-aliasing"} {
|
||||
if !strings.Contains(joined, want) {
|
||||
t.Errorf("missing %q in %v", want, argv)
|
||||
}
|
||||
}
|
||||
|
||||
oIdx := indexOf(argv, "-o")
|
||||
lmIdx := indexOf(argv, "-lm")
|
||||
if oIdx == -1 || lmIdx == -1 || lmIdx < oIdx {
|
||||
t.Errorf("-lm must come after -o: %v", argv)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileGNUDefinesOrderDeterministic(t *testing.T) {
|
||||
cfg := dsl.BuildConfig{
|
||||
Sources: []string{"s.c"},
|
||||
Defines: map[string]string{"Z": "1", "A": "2", "M": "3"},
|
||||
}
|
||||
tc := Toolchain{Class: CompilerGNU, Binary: "gcc"}
|
||||
argv1, _ := Compile(cfg, tc, "s")
|
||||
for i := 0; i < 20; i++ {
|
||||
argv2, _ := Compile(cfg, tc, "s")
|
||||
if !reflect.DeepEqual(argv1, argv2) {
|
||||
t.Fatalf("defines order not deterministic:\n %v\n %v", argv1, argv2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileMSVCRelease(t *testing.T) {
|
||||
cfg := dsl.BuildConfig{
|
||||
Standard: "c11",
|
||||
Sources: []string{"solution.c"},
|
||||
Profile: dsl.ProfileRelease,
|
||||
Warnings: dsl.WarningsStrict,
|
||||
}
|
||||
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"}
|
||||
if !reflect.DeepEqual(argv, want) {
|
||||
t.Errorf("argv =\n %v\nwant\n %v", argv, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileMSVCDebug(t *testing.T) {
|
||||
cfg := dsl.BuildConfig{
|
||||
Sources: []string{"s.c"},
|
||||
Profile: dsl.ProfileDebug,
|
||||
}
|
||||
tc := Toolchain{Class: CompilerMSVC, Binary: "cl"}
|
||||
argv, _ := Compile(cfg, tc, "s.exe")
|
||||
joined := strings.Join(argv, " ")
|
||||
for _, want := range []string{"/Od", "/Zi"} {
|
||||
if !strings.Contains(joined, want) {
|
||||
t.Errorf("missing %q in %v", want, argv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileMSVCSanitizedAddressOnly(t *testing.T) {
|
||||
cfg := dsl.BuildConfig{
|
||||
Sources: []string{"s.c"},
|
||||
Profile: dsl.ProfileSanitized,
|
||||
Sanitize: []string{"address", "undefined", "thread"},
|
||||
}
|
||||
tc := Toolchain{Class: CompilerMSVC, Binary: "cl"}
|
||||
argv, _ := Compile(cfg, tc, "s.exe")
|
||||
joined := strings.Join(argv, " ")
|
||||
if !strings.Contains(joined, "/fsanitize=address") {
|
||||
t.Errorf("msvc should emit /fsanitize=address for sanitized profile: %v", argv)
|
||||
}
|
||||
if strings.Contains(joined, "undefined") || strings.Contains(joined, "thread") {
|
||||
t.Errorf("msvc should drop unsupported sanitizers, got: %v", argv)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileMSVCIncludesAndDefines(t *testing.T) {
|
||||
cfg := dsl.BuildConfig{
|
||||
Sources: []string{"m.c"},
|
||||
Includes: []string{"include"},
|
||||
Defines: map[string]string{"FOO": "1", "BAR": ""},
|
||||
}
|
||||
tc := Toolchain{Class: CompilerMSVC, Binary: "cl"}
|
||||
argv, _ := Compile(cfg, tc, "m.exe")
|
||||
joined := strings.Join(argv, " ")
|
||||
for _, want := range []string{"/Iinclude", "/DFOO=1", "/DBAR"} {
|
||||
if !strings.Contains(joined, want) {
|
||||
t.Errorf("missing %q in %v", want, argv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileUnknownClassErrors(t *testing.T) {
|
||||
cfg := dsl.BuildConfig{Sources: []string{"s.c"}}
|
||||
tc := Toolchain{Class: CompilerUnknown, Binary: "weird"}
|
||||
if _, err := Compile(cfg, tc, "s"); err == nil {
|
||||
t.Error("expected error for unknown compiler class")
|
||||
}
|
||||
}
|
||||
|
||||
func containsSubsequence(haystack, needle []string) bool {
|
||||
if len(needle) == 0 {
|
||||
return true
|
||||
}
|
||||
for i := 0; i+len(needle) <= len(haystack); i++ {
|
||||
match := true
|
||||
for j := range needle {
|
||||
if haystack[i+j] != needle[j] {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func indexOf(xs []string, x string) int {
|
||||
for i, v := range xs {
|
||||
if v == x {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package runner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -40,8 +41,8 @@ type TestResult struct {
|
||||
Status Status
|
||||
Elapsed time.Duration
|
||||
|
||||
PeakMemory int64 // bytes; 0 if not measured
|
||||
MemoryLimit int64 // bytes; 0 if unlimited
|
||||
PeakMemory int64
|
||||
MemoryLimit int64
|
||||
|
||||
Failures []string
|
||||
|
||||
@@ -64,8 +65,40 @@ type GroupResult struct {
|
||||
Total int
|
||||
}
|
||||
|
||||
type SuiteResult struct {
|
||||
type BuildRun struct {
|
||||
Name string
|
||||
Toolchain string
|
||||
Skipped bool
|
||||
SkipReason string
|
||||
|
||||
BuildLog string
|
||||
Groups []*GroupResult
|
||||
TotalScore float64
|
||||
BuildLog string
|
||||
}
|
||||
|
||||
type SuiteResult struct {
|
||||
Builds []*BuildRun
|
||||
|
||||
TotalScore float64
|
||||
}
|
||||
|
||||
func (r *SuiteResult) AggregateScore() float64 {
|
||||
if len(r.Builds) == 0 {
|
||||
return 0
|
||||
}
|
||||
min := math.Inf(1)
|
||||
anyRan := false
|
||||
for _, b := range r.Builds {
|
||||
if b.Skipped {
|
||||
continue
|
||||
}
|
||||
anyRan = true
|
||||
if b.TotalScore < min {
|
||||
min = b.TotalScore
|
||||
}
|
||||
}
|
||||
if !anyRan {
|
||||
return 1.0
|
||||
}
|
||||
return min
|
||||
}
|
||||
|
||||
57
runner/result_test.go
Normal file
57
runner/result_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package runner
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAggregateScoreEmpty(t *testing.T) {
|
||||
r := &SuiteResult{}
|
||||
if got := r.AggregateScore(); got != 0 {
|
||||
t.Errorf("empty aggregate = %v, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateScoreSingleBuild(t *testing.T) {
|
||||
r := &SuiteResult{
|
||||
Builds: []*BuildRun{{Name: "release", TotalScore: 0.75}},
|
||||
}
|
||||
if got := r.AggregateScore(); got != 0.75 {
|
||||
t.Errorf("single build aggregate = %v, want 0.75", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateScoreTakesMinimum(t *testing.T) {
|
||||
r := &SuiteResult{
|
||||
Builds: []*BuildRun{
|
||||
{Name: "release", TotalScore: 1.0},
|
||||
{Name: "debug", TotalScore: 0.9},
|
||||
{Name: "sanitized", TotalScore: 0.95},
|
||||
},
|
||||
}
|
||||
if got := r.AggregateScore(); got != 0.9 {
|
||||
t.Errorf("aggregate = %v, want 0.9 (minimum)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateScoreIgnoresSkipped(t *testing.T) {
|
||||
r := &SuiteResult{
|
||||
Builds: []*BuildRun{
|
||||
{Name: "release", TotalScore: 1.0},
|
||||
{Name: "sanitized", Skipped: true, SkipReason: "platforms=linux"},
|
||||
{Name: "debug", TotalScore: 0.8},
|
||||
},
|
||||
}
|
||||
if got := r.AggregateScore(); got != 0.8 {
|
||||
t.Errorf("aggregate with skipped = %v, want 0.8", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateScoreAllSkipped(t *testing.T) {
|
||||
r := &SuiteResult{
|
||||
Builds: []*BuildRun{
|
||||
{Name: "a", Skipped: true},
|
||||
{Name: "b", Skipped: true},
|
||||
},
|
||||
}
|
||||
if got := r.AggregateScore(); got != 1.0 {
|
||||
t.Errorf("all-skipped aggregate = %v, want 1.0", got)
|
||||
}
|
||||
}
|
||||
325
runner/runner.go
325
runner/runner.go
@@ -21,6 +21,8 @@ type Config struct {
|
||||
WorkDir string
|
||||
BinaryName string
|
||||
Wrapper string
|
||||
|
||||
TargetBuild string
|
||||
}
|
||||
|
||||
type Runner struct {
|
||||
@@ -63,41 +65,246 @@ func (r *Runner) Run() *SuiteResult {
|
||||
|
||||
result := &SuiteResult{}
|
||||
|
||||
buildLog, err := r.build()
|
||||
result.BuildLog = buildLog
|
||||
if err != nil {
|
||||
for _, g := range r.file.Groups {
|
||||
gr := &GroupResult{
|
||||
Name: g.Name,
|
||||
Weight: g.Weight,
|
||||
Score: 0,
|
||||
}
|
||||
if len(r.file.Builds) == 0 {
|
||||
run := r.runLegacyBuild()
|
||||
result.Builds = append(result.Builds, run)
|
||||
} else {
|
||||
result.Builds = r.runStructuredBuilds()
|
||||
}
|
||||
|
||||
total := len(g.Tests)
|
||||
if g.Pattern != nil {
|
||||
total = -1
|
||||
}
|
||||
gr.Total = total
|
||||
for _, t := range g.Tests {
|
||||
gr.Tests = append(gr.Tests, &TestResult{
|
||||
Name: t.Name,
|
||||
Status: StatusBuildError,
|
||||
})
|
||||
}
|
||||
result.Groups = append(result.Groups, gr)
|
||||
}
|
||||
return result
|
||||
result.TotalScore = result.AggregateScore()
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *Runner) runLegacyBuild() *BuildRun {
|
||||
run := &BuildRun{Name: "default"}
|
||||
|
||||
if r.cfg.TargetBuild != "" && r.cfg.TargetBuild != "default" {
|
||||
run.Skipped = true
|
||||
run.SkipReason = fmt.Sprintf("--build=%q selected, but this suite has no structured builds", r.cfg.TargetBuild)
|
||||
return run
|
||||
}
|
||||
|
||||
buildLog, err := r.legacyBuild()
|
||||
run.BuildLog = buildLog
|
||||
if err != nil {
|
||||
r.fillBuildError(run)
|
||||
return run
|
||||
}
|
||||
|
||||
r.binary = resolveBinary(r.cfg.WorkDir, filepath.Base(r.binary))
|
||||
r.runGroups(run)
|
||||
return run
|
||||
}
|
||||
|
||||
for _, g := range r.file.Groups {
|
||||
gr := r.runGroup(g)
|
||||
result.Groups = append(result.Groups, gr)
|
||||
result.TotalScore += gr.Score
|
||||
func (r *Runner) resolveRuntimeToolchain() (Toolchain, string) {
|
||||
goos := runtime.GOOS
|
||||
wanted := os.Getenv("JUDGE_TOOLCHAIN")
|
||||
if wanted == "" {
|
||||
wanted = os.Getenv("JUDGE_CC")
|
||||
}
|
||||
for _, spec := range r.file.Toolchains {
|
||||
if spec.Name == wanted {
|
||||
return ResolveToolchainSpec(spec), goos
|
||||
}
|
||||
}
|
||||
return ResolveToolchain(wanted), goos
|
||||
}
|
||||
|
||||
func (r *Runner) runStructuredBuilds() []*BuildRun {
|
||||
tc, goos := r.resolveRuntimeToolchain()
|
||||
var runs []*BuildRun
|
||||
|
||||
for _, b := range r.file.Builds {
|
||||
run := &BuildRun{Name: b.Name, Toolchain: tc.Name}
|
||||
|
||||
if r.cfg.TargetBuild != "" && r.cfg.TargetBuild != b.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
effective := b.Resolve(r.file.BuildDefaults, goos)
|
||||
if !effective.AppliesTo(goos, tc.Name) {
|
||||
run.Skipped = true
|
||||
run.SkipReason = fmt.Sprintf("not applicable to %s/%s (platforms=%v, compilers=%v)", goos, tc.Name, effective.Platforms, effective.Compilers)
|
||||
runs = append(runs, run)
|
||||
continue
|
||||
}
|
||||
|
||||
log, binaryPath, err := r.compileStructured(b.Name, effective, tc)
|
||||
run.BuildLog = log
|
||||
if err != nil {
|
||||
run.Groups = r.synthesizeBuildError()
|
||||
run.TotalScore = 0
|
||||
runs = append(runs, run)
|
||||
continue
|
||||
}
|
||||
|
||||
prevBinary := r.binary
|
||||
prevWrapper := r.cfg.Wrapper
|
||||
r.binary = binaryPath
|
||||
if r.cfg.Wrapper == "" && effective.Wrapper != "" {
|
||||
r.cfg.Wrapper = effective.Wrapper
|
||||
}
|
||||
r.runGroups(run)
|
||||
r.binary = prevBinary
|
||||
r.cfg.Wrapper = prevWrapper
|
||||
|
||||
runs = append(runs, run)
|
||||
}
|
||||
|
||||
return result
|
||||
return runs
|
||||
}
|
||||
|
||||
func (r *Runner) runGroups(run *BuildRun) {
|
||||
for _, g := range r.file.Groups {
|
||||
gr := r.runGroup(g)
|
||||
run.Groups = append(run.Groups, gr)
|
||||
run.TotalScore += gr.Score
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) fillBuildError(run *BuildRun) {
|
||||
run.Groups = r.synthesizeBuildError()
|
||||
}
|
||||
|
||||
func (r *Runner) synthesizeBuildError() []*GroupResult {
|
||||
var out []*GroupResult
|
||||
for _, g := range r.file.Groups {
|
||||
gr := &GroupResult{
|
||||
Name: g.Name,
|
||||
Weight: g.Weight,
|
||||
Score: 0,
|
||||
}
|
||||
total := len(g.Tests)
|
||||
if g.Pattern != nil {
|
||||
total = -1
|
||||
}
|
||||
gr.Total = total
|
||||
for _, t := range g.Tests {
|
||||
gr.Tests = append(gr.Tests, &TestResult{
|
||||
Name: t.Name,
|
||||
Status: StatusBuildError,
|
||||
})
|
||||
}
|
||||
out = append(out, gr)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Runner) legacyBuild() (string, error) {
|
||||
buildCmd := r.buildCommand()
|
||||
|
||||
sources, err := r.findSources()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if sources != "" {
|
||||
buildCmd = strings.ReplaceAll(buildCmd, "$SOURCES", sources)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if r.file.Timeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, r.file.Timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
cmd := shellCommand(ctx, buildCmd)
|
||||
cmd.Dir = r.cfg.WorkDir
|
||||
setProcessGroup(cmd)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &out
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
killProcessGroup(cmd)
|
||||
return out.String(), fmt.Errorf("build failed: %w\n%s", err, out.String())
|
||||
}
|
||||
return out.String(), nil
|
||||
}
|
||||
|
||||
func (r *Runner) compileStructured(name string, cfg dsl.BuildConfig, tc Toolchain) (string, string, error) {
|
||||
sources, err := expandSources(r.cfg.WorkDir, cfg.Sources)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if len(sources) == 0 {
|
||||
return "", "", fmt.Errorf("build %q: no sources", name)
|
||||
}
|
||||
cfg.Sources = sources
|
||||
|
||||
outputName := cfg.Output
|
||||
if outputName == "" {
|
||||
outputName = "solution"
|
||||
}
|
||||
if runtime.GOOS == "windows" && !strings.HasSuffix(strings.ToLower(outputName), ".exe") {
|
||||
outputName += ".exe"
|
||||
}
|
||||
buildDir := filepath.Join(r.cfg.WorkDir, "build", name)
|
||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||
return "", "", fmt.Errorf("mkdir %s: %w", buildDir, err)
|
||||
}
|
||||
outputPath := filepath.Join(buildDir, outputName)
|
||||
|
||||
argv, err := Compile(cfg, tc, outputPath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if r.file.Timeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, r.file.Timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...)
|
||||
cmd.Dir = r.cfg.WorkDir
|
||||
setProcessGroup(cmd)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &out
|
||||
|
||||
logPrefix := fmt.Sprintf("$ %s\n", strings.Join(argv, " "))
|
||||
if err := cmd.Run(); err != nil {
|
||||
killProcessGroup(cmd)
|
||||
return logPrefix + out.String(), "", fmt.Errorf("build %q failed: %w\n%s", name, err, out.String())
|
||||
}
|
||||
return logPrefix + out.String(), outputPath, nil
|
||||
}
|
||||
|
||||
func expandSources(workDir string, patterns []string) ([]string, error) {
|
||||
var out []string
|
||||
seen := map[string]bool{}
|
||||
for _, pat := range patterns {
|
||||
matches, err := filepath.Glob(filepath.Join(workDir, pat))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("glob %q: %w", pat, err)
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
if _, statErr := os.Stat(filepath.Join(workDir, pat)); statErr == nil {
|
||||
matches = []string{filepath.Join(workDir, pat)}
|
||||
} else {
|
||||
return nil, fmt.Errorf("source glob %q matched no files", pat)
|
||||
}
|
||||
}
|
||||
for _, m := range matches {
|
||||
rel, err := filepath.Rel(workDir, m)
|
||||
if err != nil {
|
||||
rel = m
|
||||
}
|
||||
rel = filepath.ToSlash(rel)
|
||||
if !seen[rel] {
|
||||
seen[rel] = true
|
||||
out = append(out, rel)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Runner) buildCommand() string {
|
||||
@@ -152,40 +359,6 @@ func (r *Runner) findSources() (string, error) {
|
||||
return strings.Join(files, " "), nil
|
||||
}
|
||||
|
||||
func (r *Runner) build() (string, error) {
|
||||
buildCmd := r.buildCommand()
|
||||
|
||||
sources, err := r.findSources()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if sources != "" {
|
||||
buildCmd = strings.ReplaceAll(buildCmd, "$SOURCES", sources)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if r.file.Timeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, r.file.Timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
cmd := shellCommand(ctx, buildCmd)
|
||||
cmd.Dir = r.cfg.WorkDir
|
||||
setProcessGroup(cmd)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &out
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
killProcessGroup(cmd)
|
||||
return out.String(), fmt.Errorf("build failed: %w\n%s", err, out.String())
|
||||
}
|
||||
return out.String(), nil
|
||||
}
|
||||
|
||||
func shellCommand(ctx context.Context, cmdline string) *exec.Cmd {
|
||||
if runtime.GOOS == "windows" {
|
||||
return exec.CommandContext(ctx, "cmd", "/C", cmdline)
|
||||
@@ -392,21 +565,23 @@ func (r *Runner) runTest(t *dsl.Test) *TestResult {
|
||||
tr.addFailure("%s", f)
|
||||
}
|
||||
|
||||
for name, expected := range t.OutFiles {
|
||||
path := filepath.Join(tmpDir, name)
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
tr.addFailure("output file %q not found: %v", name, err)
|
||||
continue
|
||||
}
|
||||
actual := normalizeOutput(string(content), r.file)
|
||||
for _, f := range applyMatcher(fmt.Sprintf("file(%s)", name), dsl.ExactMatcher{Value: expected}, actual) {
|
||||
tr.addFailure("%s", f)
|
||||
}
|
||||
if len(tr.Failures) > 0 {
|
||||
tr.Status = StatusFail
|
||||
}
|
||||
|
||||
if tr.Status == StatusPass && len(tr.Failures) > 0 {
|
||||
tr.Status = StatusFail
|
||||
for name, expected := range t.OutFiles {
|
||||
actualPath := filepath.Join(tmpDir, name)
|
||||
data, err := os.ReadFile(actualPath)
|
||||
if err != nil {
|
||||
tr.Status = StatusFail
|
||||
tr.addFailure("output file %q missing: %v", name, err)
|
||||
continue
|
||||
}
|
||||
actual := normalizeOutput(string(data), r.file)
|
||||
if actual != expected {
|
||||
tr.Status = StatusFail
|
||||
tr.addFailure("output file %q mismatch\n expected: %q\n actual: %q", name, expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
return tr
|
||||
|
||||
Reference in New Issue
Block a user