diff --git a/CHANGELOG.md b/CHANGELOG.md index 1815c4150..a82742d81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -201,6 +201,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). #### Symbols, tests and the viewer +- `codegraph affected` now recognises every ecosystem's test files — Go `foo_test.go`, Python `test_foo.py`, JVM `FooTest.kt` and the rest — instead of only `.test.`/`.spec.` names, so it stops reporting "no tests affected" for projects that have them. (#1507) + - **Files under an `e2e/` directory count as tests.** Their calls no longer appear as production callers in Steps, dead-code and test badges. - **Production code under a `samples` or `examples` package path is no longer treated as test code.** A Kotlin or Java project whose package path runs through `com/google/samples/…` (Now in Android, for one) had nearly every file counted as a fixture, so the Map opened on `build-logic`, the entry points hid the app, and dead-code and test badges were wrong. Only the project layout above a `src/` folder decides now; the package path below it never does. diff --git a/__tests__/cli-affected-test-conventions.test.ts b/__tests__/cli-affected-test-conventions.test.ts new file mode 100644 index 000000000..1887e934f --- /dev/null +++ b/__tests__/cli-affected-test-conventions.test.ts @@ -0,0 +1,66 @@ +/** + * `codegraph affected` recognises every ecosystem's test-file convention (#1507). + * + * The command used to carry its own six regexes — `.test.`, `.spec.`, + * `/tests/`… — so a Go `foo_test.go`, a Python `test_foo.py` or a JVM + * `FooTest.kt` beside the changed file was never reported, and "no tests + * affected" read as "no coverage". It now shares `isTestPath` with search and + * the MCP tools. Exercised end-to-end against the built binary. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { execFileSync } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js'); + +function affected(cwd: string, args: string[]): string[] { + const out = execFileSync(process.execPath, [BIN, 'affected', ...args, '--quiet', '-p', cwd], { + encoding: 'utf-8', + env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1' }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + return out.split('\n').map((s) => s.trim()).filter(Boolean); +} + +describe('codegraph affected — test-file conventions (#1507)', () => { + let dir: string; + + beforeAll(async () => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-affected-conv-')); + const w = (rel: string, body: string) => { + fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true }); + fs.writeFileSync(path.join(dir, rel), body); + }; + w('go.mod', 'module example.com/demo\n\ngo 1.22\n'); + w('math.go', 'package demo\n\nfunc Add(a, b int) int { return a + b }\n'); + w('math_test.go', 'package demo\n\nimport "testing"\n\nfunc TestAdd(t *testing.T) { if Add(1, 2) != 3 { t.Fatal("boom") } }\n'); + w('pkg/calc.py', 'def add(a, b):\n return a + b\n'); + w('pkg/test_calc.py', 'from pkg.calc import add\n\ndef test_add():\n assert add(1, 2) == 3\n'); + w('src/main/kotlin/app/Calc.kt', 'package app\n\nclass Calc {\n fun add(a: Int, b: Int): Int = a + b\n}\n'); + w('src/test/kotlin/app/CalcTest.kt', 'package app\n\nclass CalcTest {\n fun addsNumbers() { Calc().add(1, 2) }\n}\n'); + const cg = CodeGraph.initSync(dir); + await cg.indexAll(); + cg.close(); + }); + + afterAll(() => { + fs.rmSync(dir, { recursive: true, force: true }); + }); + + it('reports the sibling Go _test.go file', () => { + expect(affected(dir, ['math.go'])).toEqual(['math_test.go']); + }); + + it('reports the Python test_ module and the JVM FooTest class', () => { + expect(affected(dir, ['pkg/calc.py'])).toEqual(['pkg/test_calc.py']); + expect(affected(dir, ['src/main/kotlin/app/Calc.kt'])).toEqual(['src/test/kotlin/app/CalcTest.kt']); + }); + + it('still honours an explicit --filter glob', () => { + expect(affected(dir, ['math.go', '--filter', '*_test.go'])).toEqual(['math_test.go']); + expect(affected(dir, ['math.go', '--filter', '*.spec.ts'])).toEqual([]); + }); +}); diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index a4dccb976..5af9671a2 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -59,6 +59,7 @@ import { getTelemetry, TELEMETRY_DOCS, recordIndexEvent } from '../telemetry'; // server itself is loaded lazily inside the `ui` action. See ui-server/constants. import { BROWSER_ENV, DEFAULT_UI_PORT } from '../ui-server/constants'; import type { UiServerHandle } from '../ui-server'; +import { isTestPath } from '../search/query-utils'; // Decided once, before `--color`/`--no-color` are stripped from argv below // (#1281). Piped/redirected stdout, NO_COLOR, or --no-color -> plain output. @@ -2434,15 +2435,6 @@ program const cg = await CodeGraph.open(projectPath); const maxDepth = parseInt(options.depth || '5', 10); - // Common test file patterns - const defaultTestPatterns = [ - /\.spec\./, - /\.test\./, - /\/__tests__\//, - /\/tests?\//, - /\/e2e\//, - /\/spec\//, - ]; // Custom filter pattern let customFilter: RegExp | null = null; @@ -2456,9 +2448,14 @@ program customFilter = new RegExp(regex); } + // One notion of "a test" for the whole tool (#1507): the CLI used to keep + // its own six regexes here, which knew `.test.` and `/tests/` but not Go's + // `_test.go`, Python's `test_x.py` or the JVM's `FooTest.kt` — so + // `affected` reported "no tests" for whole ecosystems while `search` and + // the MCP tools counted those very files as tests. function isTestFile(filePath: string): boolean { if (customFilter) return customFilter.test(filePath); - return defaultTestPatterns.some(p => p.test(filePath)); + return isTestPath(filePath); } // BFS to find all transitive dependents of changed files, filtered to test files