Files
releasekit/internal/publish/cancellation_test.go

148 lines
5.1 KiB
Go

package publish
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"time"
)
func TestRecoveryAlreadyCanceledDiscovery(t *testing.T) {
for _, override := range []string{"", "relative"} {
ctx, cancel := context.WithCancel(t.Context())
cancel()
result, err := ListAttempts(ctx, t.TempDir(), override)
requireCanceledRecovery(t, err)
if len(result.Attempts) != 0 {
t.Fatalf("unexpected attempts: %#v", result)
}
}
}
func requireCanceledRecovery(t *testing.T, err error) {
t.Helper()
var recovery *RecoveryError
if !errors.As(err, &recovery) || recovery.Kind != "canceled" || !errors.Is(err, context.Canceled) {
t.Fatalf("cancellation = %v; want canceled kind and cause", err)
}
}
func TestRecoveryCancelsActiveGitOperations(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("fixture uses a POSIX Git launcher")
}
tests := []struct {
name, command string
run func(context.Context, string) error
}{
{"default discovery root", "--show-toplevel", func(ctx context.Context, repo string) error { _, err := ListAttempts(ctx, repo, ""); return err }},
{"relative discovery root", "--show-toplevel", func(ctx context.Context, repo string) error { _, err := ListAttempts(ctx, repo, "records"); return err }},
{"default discovery metadata", "--git-common-dir", func(ctx context.Context, repo string) error { _, err := ListAttempts(ctx, repo, ""); return err }},
{"record metadata", "--git-common-dir", func(ctx context.Context, repo string) error {
_, err := SaveEvidence(ctx, repo, "", Evidence{})
return err
}},
{"record ignore guard", "check-ignore", func(ctx context.Context, repo string) error {
_, err := SaveEvidence(ctx, repo, "records", Evidence{})
return err
}},
{"publication metadata", "--git-common-dir", func(ctx context.Context, repo string) error {
result, err := Publish(ctx, Request{RepositoryDir: repo, Product: "Sample", Version: "v1.2.3"})
if !errors.Is(result.RecordError, context.Canceled) {
return fmt.Errorf("record cancellation lost: %v", result.RecordError)
}
return err
}},
{"tag validation", "check-ref-format", func(ctx context.Context, repo string) error {
_, err := Reconcile(ctx, Evidence{RepositoryDir: repo, Endpoint: repo, Tag: "v1.2.3"})
return err
}},
{"tag query", "ls-remote", func(ctx context.Context, repo string) error {
_, err := Reconcile(ctx, Evidence{RepositoryDir: repo, Endpoint: repo, Tag: "v1.2.3"})
return err
}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
repo := recordRepo(t)
if test.name == "publication metadata" {
if err := os.WriteFile(filepath.Join(repo, "go.mod"), []byte("module example.com/sample\n\ngo 1.26.0\n"), 0600); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(repo, "docs", "releases"), 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(repo, "docs", "releases", "v1.2.3.md"), []byte("# Sample v1.2.3\n\nChanges.\n"), 0600); err != nil {
t.Fatal(err)
}
recordGit(t, repo, "checkout", "-B", "main")
recordGit(t, repo, "add", ".")
recordGit(t, repo, "commit", "-qm", "Release candidate")
}
realGit, err := exec.LookPath("git")
if err != nil {
t.Fatal(err)
}
bin := t.TempDir()
ready := filepath.Join(t.TempDir(), "ready")
quote := func(s string) string { return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'" }
script := "#!/bin/sh\nfor arg in \"$@\"; do\n if [ \"$arg\" = " + quote(test.command) + " ]; then\n echo $$ > " + quote(ready) + "\n exec sleep 600\n fi\ndone\nexec " + quote(realGit) + " \"$@\"\n"
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"))
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
done := make(chan error, 1)
go func() { done <- test.run(ctx, repo) }()
// Synchronize cancellation with the intercepted operation, rather than timing its start.
ticker := time.NewTicker(5 * time.Millisecond)
defer ticker.Stop()
timeout := time.NewTimer(10 * time.Second)
defer timeout.Stop()
var pid int
waiting:
for {
select {
case err := <-done:
t.Fatalf("operation finished before interception: %v", err)
case <-timeout.C:
t.Fatal("Git operation was never intercepted")
case <-ticker.C:
data, err := os.ReadFile(ready)
if err == nil {
pid, err = strconv.Atoi(strings.TrimSpace(string(data)))
if err == nil {
break waiting
}
}
}
}
cancel()
select {
case err := <-done:
if !errors.Is(err, context.Canceled) {
t.Fatalf("lost context cause: %v", err)
}
if strings.Contains(test.name, "discovery") || test.command == "check-ref-format" || test.command == "ls-remote" {
requireCanceledRecovery(t, err)
}
case <-time.After(5 * time.Second):
process, _ := os.FindProcess(pid)
if process != nil {
_ = process.Kill()
}
<-done
t.Fatal("cancellation did not stop the Git operation")
}
})
}
}