package main import ( "crypto/sha256" "os" "os/exec" "path/filepath" "strings" "sync" "testing" ) const protoFreshnessHelperEnv = "CHECK_SAFETY_PROTO_FRESHNESS_HELPER" func TestDiffProtoOutputsIsDeterministic(t *testing.T) { before := map[string]protoOutputState{ "packages/api/src/unchanged.ts": {digest: [sha256.Size]byte{1}}, "packages/api/src/modified.ts": {digest: [sha256.Size]byte{2}}, "packages/api/src/removed.ts": {digest: [sha256.Size]byte{3}}, } after := map[string]protoOutputState{ "packages/api/src/unchanged.ts": {digest: [sha256.Size]byte{1}}, "packages/api/src/modified.ts": {digest: [sha256.Size]byte{4}}, "packages/api/src/added.ts": {digest: [sha256.Size]byte{5}}, } want := strings.Join([]string{ "?? packages/api/src/added.ts", " M packages/api/src/modified.ts", " D packages/api/src/removed.ts", }, "\n") if got := strings.Join(diffProtoOutputs(before, after), "\n"); got != want { t.Fatalf("diffProtoOutputs() = %q, want %q", got, want) } } func TestDiffProtoOutputsIgnoresPermissionDifferences(t *testing.T) { digest := [sha256.Size]byte{1} before := map[string]protoOutputState{ "packages/api/src/example.ts": {mode: 0o660, digest: digest}, } after := map[string]protoOutputState{ "packages/api/src/example.ts": {mode: 0o644, digest: digest}, } if got := diffProtoOutputs(before, after); len(got) != 0 { t.Fatalf("diffProtoOutputs() = %v, want permissions-only change ignored", got) } } func TestCheckProtoGeneratedFreshnessInSandboxLeavesSourceUntouched(t *testing.T) { repoRoot := newProtoFreshnessTestRepo(t, "@printf 'generated\\n' > packages/api/src/example.ts") tempParent := t.TempDir() result, err := checkProtoGeneratedFreshnessInSandbox(repoRoot, tempParent) if err != nil { t.Fatalf("checkProtoGeneratedFreshnessInSandbox() error = %v\n%s", err, result.output) } if !result.checked { t.Fatal("checkProtoGeneratedFreshnessInSandbox() checked = false, want true") } if !result.changed() { t.Fatal("checkProtoGeneratedFreshnessInSandbox() changed = false, want true") } if result.beforeStatus != "" { t.Fatalf("beforeStatus = %q, want clean", result.beforeStatus) } if result.afterStatus != " M packages/api/src/example.ts" { t.Fatalf("afterStatus = %q, want modified generated file", result.afterStatus) } assertProtoTestSourceAndSandboxesClean(t, repoRoot, tempParent) } func TestCheckProtoGeneratedFreshnessInSandboxReportsClean(t *testing.T) { repoRoot := newProtoFreshnessTestRepo(t, "@:") tempParent := t.TempDir() result, err := checkProtoGeneratedFreshnessInSandbox(repoRoot, tempParent) if err != nil { t.Fatalf("checkProtoGeneratedFreshnessInSandbox() error = %v\n%s", err, result.output) } if result.changed() { t.Fatalf("checkProtoGeneratedFreshnessInSandbox() changed = true, status = %q", result.afterStatus) } assertProtoTestSourceAndSandboxesClean(t, repoRoot, tempParent) } func TestCheckProtoGeneratedFreshnessInSandboxCleansUpAfterFailure(t *testing.T) { repoRoot := newProtoFreshnessTestRepo(t, "@printf 'partial\\n' > packages/api/src/example.ts; printf 'generator failed\\n' >&2; exit 7") tempParent := t.TempDir() result, err := checkProtoGeneratedFreshnessInSandbox(repoRoot, tempParent) if err == nil { t.Fatal("checkProtoGeneratedFreshnessInSandbox() error = nil, want generator failure") } if !strings.Contains(err.Error(), "make proto failed") { t.Fatalf("error = %q, want make proto context", err) } if !strings.Contains(result.output, "generator failed") { t.Fatalf("output = %q, want generator stderr", result.output) } assertProtoTestSourceAndSandboxesClean(t, repoRoot, tempParent) } func TestCheckProtoGeneratedFreshnessConcurrentProcesses(t *testing.T) { repoRoot := newProtoFreshnessTestRepo(t, "@mkdir generation-exclusive; sleep 0.05; printf 'generated in %s\\n' \"$$PWD\" > packages/api/src/example.ts") tempParent := t.TempDir() testBinary, err := os.Executable() if err != nil { t.Fatalf("resolve test executable: %v", err) } const processCount = 6 type processResult struct { index int output string err error } results := make(chan processResult, processCount) var workers sync.WaitGroup for index := range processCount { workers.Go(func() { cmd := exec.CommandContext(t.Context(), testBinary, "-test.run=^TestProtoFreshnessHelperProcess$") cmd.Env = append(os.Environ(), protoFreshnessHelperEnv+"=1", "CHECK_SAFETY_PROTO_REPO="+repoRoot, "CHECK_SAFETY_PROTO_TEMP_PARENT="+tempParent, ) output, runErr := cmd.CombinedOutput() results <- processResult{index: index, output: string(output), err: runErr} }) } workers.Wait() close(results) for result := range results { if result.err != nil { t.Errorf("checker process %d failed: %v\n%s", result.index, result.err, result.output) } } assertProtoTestSourceAndSandboxesClean(t, repoRoot, tempParent) } func TestProtoFreshnessHelperProcess(t *testing.T) { if os.Getenv(protoFreshnessHelperEnv) != "1" { return } repoRoot := os.Getenv("CHECK_SAFETY_PROTO_REPO") tempParent := os.Getenv("CHECK_SAFETY_PROTO_TEMP_PARENT") result, err := checkProtoGeneratedFreshnessInSandbox(repoRoot, tempParent) if err != nil { t.Fatalf("checkProtoGeneratedFreshnessInSandbox() error = %v\n%s", err, result.output) } if !result.changed() { t.Fatal("checkProtoGeneratedFreshnessInSandbox() changed = false, want true") } } func newProtoFreshnessTestRepo(t *testing.T, recipe string) string { t.Helper() if _, err := exec.LookPath("make"); err != nil { t.Skip("make is required for proto freshness integration coverage") } repoRoot := t.TempDir() writeTestFile(t, filepath.Join(repoRoot, "Makefile"), ".PHONY: proto\nproto:\n\t"+recipe+"\n", 0o644) writeTestFile(t, filepath.Join(repoRoot, "buf.gen.yaml"), "version: v2\n", 0o644) writeTestFile(t, filepath.Join(repoRoot, "packages", "api", "src", "example.ts"), "source\n", 0o644) return repoRoot } func assertProtoTestSourceAndSandboxesClean(t *testing.T, repoRoot, tempParent string) { t.Helper() content, err := os.ReadFile(filepath.Join(repoRoot, "packages", "api", "src", "example.ts")) if err != nil { t.Fatalf("read source generated file: %v", err) } if string(content) != "source\n" { t.Fatalf("source generated file = %q, want unchanged", content) } entries, err := os.ReadDir(tempParent) if err != nil { t.Fatalf("read sandbox parent: %v", err) } if len(entries) != 0 { t.Fatalf("sandbox parent contains residual entries: %v", entries) } }