185 lines
6.5 KiB
Go
185 lines
6.5 KiB
Go
package validation
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"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 TestRunPreservesCancellationDuringReaderShutdown(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(), "shutdown-ready")
|
|
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 '%s\n' "$$" > "$RELEASEKIT_GIT_READER_READY"
|
|
exec sleep 60`)
|
|
t.Setenv("RELEASEKIT_GIT_READER_READY", ready)
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
result := make(chan error, 1)
|
|
go func() { result <- Run(ctx, lifecycleRequest(repository, commit)) }()
|
|
waitForLifecycleFile(t, ready)
|
|
pidBytes, err := os.ReadFile(ready)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pid, err := strconv.Atoi(strings.TrimSpace(string(pidBytes)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
child, err := os.FindProcess(pid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cancel()
|
|
select {
|
|
case err := <-result:
|
|
var failure *Failure
|
|
if !errors.As(err, &failure) || failure.Check != GoFormat || !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("Run() error = %v, want GoFormat cancellation cause", err)
|
|
}
|
|
if err := child.Signal(syscall.Signal(0)); !errors.Is(err, syscall.ESRCH) && !errors.Is(err, os.ErrProcessDone) {
|
|
t.Fatalf("child signal after Run() = %v, want reaped child", err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
_ = child.Kill()
|
|
<-result
|
|
t.Fatal("Run() did not return after cancellation during reader shutdown")
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|