125 lines
4.3 KiB
Go
125 lines
4.3 KiB
Go
|
|
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)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|