Files
codegraph/__tests__/cpp-test-translation-unit.test.ts
T
Colby Mchenry bb8a0f732b fix(c,cpp): a name in a C or C++ test links to what the test's own includes declare (#2675)
When several files declare a name a C or C++ test uses, the test now links
to one its translation unit declares, when there is one: a definition in its
own file, in a file it includes (directly or through other includes), or in
the source file that implements such a header (the same-stem file that
includes it), unless that source keeps the definition to itself (a `static`,
an unnamed namespace, a class it declares). Before, the candidates were
ranked by directory and index order, so a test took whichever namesake came
first:

- protobuf's C# generator tests called test_util2.h's
  `TestUtil::TestSourceDir()`, a test suite they never include, instead of
  the `TestSourceDir()` googletest.h declares;
- protobuf's upb and hpb tests left `upb_Arena_New()` and the rest of upb's
  C API unlinked: the copies bundled for PHP and Ruby won the ranking and
  were then rejected;
- rocksdb's c_test.c left `rocksdb_open()` and the rest of the C API in
  db/c.cc unlinked, and its histogram test's `clock->SleepForMicroseconds()`
  went to emulated_clock.h's `EmulatedSystemClock`, which it doesn't include.

The narrowing applies to test files only, to the exact-name strategy, typed
receivers (resolveMethodOnType), and the class-name and receiver-word
strategies of matchMethodCall. When the translation unit declares none of
the candidates, as when a test's includes don't resolve (jemalloc's tests
include a generated header), the candidates stay as they were. A name a
test writes alone (no receiver, no qualifier) is narrowed only to what C++
finds for it outside a class: a function, a namespace-scope type or a
constructor, never another class's method or nested type, and a call the
extractor recorded without the receiver it is written on is left as it
was. The qualified-name strategy is unchanged.

Validated on main 3695818f (site-keyed edge diffs, every group triaged):
rocksdb +1,442 / 47 retargeted / -12, protobuf +2,316 / 55 / -67,
fmt +33 / 40, breakpad +13 / 14, glog +22, leveldb +10, llm.c 6
retargeted; jq, tokio, excalidraw, okhttp and typeorm byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:22:03 +00:00

257 lines
7.6 KiB
TypeScript

/**
* A C or C++ test is a program of its own, so a name it uses is one its
* translation unit declares. Of several same-named candidates, a test suite
* the test does not include is not one of them (protobuf's csharp generator
* tests called `TestSourceDir()`, which googletest.h declares, and
* test_util2.h's `TestUtil::TestSourceDir`, included by none of them, took
* the calls), and what the unit includes goes before what it doesn't (a test
* of the hpb backend calls upb's `upb_Arena_New()` from upb/mem/arena.h, not
* the copies php-upb.h and ruby-upb.h bundle; rocksdb's histogram test sleeps
* on a `SystemClock`, not on emulated_clock.h's `EmulatedSystemClock`). A
* test whose includes don't resolve (jemalloc's tests include a generated
* header) keeps the one candidate there is, and a definition in the test's
* own file still comes first (#1079).
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-test-tu-'));
const files: Record<string, string> = {
// googletest.h declares TestSourceDir(); googletest.cc defines it.
'src/google/protobuf/testing/googletest.h': `#ifndef GOOGLE_PROTOBUF_TESTING_GOOGLETEST_H__
#define GOOGLE_PROTOBUF_TESTING_GOOGLETEST_H__
#include <string>
namespace google {
namespace protobuf {
std::string TestSourceDir();
} // namespace protobuf
} // namespace google
#endif
`,
'src/google/protobuf/testing/googletest.cc': `#include "google/protobuf/testing/googletest.h"
namespace google {
namespace protobuf {
std::string TestSourceDir() { return "src"; }
} // namespace protobuf
} // namespace google
`,
// A test suite's header no csharp test includes.
'src/google/protobuf/test_util2.h': `#ifndef GOOGLE_PROTOBUF_TEST_UTIL2_H__
#define GOOGLE_PROTOBUF_TEST_UTIL2_H__
#include <string>
namespace google {
namespace protobuf {
namespace TestUtil {
inline std::string TestSourceDir() { return "src/google/protobuf"; }
} // namespace TestUtil
} // namespace protobuf
} // namespace google
#endif
`,
'src/google/protobuf/compiler/csharp/csharp_bootstrap_unittest.cc': `#include <string>
#include "google/protobuf/testing/googletest.h"
namespace google {
namespace protobuf {
namespace compiler {
namespace csharp {
int GeneratedDescriptorsAreUpToDate() {
std::string dir = TestSourceDir();
return static_cast<int>(dir.size());
}
} // namespace csharp
} // namespace compiler
} // namespace protobuf
} // namespace google
`,
// A receiver whose type isn't read is guessed by its name, among the
// classes the test includes: rocksdb's histogram test sleeps on its
// clock, which no emulated_clock.h include of its declares.
'include/rocksdb/system_clock.h': `namespace rocksdb {
class SystemClock {
public:
virtual ~SystemClock() {}
virtual void SleepForMicroseconds(int micros) = 0;
};
class SystemClockWrapper : public SystemClock {
public:
void SleepForMicroseconds(int micros) override { target_->SleepForMicroseconds(micros); }
private:
SystemClock* target_;
};
} // namespace rocksdb
`,
'env/emulated_clock.h': `#include "rocksdb/system_clock.h"
namespace rocksdb {
class EmulatedSystemClock : public SystemClockWrapper {
public:
void SleepForMicroseconds(int micros) override {}
};
} // namespace rocksdb
`,
'test_util/mock_time_env.h': `#include <memory>
#include "rocksdb/system_clock.h"
namespace rocksdb {
class MockSystemClock : public SystemClockWrapper {
public:
void MockSleepForMicroseconds(int micros) {}
};
} // namespace rocksdb
`,
'monitoring/histogram_test.cc': `#include <memory>
#include "rocksdb/system_clock.h"
#include "test_util/mock_time_env.h"
namespace rocksdb {
namespace {
std::shared_ptr<MockSystemClock> clock = std::make_shared<MockSystemClock>();
} // namespace
void PopulateHistogram(int low, int high) {
for (int i = low; i <= high; i++) {
clock->SleepForMicroseconds(i);
}
}
} // namespace rocksdb
`,
// A test that declares the class it calls on: its own file comes first,
// whatever it includes (#1079).
'src/util/collector.h': `#include <string>
class Collector {
public:
std::string last_error() const { return error_; }
private:
std::string error_;
};
`,
// upb's arena, and the copy a bundled amalgamation carries.
'php/ext/google/protobuf/php-upb.h': `typedef struct upb_Arena upb_Arena;
static inline upb_Arena* upb_Arena_New(void) { return 0; }
`,
'upb/mem/arena.h': `typedef struct upb_Arena upb_Arena;
static inline upb_Arena* upb_Arena_New(void) { return 0; }
`,
'hpb/backend/upb/interop_test.cc': `#include "upb/mem/arena.h"
int ArenaInterop() {
upb_Arena* arena = upb_Arena_New();
return arena == 0;
}
`,
'src/util/own_collector_unittest.cc': `#include "util/collector.h"
namespace {
class Collector {
public:
int last_error() const { return 0; }
};
} // namespace
int OwnCollector() {
Collector collector;
return collector.last_error();
}
`,
// jemalloc's tests include a header the build generates, so nothing
// shows the test includes its helpers: the one helper there is stays.
'deps/jemalloc/test/src/SFMT.c': `unsigned gen_rand32(void* ctx) {
return 4;
}
`,
'deps/jemalloc/test/unit/SFMT.c': `#include "test/jemalloc_test.h"
unsigned test_gen_rand32(void* ctx) {
return gen_rand32(ctx);
}
`,
};
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);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
/** `qualifiedName (file)` of every call the file's symbols make. */
const callsFrom = (file: string): string[] => {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'calls').map((e) => {
const target = cg.getNode(e.target)!;
return `${target.qualifiedName} (${target.filePath})`;
});
};
describe('a C/C++ test links what its translation unit declares', () => {
it('passes over a test suite it does not include for the declaration it does', () => {
const calls = callsFrom('src/google/protobuf/compiler/csharp/csharp_bootstrap_unittest.cc');
expect(calls).toContain('google::protobuf::TestSourceDir (src/google/protobuf/testing/googletest.cc)');
expect(calls.some((c) => c.includes('test_util2.h'))).toBe(false);
});
it('guesses a receiver among the classes its includes declare', () => {
const calls = callsFrom('monitoring/histogram_test.cc');
expect(calls).toContain('rocksdb::SystemClock::SleepForMicroseconds (include/rocksdb/system_clock.h)');
expect(calls.some((c) => c.includes('emulated_clock.h'))).toBe(false);
});
it('calls the copy of a function its includes declare', () => {
expect(callsFrom('hpb/backend/upb/interop_test.cc')).toContain('upb_Arena_New (upb/mem/arena.h)');
});
it('still prefers a declaration in the test file itself', () => {
const calls = callsFrom('src/util/own_collector_unittest.cc');
expect(calls.some((c) => c.endsWith('(src/util/own_collector_unittest.cc)') && c.includes('last_error'))).toBe(true);
expect(calls.some((c) => c.includes('(src/util/collector.h)'))).toBe(false);
});
it('keeps the only candidate when its includes do not show the unit', () => {
expect(callsFrom('deps/jemalloc/test/unit/SFMT.c')).toContain('gen_rand32 (deps/jemalloc/test/src/SFMT.c)');
});
});