Files
codegraph/__tests__/cpp-gtest-fixture-scope.test.ts
Colby Mchenry d94a92ea89 fix(cpp): a gtest TEST_F or TEST_P body finds its fixture's types, and a function's local class is seen only from that function (#2551)
A TEST_F / TEST_P body is a member function of a class derived from its
fixture, so its unqualified names find the fixture's nested types (its bases'
included) before the enclosing namespaces'. And since the index names a
function's local class after the function, every TEST_F body's local `FakeWF`
or `MyManager` shared one qualified name: a class local to a function is now
visible only from inside that function, and a namesake class's methods are
its own body's.

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

183 lines
6.8 KiB
TypeScript

/**
* A gtest `TEST_F(Fixture, Name) { … }` or `TEST_P` body is a member function
* of a class derived from its fixture, so an unqualified name in it finds the
* body's own declarations, then the fixture's members, its bases' included,
* then the enclosing namespaces'. C++ lookup from the body started at the
* function and went straight to the namespaces, so facebook/rocksdb's
* `new TestUserDefinedIndexFactory()`, a class nested in the fixture's base,
* was found only as the one class of that name the file could see (0.7), and
* a namespace-level class of the same name won over the fixture's.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import type { Node } from '../src/types';
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});
async function indexed(files: Record<string, string>): Promise<CodeGraph> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-fixture-scope-'));
roots.push(root);
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
return CodeGraph.init(root, { index: true });
}
const lines = (...text: string[]): string => [...text, ''].join('\n');
/** The `instantiates` edges of every function named `name`, by its first line, as `startLine -> qualifiedName @confidence`. */
function made(cg: CodeGraph, name: string): string[] {
return cg.getNodesByKind('function').filter((n: Node) => n.name === name).sort((a, b) => a.startLine - b.startLine)
.flatMap((fn) => cg.getOutgoingEdgesFrom([fn.id], ['instantiates'])
.map((e) => `${fn.startLine} -> ${cg.getNode(e.target)?.qualifiedName} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`));
}
describe('C++ names in a gtest TEST_F or TEST_P body', () => {
it("find the fixture's nested types, its bases' included", async () => {
const cg = await indexed({
'table/table_test.cc': lines(
'namespace rocksdb {',
'class UserDefinedIndexTestBase : public testing::Test {',
' public:',
' class TestUserDefinedIndexFactory {',
' public:',
' TestUserDefinedIndexFactory() {}',
' };',
'};',
'class UserDefinedIndexTest : public UserDefinedIndexTestBase {};',
'TEST_F(UserDefinedIndexTest, Basic) {',
' auto factory = new TestUserDefinedIndexFactory();',
'}',
'TEST_P(UserDefinedIndexTest, Params) {',
' auto factory = new TestUserDefinedIndexFactory();',
'}',
'} // namespace rocksdb',
),
});
try {
const nested = 'rocksdb::UserDefinedIndexTestBase::TestUserDefinedIndexFactory @0.9';
expect(made(cg, 'TEST_F')).toEqual([`10 -> ${nested}`]);
expect(made(cg, 'TEST_P')).toEqual([`13 -> ${nested}`]);
} finally {
cg.close();
}
});
it("find the fixture's type before a namespace's, and the body's own before both", async () => {
const cg = await indexed({
'db/db_test.cc': lines(
'namespace rocksdb {',
'class Listener {',
' public:',
' explicit Listener(int n) {}',
'};',
'class DBTest : public testing::Test {',
' public:',
' class Listener {',
' public:',
' explicit Listener(int n) {}',
' };',
'};',
'TEST_F(DBTest, Fixture) {',
' Listener listener(1);',
'}',
'TEST_F(DBTest, Local) {',
' class Listener {',
' public:',
' explicit Listener(int n) {}',
' };',
' Listener listener(2);',
'}',
'TEST(DBPlainTest, NoFixture) {',
' Listener listener(3);',
'}',
'void Helper() { Listener listener(4); }',
'} // namespace rocksdb',
),
});
try {
expect(made(cg, 'TEST_F')).toEqual(['13 -> rocksdb::DBTest::Listener @0.9', '16 -> rocksdb::TEST_F::Listener @0.9']);
expect(made(cg, 'TEST')).toEqual(['23 -> rocksdb::Listener @0.9']);
expect(made(cg, 'Helper')).toEqual(['26 -> rocksdb::Listener @0.9']);
} finally {
cg.close();
}
});
it("call the methods of the body's own local class, not another body's", async () => {
const cg = await indexed({
'db/listener_test.cc': lines(
'namespace rocksdb {',
'class ListenerTest : public testing::Test {};',
'TEST_F(ListenerTest, First) {',
' class Listener {',
' public:',
' void Fire() {}',
' };',
' Listener listener;',
' listener.Fire();',
'}',
'TEST_F(ListenerTest, Second) {',
' class Listener {',
' public:',
' void Fire() {}',
' };',
' Listener listener;',
' listener.Fire();',
'}',
'} // namespace rocksdb',
),
});
try {
const fired = cg.getNodesByKind('function').filter((n: Node) => n.name === 'TEST_F').sort((a, b) => a.startLine - b.startLine)
.map((fn) => cg.getOutgoingEdgesFrom([fn.id], ['calls'])
.map((e) => `${fn.startLine} -> ${cg.getNode(e.target)?.qualifiedName}:${cg.getNode(e.target)?.startLine} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`));
expect(fired).toEqual([['3 -> rocksdb::TEST_F::Listener::Fire:6 @0.9'], ['11 -> rocksdb::TEST_F::Listener::Fire:14 @0.9']]);
} finally {
cg.close();
}
});
it("find the body's own local class from a method of that class", async () => {
// rocksdb's env_test.cc: two bodies each declare a `CB`, and a static
// method of the second's calls `cb->Run()` on a `CB*`.
const cg = await indexed({
'env/env_test.cc': lines(
'namespace rocksdb {',
'class EnvTest : public testing::Test {};',
'TEST_F(EnvTest, RunMany) {',
' struct CB {',
' void Run() {}',
' };',
'}',
'TEST_F(EnvTest, TwoPools) {',
' class CB {',
' public:',
' static void Trampoline(void* v) {',
' CB* cb = static_cast<CB*>(v);',
' cb->Run();',
' }',
' void Run() {}',
' };',
'}',
'} // namespace rocksdb',
),
});
try {
const from = cg.getNodesByKind('method').find((n: Node) => n.name === 'Trampoline')!;
const calls = cg.getOutgoingEdgesFrom([from.id], ['calls'])
.map((e) => `${cg.getNode(e.target)?.qualifiedName}:${cg.getNode(e.target)?.startLine} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`);
expect(calls).toEqual(['rocksdb::TEST_F::CB::Run:15 @0.9']);
} finally {
cg.close();
}
});
});