From 65a749f088b5dc1155405a217ea7f9f2733130bd Mon Sep 17 00:00:00 2001 From: Amadey Vorontsov Date: Fri, 15 May 2026 14:41:17 +0000 Subject: [PATCH] [M6] tests: levenshtein + pc_scan + brute_scan + findmodule_scan + last_failure_dir + cmd_linkdb_add + codegen PkgConfig/brute-force --- build/CMakeLists.txt | 54 +++++++++++++++++++++ tests/brute_scan_parse.cpp | 89 ++++++++++++++++++++++++++++++++++ tests/cmd_linkdb_add.cpp | 65 +++++++++++++++++++++++++ tests/codegen_cmake.cpp | 67 +++++++++++++++++++++++++ tests/codegen_flake.cpp | 31 ++++++++++++ tests/findmodule_scan_live.cpp | 67 +++++++++++++++++++++++++ tests/last_failure_dir.cpp | 59 ++++++++++++++++++++++ tests/levenshtein.cpp | 36 ++++++++++++++ tests/pc_scan_parse.cpp | 89 ++++++++++++++++++++++++++++++++++ 9 files changed, 557 insertions(+) create mode 100644 tests/brute_scan_parse.cpp create mode 100644 tests/cmd_linkdb_add.cpp create mode 100644 tests/findmodule_scan_live.cpp create mode 100644 tests/last_failure_dir.cpp create mode 100644 tests/levenshtein.cpp create mode 100644 tests/pc_scan_parse.cpp diff --git a/build/CMakeLists.txt b/build/CMakeLists.txt index f294595..5001e05 100644 --- a/build/CMakeLists.txt +++ b/build/CMakeLists.txt @@ -91,6 +91,15 @@ target_link_libraries(cargoxx_bin PRIVATE # ----- tests ----- enable_testing() include(Catch) +add_executable(test_brute_scan_parse ../tests/brute_scan_parse.cpp) +target_link_libraries(test_brute_scan_parse PRIVATE + cargoxx + reproc + SQLite::SQLite3 + Catch2::Catch2 + Catch2::Catch2WithMain +) +catch_discover_tests(test_brute_scan_parse) add_executable(test_cmd_add ../tests/cmd_add.cpp) target_link_libraries(test_cmd_add PRIVATE cargoxx @@ -118,6 +127,15 @@ target_link_libraries(test_cmd_clean PRIVATE Catch2::Catch2WithMain ) catch_discover_tests(test_cmd_clean) +add_executable(test_cmd_linkdb_add ../tests/cmd_linkdb_add.cpp) +target_link_libraries(test_cmd_linkdb_add PRIVATE + cargoxx + reproc + SQLite::SQLite3 + Catch2::Catch2 + Catch2::Catch2WithMain +) +catch_discover_tests(test_cmd_linkdb_add) add_executable(test_cmd_new ../tests/cmd_new.cpp) target_link_libraries(test_cmd_new PRIVATE cargoxx @@ -208,6 +226,24 @@ target_link_libraries(test_exec_run PRIVATE Catch2::Catch2WithMain ) catch_discover_tests(test_exec_run) +add_executable(test_findmodule_scan_live ../tests/findmodule_scan_live.cpp) +target_link_libraries(test_findmodule_scan_live PRIVATE + cargoxx + reproc + SQLite::SQLite3 + Catch2::Catch2 + Catch2::Catch2WithMain +) +catch_discover_tests(test_findmodule_scan_live) +add_executable(test_last_failure_dir ../tests/last_failure_dir.cpp) +target_link_libraries(test_last_failure_dir PRIVATE + cargoxx + reproc + SQLite::SQLite3 + Catch2::Catch2 + Catch2::Catch2WithMain +) +catch_discover_tests(test_last_failure_dir) add_executable(test_layout_discovery ../tests/layout_discovery.cpp) target_link_libraries(test_layout_discovery PRIVATE cargoxx @@ -217,6 +253,15 @@ target_link_libraries(test_layout_discovery PRIVATE Catch2::Catch2WithMain ) catch_discover_tests(test_layout_discovery) +add_executable(test_levenshtein ../tests/levenshtein.cpp) +target_link_libraries(test_levenshtein PRIVATE + cargoxx + reproc + SQLite::SQLite3 + Catch2::Catch2 + Catch2::Catch2WithMain +) +catch_discover_tests(test_levenshtein) add_executable(test_linkdb_lookup ../tests/linkdb_lookup.cpp) target_link_libraries(test_linkdb_lookup PRIVATE cargoxx @@ -307,6 +352,15 @@ target_link_libraries(test_nixpkgs_probe_parse PRIVATE Catch2::Catch2WithMain ) catch_discover_tests(test_nixpkgs_probe_parse) +add_executable(test_pc_scan_parse ../tests/pc_scan_parse.cpp) +target_link_libraries(test_pc_scan_parse PRIVATE + cargoxx + reproc + SQLite::SQLite3 + Catch2::Catch2 + Catch2::Catch2WithMain +) +catch_discover_tests(test_pc_scan_parse) add_executable(test_semver_satisfies ../tests/semver_satisfies.cpp) target_link_libraries(test_semver_satisfies PRIVATE cargoxx diff --git a/tests/brute_scan_parse.cpp b/tests/brute_scan_parse.cpp new file mode 100644 index 0000000..60f8ed2 --- /dev/null +++ b/tests/brute_scan_parse.cpp @@ -0,0 +1,89 @@ +#include + +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)); +} diff --git a/tests/cmd_linkdb_add.cpp b/tests/cmd_linkdb_add.cpp new file mode 100644 index 0000000..e5b5605 --- /dev/null +++ b/tests/cmd_linkdb_add.cpp @@ -0,0 +1,65 @@ +#include + +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{"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"); +} diff --git a/tests/codegen_cmake.cpp b/tests/codegen_cmake.cpp index 8cf6ff6..123a6bc 100644 --- a/tests/codegen_cmake.cpp +++ b/tests/codegen_cmake.cpp @@ -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); +} diff --git a/tests/codegen_flake.cpp b/tests/codegen_flake.cpp index 766841b..3e21fe7 100644 --- a/tests/codegen_flake.cpp +++ b/tests/codegen_flake.cpp @@ -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 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 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"), {}, {}}; diff --git a/tests/findmodule_scan_live.cpp b/tests/findmodule_scan_live.cpp new file mode 100644 index 0000000..3228e04 --- /dev/null +++ b/tests/findmodule_scan_live.cpp @@ -0,0 +1,67 @@ +#include + +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); +} diff --git a/tests/last_failure_dir.cpp b/tests/last_failure_dir.cpp new file mode 100644 index 0000000..f3f61d6 --- /dev/null +++ b/tests/last_failure_dir.cpp @@ -0,0 +1,59 @@ +#include + +import cargoxx.resolver; +import std; + +using cargoxx::resolver::last_failure_dir; + +namespace { + +struct EnvScope { + EnvScope(const char* k, std::optional 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 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"); +} diff --git a/tests/levenshtein.cpp b/tests/levenshtein.cpp new file mode 100644 index 0000000..6313af6 --- /dev/null +++ b/tests/levenshtein.cpp @@ -0,0 +1,36 @@ +#include + +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); +} diff --git a/tests/pc_scan_parse.cpp b/tests/pc_scan_parse.cpp new file mode 100644 index 0000000..f566e37 --- /dev/null +++ b/tests/pc_scan_parse.cpp @@ -0,0 +1,89 @@ +#include + +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); +}