[M6] tests: levenshtein + pc_scan + brute_scan + findmodule_scan + last_failure_dir + cmd_linkdb_add + codegen PkgConfig/brute-force

This commit is contained in:
2026-05-15 14:41:17 +00:00
parent 94e658fdf1
commit 65a749f088
9 changed files with 557 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
#include <catch2/catch_test_macros.hpp>
import cargoxx.resolver;
import cargoxx.util;
import std;
using cargoxx::resolver::brute_scan;
using cargoxx::util::ErrorCode;
namespace {
auto fresh_store() -> std::filesystem::path {
auto d = std::filesystem::temp_directory_path() /
std::format("cargoxx-brute-scan-{}", std::random_device{}());
std::filesystem::create_directories(d / "lib");
std::filesystem::create_directories(d / "include");
return d;
}
void touch_lib(const std::filesystem::path& store, std::string_view name) {
std::ofstream{store / "lib" / std::string{name}};
}
void touch_include(const std::filesystem::path& store, std::string_view rel) {
auto p = store / "include" / rel;
std::filesystem::create_directories(p.parent_path());
std::ofstream{p};
}
} // namespace
TEST_CASE("brute_scan collects lib*.a and lib*.so files",
"[resolver][brute_scan]") {
auto store = fresh_store();
touch_lib(store, "libfoo.a");
touch_lib(store, "libbar.so");
touch_lib(store, "libbaz.so.1.2.3");
touch_lib(store, "not-a-lib.txt");
touch_include(store, "foo.h");
auto r = brute_scan(store, "foo");
REQUIRE(r.has_value());
REQUIRE(r->lib_files.size() == 3);
auto has = [&](std::string_view suffix) {
return std::ranges::any_of(r->lib_files, [&](const auto& p) {
return std::string_view{p}.ends_with(suffix);
});
};
REQUIRE(has("libfoo.a"));
REQUIRE(has("libbar.so"));
REQUIRE(has("libbaz.so.1.2.3"));
}
TEST_CASE("brute_scan exposes include/ as a single search directory",
"[resolver][brute_scan]") {
auto store = fresh_store();
touch_lib(store, "libsdl.a");
touch_include(store, "SDL2/SDL.h");
touch_include(store, "SDL2/SDL_video.h");
auto r = brute_scan(store, "sdl");
REQUIRE(r.has_value());
REQUIRE(r->include_dirs.size() == 1);
REQUIRE(std::string_view{r->include_dirs[0]}.ends_with("/include"));
}
TEST_CASE("brute_scan errors when neither libs nor headers are present",
"[resolver][brute_scan]") {
auto d = std::filesystem::temp_directory_path() /
std::format("cargoxx-brute-empty-{}", std::random_device{}());
std::filesystem::create_directories(d);
auto r = brute_scan(d, "ghost");
REQUIRE_FALSE(r.has_value());
REQUIRE(r.error().code == ErrorCode::ResolutionUnknownPackage);
}
TEST_CASE("brute_scan emits sorted lib paths for deterministic codegen",
"[resolver][brute_scan]") {
auto store = fresh_store();
touch_lib(store, "libzz.a");
touch_lib(store, "libaa.a");
touch_lib(store, "libmm.so");
auto r = brute_scan(store, "x");
REQUIRE(r.has_value());
REQUIRE(r->lib_files.size() == 3);
REQUIRE(std::ranges::is_sorted(r->lib_files));
}

65
tests/cmd_linkdb_add.cpp Normal file
View File

@@ -0,0 +1,65 @@
#include <catch2/catch_test_macros.hpp>
import cargoxx.cli;
import cargoxx.linkdb;
import cargoxx.util;
import std;
using cargoxx::cli::cmd_linkdb_add;
using cargoxx::linkdb::Database;
namespace {
auto fresh_overlay() -> std::filesystem::path {
auto d = std::filesystem::temp_directory_path() /
std::format("cargoxx-linkdb-add-test-{}", std::random_device{}());
std::filesystem::create_directories(d);
return d / "overlay.sqlite";
}
} // namespace
TEST_CASE("cmd_linkdb_add inserts a recipe that resolve() can read back",
"[cli][linkdb_add]") {
auto overlay = fresh_overlay();
auto r = cmd_linkdb_add(
"sqlite3", "*", "SQLite3 REQUIRED", {"SQLite::SQLite3"}, "sqlite",
overlay);
REQUIRE(r.has_value());
auto db = Database::open(overlay);
REQUIRE(db.has_value());
auto rec = db->resolve("sqlite3", "*");
REQUIRE(rec.has_value());
REQUIRE(rec->source == "manual");
REQUIRE(rec->find_package == "SQLite3 REQUIRED");
REQUIRE(rec->targets == std::vector<std::string>{"SQLite::SQLite3"});
REQUIRE(rec->nixpkgs_attr == "sqlite");
}
TEST_CASE("cmd_linkdb_add evicts auto-discovered rows for the same package",
"[cli][linkdb_add]") {
auto overlay = fresh_overlay();
auto db = Database::open(overlay);
REQUIRE(db.has_value());
// Seed an auto-source row first.
cargoxx::linkdb::Recipe auto_recipe{
.nixpkgs_attr = "sqlite",
.find_package = "auto CONFIG REQUIRED",
.targets = {"auto::auto"},
.source = "nix-probe",
};
REQUIRE(db->insert_provisional("sqlite3", "*", auto_recipe, "nix-probe")
.has_value());
REQUIRE(db->confirm_provisional("sqlite3", "*", "nix-probe").has_value());
REQUIRE(cmd_linkdb_add("sqlite3", "*", "SQLite3 REQUIRED",
{"SQLite::SQLite3"}, "sqlite", overlay)
.has_value());
auto rec = db->resolve("sqlite3", "*");
REQUIRE(rec.has_value());
REQUIRE(rec->source == "manual");
}

View File

@@ -342,3 +342,70 @@ TEST_CASE("cmake_lists threads dev_recipes through find_package and tests",
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);
}

View File

@@ -73,6 +73,37 @@ auto dep_pkg(std::string name, std::string version,
} // namespace
TEST_CASE("flake_nix adds pkgs.pkg-config to nativeBuildInputs only when needed",
"[codegen][flake]") {
Manifest m{pkg("app"), {dep("sqlite", "*")}, {}};
DiscoveredLayout layout{};
Lockfile lock{1, {root_pkg("app", "0.1.0")}};
std::vector<Recipe> recipes = {Recipe{
.nixpkgs_attr = "sqlite",
.find_package = "PkgConfig REQUIRED",
.targets = {"PkgConfig::SQLITE3"},
.source = "pkg-config",
.pkg_config_module = "sqlite3",
}};
GenerateInputs in{m, layout, lock, recipes, {}, "/tmp/app"};
auto out = flake_nix(in);
REQUIRE(out.find("pkgs.pkg-config") != std::string::npos);
REQUIRE(out.find("pkgs.sqlite") != std::string::npos);
}
TEST_CASE("flake_nix omits pkgs.pkg-config when no recipe needs it",
"[codegen][flake]") {
Manifest m{pkg("hello"), {dep("fmt", "*")}, {}};
DiscoveredLayout layout{};
Lockfile lock{1, {root_pkg("hello", "0.1.0")}};
std::vector<Recipe> recipes = {recipe("fmt_10")};
GenerateInputs in{m, layout, lock, recipes, {}, "/tmp/hello"};
auto out = flake_nix(in);
REQUIRE(out.find("pkgs.pkg-config") == std::string::npos);
}
TEST_CASE("flake_nix always emits the shared nixos-unstable nixpkgs input",
"[codegen][flake]") {
Manifest m{pkg("hello"), {}, {}};

View File

@@ -0,0 +1,67 @@
#include <catch2/catch_test_macros.hpp>
import cargoxx.resolver;
import cargoxx.util;
import std;
using cargoxx::resolver::findmodule_scan;
using cargoxx::util::ErrorCode;
// `findmodule_scan` shells out to `cmake -P` to discover CMAKE_ROOT.
// Gate the test on the same env var we use for other live probes so
// CI without cmake can still pass.
namespace {
auto live_tests_enabled() -> bool {
auto* e = std::getenv("CARGOXX_NETWORK_TESTS");
return e != nullptr && *e != 0;
}
} // namespace
TEST_CASE("findmodule_scan picks FindSQLite3 for 'sqlite'",
"[resolver][findmodule_scan][live]") {
if (!live_tests_enabled()) {
SKIP("CARGOXX_NETWORK_TESTS not set");
}
auto r = findmodule_scan("sqlite");
REQUIRE(r.has_value());
REQUIRE(r->find_package == "SQLite3 REQUIRED");
REQUIRE_FALSE(r->targets.empty());
auto has = [&](std::string_view t) {
return std::ranges::find(r->targets, std::string{t}) != r->targets.end();
};
REQUIRE((has("SQLite3::SQLite3") || has("SQLite::SQLite3")));
REQUIRE(r->module_file.filename() == "FindSQLite3.cmake");
}
TEST_CASE("findmodule_scan picks FindZLIB for 'zlib'",
"[resolver][findmodule_scan][live]") {
if (!live_tests_enabled()) {
SKIP("CARGOXX_NETWORK_TESTS not set");
}
auto r = findmodule_scan("zlib");
REQUIRE(r.has_value());
REQUIRE(r->find_package == "ZLIB REQUIRED");
REQUIRE(r->module_file.filename() == "FindZLIB.cmake");
}
TEST_CASE("findmodule_scan picks FindThreads for 'threads'",
"[resolver][findmodule_scan][live]") {
if (!live_tests_enabled()) {
SKIP("CARGOXX_NETWORK_TESTS not set");
}
auto r = findmodule_scan("threads");
REQUIRE(r.has_value());
REQUIRE(r->find_package == "Threads REQUIRED");
}
TEST_CASE("findmodule_scan errors for a totally unknown name",
"[resolver][findmodule_scan][live]") {
if (!live_tests_enabled()) {
SKIP("CARGOXX_NETWORK_TESTS not set");
}
auto r = findmodule_scan("definitelynotacmaketmodule12345");
REQUIRE_FALSE(r.has_value());
REQUIRE(r.error().code == ErrorCode::ResolutionUnknownPackage);
}

View File

@@ -0,0 +1,59 @@
#include <catch2/catch_test_macros.hpp>
import cargoxx.resolver;
import std;
using cargoxx::resolver::last_failure_dir;
namespace {
struct EnvScope {
EnvScope(const char* k, std::optional<std::string> v) : key(k) {
if (auto* prior = std::getenv(key)) {
previous = std::string{prior};
}
if (v) {
setenv(key, v->c_str(), 1);
} else {
unsetenv(key);
}
}
~EnvScope() {
if (previous) {
setenv(key, previous->c_str(), 1);
} else {
unsetenv(key);
}
}
const char* key;
std::optional<std::string> previous;
};
} // namespace
TEST_CASE("last_failure_dir honors XDG_CACHE_HOME when set",
"[resolver][last_failure_dir]") {
EnvScope xdg{"XDG_CACHE_HOME", "/tmp/xdg-test"};
auto p = last_failure_dir("sqlite");
REQUIRE(p.string() == "/tmp/xdg-test/cargoxx/last-failure/sqlite");
}
TEST_CASE("last_failure_dir falls back to $HOME/.cache when XDG is unset",
"[resolver][last_failure_dir]") {
EnvScope xdg{"XDG_CACHE_HOME", std::nullopt};
EnvScope home{"HOME", "/tmp/home-test"};
auto p = last_failure_dir("fmt");
REQUIRE(p.string() == "/tmp/home-test/.cache/cargoxx/last-failure/fmt");
}
TEST_CASE("last_failure_dir uses cwd-based fallback when neither var is set",
"[resolver][last_failure_dir]") {
EnvScope xdg{"XDG_CACHE_HOME", std::nullopt};
EnvScope home{"HOME", std::nullopt};
auto p = last_failure_dir("obscure");
REQUIRE(p.filename() == "obscure");
REQUIRE(p.parent_path().filename() == ".cargoxx-last-failure");
}

36
tests/levenshtein.cpp Normal file
View File

@@ -0,0 +1,36 @@
#include <catch2/catch_test_macros.hpp>
import cargoxx.util;
import std;
using cargoxx::util::levenshtein;
TEST_CASE("levenshtein of equal strings is zero", "[util][levenshtein]") {
REQUIRE(levenshtein("", "") == 0);
REQUIRE(levenshtein("sqlite", "sqlite") == 0);
}
TEST_CASE("levenshtein counts a single suffix character", "[util][levenshtein]") {
REQUIRE(levenshtein("sqlite", "sqlite3") == 1);
REQUIRE(levenshtein("fmt", "fmtlib") == 3);
}
TEST_CASE("levenshtein is symmetric", "[util][levenshtein]") {
REQUIRE(levenshtein("sqlite", "sqlite3") == levenshtein("sqlite3", "sqlite"));
REQUIRE(levenshtein("abseil-cpp", "absl") == levenshtein("absl", "abseil-cpp"));
}
TEST_CASE("levenshtein counts a single substitution", "[util][levenshtein]") {
REQUIRE(levenshtein("kitten", "sitten") == 1);
REQUIRE(levenshtein("kitten", "kittes") == 1);
}
TEST_CASE("levenshtein matches the classic kitten/sitting example",
"[util][levenshtein]") {
REQUIRE(levenshtein("kitten", "sitting") == 3);
}
TEST_CASE("levenshtein handles empty inputs", "[util][levenshtein]") {
REQUIRE(levenshtein("", "abc") == 3);
REQUIRE(levenshtein("abc", "") == 3);
}

89
tests/pc_scan_parse.cpp Normal file
View File

@@ -0,0 +1,89 @@
#include <catch2/catch_test_macros.hpp>
import cargoxx.resolver;
import cargoxx.util;
import std;
using cargoxx::resolver::pc_scan;
using cargoxx::util::ErrorCode;
namespace {
auto fresh_store() -> std::filesystem::path {
auto d = std::filesystem::temp_directory_path() /
std::format("cargoxx-pc-scan-{}", std::random_device{}());
std::filesystem::create_directories(d / "lib" / "pkgconfig");
return d;
}
void touch_pc(const std::filesystem::path& store, std::string_view name,
std::string_view content =
"Name: x\nDescription: x\nLibs: -lx\n") {
std::ofstream{store / "lib" / "pkgconfig" / std::string{name}} << content;
}
} // namespace
TEST_CASE("pc_scan picks the exact-name .pc when present",
"[resolver][pc_scan]") {
auto store = fresh_store();
touch_pc(store, "sqlite3.pc");
auto r = pc_scan(store, "sqlite3");
REQUIRE(r.has_value());
REQUIRE(r->pc_module == "sqlite3");
REQUIRE(r->pc_file.filename() == "sqlite3.pc");
}
TEST_CASE("pc_scan picks sqlite3.pc for nixpkgs name 'sqlite'",
"[resolver][pc_scan]") {
auto store = fresh_store();
touch_pc(store, "sqlite3.pc");
auto r = pc_scan(store, "sqlite");
REQUIRE(r.has_value());
REQUIRE(r->pc_module == "sqlite3");
}
TEST_CASE("pc_scan picks the best match among multiple .pc files",
"[resolver][pc_scan]") {
auto store = fresh_store();
touch_pc(store, "zlib.pc");
touch_pc(store, "sqlite3.pc");
touch_pc(store, "unrelated.pc");
auto r = pc_scan(store, "zlib");
REQUIRE(r.has_value());
REQUIRE(r->pc_module == "zlib");
}
TEST_CASE("pc_scan returns ResolutionUnknownPackage when nothing matches",
"[resolver][pc_scan]") {
auto store = fresh_store();
touch_pc(store, "totally-unrelated.pc");
auto r = pc_scan(store, "sqlite");
REQUIRE_FALSE(r.has_value());
REQUIRE(r.error().code == ErrorCode::ResolutionUnknownPackage);
}
TEST_CASE("pc_scan errors when lib/pkgconfig is missing",
"[resolver][pc_scan]") {
auto d = std::filesystem::temp_directory_path() /
std::format("cargoxx-pc-empty-{}", std::random_device{}());
std::filesystem::create_directories(d);
auto r = pc_scan(d, "sqlite");
REQUIRE_FALSE(r.has_value());
REQUIRE(r.error().code == ErrorCode::ResolutionUnknownPackage);
}
TEST_CASE("pc_scan skips .pc files that look like junk",
"[resolver][pc_scan]") {
auto store = fresh_store();
touch_pc(store, "sqlite3.pc", "this is not a real pc file\n");
auto r = pc_scan(store, "sqlite3");
REQUIRE_FALSE(r.has_value());
REQUIRE(r.error().code == ErrorCode::ResolutionUnknownPackage);
}