diff --git a/tests/unit/feature/semantic_tokens_tests.cpp b/tests/unit/feature/semantic_tokens_tests.cpp index 41fa73ea..236532b9 100644 --- a/tests/unit/feature/semantic_tokens_tests.cpp +++ b/tests/unit/feature/semantic_tokens_tests.cpp @@ -449,42 +449,21 @@ export @kw[module] @m0[foo].@m1[bar]; } TEST_CASE(ModuleImport) { - auto pcm_path = fs::createTemporaryFile("test-mod", "pcm"); - ASSERT_TRUE(pcm_path.has_value()); + add_files("main.cpp", R"( +#[mod.cppm] +export module foo; +export int x = 42; - { - Tester mod; - mod.add_main("mod.cppm", "export module foo;\nexport int x = 42;\n"); - mod.prepare("-std=c++20"); - mod.params.kind = CompilationKind::ModuleInterface; - mod.params.output_file = *pcm_path; - auto built = clice::compile(mod.params); - ASSERT_TRUE(built.completed()); - } - - add_main("main.cpp", R"cpp( +#[main.cpp] @kw[import] @mod[foo]; int y = x; -)cpp"); - prepare("-std=c++20"); - auto fmodule_arg = std::string("-fmodule-file=foo=") + *pcm_path; - owned_args.push_back(fmodule_arg); - params.arguments.clear(); - for(auto& arg: owned_args) { - params.arguments.push_back(arg.c_str()); - } - - auto built = clice::compile(params); - ASSERT_TRUE(built.completed()); - unit.emplace(std::move(built)); - +)"); + ASSERT_TRUE(compile_with_modules()); tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8); decoded = decode_utf8_tokens(unit->interested_content(), tokens); EXPECT_TOKEN("kw", SymbolKind::Keyword); EXPECT_TOKEN("mod", SymbolKind::Module); - - fs::remove(*pcm_path); } TEST_CASE(ModulePartition) { @@ -500,43 +479,21 @@ export module @m0[foo]:@m1[bar]; } TEST_CASE(ModuleReexport) { - auto pcm_path = fs::createTemporaryFile("test-mod", "pcm"); - ASSERT_TRUE(pcm_path.has_value()); + add_files("main.cppm", R"( +#[mod.cppm] +export module foo; +export int x = 42; - { - Tester mod; - mod.add_main("mod.cppm", "export module foo;\nexport int x = 42;\n"); - mod.prepare("-std=c++20"); - mod.params.kind = CompilationKind::ModuleInterface; - mod.params.output_file = *pcm_path; - auto built = clice::compile(mod.params); - ASSERT_TRUE(built.completed()); - } - - add_main("main.cppm", R"cpp( +#[main.cppm] export module bar; export @kw[import] @mod[foo]; -)cpp"); - prepare("-std=c++20"); - auto fmodule_arg = std::string("-fmodule-file=foo=") + *pcm_path; - owned_args.push_back(fmodule_arg); - params.arguments.clear(); - for(auto& arg: owned_args) { - params.arguments.push_back(arg.c_str()); - } - params.kind = CompilationKind::ModuleInterface; - - auto built = clice::compile(params); - ASSERT_TRUE(built.completed()); - unit.emplace(std::move(built)); - +)"); + ASSERT_TRUE(compile_with_modules()); tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8); decoded = decode_utf8_tokens(unit->interested_content(), tokens); EXPECT_TOKEN("kw", SymbolKind::Keyword); EXPECT_TOKEN("mod", SymbolKind::Module); - - fs::remove(*pcm_path); } TEST_CASE(GlobalModuleFragment) { diff --git a/tests/unit/test/tester.cpp b/tests/unit/test/tester.cpp index 1f49defc..e5b96042 100644 --- a/tests/unit/test/tester.cpp +++ b/tests/unit/test/tester.cpp @@ -7,6 +7,44 @@ namespace clice::testing { +namespace { + +std::vector 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::getRealFileSystem()); overlay->pushOverlay(vfs); @@ -96,16 +111,123 @@ bool Tester::compile_with_pch(llvm::StringRef standard) { params.pch = {info.path, static_cast(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 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(); + 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 deps; + }; + + auto scan_args_base = base_cc1_args(standard); + + std::vector modules; + for(auto& mod: all_modules) { + auto args = scan_args_base; + args.push_back(TestVFS::path(mod.filename)); + + std::vector 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 name_to_index; + for(std::size_t i = 0; i < modules.size(); ++i) { + name_to_index[modules[i].module_name] = i; + } + + std::vector order; + std::vector 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::getRealFileSystem()); + overlay->pushOverlay(vfs); + + llvm::StringMap 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::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(); - 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::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(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 diff --git a/tests/unit/test/tester.h b/tests/unit/test/tester.h index 1ee0c24d..c372b19e 100644 --- a/tests/unit/test/tester.h +++ b/tests/unit/test/tester.h @@ -2,6 +2,7 @@ #include #include +#include #include "test/annotation.h" #include "test/test.h" @@ -25,6 +26,16 @@ struct Tester { /// The VFS used for compilation. llvm::IntrusiveRefCntPtr vfs; + struct ModuleFile { + std::string filename; + std::string content; + }; + + std::vector module_files; + std::vector pcm_paths; + + ~Tester(); + void add_main(llvm::StringRef file, llvm::StringRef content) { src_path = file.str(); sources.add_source(file, content); @@ -39,6 +50,10 @@ struct Tester { sources.add_sources(content); } + void add_module(llvm::StringRef filename, llvm::StringRef content) { + module_files.push_back({filename.str(), content.str()}); + } + /// Fast VFS-only path: uses -cc1 directly, no system headers. void prepare(llvm::StringRef standard = "-std=c++20"); @@ -46,6 +61,8 @@ struct Tester { bool compile_with_pch(llvm::StringRef standard = "-std=c++20"); + bool compile_with_modules(llvm::StringRef standard = "-std=c++20"); + /// Driver path: uses CompilationDatabase + toolchain cache, has system headers. void prepare_driver(llvm::StringRef standard = "-std=c++20"); @@ -53,6 +70,8 @@ struct Tester { bool compile_driver_with_pch(llvm::StringRef standard = "-std=c++20"); + bool try_compile(); + std::uint32_t operator[](llvm::StringRef file, llvm::StringRef pos) { return sources.all_files.lookup(file).offsets.lookup(pos); }