feat(tests): add compile_with_modules helper to Tester (#420)
## Summary - Add `add_module()` and `compile_with_modules()` to the `Tester` test framework - Supports both separate `add_module()` calls and single-string `#[filename]` syntax via `add_files()` - Automatically scans module dependencies with `scan_precise`, topologically sorts, builds PCMs in order, then compiles the main file - Temporary PCM files cleaned up automatically in destructor - Migrated `ModuleImport` and `ModuleReexport` semantic tokens tests to use the new API ## Test plan - [x] All 505 unit tests pass - [x] All 113 integration tests pass - [x] All 2 smoke tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Centralized, module-aware test compilation with automatic module discovery, dependency ordering, and cycle detection. * Unified "compile with modules" flow; tests now add module sources directly and no longer manage temporary module artifacts manually. * Reduced duplicated compile/diagnostic logic and improved cleanup of generated artifacts. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,44 @@
|
||||
|
||||
namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<std::string> base_cc1_args(llvm::StringRef standard) {
|
||||
return {
|
||||
"clang",
|
||||
"-cc1",
|
||||
"-triple",
|
||||
LLVM_DEFAULT_TARGET_TRIPLE,
|
||||
standard.str(),
|
||||
"-ffreestanding",
|
||||
"-undef",
|
||||
"-fms-extensions",
|
||||
"-fsyntax-only",
|
||||
"-x",
|
||||
"c++",
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Tester::~Tester() {
|
||||
for(auto& path: pcm_paths) {
|
||||
fs::remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
bool Tester::try_compile() {
|
||||
auto built = clice::compile(params);
|
||||
if(!built.completed()) {
|
||||
for(auto& diag: built.diagnostics()) {
|
||||
LOG_ERROR("{}", diag.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
unit.emplace(std::move(built));
|
||||
return true;
|
||||
}
|
||||
|
||||
void Tester::prepare(llvm::StringRef standard) {
|
||||
params = CompilationParams();
|
||||
unit.reset();
|
||||
@@ -16,19 +54,7 @@ void Tester::prepare(llvm::StringRef standard) {
|
||||
vfs->add(file, source.content);
|
||||
}
|
||||
|
||||
owned_args.clear();
|
||||
// Use -cc1 mode directly to bypass the slow driver subprocess.
|
||||
owned_args.push_back("clang");
|
||||
owned_args.push_back("-cc1");
|
||||
owned_args.push_back("-triple");
|
||||
owned_args.push_back(LLVM_DEFAULT_TARGET_TRIPLE);
|
||||
owned_args.push_back(standard.str());
|
||||
owned_args.push_back("-ffreestanding");
|
||||
owned_args.push_back("-undef");
|
||||
owned_args.push_back("-fms-extensions");
|
||||
owned_args.push_back("-fsyntax-only");
|
||||
owned_args.push_back("-x");
|
||||
owned_args.push_back("c++");
|
||||
owned_args = base_cc1_args(standard);
|
||||
owned_args.push_back(TestVFS::path(src_path));
|
||||
|
||||
params.arguments.clear();
|
||||
@@ -42,17 +68,7 @@ void Tester::prepare(llvm::StringRef standard) {
|
||||
|
||||
bool Tester::compile(llvm::StringRef standard) {
|
||||
prepare(standard);
|
||||
|
||||
auto built = clice::compile(params);
|
||||
if(!built.completed()) {
|
||||
for(auto& diag: built.diagnostics()) {
|
||||
LOG_ERROR("{}", diag.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
unit.emplace(std::move(built));
|
||||
return true;
|
||||
return try_compile();
|
||||
}
|
||||
|
||||
bool Tester::compile_with_pch(llvm::StringRef standard) {
|
||||
@@ -64,7 +80,6 @@ bool Tester::compile_with_pch(llvm::StringRef standard) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use an overlay VFS so the PCH temp file on real disk is accessible.
|
||||
auto overlay =
|
||||
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(llvm::vfs::getRealFileSystem());
|
||||
overlay->pushOverlay(vfs);
|
||||
@@ -96,16 +111,123 @@ bool Tester::compile_with_pch(llvm::StringRef standard) {
|
||||
params.pch = {info.path, static_cast<std::uint32_t>(info.preamble.size())};
|
||||
params.buffers.clear();
|
||||
|
||||
auto built = clice::compile(params);
|
||||
if(!built.completed()) {
|
||||
for(auto& diag: built.diagnostics()) {
|
||||
LOG_ERROR("{}", diag.message);
|
||||
return try_compile();
|
||||
}
|
||||
|
||||
bool Tester::compile_with_modules(llvm::StringRef standard) {
|
||||
std::vector<ModuleFile> all_modules = module_files;
|
||||
for(auto& [file, source]: sources.all_files) {
|
||||
if(file == src_path) {
|
||||
continue;
|
||||
}
|
||||
auto result = scan(source.content);
|
||||
if(!result.module_name.empty() || result.need_preprocess) {
|
||||
all_modules.push_back({file.str(), source.content});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
unit.emplace(std::move(built));
|
||||
return true;
|
||||
if(all_modules.empty()) {
|
||||
return compile(standard);
|
||||
}
|
||||
|
||||
vfs = llvm::makeIntrusiveRefCnt<TestVFS>();
|
||||
for(auto& [file, source]: sources.all_files) {
|
||||
vfs->add(file, source.content);
|
||||
}
|
||||
for(auto& mod: module_files) {
|
||||
vfs->add(mod.filename, mod.content);
|
||||
}
|
||||
|
||||
struct ScannedModule {
|
||||
std::string filename;
|
||||
std::string content;
|
||||
std::string module_name;
|
||||
std::vector<std::string> deps;
|
||||
};
|
||||
|
||||
auto scan_args_base = base_cc1_args(standard);
|
||||
|
||||
std::vector<ScannedModule> modules;
|
||||
for(auto& mod: all_modules) {
|
||||
auto args = scan_args_base;
|
||||
args.push_back(TestVFS::path(mod.filename));
|
||||
|
||||
std::vector<const char*> argv;
|
||||
for(auto& arg: args) {
|
||||
argv.push_back(arg.c_str());
|
||||
}
|
||||
|
||||
auto result = scan_precise(argv, TestVFS::root(), {}, nullptr, vfs);
|
||||
modules.push_back(
|
||||
{mod.filename, mod.content, result.module_name, std::move(result.modules)});
|
||||
}
|
||||
|
||||
llvm::StringMap<std::size_t> name_to_index;
|
||||
for(std::size_t i = 0; i < modules.size(); ++i) {
|
||||
name_to_index[modules[i].module_name] = i;
|
||||
}
|
||||
|
||||
std::vector<std::size_t> order;
|
||||
std::vector<int> state(modules.size(), 0);
|
||||
|
||||
auto topo_visit = [&](this auto& self, std::size_t i) -> bool {
|
||||
if(state[i] == 2)
|
||||
return true;
|
||||
if(state[i] == 1) {
|
||||
LOG_ERROR("Circular module dependency involving {}", modules[i].module_name);
|
||||
return false;
|
||||
}
|
||||
state[i] = 1;
|
||||
for(auto& dep: modules[i].deps) {
|
||||
auto it = name_to_index.find(dep);
|
||||
if(it != name_to_index.end()) {
|
||||
if(!self(it->second))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
state[i] = 2;
|
||||
order.push_back(i);
|
||||
return true;
|
||||
};
|
||||
|
||||
for(std::size_t i = 0; i < modules.size(); ++i) {
|
||||
if(!topo_visit(i))
|
||||
return false;
|
||||
}
|
||||
|
||||
auto overlay =
|
||||
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(llvm::vfs::getRealFileSystem());
|
||||
overlay->pushOverlay(vfs);
|
||||
|
||||
llvm::StringMap<std::string> built_pcms;
|
||||
for(auto idx: order) {
|
||||
auto& mod = modules[idx];
|
||||
|
||||
auto pcm_path = fs::createTemporaryFile("clice", "pcm");
|
||||
if(!pcm_path) {
|
||||
LOG_ERROR("{}", pcm_path.error().message());
|
||||
return false;
|
||||
}
|
||||
pcm_paths.push_back(*pcm_path);
|
||||
|
||||
Tester builder;
|
||||
builder.add_main(mod.filename, mod.content);
|
||||
builder.prepare(standard);
|
||||
builder.params.kind = CompilationKind::ModuleInterface;
|
||||
builder.params.output_file = *pcm_path;
|
||||
builder.params.vfs = overlay;
|
||||
builder.params.pcms = built_pcms;
|
||||
|
||||
if(!builder.try_compile())
|
||||
return false;
|
||||
|
||||
built_pcms.try_emplace(mod.module_name, *pcm_path);
|
||||
}
|
||||
|
||||
prepare(standard);
|
||||
params.vfs = overlay;
|
||||
params.pcms = std::move(built_pcms);
|
||||
return try_compile();
|
||||
}
|
||||
|
||||
std::uint32_t Tester::point(llvm::StringRef name, llvm::StringRef file) {
|
||||
@@ -166,13 +288,11 @@ void Tester::prepare_driver(llvm::StringRef standard) {
|
||||
|
||||
params.kind = CompilationKind::Content;
|
||||
|
||||
// Use overlay VFS: real FS (for system headers) + InMemoryFS (for test files).
|
||||
auto overlay =
|
||||
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(llvm::vfs::getRealFileSystem());
|
||||
overlay->pushOverlay(vfs);
|
||||
params.vfs = overlay;
|
||||
|
||||
// Remap test files so clang sees our in-memory content.
|
||||
for(auto& [file, source]: sources.all_files) {
|
||||
if(file == src_path) {
|
||||
params.add_remapped_file(file, source.content);
|
||||
@@ -185,36 +305,11 @@ void Tester::prepare_driver(llvm::StringRef standard) {
|
||||
|
||||
bool Tester::compile_driver(llvm::StringRef standard) {
|
||||
prepare_driver(standard);
|
||||
|
||||
auto built = clice::compile(params);
|
||||
if(!built.completed()) {
|
||||
for(auto& diag: built.diagnostics()) {
|
||||
LOG_ERROR("{}", diag.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
unit.emplace(std::move(built));
|
||||
return true;
|
||||
return try_compile();
|
||||
}
|
||||
|
||||
bool Tester::compile_driver_with_pch(llvm::StringRef standard) {
|
||||
params = CompilationParams();
|
||||
unit.reset();
|
||||
vfs = llvm::makeIntrusiveRefCnt<TestVFS>();
|
||||
for(auto& [file, source]: sources.all_files) {
|
||||
vfs->add(file, source.content);
|
||||
}
|
||||
|
||||
auto command = std::format("clang++ {} {} -fms-extensions", standard, src_path);
|
||||
database.add_command("fake", src_path, command);
|
||||
|
||||
CommandOptions options;
|
||||
options.query_toolchain = true;
|
||||
options.suppress_logging = true;
|
||||
auto commands = database.lookup(src_path, options);
|
||||
assert(!commands.empty() && "lookup failed after add_command");
|
||||
params.arguments = commands.front().to_argv();
|
||||
prepare_driver(standard);
|
||||
|
||||
auto pch_path = fs::createTemporaryFile("clice", "pch");
|
||||
if(!pch_path) {
|
||||
@@ -222,16 +317,12 @@ bool Tester::compile_driver_with_pch(llvm::StringRef standard) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use overlay VFS: real FS (for system headers + PCH temp) + InMemoryFS.
|
||||
auto overlay =
|
||||
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(llvm::vfs::getRealFileSystem());
|
||||
overlay->pushOverlay(vfs);
|
||||
params.vfs = overlay;
|
||||
|
||||
// Phase 1: Build PCH from the preamble portion.
|
||||
params.kind = CompilationKind::Preamble;
|
||||
params.output_file = *pch_path;
|
||||
|
||||
// Clear buffers from prepare_driver() so we can re-add with preamble bound.
|
||||
params.buffers.clear();
|
||||
for(auto& [file, source]: sources.all_files) {
|
||||
if(file == src_path) {
|
||||
auto bound = compute_preamble_bound(source.content);
|
||||
@@ -259,25 +350,7 @@ bool Tester::compile_driver_with_pch(llvm::StringRef standard) {
|
||||
params.pch = {info.path, static_cast<std::uint32_t>(info.preamble.size())};
|
||||
params.buffers.clear();
|
||||
|
||||
for(auto& [file, source]: sources.all_files) {
|
||||
if(file == src_path) {
|
||||
params.add_remapped_file(file, source.content);
|
||||
} else {
|
||||
std::string path = path::is_absolute(file) ? file.str() : path::join(".", file);
|
||||
params.add_remapped_file(path, source.content);
|
||||
}
|
||||
}
|
||||
|
||||
auto built = clice::compile(params);
|
||||
if(!built.completed()) {
|
||||
for(auto& diag: built.diagnostics()) {
|
||||
LOG_ERROR("{}", diag.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
unit.emplace(std::move(built));
|
||||
return true;
|
||||
return try_compile();
|
||||
}
|
||||
|
||||
void Tester::clear() {
|
||||
@@ -288,6 +361,11 @@ void Tester::clear() {
|
||||
src_path.clear();
|
||||
owned_args.clear();
|
||||
vfs.reset();
|
||||
module_files.clear();
|
||||
for(auto& path: pcm_paths) {
|
||||
fs::remove(path);
|
||||
}
|
||||
pcm_paths.clear();
|
||||
}
|
||||
|
||||
} // namespace clice::testing
|
||||
|
||||
Reference in New Issue
Block a user