package build import ( "context" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "go.maximumdirect.net/releasekit/internal/candidate" "go.maximumdirect.net/releasekit/internal/releaseidentity" ) func cleanupFixture(t *testing.T, output string) releaseidentity.Identity { t.Helper() repo := t.TempDir() files := map[string]string{ "go.mod": "module example.com/cleanup\n\ngo 1.25.0\n", "main.go": fmt.Sprintf("package main\nimport \"fmt\"\nfunc main() { fmt.Println(%q) }\n", output), } for name, contents := range files { if err := os.WriteFile(filepath.Join(repo, name), []byte(contents), 0o600); err != nil { t.Fatal(err) } } for _, args := range [][]string{{"init", "-b", "main"}, {"config", "user.name", "Fixture"}, {"config", "user.email", "fixture@example.invalid"}, {"add", "."}, {"commit", "-m", "fixture"}} { command := exec.Command("git", args...) command.Dir = repo if data, err := command.CombinedOutput(); err != nil { t.Fatalf("git %v: %v: %s", args, err, data) } } return releaseidentity.Identity{Selection: releaseidentity.Selection{RepositoryDir: repo, ModuleDir: repo}, ModulePath: "example.com/cleanup", Version: "v1.2.3", Tag: "v1.2.3"} } type cancelWriter struct{ cancel context.CancelFunc } func (writer cancelWriter) Write(p []byte) (int, error) { if strings.Contains(string(p), "checking ") { writer.cancel() } return len(p), nil } func TestNativeSmokeCleanupPreservesBuildOutcome(t *testing.T) { for _, mode := range []string{"success", "smoke error", "cancellation"} { t.Run(mode, func(t *testing.T) { output := "fixture v1.2.3" if mode == "smoke error" { output = "wrong" } identity := cleanupFixture(t, output) otherArch := "arm64" if runtime.GOARCH == otherArch { otherArch = "amd64" } ctx, cancel := context.WithCancel(t.Context()) defer cancel() request := Request{Identity: identity, OutputDir: t.TempDir(), Spec: Spec{MainPackage: ".", BinaryName: "fixture", Targets: []Target{{OS: runtime.GOOS, Arch: otherArch}}}} if mode == "cancellation" { request.Output = cancelWriter{cancel} } cleanupErr := errors.New("cleanup failed") var removed string result, err := buildResolved(ctx, request, func(path string) error { removed = path _ = os.RemoveAll(path) return cleanupErr }) if !errors.Is(err, cleanupErr) || removed == "" || !strings.Contains(err.Error(), removed) || len(result.Artifacts) != 1 || result.Artifacts[0].SHA256 == "" || result.CandidateCommit == "" { t.Fatalf("build = %#v, cleanup = %q, error = %v", result, removed, err) } if mode == "success" && !result.SourceVerified { t.Fatal("completed build lost verified source") } if mode == "smoke error" && !strings.Contains(err.Error(), "native version output") { t.Fatalf("smoke failure missing: %v", err) } if mode == "cancellation" && !errors.Is(err, context.Canceled) { t.Fatalf("cancellation missing: %v", err) } }) } } func TestCandidateBuildCleanupPreservesOutcome(t *testing.T) { for _, mode := range []string{"success", "build failure", "cancellation"} { t.Run(mode, func(t *testing.T) { identity := cleanupFixture(t, "fixture v1.2.3") cleanupErr := errors.New("cleanup failed") var removed string spec := &Spec{MainPackage: ".", BinaryName: "fixture", Targets: []Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}} if mode == "build failure" { spec.MainPackage = "./missing" } ctx, cancel := context.WithCancel(t.Context()) defer cancel() var output io.Writer if mode == "cancellation" { output = cancelWriter{cancel} } callback := candidateCallback(spec, output, func(path string) error { removed = path _ = os.RemoveAll(path) return cleanupErr }) err := callback(ctx, candidate.Candidate{RepositoryDir: identity.RepositoryDir, ModuleDir: identity.ModuleDir, ModulePath: identity.ModulePath, Version: identity.Version, Tag: identity.Tag}, nil) if !errors.Is(err, cleanupErr) || removed == "" || !strings.Contains(err.Error(), removed) { t.Fatalf("candidate cleanup = %q, error = %v", removed, err) } if mode == "build failure" && !strings.Contains(err.Error(), "build candidate") { t.Fatalf("build failure missing: %v", err) } if mode == "cancellation" && !errors.Is(err, context.Canceled) { t.Fatalf("cancellation missing: %v", err) } }) } }