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

This commit is contained in:
2026-04-11 01:51:38 +03:00
parent 358e3146bc
commit 128a64a609
15 changed files with 2448 additions and 150 deletions

View File

@@ -7,13 +7,18 @@ type File struct {
BuildLinux string
BuildWindows string
BuildDarwin string
Timeout time.Duration
MemoryLimit int64 // bytes; 0 means no limit
Binary string // executable name produced by build (default: solution)
Sources string // glob pattern for source files, expanded as $SOURCES in build
NormalizeCRLF bool // strip \r before matching stdout/stderr/outFiles
TrimTrailingWS bool // trim trailing whitespace on each line before matching
BuildDefaults *BuildConfig
Builds []*BuildConfig
Toolchains []*ToolchainSpec
Timeout time.Duration
MemoryLimit int64
Binary string
Sources string
NormalizeCRLF bool
TrimTrailingWS bool
Groups []*Group
}
@@ -25,7 +30,7 @@ type Group struct {
MemoryLimit int64
Env map[string]string
Scoring ScoringMode
Wrapper string // exec wrapper command (e.g., "valgrind --error-exitcode=1")
Wrapper string
Tests []*Test
Pattern *Pattern
@@ -34,8 +39,8 @@ type Group struct {
type ScoringMode int
const (
ScoringPartial ScoringMode = iota // weight * passed/total (default)
ScoringAllOrNone // weight or 0
ScoringPartial ScoringMode = iota
ScoringAllOrNone
)
type Pattern struct {

187
dsl/build.go Normal file
View File

@@ -0,0 +1,187 @@
package dsl
// BuildProfile is a named compilation profile. The translator maps it into
// compiler-specific flag sets at execution time (e.g. ProfileRelease → "-O2"
// on gnu-like compilers, "/O2" on msvc).
type BuildProfile int
const (
ProfileUnset BuildProfile = iota
ProfileRelease
ProfileDebug
ProfileSanitized
)
func (p BuildProfile) String() string {
switch p {
case ProfileRelease:
return "release"
case ProfileDebug:
return "debug"
case ProfileSanitized:
return "sanitized"
default:
return "unset"
}
}
// WarningLevel describes how strict the compiler should be about warnings.
type WarningLevel int
const (
WarningsUnset WarningLevel = iota
WarningsDefault
WarningsStrict
WarningsPedantic
)
func (w WarningLevel) String() string {
switch w {
case WarningsDefault:
return "default"
case WarningsStrict:
return "strict"
case WarningsPedantic:
return "pedantic"
default:
return "unset"
}
}
// BuildConfig describes one structured build variant. It is the new-style
// replacement for the free-form `build "shell-string"` field.
//
// A top-level `build_defaults { ... }` in the suite file produces a
// BuildConfig stored on File.BuildDefaults. Each `build "name" { ... }`
// block produces an entry in File.Builds; the effective configuration used
// by the runner is BuildDefaults merged with the named block, then merged
// with the OS-specific override (Linux / Windows / Darwin) when present.
//
// Zero-valued fields inherit from the parent during merge. Slice and map
// fields accumulate rather than replace.
type BuildConfig struct {
// Name of the variant. Empty on BuildDefaults and on OS-override sub-blocks.
Name string
Language string // e.g. "c", "c++"
Standard string // e.g. "c11", "c++17"
Sources []string // globs, relative to work dir
Includes []string // include search paths
Output string // binary name (OS-specific extension added automatically)
Profile BuildProfile
Warnings WarningLevel
Sanitize []string
Wrapper string // e.g. "address", "undefined", "thread"
Defines map[string]string
Link []string // libraries to link against (e.g. "pthread", "m")
Extra []string // raw passthrough flags
// Filters — empty means "applies to any". A build is skipped at runtime
// if the current OS or compiler is not in the list.
Platforms []string // "linux", "windows", "darwin"
Compilers []string // "gcc", "clang", "msvc"
// OS-specific overrides. Only one level of nesting is allowed: these
// sub-configs must not themselves contain Linux/Windows/Darwin blocks.
Linux *BuildConfig
Windows *BuildConfig
Darwin *BuildConfig
}
// MergeFrom layers src on top of dst in place. Non-zero scalar fields in src
// overwrite dst; slices and maps accumulate. The Name and OS override fields
// on src are intentionally ignored — merging never copies the hierarchy,
// only the leaves.
func (dst *BuildConfig) MergeFrom(src *BuildConfig) {
if src == nil {
return
}
if src.Language != "" {
dst.Language = src.Language
}
if src.Standard != "" {
dst.Standard = src.Standard
}
if src.Output != "" {
dst.Output = src.Output
}
if src.Profile != ProfileUnset {
dst.Profile = src.Profile
}
if src.Warnings != WarningsUnset {
dst.Warnings = src.Warnings
}
if src.Wrapper != "" {
dst.Wrapper = src.Wrapper
}
dst.Sources = append(dst.Sources, src.Sources...)
dst.Includes = append(dst.Includes, src.Includes...)
dst.Sanitize = append(dst.Sanitize, src.Sanitize...)
dst.Link = append(dst.Link, src.Link...)
dst.Extra = append(dst.Extra, src.Extra...)
dst.Platforms = append(dst.Platforms, src.Platforms...)
dst.Compilers = append(dst.Compilers, src.Compilers...)
if len(src.Defines) > 0 {
if dst.Defines == nil {
dst.Defines = map[string]string{}
}
for k, v := range src.Defines {
dst.Defines[k] = v
}
}
}
// Resolve returns the effective BuildConfig for the given OS by merging
// BuildDefaults → this block → the matching OS override. The result is a
// fresh value; the receiver is not mutated.
func (b *BuildConfig) Resolve(defaults *BuildConfig, os string) BuildConfig {
var out BuildConfig
out.MergeFrom(defaults)
out.MergeFrom(b)
out.Name = b.Name
var osOverride *BuildConfig
switch os {
case "linux":
osOverride = b.Linux
case "windows":
osOverride = b.Windows
case "darwin":
osOverride = b.Darwin
}
out.MergeFrom(osOverride)
return out
}
// AppliesTo reports whether this build should run on (os, compiler).
// An empty Platforms/Compilers list means no filter on that axis.
func (b *BuildConfig) AppliesTo(os, compiler string) bool {
if len(b.Platforms) > 0 && !contains(b.Platforms, os) {
return false
}
if len(b.Compilers) > 0 && !contains(b.Compilers, compiler) {
return false
}
return true
}
func contains(xs []string, x string) bool {
for _, v := range xs {
if v == x {
return true
}
}
return false
}
type ToolchainSpec struct {
Name string
Platforms []string
Binary string
Class string
}

355
dsl/build_parser.go Normal file
View File

@@ -0,0 +1,355 @@
package dsl
import "fmt"
func (p *Parser) parseBuildBlock(name string) (*BuildConfig, error) {
return p.parseBuildBlockInner(name, false)
}
func (p *Parser) parseBuildBlockInner(name string, inOSOverride bool) (*BuildConfig, error) {
if _, err := p.expect(TOKEN_LBRACE); err != nil {
return nil, err
}
bc := &BuildConfig{Name: name}
for !p.isRBrace() {
t := p.peek()
if t.Type != TOKEN_IDENT {
return nil, fmt.Errorf("%d:%d: unexpected token %q in build block", t.Line, t.Col, t.Value)
}
switch t.Value {
case "language":
p.advance()
s, err := p.parseAssignString()
if err != nil {
return nil, err
}
bc.Language = s
case "standard":
p.advance()
s, err := p.parseAssignString()
if err != nil {
return nil, err
}
bc.Standard = s
case "output":
p.advance()
s, err := p.parseAssignString()
if err != nil {
return nil, err
}
bc.Output = s
case "wrapper":
p.advance()
s, err := p.parseAssignString()
if err != nil {
return nil, err
}
bc.Wrapper = s
case "sources":
p.advance()
xs, err := p.parseAssignStringList()
if err != nil {
return nil, err
}
bc.Sources = xs
case "includes":
p.advance()
xs, err := p.parseAssignStringList()
if err != nil {
return nil, err
}
bc.Includes = xs
case "sanitize":
p.advance()
xs, err := p.parseAssignStringList()
if err != nil {
return nil, err
}
bc.Sanitize = xs
case "link":
p.advance()
xs, err := p.parseAssignStringList()
if err != nil {
return nil, err
}
bc.Link = xs
case "extra":
p.advance()
xs, err := p.parseAssignStringList()
if err != nil {
return nil, err
}
bc.Extra = xs
case "platforms":
p.advance()
xs, err := p.parseAssignStringList()
if err != nil {
return nil, err
}
if err := validatePlatformList(xs, t.Line, t.Col); err != nil {
return nil, err
}
bc.Platforms = xs
case "compilers":
p.advance()
xs, err := p.parseAssignStringList()
if err != nil {
return nil, err
}
bc.Compilers = xs
case "profile":
p.advance()
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
return nil, err
}
id, err := p.expect(TOKEN_IDENT)
if err != nil {
return nil, err
}
prof, err := parseProfileIdent(id.Value, id.Line, id.Col)
if err != nil {
return nil, err
}
bc.Profile = prof
case "warnings":
p.advance()
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
return nil, err
}
id, err := p.expect(TOKEN_IDENT)
if err != nil {
return nil, err
}
w, err := parseWarningsIdent(id.Value, id.Line, id.Col)
if err != nil {
return nil, err
}
bc.Warnings = w
case "define":
p.advance()
if _, err := p.expect(TOKEN_LPAREN); err != nil {
return nil, err
}
key, err := p.expect(TOKEN_STRING)
if err != nil {
return nil, err
}
if _, err := p.expect(TOKEN_RPAREN); err != nil {
return nil, err
}
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
return nil, err
}
val, err := p.expect(TOKEN_STRING)
if err != nil {
return nil, err
}
if bc.Defines == nil {
bc.Defines = map[string]string{}
}
bc.Defines[key.Value] = val.Value
case "linux", "windows", "darwin":
if inOSOverride {
return nil, fmt.Errorf("%d:%d: OS override %q cannot be nested inside another OS override", t.Line, t.Col, t.Value)
}
osName := t.Value
p.advance()
sub, err := p.parseBuildBlockInner("", true)
if err != nil {
return nil, err
}
switch osName {
case "linux":
if bc.Linux != nil {
return nil, fmt.Errorf("%d:%d: duplicate linux override", t.Line, t.Col)
}
bc.Linux = sub
case "windows":
if bc.Windows != nil {
return nil, fmt.Errorf("%d:%d: duplicate windows override", t.Line, t.Col)
}
bc.Windows = sub
case "darwin":
if bc.Darwin != nil {
return nil, fmt.Errorf("%d:%d: duplicate darwin override", t.Line, t.Col)
}
bc.Darwin = sub
}
default:
return nil, fmt.Errorf("%d:%d: unknown field %q in build block", t.Line, t.Col, t.Value)
}
}
if _, err := p.expect(TOKEN_RBRACE); err != nil {
return nil, err
}
return bc, nil
}
func (p *Parser) parseAssignString() (string, error) {
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
return "", err
}
s, err := p.expect(TOKEN_STRING)
if err != nil {
return "", err
}
return s.Value, nil
}
func (p *Parser) parseAssignStringList() ([]string, error) {
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
return nil, err
}
return p.parseStringList()
}
func parseProfileIdent(v string, line, col int) (BuildProfile, error) {
switch v {
case "release":
return ProfileRelease, nil
case "debug":
return ProfileDebug, nil
case "sanitized":
return ProfileSanitized, nil
default:
return ProfileUnset, fmt.Errorf("%d:%d: unknown profile %q (expected release/debug/sanitized)", line, col, v)
}
}
func parseWarningsIdent(v string, line, col int) (WarningLevel, error) {
switch v {
case "default":
return WarningsDefault, nil
case "strict":
return WarningsStrict, nil
case "pedantic":
return WarningsPedantic, nil
default:
return WarningsUnset, fmt.Errorf("%d:%d: unknown warnings level %q (expected default/strict/pedantic)", line, col, v)
}
}
func validatePlatformList(xs []string, line, col int) error {
for _, x := range xs {
switch x {
case "linux", "windows", "darwin":
default:
return fmt.Errorf("%d:%d: unknown platform %q (expected linux/windows/darwin)", line, col, x)
}
}
return nil
}
func (p *Parser) parseToolchainsBlock() ([]*ToolchainSpec, error) {
if _, err := p.expect(TOKEN_LBRACE); err != nil {
return nil, err
}
var specs []*ToolchainSpec
seen := map[string]bool{}
for !p.isRBrace() {
nameTok := p.peek()
if nameTok.Type != TOKEN_IDENT && nameTok.Type != TOKEN_STRING {
return nil, fmt.Errorf("%d:%d: expected toolchain name, got %q", nameTok.Line, nameTok.Col, nameTok.Value)
}
p.advance()
name := nameTok.Value
if seen[name] {
return nil, fmt.Errorf("%d:%d: duplicate toolchain %q", nameTok.Line, nameTok.Col, name)
}
seen[name] = true
spec, err := p.parseToolchainEntry(name)
if err != nil {
return nil, err
}
specs = append(specs, spec)
}
if _, err := p.expect(TOKEN_RBRACE); err != nil {
return nil, err
}
return specs, nil
}
func (p *Parser) parseToolchainEntry(name string) (*ToolchainSpec, error) {
if _, err := p.expect(TOKEN_LBRACE); err != nil {
return nil, err
}
spec := &ToolchainSpec{Name: name}
for !p.isRBrace() {
t := p.peek()
if t.Type != TOKEN_IDENT {
return nil, fmt.Errorf("%d:%d: unexpected token %q in toolchain block", t.Line, t.Col, t.Value)
}
switch t.Value {
case "platforms":
p.advance()
xs, err := p.parseAssignStringList()
if err != nil {
return nil, err
}
if err := validatePlatformList(xs, t.Line, t.Col); err != nil {
return nil, err
}
spec.Platforms = xs
case "binary":
p.advance()
s, err := p.parseAssignString()
if err != nil {
return nil, err
}
spec.Binary = s
case "class":
p.advance()
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
return nil, err
}
id, err := p.expect(TOKEN_IDENT)
if err != nil {
return nil, err
}
switch id.Value {
case "gnu", "msvc":
default:
return nil, fmt.Errorf("%d:%d: unknown compiler class %q (expected gnu/msvc)", id.Line, id.Col, id.Value)
}
spec.Class = id.Value
default:
return nil, fmt.Errorf("%d:%d: unknown field %q in toolchain block", t.Line, t.Col, t.Value)
}
}
if _, err := p.expect(TOKEN_RBRACE); err != nil {
return nil, err
}
if len(spec.Platforms) == 0 {
return nil, fmt.Errorf("toolchain %q: platforms is required", name)
}
return spec, nil
}

455
dsl/build_test.go Normal file
View File

@@ -0,0 +1,455 @@
package dsl
import (
"strings"
"testing"
)
func TestParseBuildBlockMinimal(t *testing.T) {
src := `
build_defaults {
language = "c"
standard = "c11"
sources = "*.c"
output = "solution"
warnings = strict
}
build "release" {
profile = release
}
build "debug" {
profile = debug
}
group("g1") {
weight = 1.0
test("t1") {
stdout = "ok\n"
}
}
`
f, _, err := Parse(src)
if err != nil {
t.Fatalf("parse: %v", err)
}
if f.BuildDefaults == nil {
t.Fatal("BuildDefaults not populated")
}
if f.BuildDefaults.Language != "c" {
t.Errorf("language = %q", f.BuildDefaults.Language)
}
if f.BuildDefaults.Standard != "c11" {
t.Errorf("standard = %q", f.BuildDefaults.Standard)
}
if len(f.BuildDefaults.Sources) != 1 || f.BuildDefaults.Sources[0] != "*.c" {
t.Errorf("sources = %v", f.BuildDefaults.Sources)
}
if f.BuildDefaults.Output != "solution" {
t.Errorf("output = %q", f.BuildDefaults.Output)
}
if f.BuildDefaults.Warnings != WarningsStrict {
t.Errorf("warnings = %v", f.BuildDefaults.Warnings)
}
if len(f.Builds) != 2 {
t.Fatalf("expected 2 builds, got %d", len(f.Builds))
}
if f.Builds[0].Name != "release" || f.Builds[0].Profile != ProfileRelease {
t.Errorf("builds[0] = %+v", f.Builds[0])
}
if f.Builds[1].Name != "debug" || f.Builds[1].Profile != ProfileDebug {
t.Errorf("builds[1] = %+v", f.Builds[1])
}
}
func TestParseBuildLegacyStillWorks(t *testing.T) {
src := `
build "cc -O2 solution.c -o solution"
timeout 5s
group("g1") {
weight = 1.0
test("t1") { stdout = "ok\n" }
}
`
f, _, err := Parse(src)
if err != nil {
t.Fatalf("parse: %v", err)
}
if f.Build != "cc -O2 solution.c -o solution" {
t.Errorf("legacy build = %q", f.Build)
}
if f.BuildDefaults != nil || len(f.Builds) != 0 {
t.Errorf("structured fields should be empty for legacy form")
}
}
func TestParseBuildOSOverride(t *testing.T) {
src := `
build_defaults {
language = "c"
standard = "c11"
sources = "*.c"
output = "solution"
}
build "release" {
profile = release
linux { extra = "-fPIC" }
windows { extra = "/bigobj" }
}
group("g1") {
weight = 1.0
test("t1") { stdout = "ok\n" }
}
`
f, _, err := Parse(src)
if err != nil {
t.Fatalf("parse: %v", err)
}
b := f.Builds[0]
if b.Linux == nil || len(b.Linux.Extra) != 1 || b.Linux.Extra[0] != "-fPIC" {
t.Errorf("linux override = %+v", b.Linux)
}
if b.Windows == nil || len(b.Windows.Extra) != 1 || b.Windows.Extra[0] != "/bigobj" {
t.Errorf("windows override = %+v", b.Windows)
}
if b.Darwin != nil {
t.Errorf("darwin override should be nil")
}
}
func TestParseBuildPlatformsFilter(t *testing.T) {
src := `
build "sanitized" {
profile = sanitized
sanitize = "address" "undefined"
platforms = "linux"
compilers = "gcc" "clang"
}
group("g1") {
weight = 1.0
test("t1") { stdout = "ok\n" }
}
`
f, _, err := Parse(src)
if err != nil {
t.Fatalf("parse: %v", err)
}
b := f.Builds[0]
if b.Profile != ProfileSanitized {
t.Errorf("profile = %v", b.Profile)
}
if len(b.Sanitize) != 2 || b.Sanitize[0] != "address" || b.Sanitize[1] != "undefined" {
t.Errorf("sanitize = %v", b.Sanitize)
}
if len(b.Platforms) != 1 || b.Platforms[0] != "linux" {
t.Errorf("platforms = %v", b.Platforms)
}
if len(b.Compilers) != 2 {
t.Errorf("compilers = %v", b.Compilers)
}
}
func TestParseBuildDefineField(t *testing.T) {
src := `
build "rel" {
profile = release
define("NDEBUG") = "1"
define("VERSION") = "42"
}
group("g1") {
weight = 1.0
test("t1") { stdout = "ok\n" }
}
`
f, _, err := Parse(src)
if err != nil {
t.Fatalf("parse: %v", err)
}
d := f.Builds[0].Defines
if d["NDEBUG"] != "1" || d["VERSION"] != "42" {
t.Errorf("defines = %v", d)
}
}
func TestParseBuildErrors(t *testing.T) {
cases := []struct {
name string
src string
want string
}{
{
name: "unknown profile",
src: `
build "x" { profile = ultra }
group("g") { weight=1.0 test("t"){ stdout="ok\n" } }
`,
want: "unknown profile",
},
{
name: "unknown warnings",
src: `
build "x" { warnings = insane }
group("g") { weight=1.0 test("t"){ stdout="ok\n" } }
`,
want: "unknown warnings",
},
{
name: "unknown platform",
src: `
build "x" { platforms = "bsd" }
group("g") { weight=1.0 test("t"){ stdout="ok\n" } }
`,
want: "unknown platform",
},
{
name: "nested OS override",
src: `
build "x" {
linux {
windows { extra = "nope" }
}
}
group("g") { weight=1.0 test("t"){ stdout="ok\n" } }
`,
want: "cannot be nested",
},
{
name: "duplicate OS override",
src: `
build "x" {
linux { extra = "-a" }
linux { extra = "-b" }
}
group("g") { weight=1.0 test("t"){ stdout="ok\n" } }
`,
want: "duplicate linux override",
},
{
name: "unknown field",
src: `
build "x" { magic = "yes" }
group("g") { weight=1.0 test("t"){ stdout="ok\n" } }
`,
want: "unknown field",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
_, _, err := Parse(c.src)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), c.want) {
t.Errorf("error %q does not contain %q", err.Error(), c.want)
}
})
}
}
func TestParseBuildMixedLegacyAndStructuredRejected(t *testing.T) {
src := `
build "cc -O2 solution.c -o solution"
build "release" { profile = release }
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
`
_, _, err := Parse(src)
if err == nil {
t.Fatal("expected error mixing legacy and structured builds")
}
if !strings.Contains(err.Error(), "cannot mix") {
t.Errorf("error %q does not mention mixing", err.Error())
}
}
func TestParseBuildDuplicateNameRejected(t *testing.T) {
src := `
build "release" { profile = release }
build "release" { profile = debug }
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
`
_, _, err := Parse(src)
if err == nil {
t.Fatal("expected error on duplicate build name")
}
if !strings.Contains(err.Error(), "duplicate") {
t.Errorf("error %q does not mention duplicate", err.Error())
}
}
func TestBuildConfigResolveMerge(t *testing.T) {
defaults := &BuildConfig{
Language: "c",
Standard: "c11",
Sources: []string{"*.c"},
Output: "solution",
Warnings: WarningsStrict,
}
b := &BuildConfig{
Name: "release",
Profile: ProfileRelease,
Extra: []string{"-DFOO"},
Linux: &BuildConfig{
Extra: []string{"-fPIC"},
},
Windows: &BuildConfig{
Extra: []string{"/bigobj"},
Output: "solution",
},
}
linux := b.Resolve(defaults, "linux")
if linux.Language != "c" || linux.Standard != "c11" {
t.Errorf("linux defaults not merged: %+v", linux)
}
if linux.Profile != ProfileRelease {
t.Errorf("linux profile = %v", linux.Profile)
}
if linux.Warnings != WarningsStrict {
t.Errorf("linux warnings = %v", linux.Warnings)
}
if len(linux.Sources) != 1 || linux.Sources[0] != "*.c" {
t.Errorf("linux sources = %v", linux.Sources)
}
if len(linux.Extra) != 2 || linux.Extra[0] != "-DFOO" || linux.Extra[1] != "-fPIC" {
t.Errorf("linux extra = %v", linux.Extra)
}
windows := b.Resolve(defaults, "windows")
if len(windows.Extra) != 2 || windows.Extra[0] != "-DFOO" || windows.Extra[1] != "/bigobj" {
t.Errorf("windows extra = %v", windows.Extra)
}
darwin := b.Resolve(defaults, "darwin")
if len(darwin.Extra) != 1 || darwin.Extra[0] != "-DFOO" {
t.Errorf("darwin extra (no override) = %v", darwin.Extra)
}
if len(b.Extra) != 1 {
t.Errorf("receiver mutated: %v", b.Extra)
}
}
func TestParseToolchainsBlock(t *testing.T) {
src := `
toolchains {
gcc { platforms = "linux" }
clang {
platforms = "linux" "windows"
binary = "clang-17"
class = gnu
}
msvc { platforms = "windows" }
nvcc {
platforms = "linux"
class = gnu
}
}
build "release" { profile = release }
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
`
f, _, err := Parse(src)
if err != nil {
t.Fatalf("parse: %v", err)
}
if len(f.Toolchains) != 4 {
t.Fatalf("expected 4 toolchains, got %d", len(f.Toolchains))
}
gcc := f.Toolchains[0]
if gcc.Name != "gcc" || len(gcc.Platforms) != 1 || gcc.Platforms[0] != "linux" {
t.Errorf("gcc = %+v", gcc)
}
clang := f.Toolchains[1]
if clang.Name != "clang" || len(clang.Platforms) != 2 || clang.Binary != "clang-17" || clang.Class != "gnu" {
t.Errorf("clang = %+v", clang)
}
if clang.Platforms[0] != "linux" || clang.Platforms[1] != "windows" {
t.Errorf("clang platforms = %v", clang.Platforms)
}
nvcc := f.Toolchains[3]
if nvcc.Name != "nvcc" || len(nvcc.Platforms) != 1 || nvcc.Platforms[0] != "linux" || nvcc.Class != "gnu" {
t.Errorf("nvcc = %+v", nvcc)
}
}
func TestParseToolchainsErrors(t *testing.T) {
cases := []struct {
name string
src string
want string
}{
{
"missing platforms",
`toolchains { gcc { } }
group("g") { weight=1.0 test("t") { stdout="ok\n" } }`,
"platforms is required",
},
{
"unknown platform",
`toolchains { gcc { platforms = "bsd" } }
group("g") { weight=1.0 test("t") { stdout="ok\n" } }`,
"unknown platform",
},
{
"unknown class",
`toolchains { gcc { platforms = "linux" class = llvm } }
group("g") { weight=1.0 test("t") { stdout="ok\n" } }`,
"unknown compiler class",
},
{
"duplicate toolchain",
`toolchains {
gcc { platforms = "linux" }
gcc { platforms = "linux" }
}
group("g") { weight=1.0 test("t") { stdout="ok\n" } }`,
"duplicate toolchain",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
_, _, err := Parse(c.src)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), c.want) {
t.Errorf("error %q does not contain %q", err.Error(), c.want)
}
})
}
}
func TestBuildConfigAppliesTo(t *testing.T) {
cases := []struct {
name string
b BuildConfig
os, cc string
expected bool
}{
{"no filters", BuildConfig{}, "linux", "gcc", true},
{"os match", BuildConfig{Platforms: []string{"linux"}}, "linux", "gcc", true},
{"os mismatch", BuildConfig{Platforms: []string{"linux"}}, "windows", "msvc", false},
{"cc match", BuildConfig{Compilers: []string{"gcc", "clang"}}, "linux", "clang", true},
{"cc mismatch", BuildConfig{Compilers: []string{"gcc"}}, "linux", "msvc", false},
{"both match", BuildConfig{Platforms: []string{"linux"}, Compilers: []string{"gcc"}}, "linux", "gcc", true},
{"os ok cc bad", BuildConfig{Platforms: []string{"linux"}, Compilers: []string{"gcc"}}, "linux", "clang", false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := c.b.AppliesTo(c.os, c.cc)
if got != c.expected {
t.Errorf("AppliesTo(%q, %q) = %v, want %v", c.os, c.cc, got, c.expected)
}
})
}
}

View File

@@ -91,7 +91,31 @@ func (p *Parser) parseFile() (*File, error) {
if err != nil {
return nil, err
}
f.Build = s.Value
if p.peek().Type == TOKEN_LBRACE {
bc, err := p.parseBuildBlock(s.Value)
if err != nil {
return nil, err
}
f.Builds = append(f.Builds, bc)
} else {
f.Build = s.Value
}
case "build_defaults":
p.advance()
bc, err := p.parseBuildBlock("")
if err != nil {
return nil, err
}
f.BuildDefaults = bc
case "toolchains":
p.advance()
specs, err := p.parseToolchainsBlock()
if err != nil {
return nil, err
}
f.Toolchains = specs
case "build_linux":
p.advance()
@@ -195,10 +219,34 @@ func (p *Parser) parseFile() (*File, error) {
if err := p.validateWeights(f); err != nil {
return nil, err
}
if err := validateBuilds(f); err != nil {
return nil, err
}
return f, nil
}
func validateBuilds(f *File) error {
hasLegacy := f.Build != "" || f.BuildLinux != "" || f.BuildWindows != "" || f.BuildDarwin != ""
hasStructured := f.BuildDefaults != nil || len(f.Builds) > 0
if hasLegacy && hasStructured {
return fmt.Errorf("cannot mix legacy `build \"shell\"` with structured `build \"name\" { ... }` in the same suite")
}
seen := map[string]bool{}
for _, b := range f.Builds {
if b.Name == "" {
return fmt.Errorf("structured build must have a name")
}
if seen[b.Name] {
return fmt.Errorf("duplicate build name %q", b.Name)
}
seen[b.Name] = true
}
return nil
}
func (p *Parser) validateWeights(f *File) error {
if len(f.Groups) == 0 {
return nil
@@ -715,8 +763,6 @@ func (p *Parser) parseInt() (int, error) {
return n, nil
}
// parseSize accepts either a TOKEN_SIZE (e.g. "256MB", "1GiB", "512K") or a bare
// TOKEN_INT interpreted as bytes. MiB/MB are both 1024² — we use IEC semantics.
func (p *Parser) parseSize() (int64, error) {
t := p.peek()
switch t.Type {