Skip to content

C++: same-named TEST cases in one file collapse into a single node, dropping their call edges #1266

Description

@ysyecust

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-mcp 0.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 demo

src/widget.cpp

#include "widget.hpp"

namespace demo {

int assembleWidget(int size) { return size * 2; }

} // namespace demo

tests/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
}
  • signature is the last test in the file — the first two are gone.
  • out_degree is 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingparsing/qualityGraph extraction bugs, false positives, missing edgespriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions