#include import cargoxx.codegen; import cargoxx.manifest; import cargoxx.layout; import cargoxx.lockfile; import cargoxx.linkdb; import std; using cargoxx::codegen::cmake_lists; using cargoxx::codegen::GenerateInputs; using cargoxx::layout::DiscoveredLayout; using cargoxx::layout::Target; using cargoxx::layout::TargetKind; using cargoxx::linkdb::Recipe; using cargoxx::lockfile::Lockfile; using cargoxx::lockfile::LockfilePackage; using cargoxx::manifest::BuildSettings; using cargoxx::manifest::Edition; using cargoxx::manifest::Manifest; using cargoxx::manifest::Package; namespace { constexpr std::string_view ROOT = "/proj"; auto pkg(std::string name, Edition ed = Edition::Cpp23) -> Package { return Package{.name = std::move(name), .version = "0.1.0", .edition = ed, .authors = {}, .license = std::nullopt}; } auto lock_minimal() -> Lockfile { return Lockfile{1, {}}; } auto src_target(TargetKind kind, std::string name, std::string rel_entry, std::vector module_units = {}) -> Target { Target t{ .kind = kind, .name = std::move(name), .entry = std::filesystem::path{ROOT} / rel_entry, .additional_sources = {}, .module_units = {}, }; for (auto& u : module_units) { t.module_units.push_back(std::filesystem::path{ROOT} / u); } return t; } auto recipe(std::string find_pkg, std::vector targets) -> Recipe { return Recipe{ .nixpkgs_attr = "ignored", .find_package = std::move(find_pkg), .targets = std::move(targets), .source = "curated", }; } } // namespace TEST_CASE("cmake_lists for a binary-only project", "[codegen][cmake]") { Manifest m{pkg("hello"), {}, {}}; DiscoveredLayout layout{ .library = std::nullopt, .binaries = {src_target(TargetKind::Binary, "hello", "src/main.cpp")}, .tests = {}, .examples = {}, }; Lockfile lock = lock_minimal(); GenerateInputs in{m, layout, lock, {}, {}, ROOT}; auto out = cmake_lists(in); REQUIRE(out.find("project(hello LANGUAGES CXX)") != std::string::npos); REQUIRE(out.find("set(CMAKE_CXX_STANDARD 23)") != std::string::npos); REQUIRE(out.find("add_executable(hello_bin ../src/main.cpp)") != std::string::npos); REQUIRE(out.find("set_target_properties(hello_bin PROPERTIES OUTPUT_NAME hello)") != std::string::npos); REQUIRE(out.find("add_library") == std::string::npos); REQUIRE(out.find("enable_testing") == std::string::npos); } TEST_CASE("cmake_lists for a library-only project", "[codegen][cmake]") { Manifest m{pkg("widget"), {}, {}}; DiscoveredLayout layout{ .library = src_target(TargetKind::Library, "widget", "src/lib.cppm", {"src/lib.cppm"}), .binaries = {}, .tests = {}, .examples = {}, }; Lockfile lock = lock_minimal(); GenerateInputs in{m, layout, lock, {}, {}, ROOT}; auto out = cmake_lists(in); REQUIRE(out.find("add_library(widget STATIC)") != std::string::npos); REQUIRE(out.find("FILE_SET CXX_MODULES") != std::string::npos); REQUIRE(out.find("../src/lib.cppm") != std::string::npos); REQUIRE(out.find("add_executable") == std::string::npos); } TEST_CASE("cmake_lists wires up library + primary binary", "[codegen][cmake]") { Manifest m{pkg("app"), {}, {}}; DiscoveredLayout layout{ .library = src_target(TargetKind::Library, "app", "src/lib.cppm", {"src/lib.cppm"}), .binaries = {src_target(TargetKind::Binary, "app", "src/main.cpp")}, .tests = {}, .examples = {}, }; Lockfile lock = lock_minimal(); GenerateInputs in{m, layout, lock, {}, {}, ROOT}; auto out = cmake_lists(in); REQUIRE(out.find("add_library(app STATIC)") != std::string::npos); REQUIRE(out.find("add_executable(app_bin ../src/main.cpp)") != std::string::npos); // primary binary must link the library auto bin_link = out.find("target_link_libraries(app_bin PRIVATE"); REQUIRE(bin_link != std::string::npos); auto end = out.find(')', bin_link); auto block = out.substr(bin_link, end - bin_link); REQUIRE(block.find("\n app\n") != std::string::npos); } TEST_CASE("cmake_lists emits extra binaries from src/bin/", "[codegen][cmake]") { Manifest m{pkg("app"), {}, {}}; DiscoveredLayout layout{ .library = std::nullopt, .binaries = {src_target(TargetKind::Binary, "tool", "src/bin/tool.cpp"), src_target(TargetKind::Binary, "app", "src/main.cpp")}, .tests = {}, .examples = {}, }; Lockfile lock = lock_minimal(); GenerateInputs in{m, layout, lock, {}, {}, ROOT}; auto out = cmake_lists(in); REQUIRE(out.find("add_executable(app_bin ../src/main.cpp)") != std::string::npos); REQUIRE(out.find("add_executable(tool ../src/bin/tool.cpp)") != std::string::npos); } TEST_CASE("cmake_lists emits tests with add_test", "[codegen][cmake]") { Manifest m{pkg("app"), {}, {}}; DiscoveredLayout layout{ .library = src_target(TargetKind::Library, "app", "src/lib.cppm", {"src/lib.cppm"}), .binaries = {}, .tests = {src_target(TargetKind::Test, "basic", "tests/basic.cpp")}, .examples = {}, }; Lockfile lock = lock_minimal(); GenerateInputs in{m, layout, lock, {}, {}, ROOT}; auto out = cmake_lists(in); REQUIRE(out.find("enable_testing()") != std::string::npos); REQUIRE(out.find("add_executable(test_basic ../tests/basic.cpp)") != std::string::npos); REQUIRE(out.find("add_test(NAME basic COMMAND test_basic)") != std::string::npos); } TEST_CASE("cmake_lists emits examples", "[codegen][cmake]") { Manifest m{pkg("app"), {}, {}}; DiscoveredLayout layout{ .library = std::nullopt, .binaries = {src_target(TargetKind::Binary, "app", "src/main.cpp")}, .tests = {}, .examples = {src_target(TargetKind::Example, "demo", "examples/demo.cpp")}, }; Lockfile lock = lock_minimal(); GenerateInputs in{m, layout, lock, {}, {}, ROOT}; auto out = cmake_lists(in); REQUIRE(out.find("add_executable(example_demo ../examples/demo.cpp)") != std::string::npos); } TEST_CASE("cmake_lists emits find_package per dep", "[codegen][cmake]") { Manifest m{pkg("app"), {}, {}}; DiscoveredLayout layout{ .library = std::nullopt, .binaries = {src_target(TargetKind::Binary, "app", "src/main.cpp")}, .tests = {}, .examples = {}, }; Lockfile lock = lock_minimal(); std::vector recipes = { recipe("fmt CONFIG REQUIRED", {"fmt::fmt"}), recipe("Boost REQUIRED COMPONENTS filesystem system", {"Boost::filesystem", "Boost::system"}), }; GenerateInputs in{m, layout, lock, recipes, {}, ROOT}; auto out = cmake_lists(in); REQUIRE(out.find("find_package(fmt CONFIG REQUIRED)") != std::string::npos); REQUIRE(out.find("find_package(Boost REQUIRED COMPONENTS filesystem system)") != std::string::npos); auto link_block = out.find("target_link_libraries(app_bin PRIVATE"); REQUIRE(link_block != std::string::npos); auto end = out.find(')', link_block); auto block = out.substr(link_block, end - link_block); REQUIRE(block.find("fmt::fmt") != std::string::npos); REQUIRE(block.find("Boost::filesystem") != std::string::npos); REQUIRE(block.find("Boost::system") != std::string::npos); } TEST_CASE("cmake_lists honors warnings_as_errors", "[codegen][cmake]") { Manifest m{ .package = pkg("app"), .build = BuildSettings{.warnings_as_errors = true, .sanitizers = {}}, }; DiscoveredLayout layout{ .library = std::nullopt, .binaries = {src_target(TargetKind::Binary, "app", "src/main.cpp")}, .tests = {}, .examples = {}, }; Lockfile lock = lock_minimal(); GenerateInputs in{m, layout, lock, {}, {}, ROOT}; auto out = cmake_lists(in); REQUIRE(out.find("foreach(target_name IN ITEMS app_bin)") != std::string::npos); REQUIRE(out.find("-Wall -Wextra -Wpedantic -Werror") != std::string::npos); } TEST_CASE("cmake_lists honors sanitizers", "[codegen][cmake]") { Manifest m{ .package = pkg("app"), .build = BuildSettings{.warnings_as_errors = false, .sanitizers = {"address", "undefined"}}, }; DiscoveredLayout layout{ .library = std::nullopt, .binaries = {src_target(TargetKind::Binary, "app", "src/main.cpp")}, .tests = {}, .examples = {}, }; Lockfile lock = lock_minimal(); GenerateInputs in{m, layout, lock, {}, {}, ROOT}; auto out = cmake_lists(in); REQUIRE(out.find("-fsanitize=address,undefined") != std::string::npos); REQUIRE(out.find("target_link_options(${target_name} PRIVATE -fsanitize=") != std::string::npos); } TEST_CASE("cmake_lists maps editions to standard numbers", "[codegen][cmake]") { DiscoveredLayout layout{ .library = std::nullopt, .binaries = {src_target(TargetKind::Binary, "app", "src/main.cpp")}, .tests = {}, .examples = {}, }; Lockfile lock = lock_minimal(); Manifest m20{pkg("app", Edition::Cpp20), {}, {}}; REQUIRE(cmake_lists(GenerateInputs{m20, layout, lock, {}, {}, ROOT}) .find("set(CMAKE_CXX_STANDARD 20)") != std::string::npos); Manifest m26{pkg("app", Edition::Cpp26), {}, {}}; REQUIRE(cmake_lists(GenerateInputs{m26, layout, lock, {}, {}, ROOT}) .find("set(CMAKE_CXX_STANDARD 26)") != std::string::npos); } TEST_CASE("cmake_lists is deterministic", "[codegen][cmake]") { Manifest m{pkg("app"), {}, {}}; DiscoveredLayout layout{ .library = src_target(TargetKind::Library, "app", "src/lib.cppm", {"src/lib.cppm"}), .binaries = {src_target(TargetKind::Binary, "app", "src/main.cpp")}, .tests = {src_target(TargetKind::Test, "basic", "tests/basic.cpp")}, .examples = {}, }; Lockfile lock = lock_minimal(); std::vector recipes = {recipe("fmt CONFIG REQUIRED", {"fmt::fmt"})}; GenerateInputs in{m, layout, lock, recipes, {}, ROOT}; REQUIRE(cmake_lists(in) == cmake_lists(in)); } TEST_CASE("cmake_lists emits baseline warnings", "[codegen][cmake]") { Manifest m{pkg("app"), {}, {}}; DiscoveredLayout layout{ .library = std::nullopt, .binaries = {src_target(TargetKind::Binary, "app", "src/main.cpp")}, .tests = {}, .examples = {}, }; Lockfile lock = lock_minimal(); GenerateInputs in{m, layout, lock, {}, {}, ROOT}; auto out = cmake_lists(in); REQUIRE(out.find("add_compile_options(-Wall -Wextra -Wpedantic -Wconversion)") != std::string::npos); } TEST_CASE("cmake_lists emits target_include_directories for [build].include_dirs", "[codegen][cmake]") { Manifest m{ .package = pkg("widget"), .build = BuildSettings{.include_dirs = {"third_party", "vendor/json"}}, }; DiscoveredLayout layout{ .library = src_target(TargetKind::Library, "widget", "src/lib.cppm", {"src/lib.cppm"}), .binaries = {}, .tests = {}, .examples = {}, }; Lockfile lock = lock_minimal(); GenerateInputs in{m, layout, lock, {}, {}, ROOT}; auto out = cmake_lists(in); REQUIRE(out.find("target_include_directories(widget SYSTEM PRIVATE " "../third_party ../vendor/json)") != std::string::npos); } TEST_CASE("cmake_lists threads dev_recipes through find_package and tests", "[codegen][cmake]") { Manifest m{pkg("app"), {}, {}}; DiscoveredLayout layout{ .library = src_target(TargetKind::Library, "app", "src/lib.cppm", {"src/lib.cppm"}), .binaries = {}, .tests = {src_target(TargetKind::Test, "basic", "tests/basic.cpp")}, .examples = {}, }; Lockfile lock = lock_minimal(); std::vector dev_recipes = { recipe("Catch2 CONFIG REQUIRED", {"Catch2::Catch2WithMain"}), }; GenerateInputs in{m, layout, lock, {}, dev_recipes, ROOT}; auto out = cmake_lists(in); REQUIRE(out.find("find_package(Catch2 CONFIG REQUIRED)") != std::string::npos); REQUIRE(out.find("include(Catch)") != std::string::npos); REQUIRE(out.find("catch_discover_tests(test_basic)") != std::string::npos); REQUIRE(out.find("add_test(NAME basic") == std::string::npos); auto link = out.find("target_link_libraries(test_basic PRIVATE"); REQUIRE(link != std::string::npos); auto end = out.find(')', link); auto block = out.substr(link, end - link); REQUIRE(block.find("Catch2::Catch2WithMain") != std::string::npos); }