add new build system
All checks were successful
build-dsl-smoke / Discover matrix (push) Successful in 8s
build-dsl-smoke / Build judge (push) Successful in 11s
build-dsl-smoke / ${{ matrix.cell.build }} / ${{ matrix.cell.toolchain }} / ${{ matrix.cell.platform }} (push) Successful in 5s
memory-limit / Build judge (pull_request) Successful in 10s
build-dsl-smoke / SUMMARY (push) Successful in 3s
memory-limit / Linux / gcc (pull_request) Successful in 9s
memory-limit / Linux / clang (pull_request) Successful in 13s
memory-limit / Windows / clang (pull_request) Successful in 16s
memory-limit / Windows / msvc (pull_request) Successful in 17s
All checks were successful
build-dsl-smoke / Discover matrix (push) Successful in 8s
build-dsl-smoke / Build judge (push) Successful in 11s
build-dsl-smoke / ${{ matrix.cell.build }} / ${{ matrix.cell.toolchain }} / ${{ matrix.cell.platform }} (push) Successful in 5s
memory-limit / Build judge (pull_request) Successful in 10s
build-dsl-smoke / SUMMARY (push) Successful in 3s
memory-limit / Linux / gcc (pull_request) Successful in 9s
memory-limit / Linux / clang (pull_request) Successful in 13s
memory-limit / Windows / clang (pull_request) Successful in 16s
memory-limit / Windows / msvc (pull_request) Successful in 17s
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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
350
runner/runner.go
350
runner/runner.go
@@ -21,6 +21,8 @@ type Config struct {
|
||||
WorkDir string
|
||||
BinaryName string
|
||||
Wrapper string
|
||||
|
||||
TargetBuild string
|
||||
}
|
||||
|
||||
type Runner struct {
|
||||
@@ -63,41 +65,271 @@ 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
|
||||
}
|
||||
|
||||
// runLegacyBuild handles the classic `build "shell-string"` form. It produces
|
||||
// a single BuildRun named "default" so that downstream consumers always see
|
||||
// the same shape.
|
||||
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
|
||||
// runStructuredBuilds handles the new DSL form with one or more named builds.
|
||||
// Each build is resolved against the current OS and toolchain, compiled via
|
||||
// the structured translator, and then exercised against every group.
|
||||
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 {
|
||||
// User asked for a different build. Don't include this one
|
||||
// in the result at all — discovery via --list-builds is the
|
||||
// caller's responsibility.
|
||||
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
|
||||
}
|
||||
|
||||
// runGroups exercises every group/test in the suite against the currently
|
||||
// selected binary (r.binary) and records the outcome into run.
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// fillBuildError populates a BuildRun with one failing synthetic test per
|
||||
// group when the build itself failed. This keeps the reported totals at 0
|
||||
// and matches the legacy behaviour.
|
||||
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
|
||||
}
|
||||
|
||||
// legacyBuild runs the free-form shell build command. Kept under the old
|
||||
// name so reviewers can diff against the previous implementation easily.
|
||||
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
|
||||
}
|
||||
|
||||
// compileStructured compiles one structured build via the translator and
|
||||
// returns the build log plus the absolute path to the produced binary.
|
||||
// The compiler is invoked via exec.Command directly — no shell involved.
|
||||
func (r *Runner) compileStructured(name string, cfg dsl.BuildConfig, tc Toolchain) (string, string, error) {
|
||||
// Expand source globs against the work dir.
|
||||
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
|
||||
|
||||
// Decide output path: <workdir>/build/<name>/<output>[.exe].
|
||||
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
|
||||
}
|
||||
|
||||
// expandSources expands each glob in patterns against workDir and returns
|
||||
// a slice of paths relative to workDir. Globs that match no files cause an
|
||||
// error — silent zero matches are almost always a typo.
|
||||
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 {
|
||||
// Fall back to treating it as a literal path.
|
||||
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 +384,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 +590,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