Skip to content

Commit a2f3096

Browse files
committed
test(arch): add full test suite for all 8 architecture detectors
1 parent 6a23b27 commit a2f3096

1 file changed

Lines changed: 346 additions & 0 deletions

File tree

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
package detectors_test
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
9+
"github.com/hamza-hafeez82/cortex/internal/walker"
10+
"github.com/hamza-hafeez82/cortex/pkg/detector"
11+
"github.com/hamza-hafeez82/cortex/pkg/detector/detectors"
12+
)
13+
14+
func makeArchCtx(t *testing.T, files map[string]string) *detector.ScanContext {
15+
t.Helper()
16+
root := t.TempDir()
17+
for name, content := range files {
18+
abs := filepath.Join(root, filepath.FromSlash(name))
19+
if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil {
20+
t.Fatal(err)
21+
}
22+
if err := os.WriteFile(abs, []byte(content), 0o644); err != nil {
23+
t.Fatal(err)
24+
}
25+
}
26+
w := walker.New(walker.DefaultOptions())
27+
repo, err := w.Walk(root)
28+
if err != nil {
29+
t.Fatal(err)
30+
}
31+
return &detector.ScanContext{Repo: repo}
32+
}
33+
34+
// ── CX-ARCH-001: God File ─────────────────────────────────────────────────────
35+
36+
func TestGodFile_Detects(t *testing.T) {
37+
// Build a file with 500+ lines and 10+ functions
38+
var sb strings.Builder
39+
for i := 0; i < 15; i++ {
40+
sb.WriteString("func doSomething() {\n")
41+
for j := 0; j < 35; j++ {
42+
sb.WriteString(" x := 1\n")
43+
}
44+
sb.WriteString("}\n")
45+
}
46+
47+
ctx := makeArchCtx(t, map[string]string{"app.go": sb.String()})
48+
d := &detectors.GodFileDetector{}
49+
issues := d.Run(ctx)
50+
if len(issues) == 0 {
51+
t.Error("expected god file to be detected")
52+
}
53+
}
54+
55+
func TestGodFile_SmallFileIgnored(t *testing.T) {
56+
ctx := makeArchCtx(t, map[string]string{
57+
"small.go": "package main\n\nfunc main() {}\n",
58+
})
59+
d := &detectors.GodFileDetector{}
60+
issues := d.Run(ctx)
61+
if len(issues) != 0 {
62+
t.Errorf("small file should not trigger, got %d issues", len(issues))
63+
}
64+
}
65+
66+
// ── CX-ARCH-002: Circular Dependencies ───────────────────────────────────────
67+
68+
func TestCircularDeps_DetectsRelativeImport(t *testing.T) {
69+
ctx := makeArchCtx(t, map[string]string{
70+
"app/models.py": `from app import services
71+
class User:
72+
pass
73+
`,
74+
"app/services.py": `from app import models
75+
def get_user():
76+
pass
77+
`,
78+
})
79+
d := &detectors.CircularDepsDetector{}
80+
issues := d.Run(ctx)
81+
if len(issues) == 0 {
82+
t.Error("expected circular dependency to be detected")
83+
}
84+
}
85+
86+
func TestCircularDeps_NoCycleClean(t *testing.T) {
87+
ctx := makeArchCtx(t, map[string]string{
88+
"pkg/a/a.go": `package a
89+
import "fmt"
90+
`,
91+
"pkg/b/b.go": `package b
92+
import "strings"
93+
`,
94+
})
95+
d := &detectors.CircularDepsDetector{}
96+
issues := d.Run(ctx)
97+
if len(issues) != 0 {
98+
t.Errorf("no cycle should produce no issues, got %d", len(issues))
99+
}
100+
}
101+
102+
// ── CX-ARCH-003: Deep Nesting ─────────────────────────────────────────────────
103+
104+
func TestDeepNesting_Detects(t *testing.T) {
105+
ctx := makeArchCtx(t, map[string]string{
106+
"handler.go": `package main
107+
func process() {
108+
if a {
109+
for i := 0; i < 10; i++ {
110+
if b {
111+
switch c {
112+
case 1: {
113+
if d {
114+
doSomething()
115+
}
116+
}
117+
}
118+
}
119+
}
120+
}
121+
}
122+
`,
123+
})
124+
d := &detectors.DeepNestingDetector{}
125+
issues := d.Run(ctx)
126+
if len(issues) == 0 {
127+
t.Error("expected deep nesting to be detected")
128+
}
129+
}
130+
131+
func TestDeepNesting_CleanCode(t *testing.T) {
132+
ctx := makeArchCtx(t, map[string]string{
133+
"clean.go": `package main
134+
func process() {
135+
if err != nil {
136+
return err
137+
}
138+
doSomething()
139+
}
140+
`,
141+
})
142+
d := &detectors.DeepNestingDetector{}
143+
issues := d.Run(ctx)
144+
if len(issues) != 0 {
145+
t.Errorf("flat code should not trigger, got %d issues", len(issues))
146+
}
147+
}
148+
149+
// ── CX-ARCH-004: Missing Error Handling ──────────────────────────────────────
150+
151+
func TestMissingErrorHandling_GoBlankErr(t *testing.T) {
152+
ctx := makeArchCtx(t, map[string]string{
153+
"db.go": `package main
154+
func query() {
155+
rows, _ := db.Query("SELECT * FROM users")
156+
defer rows.Close()
157+
}
158+
`,
159+
})
160+
d := &detectors.MissingErrorHandlingDetector{}
161+
issues := d.Run(ctx)
162+
if len(issues) == 0 {
163+
t.Error("expected discarded error to be detected")
164+
}
165+
}
166+
167+
func TestMissingErrorHandling_JSPromiseNoCatch(t *testing.T) {
168+
ctx := makeArchCtx(t, map[string]string{
169+
"api.js": `async function fetchUser(id) {
170+
fetch('/api/users/' + id)
171+
.then(res => res.json())
172+
.then(data => console.log(data))
173+
}
174+
`,
175+
})
176+
d := &detectors.MissingErrorHandlingDetector{}
177+
issues := d.Run(ctx)
178+
if len(issues) == 0 {
179+
t.Error("expected missing .catch() to be detected")
180+
}
181+
}
182+
183+
func TestMissingErrorHandling_PythonBareExcept(t *testing.T) {
184+
ctx := makeArchCtx(t, map[string]string{
185+
"handler.py": `def process():
186+
try:
187+
risky_operation()
188+
except:
189+
pass
190+
`,
191+
})
192+
d := &detectors.MissingErrorHandlingDetector{}
193+
issues := d.Run(ctx)
194+
if len(issues) == 0 {
195+
t.Error("expected bare except with pass to be detected")
196+
}
197+
}
198+
199+
// ── CX-ARCH-005: Magic Numbers ────────────────────────────────────────────────
200+
201+
func TestMagicNumbers_Detects(t *testing.T) {
202+
ctx := makeArchCtx(t, map[string]string{
203+
"auth.go": `package main
204+
func isExpired(age int) bool {
205+
return age > 86400
206+
}
207+
`,
208+
})
209+
d := &detectors.MagicNumbersDetector{}
210+
issues := d.Run(ctx)
211+
if len(issues) == 0 {
212+
t.Error("expected magic number 86400 to be detected")
213+
}
214+
}
215+
216+
func TestMagicNumbers_ConstIgnored(t *testing.T) {
217+
ctx := makeArchCtx(t, map[string]string{
218+
"auth.go": `package main
219+
const MaxSessionAge = 86400
220+
func isExpired(age int) bool {
221+
return age > MaxSessionAge
222+
}
223+
`,
224+
})
225+
d := &detectors.MagicNumbersDetector{}
226+
issues := d.Run(ctx)
227+
// const declaration line should not be flagged
228+
for _, issue := range issues {
229+
if strings.Contains(issue.Snippet, "const ") {
230+
t.Error("const declaration should not trigger magic number detector")
231+
}
232+
}
233+
}
234+
235+
// ── CX-ARCH-006: Dead Code ────────────────────────────────────────────────────
236+
237+
func TestDeadCode_DetectsUnreachable(t *testing.T) {
238+
ctx := makeArchCtx(t, map[string]string{
239+
"handler.go": `package main
240+
func getUser() *User {
241+
return nil
242+
log.Println("this never runs")
243+
}
244+
`,
245+
})
246+
d := &detectors.DeadCodeDetector{}
247+
issues := d.Run(ctx)
248+
if len(issues) == 0 {
249+
t.Error("expected unreachable code after return to be detected")
250+
}
251+
}
252+
253+
func TestDeadCode_DetectsTODO(t *testing.T) {
254+
ctx := makeArchCtx(t, map[string]string{
255+
"service.go": `package main
256+
func process() {
257+
// TODO: implement retry logic
258+
doSomething()
259+
}
260+
`,
261+
})
262+
d := &detectors.DeadCodeDetector{}
263+
issues := d.Run(ctx)
264+
if len(issues) == 0 {
265+
t.Error("expected TODO comment to be detected")
266+
}
267+
}
268+
269+
// ── CX-ARCH-007: Missing Tests ────────────────────────────────────────────────
270+
271+
func TestMissingTests_Detects(t *testing.T) {
272+
// Large file with no test counterpart
273+
var sb strings.Builder
274+
for i := 0; i < 35; i++ {
275+
sb.WriteString("func doWork() { x := 1 }\n")
276+
}
277+
ctx := makeArchCtx(t, map[string]string{
278+
"service.go": sb.String(),
279+
})
280+
d := &detectors.MissingTestsDetector{}
281+
issues := d.Run(ctx)
282+
if len(issues) == 0 {
283+
t.Error("expected missing test file to be detected")
284+
}
285+
}
286+
287+
func TestMissingTests_HasTestFile(t *testing.T) {
288+
var sb strings.Builder
289+
for i := 0; i < 35; i++ {
290+
sb.WriteString("func doWork() { x := 1 }\n")
291+
}
292+
ctx := makeArchCtx(t, map[string]string{
293+
"service.go": sb.String(),
294+
"service_test.go": "package main\nfunc TestDoWork(t *testing.T) {}\n",
295+
})
296+
d := &detectors.MissingTestsDetector{}
297+
issues := d.Run(ctx)
298+
// service.go should not trigger since service_test.go exists
299+
for _, issue := range issues {
300+
if strings.Contains(issue.File, "service.go") {
301+
t.Error("service.go should not trigger when service_test.go exists")
302+
}
303+
}
304+
}
305+
306+
// ── CX-ARCH-008: Naming Conventions ──────────────────────────────────────────
307+
308+
func TestNamingConventions_JSSnakeCase(t *testing.T) {
309+
ctx := makeArchCtx(t, map[string]string{
310+
"app.js": `const user_name = "john"
311+
const first_name = "doe"
312+
`,
313+
})
314+
d := &detectors.NamingConventionsDetector{}
315+
issues := d.Run(ctx)
316+
if len(issues) == 0 {
317+
t.Error("expected snake_case in JS to be detected")
318+
}
319+
}
320+
321+
func TestNamingConventions_PythonCamelCase(t *testing.T) {
322+
ctx := makeArchCtx(t, map[string]string{
323+
"models.py": `userName = "john"
324+
firstName = "doe"
325+
`,
326+
})
327+
d := &detectors.NamingConventionsDetector{}
328+
issues := d.Run(ctx)
329+
if len(issues) == 0 {
330+
t.Error("expected camelCase in Python to be detected")
331+
}
332+
}
333+
334+
func TestNamingConventions_CorrectJS(t *testing.T) {
335+
ctx := makeArchCtx(t, map[string]string{
336+
"app.js": `const userName = "john"
337+
const firstName = "doe"
338+
const MAX_RETRIES = 3
339+
`,
340+
})
341+
d := &detectors.NamingConventionsDetector{}
342+
issues := d.Run(ctx)
343+
if len(issues) != 0 {
344+
t.Errorf("correct camelCase JS should not trigger, got %d issues", len(issues))
345+
}
346+
}

0 commit comments

Comments
 (0)