#include import cargoxx.layout; import cargoxx.util; import std; using cargoxx::layout::DiscoveredLayout; using cargoxx::layout::discover; using cargoxx::layout::Target; using cargoxx::layout::TargetKind; using cargoxx::util::ErrorCode; namespace { class TempProject { public: TempProject() { root_ = std::filesystem::temp_directory_path() / std::format("cargoxx-layout-test-{}", std::random_device{}()); std::filesystem::create_directories(root_); } ~TempProject() { std::error_code ec; std::filesystem::remove_all(root_, ec); } TempProject(const TempProject&) = delete; TempProject& operator=(const TempProject&) = delete; TempProject(TempProject&&) = delete; TempProject& operator=(TempProject&&) = delete; auto root() const -> const std::filesystem::path& { return root_; } auto touch(std::string_view rel) -> std::filesystem::path { auto p = root_ / rel; std::filesystem::create_directories(p.parent_path()); std::ofstream{p} << "// stub\n"; return p; } private: std::filesystem::path root_; }; } // namespace TEST_CASE("discover returns LayoutNoTarget for an empty src", "[layout]") { TempProject p; std::filesystem::create_directories(p.root() / "src"); auto r = discover(p.root(), "foo"); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::LayoutNoTarget); } TEST_CASE("discover finds main.cpp as a binary", "[layout]") { TempProject p; auto main = p.touch("src/main.cpp"); auto r = discover(p.root(), "foo"); REQUIRE(r.has_value()); REQUIRE_FALSE(r->library.has_value()); REQUIRE(r->binaries.size() == 1); REQUIRE(r->binaries[0].kind == TargetKind::Binary); REQUIRE(r->binaries[0].name == "foo"); REQUIRE(r->binaries[0].entry == main); } TEST_CASE("discover finds lib.cppm as a library", "[layout]") { TempProject p; auto lib = p.touch("src/lib.cppm"); auto r = discover(p.root(), "foo"); REQUIRE(r.has_value()); REQUIRE(r->library.has_value()); REQUIRE(r->library->kind == TargetKind::Library); REQUIRE(r->library->name == "foo"); REQUIRE(r->library->entry == lib); REQUIRE(r->library->module_units == std::vector{lib}); REQUIRE(r->library->additional_sources.empty()); REQUIRE(r->binaries.empty()); } TEST_CASE("discover supports lib + main together", "[layout]") { TempProject p; p.touch("src/lib.cppm"); p.touch("src/main.cpp"); auto r = discover(p.root(), "foo"); REQUIRE(r.has_value()); REQUIRE(r->library.has_value()); REQUIRE(r->binaries.size() == 1); REQUIRE(r->binaries[0].entry.filename() == "main.cpp"); } TEST_CASE("discover collects nested .cppm and .cpp into the library", "[layout]") { TempProject p; auto lib = p.touch("src/lib.cppm"); auto inner_cppm = p.touch("src/internal/foo.cppm"); auto inner_cpp = p.touch("src/internal/foo.cpp"); auto r = discover(p.root(), "foo"); REQUIRE(r.has_value()); REQUIRE(r->library->module_units.size() == 2); REQUIRE(std::ranges::find(r->library->module_units, lib) != r->library->module_units.end()); REQUIRE(std::ranges::find(r->library->module_units, inner_cppm) != r->library->module_units.end()); REQUIRE(r->library->additional_sources == std::vector{inner_cpp}); } TEST_CASE("discover excludes src/bin/* from the library walk", "[layout]") { TempProject p; p.touch("src/lib.cppm"); p.touch("src/bin/tool.cpp"); auto r = discover(p.root(), "foo"); REQUIRE(r.has_value()); REQUIRE(r->library->additional_sources.empty()); REQUIRE(r->binaries.size() == 1); REQUIRE(r->binaries[0].name == "tool"); } TEST_CASE("discover lists src/bin/*.cpp as additional binaries", "[layout]") { TempProject p; p.touch("src/main.cpp"); p.touch("src/bin/foo.cpp"); p.touch("src/bin/bar.cpp"); auto r = discover(p.root(), "pkg"); REQUIRE(r.has_value()); REQUIRE(r->binaries.size() == 3); // sorted alphabetically by target name REQUIRE(r->binaries[0].name == "bar"); REQUIRE(r->binaries[1].name == "foo"); REQUIRE(r->binaries[2].name == "pkg"); } TEST_CASE("discover ignores src/bin// subdirs without main.cpp", "[layout]") { TempProject p; p.touch("src/main.cpp"); p.touch("src/bin/foo.cpp"); p.touch("src/bin/sub/nested.cpp"); // no main.cpp at sub/ root, ignored auto r = discover(p.root(), "pkg"); REQUIRE(r.has_value()); REQUIRE(r->binaries.size() == 2); } TEST_CASE("discover lists src/bin//main.cpp as a binary named ", "[layout]") { TempProject p; p.touch("src/main.cpp"); p.touch("src/bin/extra/main.cpp"); p.touch("src/bin/extra/helpers.cpp"); // not collected in v1 auto r = discover(p.root(), "pkg"); REQUIRE(r.has_value()); REQUIRE(r->binaries.size() == 2); REQUIRE(r->binaries[0].name == "extra"); REQUIRE(r->binaries[0].entry.filename() == "main.cpp"); REQUIRE(r->binaries[0].entry.parent_path().filename() == "extra"); REQUIRE(r->binaries[1].name == "pkg"); } TEST_CASE("discover lists tests/*.cpp", "[layout]") { TempProject p; p.touch("src/main.cpp"); p.touch("tests/alpha.cpp"); p.touch("tests/beta.cpp"); p.touch("tests/sub/nested.cpp"); // ignored, no recursion auto r = discover(p.root(), "pkg"); REQUIRE(r.has_value()); REQUIRE(r->tests.size() == 2); REQUIRE(r->tests[0].name == "alpha"); REQUIRE(r->tests[0].kind == TargetKind::Test); REQUIRE(r->tests[1].name == "beta"); } TEST_CASE("discover lists examples/*.cpp", "[layout]") { TempProject p; p.touch("src/main.cpp"); p.touch("examples/demo.cpp"); auto r = discover(p.root(), "pkg"); REQUIRE(r.has_value()); REQUIRE(r->examples.size() == 1); REQUIRE(r->examples[0].name == "demo"); REQUIRE(r->examples[0].kind == TargetKind::Example); } TEST_CASE("discover ignores non .cpp/.cppm files", "[layout]") { TempProject p; p.touch("src/main.cpp"); p.touch("src/notes.txt"); p.touch("src/lib.cppm"); p.touch("src/internal/header.h"); // headers in src/ are ignored auto r = discover(p.root(), "pkg"); REQUIRE(r.has_value()); REQUIRE(r->library->module_units.size() == 1); REQUIRE(r->library->additional_sources.empty()); } TEST_CASE("discover output is deterministic across runs", "[layout]") { TempProject p; p.touch("src/lib.cppm"); p.touch("src/zeta.cppm"); p.touch("src/alpha.cppm"); p.touch("src/middle.cppm"); auto r1 = discover(p.root(), "pkg"); auto r2 = discover(p.root(), "pkg"); REQUIRE(r1.has_value()); REQUIRE(r2.has_value()); REQUIRE(*r1 == *r2); REQUIRE(std::ranges::is_sorted(r1->library->module_units)); }