Skip to content

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

Closed

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.

Activity

  1. added
    bugSomething isn't working
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    on Jul 28, 2026
  2. added this to the 0.9.1-rc milestone on Jul 28, 2026
  3. DeusData commented on Jul 28, 2026

    @DeusData
    Owner

    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.

  4. harshitaajoshi commented on Jul 29, 2026

    @harshitaajoshi
    Contributor

    Hi, I would like to work on this.

    The root cause is confirmed in the comment: cbm_resolve_func_name returns the bare macro name TEST with 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) in extract_func_def and 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 set is_test = true for these nodes. Regression tests included.

    Happy to adjust the approach based on feedback.

  5. added a commit that references this issue on Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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