Skip to content

Commit 2743a0d

Browse files
test(server): RPC-iterating drift guard for the gRPC surface (done-bar §4) (#48)
Add TestGRPCSurface_EveryRPCHasRoundTripTest — the registry-iterating done-bar guard (rule 18) for the provisioner gRPC surface. It iterates the proto-generated ProvisionerService_ServiceDesc.Methods (single source of truth for which RPCs exist) and fails CI if any RPC lacks a maintained, existing real-backend round-trip test. Closes the silent-untested-RPC class: today's round-trip suite proves ProvisionResource/DeprovisionResource/GetStorageBytes/RegradeResource are exercised end-to-end, but adding a new RPC to the proto would not red that suite — it would ship with zero real-backend coverage, silently. This guard reds on: - a new RPC with no rpcCoverage entry (unmapped), - a mapping pointing at a deleted/renamed test (source-parsed, so the name must really exist), or - a stale mapping/exemption for an RPC removed from the proto. Pure descriptor + source-scan test: no backends, no env, never skips — runs unconditionally in the `go test -short` deploy gate, as a drift guard must. Exemptions require both an exemptedRPCs entry and a justification row in INTEGRATION-COVERAGE-EXCLUSIONS.md (none today). All 4 RPCs already have real round-trip tests; this test only guards against future drift. make gate green; failing-then-passing verified (dropping a mapping or renaming a mapped test reds it). Co-authored-by: Manas Srivastava <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 69ad7f2 commit 2743a0d

2 files changed

Lines changed: 204 additions & 0 deletions

File tree

INTEGRATION-COVERAGE-EXCLUSIONS.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,19 @@ Everything else in the gRPC handler layer — every `ProvisionResource`,
4848
redis, mongo, queue, and storage — IS driven by a real-backend round-trip
4949
(`server_live_roundtrip_test.go` + `server_live_roundtrip_mqs_test.go`) and sits
5050
at 100% function coverage.
51+
52+
## Drift guard — the RPC-iterating done-bar test (rule 18)
53+
54+
`internal/server/server_rpc_coverage_guard_test.go`
55+
(`TestGRPCSurface_EveryRPCHasRoundTripTest`) iterates the proto-generated
56+
`ProvisionerService_ServiceDesc.Methods` (the single source of truth for which
57+
RPCs exist) and FAILS CI if any RPC lacks a maintained, existing round-trip test
58+
— catching the silent-untested-RPC class the day a new RPC is added to
59+
`proto/provisioner/v1/provisioner.proto`. It is a pure descriptor + source-scan
60+
test (no backends, no env) so it runs unconditionally in the `go test -short`
61+
deploy gate, never skips, and reds on: an unmapped new RPC, a mapping pointing at
62+
a deleted/renamed test, or a stale mapping for a removed RPC.
63+
64+
To intentionally exempt an RPC from round-trip coverage, add it to the
65+
`exemptedRPCs` set in that test AND add a justification row to this file. There
66+
are **no exemptions today** — every RPC has a real round-trip.
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package server_test
2+
3+
// server_rpc_coverage_guard_test.go — the DRIFT GUARD for the provisioner gRPC
4+
// surface (done-bar §4, rule 18: registry-iterating, not hand-typed).
5+
//
6+
// Why this file exists: the real-backend round-trip coverage in
7+
// server_live_roundtrip_test.go + server_live_roundtrip_mqs_test.go proves each
8+
// of today's RPCs (ProvisionResource / DeprovisionResource / GetStorageBytes /
9+
// RegradeResource) is exercised end-to-end against a genuine backend. But that
10+
// guarantee is a snapshot: the day someone adds a new RPC to
11+
// proto/provisioner/v1/provisioner.proto, the round-trip suite would NOT fail —
12+
// the new method would simply ship with zero real-backend coverage, silently
13+
// (the silent-untested-RPC class).
14+
//
15+
// This test closes that hole. It iterates the proto-GENERATED service descriptor
16+
// (ProvisionerService_ServiceDesc.Methods — the single source of truth for which
17+
// RPCs exist) and asserts every RPC maps to a maintained round-trip test that
18+
// actually exists in this package's source. It fails CI if:
19+
// - a new RPC is added to the proto without a mapping here (unmapped RPC), or
20+
// - a mapping points at a test that no longer exists (deleted/renamed test), or
21+
// - a mapping is removed while its RPC still exists in the descriptor.
22+
//
23+
// It is a pure descriptor + source-scan test: no backends, no env — it runs in
24+
// the `go test -short` deploy.yml gate and never skips. That is deliberate: the
25+
// drift guard must be unconditional, or it cannot catch the drift it exists for.
26+
27+
import (
28+
"go/ast"
29+
"go/parser"
30+
"go/token"
31+
"os"
32+
"path/filepath"
33+
"sort"
34+
"strings"
35+
"testing"
36+
37+
provisionerv1 "instant.dev/proto/provisioner/v1"
38+
)
39+
40+
// rpcCoverage maps each gRPC RPC (by its proto MethodName) to the round-trip
41+
// test(s) that drive it against a real backend. At least one mapped test must
42+
// exist in this package's source for every RPC in the service descriptor.
43+
//
44+
// To exempt an RPC from round-trip coverage, give it an empty slice AND add a
45+
// justification row to INTEGRATION-COVERAGE-EXCLUSIONS.md (the test enforces the
46+
// empty-slice case is intentional via the exemptedRPCs set below).
47+
var rpcCoverage = map[string][]string{
48+
"ProvisionResource": {
49+
"TestServer_Postgres_Provision_Regrade_Deprovision_LiveRoundTrip",
50+
"TestServer_Redis_Provision_Deprovision_LiveRoundTrip",
51+
"TestServer_Mongo_Provision_StorageBytes_Deprovision_LiveRoundTrip",
52+
"TestServer_Queue_Provision_Deprovision_LiveRoundTrip",
53+
},
54+
"DeprovisionResource": {
55+
"TestServer_Postgres_Provision_Regrade_Deprovision_LiveRoundTrip",
56+
"TestServer_Postgres_Reprovision_AfterDeprovision_LiveRoundTrip",
57+
"TestServer_Redis_Provision_Deprovision_LiveRoundTrip",
58+
"TestServer_Mongo_Provision_StorageBytes_Deprovision_LiveRoundTrip",
59+
"TestServer_Queue_Provision_Deprovision_LiveRoundTrip",
60+
},
61+
"GetStorageBytes": {
62+
"TestServer_Mongo_Provision_StorageBytes_Deprovision_LiveRoundTrip",
63+
"TestServer_Queue_Provision_Deprovision_LiveRoundTrip",
64+
"TestServer_Storage_GetStorageBytes_LiveRoundTrip",
65+
},
66+
"RegradeResource": {
67+
"TestServer_Postgres_Provision_Regrade_Deprovision_LiveRoundTrip",
68+
"TestServer_Mongo_Provision_StorageBytes_Deprovision_LiveRoundTrip",
69+
"TestServer_Queue_Provision_Deprovision_LiveRoundTrip",
70+
},
71+
}
72+
73+
// exemptedRPCs is the set of RPCs intentionally mapped to zero round-trip tests.
74+
// An RPC may appear here ONLY if it also has a justification row in
75+
// INTEGRATION-COVERAGE-EXCLUSIONS.md. Empty today — every RPC has a round-trip.
76+
var exemptedRPCs = map[string]bool{}
77+
78+
// TestGRPCSurface_EveryRPCHasRoundTripTest is the registry-iterating drift guard.
79+
// It walks the proto-generated service descriptor and asserts each RPC has a
80+
// maintained, existing round-trip test (or a justified exemption).
81+
func TestGRPCSurface_EveryRPCHasRoundTripTest(t *testing.T) {
82+
// (1) The authoritative RPC list: the proto-generated service descriptor.
83+
descRPCs := descriptorRPCNames()
84+
if len(descRPCs) == 0 {
85+
t.Fatal("ProvisionerService_ServiceDesc.Methods is empty — descriptor introspection broke; the drift guard cannot run")
86+
}
87+
88+
// (2) The set of test function names that actually exist in this package.
89+
existing := existingTestFuncs(t)
90+
91+
descSet := make(map[string]bool, len(descRPCs))
92+
for _, rpc := range descRPCs {
93+
descSet[rpc] = true
94+
}
95+
96+
// (3) Every RPC in the descriptor must be mapped (or exempted) AND its
97+
// mapped tests must really exist.
98+
for _, rpc := range descRPCs {
99+
mapped, ok := rpcCoverage[rpc]
100+
if !ok {
101+
t.Errorf("RPC %q exists in ProvisionerService_ServiceDesc but has NO entry in rpcCoverage. "+
102+
"A new RPC shipped without a real-backend round-trip test. Add a round-trip test "+
103+
"(reuse the harness in server_live_roundtrip_mqs_test.go) and map it here, or add an "+
104+
"exemption to exemptedRPCs + a justification row in INTEGRATION-COVERAGE-EXCLUSIONS.md.", rpc)
105+
continue
106+
}
107+
if len(mapped) == 0 {
108+
if !exemptedRPCs[rpc] {
109+
t.Errorf("RPC %q maps to zero round-trip tests but is not in exemptedRPCs. "+
110+
"Either add a round-trip test or mark it exempt (with a justification row in "+
111+
"INTEGRATION-COVERAGE-EXCLUSIONS.md).", rpc)
112+
}
113+
continue
114+
}
115+
for _, name := range mapped {
116+
if !existing[name] {
117+
t.Errorf("RPC %q maps to test %q, but no such test function exists in package server_test. "+
118+
"A round-trip test was deleted or renamed without updating rpcCoverage — the RPC is now "+
119+
"silently untested. Restore the test or fix the mapping.", rpc, name)
120+
}
121+
}
122+
}
123+
124+
// (4) No stale mappings: every key in rpcCoverage must be a real RPC. This
125+
// catches a mapping left behind after an RPC was removed from the proto.
126+
for rpc := range rpcCoverage {
127+
if !descSet[rpc] {
128+
t.Errorf("rpcCoverage maps %q, which is NOT an RPC in ProvisionerService_ServiceDesc. "+
129+
"Remove the stale mapping (the RPC was renamed/removed in the proto).", rpc)
130+
}
131+
}
132+
for rpc := range exemptedRPCs {
133+
if !descSet[rpc] {
134+
t.Errorf("exemptedRPCs lists %q, which is NOT an RPC in ProvisionerService_ServiceDesc. "+
135+
"Remove the stale exemption.", rpc)
136+
}
137+
}
138+
}
139+
140+
// descriptorRPCNames returns the sorted list of RPC MethodNames from the
141+
// proto-generated grpc.ServiceDesc — the single source of truth.
142+
func descriptorRPCNames() []string {
143+
names := make([]string, 0, len(provisionerv1.ProvisionerService_ServiceDesc.Methods))
144+
for _, m := range provisionerv1.ProvisionerService_ServiceDesc.Methods {
145+
names = append(names, m.MethodName)
146+
}
147+
// Streaming RPCs (none today) would also need coverage; include them so a
148+
// future streaming RPC is caught too.
149+
for _, s := range provisionerv1.ProvisionerService_ServiceDesc.Streams {
150+
names = append(names, s.StreamName)
151+
}
152+
sort.Strings(names)
153+
return names
154+
}
155+
156+
// existingTestFuncs parses every *_test.go file in this directory and returns
157+
// the set of top-level `func Test...` names. This is what makes a deleted or
158+
// renamed round-trip test fail the guard: the mapping references a name that no
159+
// longer parses out of the source.
160+
func existingTestFuncs(t *testing.T) map[string]bool {
161+
t.Helper()
162+
out := map[string]bool{}
163+
entries, err := os.ReadDir(".")
164+
if err != nil {
165+
t.Fatalf("read package dir: %v", err)
166+
}
167+
fset := token.NewFileSet()
168+
for _, e := range entries {
169+
name := e.Name()
170+
if e.IsDir() || !strings.HasSuffix(name, "_test.go") {
171+
continue
172+
}
173+
f, perr := parser.ParseFile(fset, filepath.Join(".", name), nil, 0)
174+
if perr != nil {
175+
t.Fatalf("parse %s: %v", name, perr)
176+
}
177+
for _, decl := range f.Decls {
178+
fn, ok := decl.(*ast.FuncDecl)
179+
if !ok || fn.Recv != nil {
180+
continue
181+
}
182+
if strings.HasPrefix(fn.Name.Name, "Test") {
183+
out[fn.Name.Name] = true
184+
}
185+
}
186+
}
187+
return out
188+
}

0 commit comments

Comments
 (0)