package gotool import ( "context" "encoding/json" "errors" "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "time" ) type goPreparationRecord struct { Arguments []string Directory string Workspace string Toolchain string Proxy string Private string NoSumDB string ModuleCache string } func TestMain(m *testing.M) { if os.Getenv("RELEASEKIT_GO_FIXTURE") == "1" { os.Exit(runGoPreparationFixture()) } os.Exit(m.Run()) } func runGoPreparationFixture() int { record := goPreparationRecord{ Arguments: os.Args[1:], Directory: mustGetwd(), Workspace: os.Getenv("GOWORK"), Toolchain: os.Getenv("GOTOOLCHAIN"), Proxy: os.Getenv("GOPROXY"), Private: os.Getenv("GOPRIVATE"), NoSumDB: os.Getenv("GONOSUMDB"), ModuleCache: os.Getenv("GOMODCACHE"), } contents, err := json.Marshal(record) if err != nil { return 10 } if err := os.WriteFile(os.Getenv("RELEASEKIT_GO_FIXTURE_RECORD"), contents, 0o600); err != nil { return 11 } switch os.Getenv("RELEASEKIT_GO_FIXTURE_MODE") { case "discovery": settings := make(map[string]string) for _, key := range os.Args[3:] { settings[key] = os.Getenv(key) } settings["GOHOSTOS"] = runtime.GOOS settings["GOHOSTARCH"] = runtime.GOARCH fmt.Fprintln(os.Stderr, "fixture discovery diagnostic") if err := json.NewEncoder(os.Stdout).Encode(settings); err != nil { return 12 } return 0 case "fail": fmt.Fprintln(os.Stderr, "fixture dependency failure") return 23 case "wait": for { time.Sleep(time.Second) } default: return 0 } } func TestDiscoveryAcceptsDiagnosticsWithValidConfiguration(t *testing.T) { repository, _ := installGoPreparationFixture(t) t.Setenv("RELEASEKIT_GO_FIXTURE_MODE", "discovery") cache := filepath.Join(t.TempDir(), "cache") t.Setenv("GOCACHE", cache) tool, err := New(context.Background(), repository) if err != nil { t.Fatalf("New() rejected successful discovery: %v", err) } if tool.HostTarget() != (Target{OS: runtime.GOOS, Arch: runtime.GOARCH}) || tool.settings["GOCACHE"] != cache { t.Fatalf("discovered host=%v cache=%q", tool.HostTarget(), tool.settings["GOCACHE"]) } } func TestDiscoveryPreservesFailureDiagnosticsAndCause(t *testing.T) { repository, _ := installGoPreparationFixture(t) t.Setenv("RELEASEKIT_GO_FIXTURE_MODE", "fail") _, err := New(context.Background(), repository) var exitError *exec.ExitError if !errors.As(err, &exitError) || exitError.ExitCode() != 23 || !strings.Contains(err.Error(), "fixture dependency failure") { t.Fatalf("New() error = %v", err) } } func mustGetwd() string { directory, err := os.Getwd() if err != nil { return "" } return directory } func installGoPreparationFixture(t *testing.T) (repository, recordPath string) { t.Helper() directory := t.TempDir() repository = t.TempDir() executable, err := os.Executable() if err != nil { t.Fatal(err) } contents, err := os.ReadFile(executable) if err != nil { t.Fatal(err) } name := "go" if runtime.GOOS == "windows" { name += ".exe" } if err := os.WriteFile(filepath.Join(directory, name), contents, 0o755); err != nil { t.Fatal(err) } recordPath = filepath.Join(t.TempDir(), "go-command.json") t.Setenv("PATH", directory+string(os.PathListSeparator)+os.Getenv("PATH")) t.Setenv("RELEASEKIT_GO_FIXTURE", "1") t.Setenv("RELEASEKIT_GO_FIXTURE_RECORD", recordPath) return repository, recordPath } func TestPrepareBuildDependenciesPreservesPreparationEnvironment(t *testing.T) { repository, recordPath := installGoPreparationFixture(t) moduleCache := filepath.Join(t.TempDir(), "module-cache") t.Setenv("GOWORK", filepath.Join(t.TempDir(), "go.work")) t.Setenv("GOTOOLCHAIN", "auto") t.Setenv("GOPROXY", "https://proxy.example.invalid") t.Setenv("GOPRIVATE", "example.invalid/private") t.Setenv("GONOSUMDB", "example.invalid/private") t.Setenv("GOMODCACHE", moduleCache) if err := PrepareBuildDependencies(context.Background(), repository); err != nil { t.Fatal(err) } contents, err := os.ReadFile(recordPath) if err != nil { t.Fatal(err) } var record goPreparationRecord if err := json.Unmarshal(contents, &record); err != nil { t.Fatal(err) } if strings.Join(record.Arguments, " ") != "mod download" || record.Directory != repository { t.Fatalf("Go invocation = %#v", record) } if record.Workspace != "off" || record.Toolchain != "local" { t.Fatalf("preparation Go settings = %#v", record) } if record.Proxy != "https://proxy.example.invalid" || record.Private != "example.invalid/private" || record.NoSumDB != "example.invalid/private" || record.ModuleCache != moduleCache { t.Fatalf("caller dependency settings were not preserved: %#v", record) } } func TestPrepareBuildDependenciesPropagatesCommandFailure(t *testing.T) { repository, _ := installGoPreparationFixture(t) t.Setenv("RELEASEKIT_GO_FIXTURE_MODE", "fail") err := PrepareBuildDependencies(context.Background(), repository) var exitError *exec.ExitError if !errors.As(err, &exitError) || exitError.ExitCode() != 23 || !strings.Contains(err.Error(), "fixture dependency failure") { t.Fatalf("PrepareBuildDependencies() error = %v", err) } } func TestPrepareBuildDependenciesPropagatesCancellation(t *testing.T) { repository, recordPath := installGoPreparationFixture(t) t.Setenv("RELEASEKIT_GO_FIXTURE_MODE", "wait") ctx, cancel := context.WithCancel(context.Background()) defer cancel() done := make(chan error, 1) go func() { done <- PrepareBuildDependencies(ctx, repository) }() deadline := time.Now().Add(5 * time.Second) for { if _, err := os.Stat(recordPath); err == nil { break } if time.Now().After(deadline) { t.Fatal("Go fixture did not start") } time.Sleep(10 * time.Millisecond) } cancel() select { case err := <-done: if !errors.Is(err, context.Canceled) { t.Fatalf("PrepareBuildDependencies() error = %v; want cancellation", err) } case <-time.After(5 * time.Second): t.Fatal("Go fixture did not stop after cancellation") } } func TestNativeAndStaticCommandsUseOwnedBaseline(t *testing.T) { clearGoSettings(t) t.Setenv("GOENV", "off") t.Setenv("GOFLAGS", "-run=^$") t.Setenv("GOOS", "windows") t.Setenv("GOARCH", "arm64") t.Setenv("GOAMD64", "v2") t.Setenv("GOEXPERIMENT", "loopvar") tool, err := New(context.Background(), t.TempDir()) if err != nil { t.Fatal(err) } if tool.HostTarget() != (Target{OS: runtime.GOOS, Arch: runtime.GOARCH}) { t.Fatalf("Go host = %#v, want runtime host %s/%s", tool.HostTarget(), runtime.GOOS, runtime.GOARCH) } native := readSettings(t, tool, Native, Target{}, "GOENV", "GOOS", "GOARCH", "GOFLAGS", "GOAMD64", "GOEXPERIMENT", "CGO_ENABLED") if native["GOENV"] != "" || native["GOOS"] != runtime.GOOS || native["GOARCH"] != runtime.GOARCH { t.Fatalf("native environment = %#v", native) } if native["GOFLAGS"] != "-mod=readonly" || native["GOAMD64"] == "v2" || native["GOEXPERIMENT"] != "" { t.Fatalf("native Go settings = %#v", native) } if native["CGO_ENABLED"] == "" { t.Fatalf("native cgo setting was not resolved: %#v", native) } static := readSettings(t, tool, Static, Target{OS: "linux", Arch: "amd64"}, "GOOS", "GOARCH", "GOFLAGS", "CGO_ENABLED", "GOAMD64", "GOEXPERIMENT") if static["GOOS"] != "linux" || static["GOARCH"] != "amd64" || static["CGO_ENABLED"] != "0" { t.Fatalf("static environment = %#v", static) } if static["GOFLAGS"] != "-mod=readonly" || static["GOAMD64"] != "v1" || static["GOEXPERIMENT"] != "" { t.Fatalf("static baseline settings = %#v", static) } } func TestPersistentSettingsPreserveCachesAndNativeCTools(t *testing.T) { clearGoSettings(t) cache := filepath.Join(t.TempDir(), "configured-cache") moduleCache := filepath.Join(t.TempDir(), "configured-module-cache") temporary := filepath.Join(t.TempDir(), "configured-temporary") if err := os.Mkdir(temporary, 0o700); err != nil { t.Fatal(err) } goEnv := filepath.Join(t.TempDir(), "go-env") contents := strings.Join([]string{ "GOFLAGS=-run=^$", "GOOS=windows", "GOARCH=arm64", "GOAMD64=v2", "GOEXPERIMENT=loopvar", "GOCACHE=" + cache, "GOMODCACHE=" + moduleCache, "GOTMPDIR=" + temporary, "CGO_ENABLED=1", "CC=releasekit-test-cc", "CXX=releasekit-test-cxx", "CGO_CFLAGS=-O0 -g", "PKG_CONFIG=releasekit-test-pkg-config", }, "\n") + "\n" if err := os.WriteFile(goEnv, []byte(contents), 0o600); err != nil { t.Fatal(err) } t.Setenv("GOENV", goEnv) tool, err := New(context.Background(), t.TempDir()) if err != nil { t.Fatal(err) } if tool.HostTarget() != (Target{OS: runtime.GOOS, Arch: runtime.GOARCH}) { t.Fatalf("Go host = %#v, want runtime host %s/%s", tool.HostTarget(), runtime.GOOS, runtime.GOARCH) } native := readSettings(t, tool, Native, Target{}, "GOENV", "GOOS", "GOARCH", "GOFLAGS", "GOAMD64", "GOEXPERIMENT", "CGO_ENABLED", "CC", "CXX", "CGO_CFLAGS", "GOCACHE", "GOMODCACHE", "GOTMPDIR", "PKG_CONFIG") if native["GOENV"] != "" || native["GOOS"] != runtime.GOOS || native["GOARCH"] != runtime.GOARCH { t.Fatalf("native environment = %#v", native) } if native["GOFLAGS"] != "-mod=readonly" || native["GOAMD64"] == "v2" || native["GOEXPERIMENT"] != "" { t.Fatalf("persisted overrides were not cleared: %#v", native) } if native["CGO_ENABLED"] != "1" || native["CC"] != "releasekit-test-cc" || native["CXX"] != "releasekit-test-cxx" || native["CGO_CFLAGS"] != "-O0 -g" || native["PKG_CONFIG"] != "releasekit-test-pkg-config" { t.Fatalf("persistent native C settings = %#v", native) } if native["GOCACHE"] != cache || native["GOMODCACHE"] != moduleCache || native["GOTMPDIR"] != temporary { t.Fatalf("persistent cache settings = %#v", native) } static := readSettings(t, tool, Static, Target{OS: "linux", Arch: "amd64"}, "GOENV", "GOOS", "GOARCH", "GOFLAGS", "CGO_ENABLED", "GOAMD64", "GOEXPERIMENT", "GOCACHE") if static["GOENV"] != "" || static["GOOS"] != "linux" || static["GOARCH"] != "amd64" || static["CGO_ENABLED"] != "0" || static["GOAMD64"] != "v1" || static["GOEXPERIMENT"] != "" || static["GOCACHE"] != cache { t.Fatalf("persistent static environment = %#v", static) } } func readSettings(t *testing.T, tool *Tool, mode Mode, target Target, names ...string) map[string]string { t.Helper() args := append([]string{"env", "-json"}, names...) output, err := tool.Run(context.Background(), t.TempDir(), mode, target, nil, args...) if err != nil { t.Fatal(err) } var settings map[string]string if err := json.Unmarshal(output, &settings); err != nil { t.Fatalf("decode Go environment %q: %v", output, err) } return settings } func clearGoSettings(t *testing.T) { t.Helper() for _, key := range []string{ "GOENV", "GOFLAGS", "GOWORK", "GOTOOLCHAIN", "GOPROXY", "GONOPROXY", "GOSUMDB", "GOOS", "GOARCH", "GOEXPERIMENT", "GOAMD64", "GOARM", "GOARM64", "GO386", "GOMIPS", "GOMIPS64", "GOPPC64", "GORISCV64", "GOWASM", "CGO_ENABLED", "CC", "CXX", "CGO_CFLAGS", "CGO_CPPFLAGS", "CGO_CXXFLAGS", "CGO_LDFLAGS", "GOCACHE", "GOCACHEPROG", "GOMODCACHE", "GOPATH", "GOROOT", "GOTMPDIR", "PKG_CONFIG", } { value, exists := os.LookupEnv(key) if err := os.Unsetenv(key); err != nil { t.Fatal(err) } t.Cleanup(func() { if exists { _ = os.Setenv(key, value) } else { _ = os.Unsetenv(key) } }) } }