Files
cargoxx/tests/nix_cmake_scan_parse.cpp

254 lines
10 KiB
C++

#include <catch2/catch_test_macros.hpp>
import cargoxx.resolver;
import cargoxx.util;
import std;
using cargoxx::resolver::config_stem_to_package;
using cargoxx::resolver::filter_public_targets;
using cargoxx::resolver::nix_cmake_scan;
using cargoxx::resolver::scan_imported_targets;
using cargoxx::util::ErrorCode;
TEST_CASE("config_stem_to_package strips Config / -config / .cmake",
"[resolver][nix_cmake_scan]") {
REQUIRE(config_stem_to_package("fmtConfig.cmake") == "fmt");
REQUIRE(config_stem_to_package("fmt-config.cmake") == "fmt");
REQUIRE(config_stem_to_package("Catch2Config.cmake") == "Catch2");
REQUIRE(config_stem_to_package("range-v3-config.cmake") == "range-v3");
REQUIRE(config_stem_to_package("/abs/path/sub/spdlogConfig.cmake") == "spdlog");
}
TEST_CASE("scan_imported_targets picks up IMPORTED libraries",
"[resolver][nix_cmake_scan]") {
constexpr std::string_view text = R"(
# noise here
add_library(fmt::fmt SHARED IMPORTED)
set_target_properties(fmt::fmt PROPERTIES IMPORTED_LOCATION "/x")
)";
auto out = scan_imported_targets(text);
REQUIRE(out == std::vector<std::string>{"fmt::fmt"});
}
TEST_CASE("scan_imported_targets picks up ALIAS targets",
"[resolver][nix_cmake_scan]") {
constexpr std::string_view text = R"(
add_library(absl_strings_internal STATIC IMPORTED)
add_library(absl::strings ALIAS absl_strings_internal)
)";
auto out = scan_imported_targets(text);
// Both the imported impl and the alias should be reported, deduped,
// in declaration order.
REQUIRE(out.size() == 2);
REQUIRE(out[0] == "absl_strings_internal");
REQUIRE(out[1] == "absl::strings");
}
TEST_CASE("scan_imported_targets ignores plain (non-IMPORTED) add_library",
"[resolver][nix_cmake_scan]") {
constexpr std::string_view text = R"(
add_library(local STATIC src.cpp)
add_library(other_lib SHARED other.cpp)
)";
auto out = scan_imported_targets(text);
REQUIRE(out.empty());
}
TEST_CASE("scan_imported_targets does not match _add_library or similar",
"[resolver][nix_cmake_scan]") {
constexpr std::string_view text = R"(
# function definition uses the marker as a substring
function(_add_library_helper)
endfunction()
)";
auto out = scan_imported_targets(text);
REQUIRE(out.empty());
}
TEST_CASE("filter_public_targets keeps namespaced targets when present",
"[resolver][nix_cmake_scan]") {
auto out = filter_public_targets(
{"fmt::fmt", "fmt::fmt-header-only", "fmt_internal_helper"}, "fmt");
REQUIRE(out == std::vector<std::string>{"fmt::fmt", "fmt::fmt-header-only"});
}
TEST_CASE("filter_public_targets picks the umbrella when no targets are namespaced",
"[resolver][nix_cmake_scan]") {
// Mirrors LLVMExports: 213 bare targets, only `LLVM` is the
// intended downstream entry point.
auto out = filter_public_targets(
{"LLVM", "LLVMSupport", "LLVMCore", "LLVMWindowsManifest"}, "LLVM");
REQUIRE(out == std::vector<std::string>{"LLVM"});
}
TEST_CASE("filter_public_targets is case-insensitive when picking the umbrella",
"[resolver][nix_cmake_scan]") {
auto out = filter_public_targets({"GRPC", "grpc_internal"}, "gRPC");
REQUIRE(out == std::vector<std::string>{"GRPC"});
}
TEST_CASE("filter_public_targets keeps everything when no umbrella matches",
"[resolver][nix_cmake_scan]") {
// Stem doesn't match any bare target — fall back to passthrough so
// we still emit a non-empty link line.
auto out = filter_public_targets({"alpha", "beta", "gamma"}, "Whatever");
REQUIRE(out == std::vector<std::string>{"alpha", "beta", "gamma"});
}
TEST_CASE("scan_imported_targets dedupes duplicate target names",
"[resolver][nix_cmake_scan]") {
constexpr std::string_view text = R"(
add_library(fmt::fmt SHARED IMPORTED)
# benign duplicate (e.g. config-version reuse)
add_library(fmt::fmt SHARED IMPORTED)
)";
auto out = scan_imported_targets(text);
REQUIRE(out == std::vector<std::string>{"fmt::fmt"});
}
namespace {
auto fresh_store() -> std::filesystem::path {
auto d = std::filesystem::temp_directory_path() /
std::format("cargoxx-cmake-scan-{}", std::random_device{}());
std::filesystem::create_directories(d / "lib" / "cmake");
return d;
}
void touch_config(const std::filesystem::path& store, std::string_view rel,
std::string_view content) {
auto p = store / "lib" / "cmake" / rel;
std::filesystem::create_directories(p.parent_path());
std::ofstream{p} << content;
}
} // namespace
TEST_CASE("nix_cmake_scan finds the canonical config", "[resolver][nix_cmake_scan]") {
auto store = fresh_store();
touch_config(store, "fmt/fmtConfig.cmake",
R"(add_library(fmt::fmt SHARED IMPORTED))");
auto r = nix_cmake_scan(store, "fmt");
REQUIRE(r.has_value());
REQUIRE(r->find_package == "fmt CONFIG REQUIRED");
REQUIRE(r->targets == std::vector<std::string>{"fmt::fmt"});
REQUIRE(r->config_file.filename() == "fmtConfig.cmake");
}
TEST_CASE("nix_cmake_scan prefers exact case-insensitive match",
"[resolver][nix_cmake_scan]") {
auto store = fresh_store();
touch_config(store, "absl/abslConfig.cmake",
R"(add_library(absl::strings SHARED IMPORTED))");
touch_config(store, "absl-extras/abslExtrasConfig.cmake",
R"(add_library(absl::extras SHARED IMPORTED))");
auto r = nix_cmake_scan(store, "absl");
REQUIRE(r.has_value());
REQUIRE(r->find_package == "absl CONFIG REQUIRED");
}
TEST_CASE("nix_cmake_scan returns ResolutionUnknownPackage when nothing found",
"[resolver][nix_cmake_scan]") {
auto store = fresh_store();
auto r = nix_cmake_scan(store, "nothing");
REQUIRE_FALSE(r.has_value());
REQUIRE(r.error().code == ErrorCode::ResolutionUnknownPackage);
}
TEST_CASE("nix_cmake_scan ignores Config files with no IMPORTED targets",
"[resolver][nix_cmake_scan]") {
auto store = fresh_store();
touch_config(store, "junk/junkConfig.cmake",
R"(message(STATUS "no targets here"))");
auto r = nix_cmake_scan(store, "junk");
REQUIRE_FALSE(r.has_value());
REQUIRE(r.error().code == ErrorCode::ResolutionUnknownPackage);
}
TEST_CASE("nix_cmake_scan strips a leading lib prefix when scoring",
"[resolver][nix_cmake_scan]") {
// Real layout for nixpkgs `libllvm.dev`. `libllvm` should match
// `LLVMConfig.cmake`'s parent dir (`llvm`) after lib-stripping
// and beat `polly/PollyConfig.cmake`.
auto store = fresh_store();
touch_config(store, "llvm/LLVMConfig.cmake",
R"(add_library(LLVM SHARED IMPORTED))");
touch_config(store, "polly/PollyConfig.cmake",
R"(add_library(Polly SHARED IMPORTED))");
auto r = nix_cmake_scan(store, "libllvm");
REQUIRE(r.has_value());
REQUIRE(r->find_package == "LLVM CONFIG REQUIRED");
REQUIRE(r->config_file.filename() == "LLVMConfig.cmake");
}
TEST_CASE("nix_cmake_scan picks the canonical config from a versioned dir",
"[resolver][nix_cmake_scan]") {
// Boost ships a top-level `Boost-1.89.0/BoostConfig.cmake` plus
// 47 modular component configs at sibling versioned dirs.
// After version-suffix stripping the top-level Boost should win.
auto store = fresh_store();
touch_config(store, "Boost-1.89.0/BoostConfig.cmake",
R"(add_library(Boost::headers INTERFACE IMPORTED))");
touch_config(store, "boost_atomic-1.89.0/boost_atomic-config.cmake",
R"(add_library(Boost::atomic SHARED IMPORTED))");
touch_config(store, "boost_filesystem-1.89.0/boost_filesystem-config.cmake",
R"(add_library(Boost::filesystem SHARED IMPORTED))");
auto r = nix_cmake_scan(store, "boost");
REQUIRE(r.has_value());
REQUIRE(r->find_package == "Boost CONFIG REQUIRED");
REQUIRE(r->config_file.filename() == "BoostConfig.cmake");
}
TEST_CASE("nix_cmake_scan ignores satellite configs in multi-config stores",
"[resolver][nix_cmake_scan]") {
// Mirrors `protobuf` which ships protobuf-config.cmake plus an
// unrelated utf8_range-config.cmake under the same prefix tree.
auto store = fresh_store();
touch_config(store, "protobuf/protobuf-config.cmake",
R"(add_library(protobuf::libprotobuf SHARED IMPORTED))");
touch_config(store, "utf8_range/utf8_range-config.cmake",
R"(add_library(utf8_range::utf8_range SHARED IMPORTED))");
auto r = nix_cmake_scan(store, "protobuf");
REQUIRE(r.has_value());
REQUIRE(r->find_package == "protobuf CONFIG REQUIRED");
}
TEST_CASE("nix_cmake_scan normalizes case for spelling variants",
"[resolver][nix_cmake_scan]") {
// grpc ships `gRPCConfig.cmake` under `lib/cmake/grpc/`.
auto store = fresh_store();
touch_config(store, "grpc/gRPCConfig.cmake",
R"(add_library(gRPC::grpc++ SHARED IMPORTED))");
auto r = nix_cmake_scan(store, "grpc");
REQUIRE(r.has_value());
REQUIRE(r->find_package == "gRPC CONFIG REQUIRED");
}
TEST_CASE("nix_cmake_scan scans sibling *-targets.cmake files",
"[resolver][nix_cmake_scan]") {
// Mirrors fmt's real on-disk layout: the *-config.cmake is empty of
// IMPORTED targets but include()s a sibling *-targets.cmake that
// carries them.
auto store = fresh_store();
touch_config(store, "fmt/fmt-config.cmake",
R"(include("${CMAKE_CURRENT_LIST_DIR}/fmt-targets.cmake"))");
touch_config(store, "fmt/fmt-targets.cmake",
R"(add_library(fmt::fmt SHARED IMPORTED)
add_library(fmt::fmt-header-only INTERFACE IMPORTED))");
auto r = nix_cmake_scan(store, "fmt");
REQUIRE(r.has_value());
REQUIRE(r->find_package == "fmt CONFIG REQUIRED");
REQUIRE(r->targets.size() == 2);
REQUIRE(std::ranges::find(r->targets, std::string{"fmt::fmt"}) !=
r->targets.end());
REQUIRE(std::ranges::find(r->targets, std::string{"fmt::fmt-header-only"}) !=
r->targets.end());
}