diff --git a/assets/vendor/toolrelay.cpp b/assets/vendor/toolrelay.cpp index 6939f24..73f9970 100644 --- a/assets/vendor/toolrelay.cpp +++ b/assets/vendor/toolrelay.cpp @@ -79,8 +79,16 @@ HANDLE MakeKillOnCloseJob() { } bool IsMtExe(const wchar_t *path) { - const wchar_t *name = wcsrchr(path, L'\\'); - name = name ? name + 1 : path; + // vintner passes toolExePath straight through from Go's filepath.Join, + // which uses forward slashes even for a path that's about to be handed + // to a native Windows process - so the separator here isn't reliably + // '\\'. Check both; using whichever comes later in the string covers a + // mixed-separator path too. + const wchar_t *back = wcsrchr(path, L'\\'); + const wchar_t *fwd = wcsrchr(path, L'/'); + const wchar_t *sep = back; + if (fwd && (!sep || fwd > sep)) sep = fwd; + const wchar_t *name = sep ? sep + 1 : path; return _wcsicmp(name, L"mt.exe") == 0; } diff --git a/internal/download/extract_test.go b/internal/download/extract_test.go new file mode 100644 index 0000000..841e4b2 --- /dev/null +++ b/internal/download/extract_test.go @@ -0,0 +1,152 @@ +package download + +import ( + "os" + "path/filepath" + "testing" +) + +func writeFile(t *testing.T, path, content string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatal(err) + } +} + +func TestCombineDirTreesNonexistentSrcIsNoop(t *testing.T) { + dest := t.TempDir() + if err := combineDirTrees(filepath.Join(t.TempDir(), "does-not-exist"), dest); err != nil { + t.Fatalf("combineDirTrees with a nonexistent src returned an error: %v", err) + } +} + +func TestCombineDirTreesRenamesWholesaleWhenDestMissing(t *testing.T) { + root := t.TempDir() + src := filepath.Join(root, "src") + dest := filepath.Join(root, "nested", "dest") + writeFile(t, filepath.Join(src, "file.txt"), "hello") + + if err := combineDirTrees(src, dest); err != nil { + t.Fatal(err) + } + if _, err := os.ReadFile(filepath.Join(dest, "file.txt")); err != nil { + t.Errorf("expected %s/file.txt to exist after combine: %v", dest, err) + } + if isDir(src) { + t.Error("src should have been moved (renamed), not copied") + } +} + +func TestCombineDirTreesMergesNewSubdir(t *testing.T) { + root := t.TempDir() + src, dest := filepath.Join(root, "src"), filepath.Join(root, "dest") + writeFile(t, filepath.Join(src, "NewDir", "a.txt"), "a") + writeFile(t, filepath.Join(dest, "Existing.txt"), "keep me") + + if err := combineDirTrees(src, dest); err != nil { + t.Fatal(err) + } + if _, err := os.ReadFile(filepath.Join(dest, "NewDir", "a.txt")); err != nil { + t.Errorf("expected merged NewDir/a.txt: %v", err) + } + if _, err := os.ReadFile(filepath.Join(dest, "Existing.txt")); err != nil { + t.Errorf("pre-existing dest file was lost: %v", err) + } +} + +func TestCombineDirTreesMergesCaseInsensitiveCollision(t *testing.T) { + root := t.TempDir() + src, dest := filepath.Join(root, "src"), filepath.Join(root, "dest") + // src has "Include" (capital I), dest already has "include" (lowercase) - + // this is exactly the MSVC/WinSDK casing-inconsistency scenario the + // function's doc comment describes. + writeFile(t, filepath.Join(src, "Include", "new.h"), "new") + writeFile(t, filepath.Join(dest, "include", "old.h"), "old") + + if err := combineDirTrees(src, dest); err != nil { + t.Fatal(err) + } + if _, err := os.ReadFile(filepath.Join(dest, "include", "new.h")); err != nil { + t.Errorf("new.h should have merged into the existing lowercase 'include' dir: %v", err) + } + if _, err := os.ReadFile(filepath.Join(dest, "include", "old.h")); err != nil { + t.Errorf("old.h should still be there: %v", err) + } + if isDir(filepath.Join(dest, "Include")) { + t.Error("a separate capital-I 'Include' dir should not have been created") + } +} + +func TestCombineDirTreesRecursesIntoExactNameMatch(t *testing.T) { + root := t.TempDir() + src, dest := filepath.Join(root, "src"), filepath.Join(root, "dest") + writeFile(t, filepath.Join(src, "lib", "x64", "new.lib"), "new") + writeFile(t, filepath.Join(dest, "lib", "x64", "old.lib"), "old") + + if err := combineDirTrees(src, dest); err != nil { + t.Fatal(err) + } + for _, f := range []string{"new.lib", "old.lib"} { + if _, err := os.ReadFile(filepath.Join(dest, "lib", "x64", f)); err != nil { + t.Errorf("expected lib/x64/%s to survive the merge: %v", f, err) + } + } +} + +func TestCopyRedirectedAssembliesNoConfigIsNoop(t *testing.T) { + dir := t.TempDir() + app := filepath.Join(dir, "MSBuild.exe") + if err := CopyRedirectedAssemblies(app); err != nil { + t.Fatalf("with no .config file present, expected no error, got: %v", err) + } +} + +func TestCopyRedirectedAssembliesCopiesReferencedDLL(t *testing.T) { + dir := t.TempDir() + app := filepath.Join(dir, "MSBuild.exe") + writeFile(t, app+".config", ` + + + + + + + + +`) + writeFile(t, filepath.Join(dir, "amd64", "Some.Assembly.dll"), "binary-content") + + if err := CopyRedirectedAssemblies(app); err != nil { + t.Fatal(err) + } + + got, err := os.ReadFile(filepath.Join(dir, "Some.Assembly.dll")) + if err != nil { + t.Fatalf("expected Some.Assembly.dll copied next to MSBuild.exe: %v", err) + } + if string(got) != "binary-content" { + t.Errorf("copied file content = %q, want %q", got, "binary-content") + } +} + +func TestCopyRedirectedAssembliesSkipsMissingTarget(t *testing.T) { + dir := t.TempDir() + app := filepath.Join(dir, "MSBuild.exe") + writeFile(t, app+".config", ` + + + + + + + + +`) + + if err := CopyRedirectedAssemblies(app); err != nil { + t.Fatalf("a redirect pointing at a nonexistent file should be silently skipped, got: %v", err) + } +} diff --git a/internal/download/fetch_test.go b/internal/download/fetch_test.go new file mode 100644 index 0000000..cf2a715 --- /dev/null +++ b/internal/download/fetch_test.go @@ -0,0 +1,59 @@ +package download + +import ( + "os" + "path/filepath" + "testing" + "time" +) + +func TestRetryBackoff(t *testing.T) { + for _, tc := range []struct { + attempt int + want time.Duration + }{ + {1, 1 * time.Second}, + {2, 2 * time.Second}, + {3, 4 * time.Second}, + {4, 8 * time.Second}, + {5, 10 * time.Second}, // capped + {10, 10 * time.Second}, + } { + if got := retryBackoff(tc.attempt); got != tc.want { + t.Errorf("retryBackoff(%d) = %v, want %v", tc.attempt, got, tc.want) + } + } +} + +func TestSHA256File(t *testing.T) { + path := filepath.Join(t.TempDir(), "f") + if err := os.WriteFile(path, []byte("hello"), 0o644); err != nil { + t.Fatal(err) + } + got, err := sha256File(path) + if err != nil { + t.Fatal(err) + } + // echo -n hello | sha256sum + want := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" + if got != want { + t.Errorf("sha256File(hello) = %q, want %q", got, want) + } +} + +func TestEqualFoldHex(t *testing.T) { + for _, tc := range []struct { + a, b string + want bool + }{ + {"ABCDEF", "abcdef", true}, + {"abc123", "ABC123", true}, + {"abc123", "abc124", false}, + {"abc", "abcd", false}, + {"", "", true}, + } { + if got := equalFoldHex(tc.a, tc.b); got != tc.want { + t.Errorf("equalFoldHex(%q, %q) = %v, want %v", tc.a, tc.b, got, tc.want) + } + } +} diff --git a/internal/download/manifest_test.go b/internal/download/manifest_test.go new file mode 100644 index 0000000..6042854 --- /dev/null +++ b/internal/download/manifest_test.go @@ -0,0 +1,134 @@ +package download + +import ( + "encoding/json" + "testing" +) + +func TestPayloadName(t *testing.T) { + for _, tc := range []struct { + fileName string + want string + }{ + {"payload.msi", "payload.msi"}, + {"folder/payload.msi", "payload.msi"}, + {`folder\payload.msi`, "payload.msi"}, + {`a\b/c\payload.msi`, "payload.msi"}, + {"", ""}, + } { + p := Payload{FileName: tc.fileName} + if got := p.Name(); got != tc.want { + t.Errorf("Payload{FileName: %q}.Name() = %q, want %q", tc.fileName, got, tc.want) + } + } +} + +func TestPackageKey(t *testing.T) { + for _, tc := range []struct { + name string + p Package + want string + }{ + {"id only", Package{ID: "Foo"}, "Foo"}, + {"id+version", Package{ID: "Foo", Version: "1.0"}, "Foo-1.0"}, + { + "id+version+all arches", + Package{ID: "Foo", Version: "1.0", Chip: "x64", MachineArch: "x86", ProductArch: "neutral"}, + "Foo-1.0-chip.x64-machineArch.x86-productArch.neutral", + }, + } { + t.Run(tc.name, func(t *testing.T) { + if got := tc.p.Key(); got != tc.want { + t.Errorf("Key() = %q, want %q", got, tc.want) + } + }) + } +} + +func TestPackageLocalized(t *testing.T) { + noResources := Package{} + if got := noResources.Localized("en"); got != nil { + t.Errorf("Localized() on a package with no LocalizedResources = %v, want nil", got) + } + + p := Package{LocalizedResources: []LocalizedResource{ + {Language: "de-DE", Title: "Deutsch"}, + {Language: "en-US", Title: "English"}, + {Language: "ru-RU", Title: "Русский"}, + }} + + for _, tc := range []struct { + lang string + wantTitle string + }{ + {"ru", "Русский"}, + {"ru-RU", "Русский"}, + {"en", "English"}, + {"", "English"}, // "" defaults to "en" + {"fr", "English"}, // no fr variant, falls back to the en-* one + } { + t.Run("lang="+tc.lang, func(t *testing.T) { + got := p.Localized(tc.lang) + if got == nil { + t.Fatalf("Localized(%q) = nil", tc.lang) + } + if got.Title != tc.wantTitle { + t.Errorf("Localized(%q).Title = %q, want %q", tc.lang, got.Title, tc.wantTitle) + } + }) + } +} + +func TestPackageSizes(t *testing.T) { + p := Package{ + InstallSizes: map[string]int64{"x86": 100, "x64": 200}, + Payloads: []Payload{{Size: 10}, {Size: 20}, {Size: 30}}, + } + if got := p.InstalledSize(); got != 300 { + t.Errorf("InstalledSize() = %d, want 300", got) + } + if got := p.DownloadSize(); got != 60 { + t.Errorf("DownloadSize() = %d, want 60", got) + } +} + +func TestPackageDependenciesNormalizesBothShapes(t *testing.T) { + p := Package{DependenciesRaw: map[string]json.RawMessage{ + "Bare.Version": json.RawMessage(`"1.0"`), + "Full.Object": json.RawMessage(`{"version":"2.0","type":"Optional","id":"Real.Target"}`), + "Recommended.Dep": json.RawMessage(`{"version":"3.0","type":"Recommended"}`), + }} + deps := p.Dependencies() + + if d := deps["Bare.Version"]; d.Version != "1.0" || d.TargetID != "" || d.Type != "" { + t.Errorf("Bare.Version = %+v, want Version=1.0 TargetID='' Type=''", d) + } + if d := deps["Full.Object"]; d.Version != "2.0" || d.TargetID != "Real.Target" || d.Type != "Optional" { + t.Errorf("Full.Object = %+v, want Version=2.0 TargetID=Real.Target Type=Optional", d) + } + if d := deps["Recommended.Dep"]; d.Version != "3.0" || d.Type != "Recommended" { + t.Errorf("Recommended.Dep = %+v, want Version=3.0 Type=Recommended", d) + } + + // Calling Dependencies() again must return the same cached map, not + // re-parse (and must not panic on the second call). + if d2 := p.Dependencies(); len(d2) != len(deps) { + t.Errorf("second Dependencies() call returned a different map: %v vs %v", d2, deps) + } +} + +func TestHumanizeBytes(t *testing.T) { + for _, tc := range []struct { + size int64 + want string + }{ + {500, "500 bytes"}, + {2048, "2.0 KB"}, + {5 * 1024 * 1024, "5.0 MB"}, + {2 * 1024 * 1024 * 1024, "2.0 GB"}, + } { + if got := HumanizeBytes(tc.size); got != tc.want { + t.Errorf("HumanizeBytes(%d) = %q, want %q", tc.size, got, tc.want) + } + } +} diff --git a/internal/download/wdk_test.go b/internal/download/wdk_test.go new file mode 100644 index 0000000..67c7107 --- /dev/null +++ b/internal/download/wdk_test.go @@ -0,0 +1,118 @@ +package download + +import ( + "os" + "path/filepath" + "testing" +) + +func TestWDKNuGetID(t *testing.T) { + for _, tc := range []struct{ arch, want string }{ + {"x64", "Microsoft.Windows.WDK.x64"}, + {"x86", "Microsoft.Windows.WDK.x64"}, // no 32-bit package exists + {"arm64", "Microsoft.Windows.WDK.ARM64"}, + } { + if got := WDKNuGetID(tc.arch); got != tc.want { + t.Errorf("WDKNuGetID(%q) = %q, want %q", tc.arch, got, tc.want) + } + } +} + +func TestSDKBuildPrefix(t *testing.T) { + for _, tc := range []struct { + name string + selected []*Package + want string + }{ + {"no SDK package", []*Package{{ID: "Something.Else"}}, ""}, + {"win10sdk", []*Package{{ID: "Win10SDK_10.0.26100", Version: "10.0.26100.1742"}}, "10.0.26100"}, + {"win11sdk case-insensitive id", []*Package{{ID: "WIN11SDK_10.0.22621", Version: "10.0.22621.5"}}, "10.0.22621"}, + {"short version", []*Package{{ID: "Win10SDK_x", Version: "10.0"}}, ""}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := SDKBuildPrefix(tc.selected); got != tc.want { + t.Errorf("SDKBuildPrefix(...) = %q, want %q", got, tc.want) + } + }) + } +} + +func TestFillMissingHostToolsCopiesWithoutOverwriting(t *testing.T) { + cDir := t.TempDir() + writeFile(t, filepath.Join(cDir, "bin", "10.0.26100.0", "x64", "stampinf.exe"), "x64-stampinf") + writeFile(t, filepath.Join(cDir, "bin", "10.0.26100.0", "x64", "inf2cat.exe"), "x64-inf2cat") + // x86 already ships its own real inf2cat.exe - must not be clobbered. + writeFile(t, filepath.Join(cDir, "bin", "10.0.26100.0", "x86", "inf2cat.exe"), "real-x86-inf2cat") + + if err := fillMissingHostTools(cDir); err != nil { + t.Fatal(err) + } + + x86Dir := filepath.Join(cDir, "bin", "10.0.26100.0", "x86") + stampinf, err := os.ReadFile(filepath.Join(x86Dir, "stampinf.exe")) + if err != nil { + t.Fatalf("expected stampinf.exe to be copied into x86: %v", err) + } + if string(stampinf) != "x64-stampinf" { + t.Errorf("copied stampinf.exe content = %q, want the x64 copy's content", stampinf) + } + + inf2cat, err := os.ReadFile(filepath.Join(x86Dir, "inf2cat.exe")) + if err != nil { + t.Fatal(err) + } + if string(inf2cat) != "real-x86-inf2cat" { + t.Errorf("inf2cat.exe = %q, want the original x86 file preserved (not overwritten by the x64 copy)", inf2cat) + } +} + +func TestFillMissingHostToolsNoBinDirIsNoop(t *testing.T) { + if err := fillMissingHostTools(t.TempDir()); err != nil { + t.Fatalf("missing bin/ dir should be a no-op, got: %v", err) + } +} + +func TestDuplicateVersionedBuildTaskAssemblies(t *testing.T) { + cDir := t.TempDir() + buildDir := filepath.Join(cDir, "build") + writeFile(t, filepath.Join(buildDir, "Microsoft.DriverKit.Build.Tasks.17.0.dll"), "task-dll-bytes") + writeFile(t, filepath.Join(buildDir, "Microsoft.DriverKit.Build.Tasks.18.0.dll"), "already-there") + writeFile(t, filepath.Join(buildDir, "unrelated.dll"), "unrelated") + + if err := duplicateVersionedBuildTaskAssemblies(cDir, "18.0"); err != nil { + t.Fatal(err) + } + + // Already had an 18.0 copy - must not have been overwritten. + got, err := os.ReadFile(filepath.Join(buildDir, "Microsoft.DriverKit.Build.Tasks.18.0.dll")) + if err != nil { + t.Fatal(err) + } + if string(got) != "already-there" { + t.Errorf("pre-existing 18.0 dll was overwritten: got %q", got) + } +} + +func TestDuplicateVersionedBuildTaskAssembliesCreatesMissingCopy(t *testing.T) { + cDir := t.TempDir() + buildDir := filepath.Join(cDir, "build") + writeFile(t, filepath.Join(buildDir, "sub", "Foo.Bar.17.0.dll"), "bytes") + + if err := duplicateVersionedBuildTaskAssemblies(cDir, "18.0"); err != nil { + t.Fatal(err) + } + + got, err := os.ReadFile(filepath.Join(buildDir, "sub", "Foo.Bar.18.0.dll")) + if err != nil { + t.Fatalf("expected a Foo.Bar.18.0.dll duplicate: %v", err) + } + if string(got) != "bytes" { + t.Errorf("duplicated dll content = %q, want %q", got, "bytes") + } +} + +func TestDuplicateVersionedBuildTaskAssembliesNoBuildDirIsNoop(t *testing.T) { + if err := duplicateVersionedBuildTaskAssemblies(t.TempDir(), "18.0"); err != nil { + t.Fatalf("missing build/ dir should be a no-op, got: %v", err) + } +} diff --git a/internal/wrapper/clpost_test.go b/internal/wrapper/clpost_test.go new file mode 100644 index 0000000..aca1887 --- /dev/null +++ b/internal/wrapper/clpost_test.go @@ -0,0 +1,61 @@ +package wrapper + +import ( + "os" + "path/filepath" + "testing" +) + +func TestClPostProcessRewritesLineDirectivesInPreprocessedOutput(t *testing.T) { + dir := t.TempDir() + fi := filepath.Join(dir, "out.i") + // A #line directive as cl.exe's /P emits it: z:-prefixed, backslash + // path, doubled ("escaped") backslashes, CRLF line ending. + input := "#line 1 \"z:\\\\home\\\\user\\\\src\\\\hello.c\"\r\n" + + "int main(void) { return 0; }\r\n" + if err := os.WriteFile(fi, []byte(input), 0o644); err != nil { + t.Fatal(err) + } + + clPostProcess([]string{"/P", "/Fi" + fi, "hello.c"}) + + got, err := os.ReadFile(fi) + if err != nil { + t.Fatal(err) + } + want := "#line 1 \"/home/user/src/hello.c\"\n" + + "int main(void) { return 0; }\n" + if string(got) != want { + t.Errorf("clPostProcess output = %q, want %q", got, want) + } +} + +func TestClPostProcessNoopWithoutP(t *testing.T) { + dir := t.TempDir() + fi := filepath.Join(dir, "out.i") + original := "#line 1 \"z:\\\\foo.c\"\r\n" + if err := os.WriteFile(fi, []byte(original), 0o644); err != nil { + t.Fatal(err) + } + + // No "/P" flag - should leave the file untouched even though -Fi is present. + clPostProcess([]string{"/Fi" + fi, "foo.c"}) + + got, err := os.ReadFile(fi) + if err != nil { + t.Fatal(err) + } + if string(got) != original { + t.Errorf("file was modified without /P: got %q, want unchanged %q", got, original) + } +} + +func TestClPostProcessNoopWithoutFi(t *testing.T) { + // Must not panic or error when -Fi wasn't passed - just silently skip. + clPostProcess([]string{"/P", "foo.c"}) +} + +func TestClPostProcessMissingFileIsSilent(t *testing.T) { + // The referenced -Fi file doesn't exist - clPostProcess must not panic. + clPostProcess([]string{"/P", "/Fi" + filepath.Join(t.TempDir(), "missing.i"), "foo.c"}) +} diff --git a/internal/wrapper/msbuildenv_test.go b/internal/wrapper/msbuildenv_test.go new file mode 100644 index 0000000..3bbd568 --- /dev/null +++ b/internal/wrapper/msbuildenv_test.go @@ -0,0 +1,117 @@ +package wrapper + +import ( + "os" + "path/filepath" + "testing" + + "github.com/Cheviiot/vintner/internal/wineenv" +) + +func TestMsbuildPlatform(t *testing.T) { + for _, tc := range []struct{ arch, want string }{ + {"x86", "Win32"}, + {"x64", "x64"}, + {"arm", "ARM"}, + {"arm64", "ARM64"}, + } { + if got := msbuildPlatform(tc.arch); got != tc.want { + t.Errorf("msbuildPlatform(%q) = %q, want %q", tc.arch, got, tc.want) + } + } +} + +func newTestPaths(t *testing.T, cfg *wineenv.Config) (*wineenv.Paths, string) { + t.Helper() + base := t.TempDir() + return wineenv.NewPaths(cfg, base), base +} + +func TestMsbuildEnvBasics(t *testing.T) { + cfg := &wineenv.Config{Arch: "x64", Host: "x64", DotnetHost: "amd64", MSVCVer: "14.51.36231", SDKVer: "10.0.26100.0"} + paths, _ := newTestPaths(t, cfg) + + env := msbuildEnv(cfg, paths) + + if env["TZ"] != "UTC" { + t.Errorf(`env["TZ"] = %q, want "UTC"`, env["TZ"]) + } + if env["DisableRegistryUse"] != "true" { + t.Errorf(`env["DisableRegistryUse"] = %q, want "true"`, env["DisableRegistryUse"]) + } + if env["VCToolsVersion"] != cfg.MSVCVer { + t.Errorf(`env["VCToolsVersion"] = %q, want %q`, env["VCToolsVersion"], cfg.MSVCVer) + } + if env["WindowsTargetPlatformVersion"] != cfg.SDKVer { + t.Errorf(`env["WindowsTargetPlatformVersion"] = %q, want %q`, env["WindowsTargetPlatformVersion"], cfg.SDKVer) + } + if env["Platform"] != "x64" { + t.Errorf(`env["Platform"] = %q, want "x64"`, env["Platform"]) + } + if env["SignMode"] != "off" { + t.Errorf(`env["SignMode"] = %q, want "off" (driver builds must not attempt real signing)`, env["SignMode"]) + } + // No WDK content on disk in this test - must not claim otherwise. + if _, ok := env["WDKContentRoot"]; ok { + t.Error(`env["WDKContentRoot"] set even though no wdk//c directory exists`) + } +} + +func TestMsbuildEnvDiscoversEveryToolsetVersion(t *testing.T) { + cfg := &wineenv.Config{Arch: "x64", Host: "x64", DotnetHost: "amd64", MSVCVer: "14.51.36231", SDKVer: "10.0.26100.0"} + paths, base := newTestPaths(t, cfg) + + for _, v := range []string{"v145", "v180", "not-a-version"} { + if err := os.MkdirAll(filepath.Join(base, "MSBuild", "Microsoft", "VC", v), 0o755); err != nil { + t.Fatal(err) + } + } + + env := msbuildEnv(cfg, paths) + + for _, n := range []string{"145", "180"} { + if _, ok := env["VCInstallDir_"+n]; !ok { + t.Errorf("expected VCInstallDir_%s to be set", n) + } + if _, ok := env["VCToolsInstallDir_"+n]; !ok { + t.Errorf("expected VCToolsInstallDir_%s to be set", n) + } + } + if _, ok := env["VCInstallDir_not-a-version"]; ok { + t.Error("a directory not matching v should not have produced a VCInstallDir_ entry") + } +} + +func TestMsbuildEnvDetectsWDKContentRoot(t *testing.T) { + cfg := &wineenv.Config{Arch: "x64", Host: "x64", DotnetHost: "amd64", MSVCVer: "14.51.36231", SDKVer: "10.0.26100.0"} + paths, base := newTestPaths(t, cfg) + + if err := os.MkdirAll(filepath.Join(base, "wdk", "x64", "c"), 0o755); err != nil { + t.Fatal(err) + } + + env := msbuildEnv(cfg, paths) + + if env["WDKContentRoot"] == "" { + t.Error("expected WDKContentRoot to be set once wdk/x64/c exists on disk") + } + if env["WDKBuildFolder"] != cfg.SDKVer { + t.Errorf(`env["WDKBuildFolder"] = %q, want %q`, env["WDKBuildFolder"], cfg.SDKVer) + } +} + +func TestMsbuildEnvPreferredToolArchitecture(t *testing.T) { + // PreferredToolArchitecture should only be set when the host toolset + // bin dir is the 64-bit ("amd64") .NET host - not for arm64. + cfg64 := &wineenv.Config{Arch: "x64", Host: "x64", DotnetHost: "amd64", MSVCVer: "1", SDKVer: "1"} + paths64, _ := newTestPaths(t, cfg64) + if env := msbuildEnv(cfg64, paths64); env["PreferredToolArchitecture"] != "x64" { + t.Errorf(`with DotnetHost=amd64, PreferredToolArchitecture = %q, want "x64"`, env["PreferredToolArchitecture"]) + } + + cfgARM := &wineenv.Config{Arch: "arm64", Host: "arm64", DotnetHost: "arm64", MSVCVer: "1", SDKVer: "1"} + pathsARM, _ := newTestPaths(t, cfgARM) + if env := msbuildEnv(cfgARM, pathsARM); env["PreferredToolArchitecture"] != "" { + t.Errorf(`with DotnetHost=arm64, PreferredToolArchitecture = %q, want unset`, env["PreferredToolArchitecture"]) + } +} diff --git a/internal/wrapper/tools_test.go b/internal/wrapper/tools_test.go new file mode 100644 index 0000000..1b5dedd --- /dev/null +++ b/internal/wrapper/tools_test.go @@ -0,0 +1,45 @@ +package wrapper + +import ( + "testing" + + "github.com/Cheviiot/vintner/internal/wineenv" +) + +func TestSpecExeDir(t *testing.T) { + paths := &wineenv.Paths{ + BinDir: "/bin-dir", + SDKBinDir: "/sdk-bin-dir", + MSBuildBinDir: "/msbuild-bin-dir", + } + for _, tc := range []struct { + name string + dir dirKind + want string + }{ + {"dirBin", dirBin, "/bin-dir"}, + {"dirSDK", dirSDK, "/sdk-bin-dir"}, + {"dirMSBuild", dirMSBuild, "/msbuild-bin-dir"}, + } { + s := spec{dir: tc.dir} + if got := s.exeDir(paths); got != tc.want { + t.Errorf("%s: exeDir() = %q, want %q", tc.name, got, tc.want) + } + } +} + +func TestToolsAndNativeToolsAreDisjoint(t *testing.T) { + for name := range Tools { + if nativeTools[name] { + t.Errorf("%q is in both Tools and nativeTools", name) + } + } +} + +func TestEveryToolHasAnExeName(t *testing.T) { + for name, s := range Tools { + if s.exeName == "" { + t.Errorf("Tools[%q] has no exeName", name) + } + } +}