Files
codegraph/__tests__/cli-affected-test-conventions.test.ts
Colby Mchenry 1d3619d657 fix(c,cpp): a file in a unittests/ or foo_unittest/ folder is a test (#2484)
isTestPath knew `tests/`, `test/`, `__tests__/`, CamelCase source sets and
`foo-test/` modules, but not the folders LLVM, Clang, clangd, lldb, offload
and Breakpad keep their unit tests in (`unittests/`), nor glog's
`dcheck_unittest/`-style test programs: `/unittests/` holds `tests/` only
after a `t`, and the `foo_test/` form wants a separator right before `test`.
So a helper there with no test-like name (LLVM's CountCopyAndMove.h,
SelectionDAGTestBase.h, Breakpad's dump_analysis.cc) read as production code.

isTestPath now also counts a `unittests/` folder and a `foo_unittest/` or
`foo-unittests/` one. A bare `unittest/` stays production: CPython's
`Lib/unittest/` is the framework, and LLVM vendors googletest under
`third-party/unittest/`.

isTestSuitePath counts the same folders as suites, as it counts `tests/`:
their files build into test binaries and never link into the program, and
glog builds each `foo_unittest/` folder as a test program of its own (a
`foo-test/` module stays out, as shipped test support). That matters for
C/C++ because the suite rule's CamelCase file names skip `.cpp`, so on main
none of LLVM's `*Test.cpp` files were suites and production locals named
`Ctx`, `KV`, `V` or `OpInfo` linked to what a unit test declares.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:09:06 +00:00

81 lines
3.9 KiB
TypeScript

/**
* `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');
w('src/wire_format.h', '#pragma once\n\nint ByteSize(int value);\n');
w('src/wire_format.cc', '#include "wire_format.h"\n\nint ByteSize(int value) { return value; }\n');
w('src/wire_format_unittest.cc', '#include "wire_format.h"\n\nvoid ComputesByteSize() { ByteSize(1); }\n');
w('include/llvm/ADT/FoldingSet.h', '#pragma once\n\nint FoldingSetSize();\n');
w('lib/Support/FoldingSet.cpp', '#include "llvm/ADT/FoldingSet.h"\n\nint FoldingSetSize() { return 0; }\n');
w('unittests/ADT/FoldingSet.cpp', '#include "llvm/ADT/FoldingSet.h"\n\nvoid SizesTheSet() { FoldingSetSize(); }\n');
const cg = CodeGraph.initSync(dir);
await cg.indexAll();
cg.close();
}, 60_000);
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("reports Google's C++ _unittest.cc beside the code it tests", () => {
expect(affected(dir, ['src/wire_format.h', 'src/wire_format.cc'])).toEqual(['src/wire_format_unittest.cc']);
});
it("reports a file in LLVM's unittests/ tree, whatever its name", () => {
expect(affected(dir, ['include/llvm/ADT/FoldingSet.h', 'lib/Support/FoldingSet.cpp'])).toEqual(['unittests/ADT/FoldingSet.cpp']);
});
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([]);
});
});