193 lines
6.4 KiB
Go
193 lines
6.4 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 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)
|
|
}
|
|
}
|