Repository navigation
Description
Summary
In C++ projects, multiple GoogleTest TEST(...) cases defined in the same file collapse into a single graph node. Only the last one survives, and the call edges of all the others are dropped.
The practical effect is that trace_path / caller analysis silently under-reports test callers: in a test-heavy C++ codebase, most of the tests that exercise a symbol become invisible.
Environment
codebase-memory-mcp0.9.0- macOS 15 (Darwin 25.6.0), arm64
- Indexed with
index_repository(mode="full")
Minimal reproduction
Four files, no build required (indexing is static; GoogleTest does not need to be installed).
src/widget.hpp
#pragma once
namespace demo {
int assembleWidget(int size);
} // namespace demosrc/widget.cpp
#include "widget.hpp"
namespace demo {
int assembleWidget(int size) { return size * 2; }
} // namespace demotests/direct_test.cpp — three test cases in one file, each calling the symbol directly
#include "widget.hpp"
#include <gtest/gtest.h>
TEST(WidgetSuite, DoublesSmallSize) { EXPECT_EQ(demo::assembleWidget(1), 2); }
TEST(WidgetSuite, DoublesZero) { EXPECT_EQ(demo::assembleWidget(0), 0); }
TEST(WidgetSuite, DoublesLargeSize) {
EXPECT_EQ(demo::assembleWidget(1000), 2000);
}tests/indirect_test.cpp — one test reaching the symbol through a helper
#include "widget.hpp"
#include <gtest/gtest.h>
namespace {
int makeFixture() { return demo::assembleWidget(21); }
} // namespace
TEST(FixtureSuite, UsesHelper) { EXPECT_EQ(makeFixture(), 42); }Steps
index_repository(repo_path="<repro>", name="gtest-caller-repro", mode="full")
search_graph(project="gtest-caller-repro", file_pattern="%direct_test%")
trace_path(project="gtest-caller-repro", function_name="assembleWidget",
direction="inbound", include_tests=true, depth=3)
Expected
tests/direct_test.cpp contributes three distinct function nodes (one per test case), each with a CALLS edge to assembleWidget.
Actual
search_graph returns exactly one node for the whole file:
{
"name": "TEST",
"qualified_name": "gtest-caller-repro.tests.direct_test.TEST",
"file_path": "tests/direct_test.cpp",
"signature": "(WidgetSuite, DoublesLargeSize)",
"lines": 3,
"out_degree": 1
}signatureis the last test in the file — the first two are gone.out_degreeis 1, so two of the three call sites were dropped.
trace_path correspondingly reports one caller for that file instead of three:
{"function":"assembleWidget","direction":"inbound","callers":[
{"name":"TEST","qualified_name":"gtest-caller-repro.tests.direct_test.TEST","hop":1},
{"name":"makeFixture","qualified_name":"gtest-caller-repro.tests.indirect_test.makeFixture","hop":1},
{"name":"TEST","qualified_name":"gtest-caller-repro.tests.indirect_test.TEST","hop":2}
]}Why this happens (guess)
TEST(Suite, Case) parses as a function-like construct whose name is TEST, so every case in a file maps to the same qualified name <...>.<file>.TEST and the nodes overwrite each other. The same applies to TEST_F, TEST_P, TYPED_TEST, and to any macro-defined entry point that repeats a name within a file (e.g. Catch2's TEST_CASE, which is name-plus-string).
A fix could derive the qualified name from the macro arguments (...direct_test.TEST.WidgetSuite.DoublesZero) or disambiguate by start line.
Impact
Caller and impact analysis under-count test coverage of a symbol. For C++ repositories where GoogleTest is the norm, a file with N test cases reports 1 caller instead of N, and "which tests exercise this function" — a primary reason to consult the graph before editing — becomes unreliable.
Secondary observation (not minimally reproducible)
On a large C++ codebase (~129k nodes / 383k edges) I hit a stronger version of this: a free function in a nested namespace is called directly by five same-named TEST cases in one file, and trace_path(direction="inbound", depth=3, include_tests=true) returns no caller from that file at all — only callers from a different file. The collapsed node for that file does exist, is reachable via search_graph, has out_degree: 12, and its body tokens include the callee's name — yet no inbound edge is reported.
I could not reduce this to a minimal case: in the repro above the direct edge is found (including when the call goes through a using-declaration with an unqualified call). So it may be a separate resolution issue that only appears at scale, or an additional consequence of the collapsing above. Happy to gather more diagnostics if you can suggest what would be useful — I can query the graph but cannot share that codebase.
Activity
DeusData commentedon Jul 28, 2026
Thanks for the minimal GoogleTest fixture and for separating the unreduced secondary observation.
Current main has no GoogleTest macro-specific definition naming path: repeated function-like definitions are keyed by the extracted name/qualified name, and no TEST/TEST_F/TYPED_TEST argument-derived identity is present in the extractor. That permits same-file TEST nodes to collide and explains why only one set of CALLS edges survives.
I have routed the reproducible node-collision bug to 0.9.1-rc at priority/high. The large-repo no-edge observation should remain separate unless it reduces to the same collision. Thank you for being explicit about that uncertainty.
harshitaajoshi commentedon Jul 29, 2026
Hi, I would like to work on this.
The root cause is confirmed in the comment:
cbm_resolve_func_namereturns the bare macro nameTESTwith no argument-derived identity, so same-file test cases collide on qualified name and only the last one survives.I plan to detect GoogleTest macro names (
TEST,TEST_F,TEST_P,TYPED_TEST,TYPED_TEST_P) inextract_func_defand compose a unique name from the two macro arguments (e.g.TEST_WidgetSuite_DoublesSmallSize), following the same disambiguation pattern used for Rust cfg-gated twins. Will also setis_test = truefor these nodes. Regression tests included.Happy to adjust the approach based on feedback.
fix(extract): derive unique names for GoogleTest macros to prevent sa…
Merge pull request #1329 from harshitaajoshi/fix/cpp-gtest-name-colli…