package validation import ( "context" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" "time" ) func TestRunAttributesUnexpectedReaderCloseFailureToLastContentCheck(t *testing.T) { repository := newTreeRepository(t) writeTreeFile(t, repository, "go.mod", []byte("module fixture.example/app\n\ngo 1.25.0\n")) writeTreeFile(t, repository, "main.go", []byte("package app\n\nconst value = 1\n")) commit := commitCandidateTree(t, repository) installLifecycleTools(t, `while IFS= read -r object; do size=$("$RELEASEKIT_REAL_GIT" -C "$PWD" cat-file -s "$object") printf '%s blob %s\n' "$object" "$size" "$RELEASEKIT_REAL_GIT" -C "$PWD" cat-file blob "$object" printf '\n' done printf 'batch process failed during cleanup\n' >&2 exit 23`) err := Run(context.Background(), lifecycleRequest(repository, commit)) var failure *Failure if !errors.As(err, &failure) || failure.Check != GoFormat { t.Fatalf("Run() error = %v, want cleanup failure attributed to GoFormat", err) } var exitErr *exec.ExitError if !errors.As(failure.Cause, &exitErr) || exitErr.ExitCode() != 23 { t.Fatalf("cleanup cause = %v, want Git child exit code 23", failure.Cause) } if !strings.Contains(failure.Cause.Error(), "batch process failed during cleanup") { t.Fatalf("cleanup cause = %v, want captured Git stderr", failure.Cause) } } func TestRunPreservesCheckFailureDuringReaderCleanup(t *testing.T) { repository := newTreeRepository(t) writeTreeFile(t, repository, "go.mod", []byte("module fixture.example/app\n\ngo 1.25.0\n")) writeTreeFile(t, repository, "broken.go", []byte("package app\nfunc ( {\n")) commit := commitCandidateTree(t, repository) installLifecycleTools(t, `while IFS= read -r object; do size=$("$RELEASEKIT_REAL_GIT" -C "$PWD" cat-file -s "$object") printf '%s blob %s\n' "$object" "$size" "$RELEASEKIT_REAL_GIT" -C "$PWD" cat-file blob "$object" printf '\n' done printf 'secondary cleanup failure\n' >&2 exit 24`) err := Run(context.Background(), lifecycleRequest(repository, commit)) var failure *Failure if !errors.As(err, &failure) || failure.Check != GoFormat || failure.Cause == nil { t.Fatalf("Run() error = %v, want original GoFormat check failure", err) } if len(failure.Findings) != 1 || failure.Findings[0].Path != "broken.go" { t.Fatalf("Run() findings = %#v, want original broken.go finding", failure.Findings) } if strings.Contains(failure.Cause.Error(), "secondary cleanup failure") { t.Fatalf("Run() replaced the check cause with cleanup output: %v", failure.Cause) } } func TestRunPreservesCancellationDuringReaderCleanup(t *testing.T) { repository := newTreeRepository(t) writeTreeFile(t, repository, "go.mod", []byte("module fixture.example/app\n\ngo 1.25.0\n")) writeTreeFile(t, repository, "main.go", []byte("package app\n\nconst value = 1\n")) commit := commitCandidateTree(t, repository) ready := filepath.Join(t.TempDir(), "reader-ready") installLifecycleTools(t, `IFS= read -r object : > "$RELEASEKIT_GIT_READER_READY" exec sleep 60`) t.Setenv("RELEASEKIT_GIT_READER_READY", ready) ctx, cancel := context.WithCancel(t.Context()) result := make(chan error, 1) go func() { result <- Run(ctx, lifecycleRequest(repository, commit)) }() waitForLifecycleFile(t, ready) cancel() select { case err := <-result: var failure *Failure if !errors.As(err, &failure) || failure.Check != GoFormat || !errors.Is(failure.Cause, context.Canceled) { t.Fatalf("Run() error = %v, want GoFormat cancellation cause", err) } case <-time.After(5 * time.Second): t.Fatal("Run() did not return after candidate reader cancellation") } } func lifecycleRequest(repository, commit string) Request { return Request{ RepositoryDir: repository, Commit: commit, Settings: Settings{GoFormat: true}, } } func installLifecycleTools(t *testing.T, batchScript string) { t.Helper() realGit, err := exec.LookPath("git") if err != nil { t.Fatal(err) } installGoShim(t, "", false) t.Setenv("RELEASEKIT_REAL_GIT", realGit) directory := t.TempDir() gitWrapper := filepath.Join(directory, "git") script := "#!/bin/sh\nset -eu\nif [ \"${1-}\" = cat-file ] && [ \"${2-}\" = --batch ]; then\n" + batchScript + "\nfi\nexec \"$RELEASEKIT_REAL_GIT\" \"$@\"\n" if err := os.WriteFile(gitWrapper, []byte(script), 0o700); err != nil { t.Fatal(err) } t.Setenv("PATH", directory+string(os.PathListSeparator)+os.Getenv("PATH")) } func waitForLifecycleFile(t *testing.T, path string) { t.Helper() deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { if _, err := os.Stat(path); err == nil { return } else if !errors.Is(err, os.ErrNotExist) { t.Fatal(fmt.Errorf("inspect reader readiness signal: %w", err)) } time.Sleep(5 * time.Millisecond) } t.Fatal("candidate blob reader did not reach its blocking exchange") }