148 lines
5.1 KiB
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")
|
|
}
|
|
})
|
|
}
|
|
}
|