check-safety/proto_freshness_test.go
2026-08-25 21:27:37 +08:00

179 lines
5.9 KiB
Go

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 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)
}
}