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) } }) } }