package releasekit_test import ( "context" "errors" "fmt" "os" "os/exec" "path/filepath" "runtime" "testing" "time" releasekit "go.maximumdirect.net/releasekit" ) // inspectionDeadlineContext models deadline expiration at the readiness signal, // so unrelated setup time cannot consume the source-inspection test's deadline. type inspectionDeadlineContext struct{ context.Context } func (ctx inspectionDeadlineContext) Err() error { return context.Cause(ctx.Context) } func TestBuildSourceInspectionCancellation(t *testing.T) { const source = `package main import("fmt";"os";"go.maximumdirect.net/releasekit/buildinfo") func main(){ if len(os.Args)>1 && os.Args[1]=="version" {fmt.Println("fixture",buildinfo.Version())} } ` for _, tc := range []struct { name string status int deadline bool binary bool }{ {"initial", 1, false, false}, {"deadline", 1, true, false}, {"final", 2, false, true}, } { t.Run(tc.name, func(t *testing.T) { repo := newBuildConsumer(t, source) realGit, err := exec.LookPath("git") if err != nil { t.Fatal(err) } bin := t.TempDir() count := filepath.Join(t.TempDir(), "count") ready := filepath.Join(t.TempDir(), "ready") script := `#!/bin/sh for arg in "$@"; do if [ "$arg" = status ]; then count=0 if [ -f "$INSPECT_COUNT" ]; then count=$(/bin/cat "$INSPECT_COUNT"); fi count=$((count+1)); printf '%s' "$count" > "$INSPECT_COUNT" if [ "$count" = "$INSPECT_BLOCK" ]; then : > "$INSPECT_READY"; exec /bin/sleep 60; fi fi done exec "$INSPECT_GIT" "$@" ` if err := os.WriteFile(filepath.Join(bin, "git"), []byte(script), 0755); err != nil { t.Fatal(err) } t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) t.Setenv("INSPECT_GIT", realGit) t.Setenv("INSPECT_COUNT", count) t.Setenv("INSPECT_READY", ready) t.Setenv("INSPECT_BLOCK", fmt.Sprint(tc.status)) var ctx context.Context var cancel context.CancelFunc if tc.deadline { parent, expire := context.WithCancelCause(context.Background()) ctx = inspectionDeadlineContext{parent} cancel = func() { expire(context.DeadlineExceeded) } } else { ctx, cancel = context.WithCancel(context.Background()) } defer cancel() request := releasekit.BuildRequest{RepositoryDir: repo, Version: "v1.2.3", OutputDir: t.TempDir()} if tc.binary { request.Spec = releasekit.BuildSpec{MainPackage: "./cmd/app", BinaryName: "fixture", Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}} } type outcome struct { result releasekit.BuildResult err error } done := make(chan outcome, 1) go func() { result, err := releasekit.Build(ctx, request); done <- outcome{result, err} }() timer := time.NewTimer(45 * time.Second) defer timer.Stop() ticker := time.NewTicker(10 * time.Millisecond) defer ticker.Stop() waiting := true for waiting { if _, err := os.Stat(ready); err == nil { waiting = false break } select { case result := <-done: t.Fatalf("build ended before inspection: %#v, %v", result.result, result.err) case <-timer.C: cancel() result := <-done t.Fatalf("inspection did not start: %#v, %v", result.result, result.err) case <-ticker.C: } } cancel() var result outcome select { case result = <-done: case <-timer.C: t.Fatal("build did not stop after source inspection interruption") } want := context.Canceled if tc.deadline { want = context.DeadlineExceeded } if !errors.Is(result.err, want) || result.result.SourceVerified { t.Fatalf("result = %#v, error = %v", result.result, result.err) } if tc.binary { if len(result.result.Artifacts) != 1 || result.result.Artifacts[0].SHA256 == "" { t.Fatalf("completed artifact missing: %#v", result.result.Artifacts) } if _, err := os.Stat(result.result.Artifacts[0].Path); err != nil { t.Fatal(err) } } }) } } func TestBuildSourceInspectionPreservesGitFailure(t *testing.T) { repo := newBuildConsumer(t, "package main\nfunc main() {}\n") realGit, err := exec.LookPath("git") if err != nil { t.Fatal(err) } bin := t.TempDir() script := "#!/bin/sh\nfor arg do if [ \"$arg\" = status ]; then exit 37; fi; done\nexec \"$INSPECT_GIT\" \"$@\"\n" if err := os.WriteFile(filepath.Join(bin, "git"), []byte(script), 0o755); err != nil { t.Fatal(err) } t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) t.Setenv("INSPECT_GIT", realGit) result, err := releasekit.Build(context.Background(), releasekit.BuildRequest{RepositoryDir: repo, Version: "v1.2.3"}) var exitErr *exec.ExitError if !errors.As(err, &exitErr) || exitErr.ExitCode() != 37 || result.SourceVerified { t.Fatalf("result = %#v, error = %v", result, err) } }