434 lines
17 KiB
C++
434 lines
17 KiB
C++
#include <catch2/catch_test_macros.hpp>
|
|
|
|
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<std::string> 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<std::string> 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 VERSION 0.1.0 LANGUAGES CXX)") != std::string::npos);
|
|
REQUIRE(out.find("include(GNUInstallDirs)") != 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\n"
|
|
" OUTPUT_NAME hello\n"
|
|
" RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/bin\")") !=
|
|
std::string::npos);
|
|
REQUIRE(out.find("install(TARGETS hello_bin RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})") !=
|
|
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);
|
|
// Library projects emit install rules + Config.cmake + .pc.
|
|
REQUIRE(out.find("install(TARGETS widget\n EXPORT widgetTargets") !=
|
|
std::string::npos);
|
|
REQUIRE(out.find("install(EXPORT widgetTargets") != std::string::npos);
|
|
REQUIRE(out.find("configure_package_config_file(") != std::string::npos);
|
|
REQUIRE(out.find("write_basic_package_version_file(") != std::string::npos);
|
|
REQUIRE(out.find("widget.pc.in") != std::string::npos);
|
|
REQUIRE(out.find("DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig") !=
|
|
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);
|
|
REQUIRE(out.find("set_target_properties(app_bin PROPERTIES\n"
|
|
" OUTPUT_NAME app\n"
|
|
" RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/bin\")") !=
|
|
std::string::npos);
|
|
REQUIRE(out.find("set_target_properties(tool PROPERTIES\n"
|
|
" RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/bin\")") !=
|
|
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<Recipe> 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<Recipe> 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 "
|
|
"-Wno-missing-field-initializers)") !=
|
|
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<Recipe> 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);
|
|
}
|
|
|
|
TEST_CASE("cmake_lists emits pkg_check_modules for pkg_config recipes",
|
|
"[codegen][cmake]") {
|
|
Manifest m{
|
|
.package = pkg("app"),
|
|
.dependencies = {{.name = "sqlite", .version_spec = "*"}},
|
|
};
|
|
DiscoveredLayout layout{
|
|
.library = std::nullopt,
|
|
.binaries = {src_target(TargetKind::Binary, "app", "src/main.cpp")},
|
|
.tests = {},
|
|
.examples = {},
|
|
};
|
|
Lockfile lock = lock_minimal();
|
|
Recipe sqlite{
|
|
.nixpkgs_attr = "sqlite",
|
|
.find_package = "PkgConfig REQUIRED",
|
|
.targets = {"PkgConfig::SQLITE3"},
|
|
.source = "pkg-config",
|
|
.pkg_config_module = "sqlite3",
|
|
};
|
|
GenerateInputs in{m, layout, lock, {sqlite}, {}, ROOT};
|
|
|
|
auto out = cmake_lists(in);
|
|
REQUIRE(out.find("find_package(PkgConfig REQUIRED)") != std::string::npos);
|
|
REQUIRE(out.find("pkg_check_modules(SQLITE3 REQUIRED IMPORTED_TARGET sqlite3)") !=
|
|
std::string::npos);
|
|
auto link = out.find("target_link_libraries(app_bin PRIVATE");
|
|
REQUIRE(link != std::string::npos);
|
|
auto block = out.substr(link, out.find(')', link) - link);
|
|
REQUIRE(block.find("PkgConfig::SQLITE3") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("cmake_lists synthesizes INTERFACE IMPORTED target for brute-force "
|
|
"recipes",
|
|
"[codegen][cmake]") {
|
|
Manifest m{
|
|
.package = pkg("app"),
|
|
.dependencies = {{.name = "obscure", .version_spec = "*"}},
|
|
};
|
|
DiscoveredLayout layout{
|
|
.library = std::nullopt,
|
|
.binaries = {src_target(TargetKind::Binary, "app", "src/main.cpp")},
|
|
.tests = {},
|
|
.examples = {},
|
|
};
|
|
Lockfile lock = lock_minimal();
|
|
Recipe brute{
|
|
.nixpkgs_attr = "obscure",
|
|
.find_package = "",
|
|
.targets = {"obscure::obscure"},
|
|
.source = "brute-force",
|
|
.brute_force_libs = {"/nix/store/abc-obscure/lib/libobscure.a"},
|
|
.brute_force_includes = {"/nix/store/abc-obscure/include"},
|
|
};
|
|
GenerateInputs in{m, layout, lock, {brute}, {}, ROOT};
|
|
|
|
auto out = cmake_lists(in);
|
|
REQUIRE(out.find("add_library(obscure::obscure INTERFACE IMPORTED)") !=
|
|
std::string::npos);
|
|
REQUIRE(out.find("INTERFACE_LINK_LIBRARIES") != std::string::npos);
|
|
REQUIRE(out.find("/nix/store/abc-obscure/lib/libobscure.a") !=
|
|
std::string::npos);
|
|
REQUIRE(out.find("INTERFACE_INCLUDE_DIRECTORIES") != std::string::npos);
|
|
REQUIRE(out.find("/nix/store/abc-obscure/include") != std::string::npos);
|
|
REQUIRE(out.find("find_package()") == std::string::npos);
|
|
}
|