-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathcoverage_test.go
More file actions
265 lines (251 loc) · 36.7 KB
/
Copy pathcoverage_test.go
File metadata and controls
265 lines (251 loc) · 36.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
package cachecov
// TestCacheVersionCoverage is the regression guard for Enola's core promise: every
// cacheVersion bump documented in internal/engine/cache.go must be backed by a real
// test. cache.go's changelog (the `// vN:` lines) is the spec for the extractors'
// deterministic, multi-language behavior; this guard makes it impossible to add a
// new vN — and therefore ship an extractor behavior change — without registering a
// covering test here.
//
// The guard fails (before CI, if wired into a git pre-push hook) when any of:
// - a `// vN:` line exists in cache.go with no entry in versionCoverage,
// - versionCoverage names a test function that does not exist in the tree,
// - the changelog is not the contiguous range v2..cacheVersion,
// - versionCoverage has a stale entry for a vN cache.go no longer documents.
//
// When you bump cacheVersion and add a `// vN+1:` line, add a matching entry below
// pointing at the test(s) that assert the new behavior. If you rename a referenced
// test, update its name here — that coupling is intentional: it keeps the map honest.
//
// This package is deliberately dependency-free (no engine/extractor imports, no
// tree-sitter), so it builds and runs in milliseconds.
import (
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
)
// versionCoverage maps each cacheVersion (the integer N in "vN") to one or more
// test functions that assert the behavior that version introduced. v1 is the
// implicit baseline (no changelog line), so coverage starts at v2.
var versionCoverage = map[int][]string{
2: {"TestExtractURLSessionFacts"}, // Swift URLSession precision
3: {"TestExtractFile_FastAPIRoute_Get"}, // Python route Name shape
4: {"TestRestTemplate_ClientCalls", "TestFeignClient_AndControllerDiscriminated"}, // Java RestTemplate/@FeignClient
5: {"TestLaravelRoutes_Resource", "TestPHPHTTPClient_GuzzleRequest"}, // PHP HTTP client + route DSLs
6: {"TestExtractFile_BareConstantReferences"}, // Ruby bare-constant RelCalls
7: {"TestExtractFile_BuiltinConstantsSkipped"}, // Ruby builtin-constant skip + serializer fold
8: {"TestCRegistrationMacro", "TestCRegistrationMacroMultiArg"}, // C/C++ file-scope registration macros
9: {"TestCFuncPtrFieldAssignment", "TestCMacroBodyCall"}, // C/C++ func-ptr fields + macro-body refs
10: {"TestCStaticRegistrationMacro"}, // C/C++ qualifier-prefixed reg macro
11: {"TestCCompoundLiteralAssignment"}, // C/C++ in-body compound-literal init
12: {"TestCMacroBodyValuePosition"}, // C/C++ macro-body value-position func ptrs
13: {"TestCMacroExpansionTokenPaste"}, // C/C++ token-paste macro expansion
14: {"TestCStaticSingleArgAttr", "TestCDefineShowAttribute"}, // C/C++ single-arg DEVICE_ATTR, all-ident scan
15: {"TestCMachineDescCleanErrorRegion"}, // C/C++ clean ERROR-node machine_desc salvage
16: {"TestCMachineDescErrorRegion"}, // C/C++ assignment/field_expression fragment salvage
17: {"TestCMachineDescSalvageSkipsFunctionBodies"}, // C/C++ full-tree salvage skips function bodies
18: {"TestExtractTestRefsAST", "TestExtractFile_CustomClassMacroRecorded"}, // Ruby custom macros + KindTestRef
19: {"TestExtractFile_ClassBodyQualifiedCall"}, // Ruby class-body calls + KindFileRef
20: {"TestExtractFile_TopLevelAssignmentRHS"}, // Ruby per-scope pass (assignment RHS)
21: {"TestExtractFile_InterpolatedSymbolPrefix"}, // Ruby interpolated-symbol prefix
22: {"TestExtractFile_SuperReferencesAncestor", "TestExtractFile_LiteralSymbolDispatch"}, // Ruby super + literal-symbol dispatch
23: {"TestExtractFile_ChainedNoArgCall"}, // Ruby chained-receiver no-arg call
24: {"TestExtractFile_DefaultParamCall", "TestExtractFile_PredicateBangSingleLevelCall"}, // Ruby default-param + predicate/bang calls
25: {"TestExtractFile_DelegateFold"}, // Ruby delegate :a, to: X fold
26: {"TestExtractFile_LocalRelationScopeCall"}, // Ruby scope-like call on identifier receiver
27: {"TestExtractFile_IvarUnderscoredCall", "TestExtractFile_GvarUnderscoredCall"}, // Ruby @ivar/@@cvar/$gvar receivers
28: {"TestExtractFile_KlassReceiverDispatch", "TestExtractFile_ClazzKlazzReceiverDispatch"}, // Ruby klass/clazz/klazz dispatch
29: {"TestExtractFile_InterpolatedStringPrefix"}, // Ruby interpolated-string prefix
30: {"TestExtractFile_StringPrefixGatedOnDispatcher"}, // Ruby string-prefix dispatcher gating
31: {"TestRbComplexity_BlockParamNotCall", "TestRbComplexity_InBatchesNotElementLoop"}, // Ruby block params + find/in_batches
32: {"TestRbComplexity_SuperIsNotRecursion", "TestRbComplexity_DelegationIsNotRecursion"}, // Ruby super/decorator not recursion
33: {"TestRbComplexity_SelfClassSiblingIsNotRecursion", "TestRbComplexity_ConstSelfClassMethodIsRecursion"}, // Ruby same-object recursion gating
34: {"TestRbComplexity_ConstantBoundLoopNoDepth"}, // Ruby constant-bounded loops
35: {"TestRbComplexity_LiteralChainLoopIsBounded"}, // Ruby bounded-loop chain unwrap
36: {"TestAST_ModuleAbstractness", "TestAST_ConcernDetection"}, // Ruby module abstract prop
37: {"TestParseXcodeGenProject", "TestManifest_TargetDependencyEdges"}, // Swift SPM/XcodeGen target modules
38: {"TestModuleResolver_SharedSourceRootCollapses"}, // Swift shared-source-root collapse
39: {"TestSymbolKind_MethodVsFunc", "TestRelCalls_SelfOptionalChaining"}, // Swift SymbolMethod + member-call edges
40: {"TestFileScope_TopLevelCallsEmitFileRef", "TestOperator_CustomInfixTracked"}, // Swift file-scope refs + custom operators
41: {"TestOperator_StandardMulticharNotTracked"}, // Swift stdlib-operator exclusion
42: {"TestExtract_FlattenedTypeMethodResolves", "TestExtensionPropertyGetterCalls"}, // Swift funcIndex fallback + extension property owner
43: {"TestClassPropertyInitAttachesToType"}, // Swift class-property init attaches to type
44: {"TestSwComplexity_BoundedForRange", "TestSwComplexity_ComputedPropertyGetter"}, // Swift bounded loops + property metrics
45: {"TestSwComplexity_SubscriptOnSameNameLocalNotRecursion"}, // Swift subscript-not-call
46: {"TestSwComplexity_OverloadDelegationNotRecursion"}, // Swift label-aware recursion
47: {"TestSwIO_DirectNetworkPrimitiveSetsIODirect", "TestComputePerformsIO_TransitiveThroughAmbiguousEdge"}, // Swift io_direct/performs_io
48: {"TestResolveInheritedCalls_SubclassBaseMethod"}, // Swift inherited-method resolution
49: {"TestTargetPriorityAndRoles"}, // Swift test-bundle module + module_role
50: {"TestTargetPriorityAndRoles"}, // shared ModuleRoleForPath heuristic
51: {"TestExtract_NoFalseCrossTargetCycle"}, // Swift no false module cycle
52: {"TestAST_MemberFunctionIsMethodKind", "TestAST_OverrideAndDIProviderProps"}, // Kotlin SymbolMethod + nav edges + di/override
53: {"TestDetectBasePackage_GroovyAndKotlinDSL"}, // Kotlin Groovy namespace base package
54: {"TestAST_CallsOutsideFunctionBody"}, // Kotlin default-param + ctor-delegation calls
55: {"TestAST_CallableReference", "TestAST_QualifiedCallableReference"}, // Kotlin ::foo / Type::foo callable refs
56: {"TestKtComplexity_OverloadDelegationNotRecursion", "TestKtComplexity_ReactiveChainNotLoop"}, // Kotlin arity recursion + RxJava/Flow
57: {"TestKtComplexity_SuperDelegationNotRecursion", "TestKtComplexity_RetrofitMethodPerformsIO"}, // Kotlin super-delegation + Retrofit/Room io
58: {"TestBuildPackageIndex_MultiModuleSamePrefix", "TestAST_SealedClassIsAbstract"}, // Kotlin/Java multi-module import + sealed abstract
59: {"TestBuildPackageIndex_MainWinsOverVariant"}, // Kotlin main-source-set preference
60: {"TestModuleRole", "TestDagger_DIvsSpringComponent"}, // compound test module + Dagger di_component/module
61: {"TestExtract_FileRef_JSXComponentUsage", "TestExtract_DynamicAndRequireDependencyEdges"}, // TS JSX/require file refs + dep edges
62: {"TestExtract_FileRef_SameModuleUsePositions"}, // TS same-module use positions
63: {"TestExtract_AnonymousDefaultExport_NamedByFile"}, // TS default import -> default export
64: {"TestExtract_ThisMemberReference_EventHandler"}, // TS this.member reference
65: {"TestExtract_SkipsMinifiedBundle", "TestIsMinifiedSource"}, // TS minified-file skip
66: {"TestTsIO_DirectPrimitiveSetsIODirect", "TestTsIO_PerformsIOPropagatesToCaller"}, // TS io_direct/performs_io
67: {"TestTsIO_NamedImportsFromNetworkModuleNotIO"}, // TS tightened io_direct
68: {"TestExtract_AbstractClass"}, // TS abstract class prop
69: {"TestExtractURLSessionFacts_TestSourcesSkipped", "TestExtractEndpointFacts"}, // Swift endpoint-enum + test-source skip
70: {"TestExtractEndpointFacts_DefaultPrefix"}, // Swift endpoint version-prefix
71: {"TestExtractStoredMethodEndpointFacts"}, // Swift stored-method endpoints
72: {"TestWrapperEndpoint_PathAndVerbFromCallSite", "TestRoutes_NestedSingularResource"}, // Swift request-wrapper + Ruby nested resources
73: {"TestExtract_ServerRoutesPerRPC", "TestGRPCClient_OnlyCalledMethodsEmitted"}, // gRPC proto server routes + TS gRPC-web client routes
74: {"TestGoGRPCClient_EmitsClientRoutes"}, // Go gRPC client call-site routes
75: {"TestConnectES_CreateClient", "TestGoGRPCClient_ConnectGo"}, // connect-es + connect-go + struct-field-injected clients
76: {"TestGRPCWebClient", "TestGoGRPCClient_PackageVar"}, // grpc-web clients + Go package-level-var clients
77: {"TestDetect_LooseRubyFileNoGemfile", "TestDetect_ExtensionlessRubyShebang"}, // Ruby Gemfile-less detection + extensionless shebang indexing
78: {"TestAST_AbsoluteImportEmitsCallEdge", "TestResolveCallTargets_AbsoluteInternal_RewritesToSlashSymbol", "TestResolveCallTargets_External_DropsEdge", "TestAST_TopLevelCallEmitsFileRef", "TestAST_DecoratorEmitsRef"}, // Python absolute-import call edges + top-level/decorator refs
79: {"TestAST_LazyImportInsideFunctionResolves", "TestAST_CallArgumentValueRefEmitsEdge", "TestAST_ParameterDefaultCallEmitsEdge", "TestAST_DecoratorArgumentCallEmitsFileRef", "TestAST_ClickCommandTagged"}, // Python lazy imports, value-ref args, param defaults, decorator args, click tagging
80: {"TestExtractFile_FastAPIRoute_PathKeyword", "TestExtractEntryPoints_ProviderInfo", "TestAST_DottedStringLiteralEmitsRef"}, // Python path= keyword routes, pyproject entry-points, dotted-string refs
81: {"TestAST_ClassBodyCallAndValueRefEmitEdges", "TestAST_SameModuleValueRef", "TestAST_ParamPassedAsValue_NoFalseEdge", "TestAST_ComputedPathRouteHandlerTagged"}, // Python class-body wiring, same-module value-refs, computed-path route-handler tag
82: {"TestAST_RegistrationDecoratorsMarkUsed", "TestAST_AttributeArgValueRef", "TestAST_DictValueRef", "TestAST_ListOfLocalsNoFalseRef"}, // Python registration decorators + attribute/collection value-refs
83: {"TestPyComplexity_ScalingLoopDepth_BoundedInnerDiscounted", "TestPyComplexity_ScalingLoopDepth_FullyBounded", "TestPyComplexity_ScalingLoopDepth_WhileTrueBounded", "TestPyComplexity_IODirect", "TestPyComputePerformsIO_Transitive", "TestExtract_ScalingLoopDepth_BoundedDiscounted"}, // scaling_loop_depth (Python/Go) + Python io_direct/performs_io
84: {"TestPyComplexity_CallsInScalingLoop_BoundedExcluded", "TestExtract_CallsInScalingLoop_BoundedExcluded", "TestTsComplexity_CallsInScalingLoop_BoundedExcluded"}, // calls_in_scaling_loop (Python/Go/TS)
85: {"TestAST_DataClassAndEnumProps", "TestAST_InformalAbstractDetection"}, // Python enum/data_class props + informal-abstract (raise NotImplementedError)
86: {"TestAST_DataClassAndEnumProps"}, // Python data_class broadened to RootModel/*BaseModel subclasses (StrictBaseModel)
87: {"TestAST_ParamCall_NoEdge", "TestAST_LocalCallable_NoEdge", "TestAST_LoopVarCall_NoEdge", "TestAST_SameModuleCall_StillResolves"}, // Python resolveCall no longer fabricates same-module edges for params/locals/loop vars
88: {"TestPyGRPC_ClientStubCall_EmitsRoute", "TestPyGRPC_StubRebinding_PositionalBinding", "TestPyGRPC_DynamicStubClass_NoRoute", "TestPyGRPC_NoStubImport_NoRoute"}, // Python gRPC client-role routes from stub.Method() call sites
89: {"TestMethodNear_BeforeAndEnumForms", "TestExtractEndpointFacts_ExternalHost"}, // Swift widened method inference + external-host tagging
90: {"TestRetrofit_AbsoluteURLExternal", "TestRoutes_MatchViaVerbs", "TestRoutes_ScopePathKeyword"}, // Kotlin Retrofit external tagging + Ruby match via:/scope path:
91: {"TestRoutes_ResourcesUpdatePutAndPatch"}, // Ruby resources/resource update emits PATCH + PUT
92: {"TestRoutes_SymbolPathArg", "TestRoutes_ScopeBareSymbolPrefix", "TestRoutes_ResourcePathOverride"}, // Ruby symbol path args + scope :symbol + resource path: override
93: {"TestSwitchReturns_MultiLineCaseLabels", "TestExtractEndpointFacts_MultiLineMethodCase"}, // Swift multi-line case-label method parsing
94: {"TestExtractEndpointFacts_ConstantMethod"}, // Swift single-value (constant) method property
95: {"TestResolveImports_CouplingKindTagged", "TestResolveImports_ReferenceBeatsAssociation"}, // Ruby synthetic-edge coupling_kind prop + framework-const ignore list
96: {"TestAST_AssignedAndReturnedCallback", "TestAST_AssignedCallback_ShadowGuarded", "TestAST_ReturnedPlainVariable_NoPhantomRef", "TestAST_ReturnedForwardReference_Resolves", "TestAST_ShadowedLoopVarNotResolvedAsCall"}, // Python assignment/return value-refs + scope-wide (not just param) shadow guard
97: {"TestMatchAnyGlob_MidPatternDoublestar", "TestGolden"}, // Directory-scoped Ruby test globs: production *_ab_test.rb no longer deleted from the graph
}
func TestCacheVersionCoverage(t *testing.T) {
root := repoRoot(t)
current, changelog := parseCacheVersions(t, filepath.Join(root, "internal", "engine", "cache.go"))
// 1. Structural: the changelog must be exactly the contiguous range v2..current.
if current < 2 {
t.Fatalf("cacheVersion = v%d, want >= v2", current)
}
for v := 2; v <= current; v++ {
if !changelog[v] {
t.Errorf("cache.go has no `// v%d:` changelog line, but cacheVersion is v%d — every version must be documented", v, current)
}
}
for v := range changelog {
if v > current {
t.Errorf("cache.go documents v%d but cacheVersion is only v%d — bump cacheVersion or remove the changelog line", v, current)
}
}
// 2. Every documented version must have a coverage entry.
for v := range changelog {
tests, ok := versionCoverage[v]
if !ok || len(tests) == 0 {
t.Errorf("v%d is documented in cache.go but has no entry in versionCoverage — register the test(s) that assert it", v)
}
}
// 3. No stale coverage entries for versions cache.go no longer documents.
for v := range versionCoverage {
if !changelog[v] {
t.Errorf("versionCoverage has a stale entry for v%d, which cache.go does not document", v)
}
}
// 4. Every referenced test must actually exist in the tree.
existing := testFuncNames(t, root)
for v, tests := range versionCoverage {
for _, name := range tests {
if !existing[name] {
t.Errorf("v%d references test %q, which does not exist — fix the name in versionCoverage (renamed?) or add the test", v, name)
}
}
}
}
// repoRoot walks up from the test's working directory until it finds go.mod.
func repoRoot(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
if err != nil {
t.Fatalf("getwd: %v", err)
}
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
t.Fatal("could not locate go.mod above the test working directory")
}
dir = parent
}
}
var (
cacheVersionConst = regexp.MustCompile(`cacheVersion\s*=\s*"v(\d+)"`)
changelogLine = regexp.MustCompile(`^//\s*v(\d+):`)
)
// parseCacheVersions returns the current cacheVersion integer and the set of
// version integers documented by `// vN:` lines in cache.go.
func parseCacheVersions(t *testing.T, path string) (current int, changelog map[int]bool) {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read cache.go: %v", err)
}
changelog = map[int]bool{}
for _, raw := range strings.Split(string(data), "\n") {
line := strings.TrimSpace(raw)
if m := changelogLine.FindStringSubmatch(line); m != nil {
n, _ := strconv.Atoi(m[1])
changelog[n] = true
continue
}
if m := cacheVersionConst.FindStringSubmatch(line); m != nil {
current, _ = strconv.Atoi(m[1])
}
}
if current == 0 {
t.Fatal("could not find the cacheVersion constant in cache.go")
}
return current, changelog
}
// testFuncNames scans every *_test.go under internal/ and pkg/ and returns the set
// of top-level `func TestXxx(` names.
func testFuncNames(t *testing.T, root string) map[string]bool {
t.Helper()
funcRe := regexp.MustCompile(`^func (Test[A-Za-z0-9_]+)\(`)
names := map[string]bool{}
for _, sub := range []string{"internal", "pkg"} {
base := filepath.Join(root, sub)
err := filepath.WalkDir(base, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || !strings.HasSuffix(path, "_test.go") {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
for _, line := range strings.Split(string(data), "\n") {
if m := funcRe.FindStringSubmatch(line); m != nil {
names[m[1]] = true
}
}
return nil
})
if err != nil {
t.Fatalf("walk %s: %v", base, err)
}
}
if len(names) == 0 {
t.Fatal("found no test functions — scan path is wrong")
}
return names
}