[M5+] add resolver::verify_link + provisional overlay lifecycle
This commit is contained in:
14
CHANGELOG.md
14
CHANGELOG.md
@@ -78,6 +78,20 @@ All notable changes to cargoxx will be documented in this file.
|
||||
and `[build]` honoring `warnings_as_errors` and `sanitizers`. Source
|
||||
paths emitted relative to `build/` (i.e. prefixed with `../`).
|
||||
Output is deterministic. `tests/codegen_cmake.cpp` covers 11 cases.
|
||||
- `cargoxx.linkdb::Database::insert_provisional`,
|
||||
`confirm_provisional`, and `abort_provisional` — three-step lifecycle
|
||||
for non-`manual` overlay rows: insert with `verified_at = 0`, run a
|
||||
build, then either bump `verified_at = now` on success or `DELETE`
|
||||
the row on failure.
|
||||
- `cargoxx.resolver::verify_link(req, build_fn)` — scaffolds a tiny
|
||||
`Cargoxx.toml` + `int main() {}` project under a scratch dir, writes
|
||||
a provisional overlay row pointing at the candidate `Recipe`, runs
|
||||
the caller-supplied `BuildFn` (typically `cli::cmd_build` injected
|
||||
to avoid a resolver-on-cli dep cycle), and on success/failure
|
||||
confirms or aborts the provisional row. Cleans the scratch project
|
||||
via RAII regardless of outcome. `tests/verify_link_unit.cpp` exercises
|
||||
both success and failure paths against a mock `BuildFn`, verifying
|
||||
that the overlay row's lifecycle matches the build outcome.
|
||||
- `cargoxx.resolver::vcpkg_probe(name)` — fetches
|
||||
`https://raw.githubusercontent.com/microsoft/vcpkg/master/ports/<name>/usage`
|
||||
and feeds it through `parse_vcpkg_usage`. The pure parser extracts
|
||||
|
||||
@@ -56,6 +56,7 @@ target_sources(cargoxx
|
||||
src/resolver/nix_cmake_scan.cpp
|
||||
src/resolver/conan_probe.cpp
|
||||
src/resolver/vcpkg_probe.cpp
|
||||
src/resolver/verify_link.cpp
|
||||
src/cli/cmd_new.cpp
|
||||
src/cli/cmd_build.cpp
|
||||
src/cli/cmd_run.cpp
|
||||
|
||||
@@ -195,8 +195,8 @@ auto verify_link(const Recipe& candidate,
|
||||
| 1. nixpkgs_probe + JSON parser | ✅ | `1c7ff39` |
|
||||
| 2. nix_cmake_scan | ✅ | `e63ac69` |
|
||||
| 3. conan_probe + parse_conanfile | ✅ | `e5c173b` |
|
||||
| 4. vcpkg_probe + parse_vcpkg_usage | ✅ | (this commit) |
|
||||
| 5. verify_link (tmp project + cmd_build) | pending | — |
|
||||
| 4. vcpkg_probe + parse_vcpkg_usage | ✅ | `941d5b3` |
|
||||
| 5. verify_link (tmp project + cmd_build) | ✅ | (this commit) |
|
||||
| 6. Database::discover + cmd_add wire-up + failure caching | pending | — |
|
||||
|
||||
## Testing strategy
|
||||
|
||||
@@ -71,6 +71,25 @@ auto overlay_insert_manual(OverlayState& state, const std::string& package,
|
||||
const cargoxx::linkdb::Recipe& r)
|
||||
-> cargoxx::util::Result<void>;
|
||||
|
||||
// Insert a row from a non-manual probe. `verified_at = 0` flags the row as
|
||||
// provisional; the verify-link step bumps it to the current epoch on
|
||||
// success or deletes the row on failure.
|
||||
auto overlay_insert_provisional(OverlayState& state, const std::string& package,
|
||||
const std::string& version_range,
|
||||
const cargoxx::linkdb::Recipe& r,
|
||||
const std::string& source)
|
||||
-> cargoxx::util::Result<void>;
|
||||
|
||||
auto overlay_confirm_provisional(OverlayState& state, const std::string& package,
|
||||
const std::string& version_range,
|
||||
const std::string& source)
|
||||
-> cargoxx::util::Result<void>;
|
||||
|
||||
auto overlay_delete_recipe(OverlayState& state, const std::string& package,
|
||||
const std::string& version_range,
|
||||
const std::string& source)
|
||||
-> cargoxx::util::Result<void>;
|
||||
|
||||
auto overlay_query(OverlayState& state, const std::string& package)
|
||||
-> cargoxx::util::Result<std::vector<OverlayRow>>;
|
||||
|
||||
@@ -92,6 +111,20 @@ class Database {
|
||||
auto add_manual(const std::string& package, const std::string& version_range,
|
||||
const Recipe& r) -> util::Result<void>;
|
||||
|
||||
// Provisional-recipe lifecycle, used by the resolver's verify-link step.
|
||||
// `source` should be one of "conan", "vcpkg", "nix-probe", or any other
|
||||
// probe identifier (NOT "manual"/"curated", which have their own
|
||||
// contracts).
|
||||
auto insert_provisional(const std::string& package,
|
||||
const std::string& version_range, const Recipe& r,
|
||||
const std::string& source) -> util::Result<void>;
|
||||
auto confirm_provisional(const std::string& package,
|
||||
const std::string& version_range,
|
||||
const std::string& source) -> util::Result<void>;
|
||||
auto abort_provisional(const std::string& package,
|
||||
const std::string& version_range,
|
||||
const std::string& source) -> util::Result<void>;
|
||||
|
||||
private:
|
||||
Database() = default;
|
||||
std::map<std::string, std::vector<detail::CuratedRecipe>> curated_;
|
||||
|
||||
@@ -128,6 +128,98 @@ auto overlay_insert_manual(OverlayState& state, const std::string& package,
|
||||
return {};
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
auto overlay_insert(OverlayState& state, const std::string& package,
|
||||
const std::string& version_range, const Recipe& r,
|
||||
const std::string& source, std::int64_t verified_at)
|
||||
-> util::Result<void> {
|
||||
constexpr const char* SQL =
|
||||
"INSERT OR REPLACE INTO recipes "
|
||||
"(package, version_range, nixpkgs_attr, find_package, targets, components, source, "
|
||||
" verified_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, NULL, ?, ?)";
|
||||
|
||||
sqlite3* db = state.handle();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db, SQL, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return std::unexpected(sqlite_error(db, "prepare insert"));
|
||||
}
|
||||
|
||||
auto targets_str = nlohmann::json(r.targets).dump();
|
||||
|
||||
sqlite3_bind_text(stmt, 1, package.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, version_range.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 3, r.nixpkgs_attr.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 4, r.find_package.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 5, targets_str.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 6, source.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int64(stmt, 7, verified_at);
|
||||
|
||||
auto rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
return std::unexpected(sqlite_error(db, "step insert"));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto overlay_insert_provisional(OverlayState& state, const std::string& package,
|
||||
const std::string& version_range, const Recipe& r,
|
||||
const std::string& source) -> util::Result<void> {
|
||||
return overlay_insert(state, package, version_range, r, source, /*verified_at=*/0);
|
||||
}
|
||||
|
||||
auto overlay_confirm_provisional(OverlayState& state, const std::string& package,
|
||||
const std::string& version_range,
|
||||
const std::string& source) -> util::Result<void> {
|
||||
constexpr const char* SQL =
|
||||
"UPDATE recipes SET verified_at = ? "
|
||||
"WHERE package = ? AND version_range = ? AND source = ?";
|
||||
|
||||
sqlite3* db = state.handle();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db, SQL, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return std::unexpected(sqlite_error(db, "prepare update"));
|
||||
}
|
||||
auto now = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
sqlite3_bind_int64(stmt, 1, now);
|
||||
sqlite3_bind_text(stmt, 2, package.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 3, version_range.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 4, source.c_str(), -1, SQLITE_TRANSIENT);
|
||||
auto rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
return std::unexpected(sqlite_error(db, "step update"));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto overlay_delete_recipe(OverlayState& state, const std::string& package,
|
||||
const std::string& version_range,
|
||||
const std::string& source) -> util::Result<void> {
|
||||
constexpr const char* SQL =
|
||||
"DELETE FROM recipes WHERE package = ? AND version_range = ? AND source = ?";
|
||||
sqlite3* db = state.handle();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db, SQL, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return std::unexpected(sqlite_error(db, "prepare delete"));
|
||||
}
|
||||
sqlite3_bind_text(stmt, 1, package.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, version_range.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 3, source.c_str(), -1, SQLITE_TRANSIENT);
|
||||
auto rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
return std::unexpected(sqlite_error(db, "step delete"));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto overlay_query(OverlayState& state, const std::string& package)
|
||||
-> util::Result<std::vector<OverlayRow>> {
|
||||
constexpr const char* SQL =
|
||||
@@ -192,4 +284,45 @@ auto Database::add_manual(const std::string& package, const std::string& version
|
||||
return detail::overlay_insert_manual(*overlay_, package, version_range, r);
|
||||
}
|
||||
|
||||
namespace {
|
||||
auto require_overlay(const std::unique_ptr<detail::OverlayState>& o)
|
||||
-> util::Result<void> {
|
||||
if (!o) {
|
||||
return std::unexpected(util::Error{
|
||||
util::ErrorCode::Internal, "no overlay database is open", "",
|
||||
std::nullopt, std::nullopt,
|
||||
});
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
auto Database::insert_provisional(const std::string& package,
|
||||
const std::string& version_range,
|
||||
const Recipe& r, const std::string& source)
|
||||
-> util::Result<void> {
|
||||
if (auto ok = require_overlay(overlay_); !ok) {
|
||||
return std::unexpected(ok.error());
|
||||
}
|
||||
return detail::overlay_insert_provisional(*overlay_, package, version_range, r, source);
|
||||
}
|
||||
|
||||
auto Database::confirm_provisional(const std::string& package,
|
||||
const std::string& version_range,
|
||||
const std::string& source) -> util::Result<void> {
|
||||
if (auto ok = require_overlay(overlay_); !ok) {
|
||||
return std::unexpected(ok.error());
|
||||
}
|
||||
return detail::overlay_confirm_provisional(*overlay_, package, version_range, source);
|
||||
}
|
||||
|
||||
auto Database::abort_provisional(const std::string& package,
|
||||
const std::string& version_range,
|
||||
const std::string& source) -> util::Result<void> {
|
||||
if (auto ok = require_overlay(overlay_); !ok) {
|
||||
return std::unexpected(ok.error());
|
||||
}
|
||||
return detail::overlay_delete_recipe(*overlay_, package, version_range, source);
|
||||
}
|
||||
|
||||
} // namespace cargoxx::linkdb
|
||||
|
||||
@@ -4,6 +4,7 @@ import std;
|
||||
import cargoxx.util;
|
||||
import cargoxx.exec;
|
||||
import cargoxx.linkdb;
|
||||
import cargoxx.manifest;
|
||||
|
||||
export namespace cargoxx::resolver {
|
||||
|
||||
@@ -89,4 +90,29 @@ auto parse_vcpkg_usage(std::string_view usage_text)
|
||||
// ResolutionUnknownPackage; transport errors → ResolutionNetworkError.
|
||||
auto vcpkg_probe(const std::string& name) -> util::Result<VcpkgRecipe>;
|
||||
|
||||
// Caller-supplied closure that runs `cargoxx build` (or any equivalent
|
||||
// build) on a project rooted at the given path. Injected so the resolver
|
||||
// stays decoupled from `cargoxx.cli`.
|
||||
using BuildFn =
|
||||
std::function<util::Result<void>(const std::filesystem::path& project_root)>;
|
||||
|
||||
struct VerifyLinkRequest {
|
||||
linkdb::Recipe candidate; // recipe under test
|
||||
std::string source; // "conan" | "vcpkg" | "nix-probe"
|
||||
std::string package_name;
|
||||
std::string version_spec; // user-supplied spec (e.g. "*", "1.2")
|
||||
std::vector<std::string> components;
|
||||
std::filesystem::path overlay_path; // sqlite file we read/write
|
||||
std::filesystem::path scratch_root; // parent dir for the tmp project
|
||||
};
|
||||
|
||||
// Scaffolds a tiny Cargoxx project under `req.scratch_root`, writes a
|
||||
// provisional overlay row pointing at `req.candidate`, runs `build_fn` on
|
||||
// the project (typically `cli::cmd_build`), and depending on the build
|
||||
// result either confirms the provisional row (verified_at = now) or
|
||||
// deletes it. Cleans the tmp project regardless. Returns success only
|
||||
// when the build itself succeeded.
|
||||
auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
|
||||
-> util::Result<void>;
|
||||
|
||||
} // namespace cargoxx::resolver
|
||||
|
||||
162
src/resolver/verify_link.cpp
Normal file
162
src/resolver/verify_link.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
module cargoxx.resolver;
|
||||
|
||||
import std;
|
||||
import cargoxx.util;
|
||||
import cargoxx.linkdb;
|
||||
import cargoxx.manifest;
|
||||
|
||||
namespace cargoxx::resolver {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
auto io_error(std::string msg, fs::path path) -> util::Error {
|
||||
return util::Error{
|
||||
util::ErrorCode::Internal, std::move(msg), "", std::move(path), std::nullopt,
|
||||
};
|
||||
}
|
||||
|
||||
auto write_text(const fs::path& path, std::string_view content) -> util::Result<void> {
|
||||
std::ofstream out{path};
|
||||
if (!out) {
|
||||
return std::unexpected(
|
||||
io_error(std::format("cannot open for writing: {}", path.string()), path));
|
||||
}
|
||||
out << content;
|
||||
if (!out) {
|
||||
return std::unexpected(
|
||||
io_error(std::format("write failed: {}", path.string()), path));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
class TmpProject {
|
||||
public:
|
||||
explicit TmpProject(fs::path root) : root_(std::move(root)) {}
|
||||
~TmpProject() {
|
||||
std::error_code ec;
|
||||
fs::remove_all(root_, ec);
|
||||
}
|
||||
TmpProject(const TmpProject&) = delete;
|
||||
TmpProject& operator=(const TmpProject&) = delete;
|
||||
TmpProject(TmpProject&&) = delete;
|
||||
TmpProject& operator=(TmpProject&&) = delete;
|
||||
|
||||
[[nodiscard]] auto path() const -> const fs::path& { return root_; }
|
||||
|
||||
private:
|
||||
fs::path root_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
|
||||
-> util::Result<void> {
|
||||
if (req.package_name.empty()) {
|
||||
return std::unexpected(util::Error{
|
||||
util::ErrorCode::Internal,
|
||||
"verify_link: package_name is empty",
|
||||
"", std::nullopt, std::nullopt,
|
||||
});
|
||||
}
|
||||
|
||||
// Insert the provisional overlay row. It persists through the build's
|
||||
// own Database::open() call, which is how the candidate recipe gets
|
||||
// surfaced to cmake_lists codegen via Database::resolve.
|
||||
{
|
||||
auto db = linkdb::Database::open(req.overlay_path);
|
||||
if (!db) {
|
||||
return std::unexpected(db.error());
|
||||
}
|
||||
if (auto r = db->insert_provisional(req.package_name, req.version_spec,
|
||||
req.candidate, req.source);
|
||||
!r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
} // close db before re-opening it inside build_fn
|
||||
|
||||
// Scaffold a tmp project. We bypass cargoxx::cli::cmd_new to avoid a
|
||||
// resolver-on-cli dependency cycle; the manifest + src/main.cpp are
|
||||
// exactly what cmd_build needs for its codegen.
|
||||
std::error_code ec;
|
||||
fs::create_directories(req.scratch_root, ec);
|
||||
if (ec) {
|
||||
return std::unexpected(io_error(
|
||||
std::format("cannot create scratch root: {}", ec.message()),
|
||||
req.scratch_root));
|
||||
}
|
||||
auto proj_root = req.scratch_root /
|
||||
std::format("cargoxx-verify-{}-{}", req.package_name,
|
||||
std::random_device{}());
|
||||
TmpProject scope{proj_root};
|
||||
|
||||
fs::create_directories(proj_root / "src", ec);
|
||||
if (ec) {
|
||||
// Roll back the provisional row before bailing.
|
||||
auto db = linkdb::Database::open(req.overlay_path);
|
||||
if (db) {
|
||||
(void)db->abort_provisional(req.package_name, req.version_spec,
|
||||
req.source);
|
||||
}
|
||||
return std::unexpected(io_error(
|
||||
std::format("cannot create '{}': {}", (proj_root / "src").string(),
|
||||
ec.message()),
|
||||
proj_root));
|
||||
}
|
||||
|
||||
manifest::Manifest m{
|
||||
.package = manifest::Package{
|
||||
.name = req.package_name + "_verify",
|
||||
.version = "0.0.0",
|
||||
.edition = manifest::Edition::Cpp23,
|
||||
.authors = {},
|
||||
.license = std::nullopt,
|
||||
},
|
||||
.dependencies = {manifest::Dependency{
|
||||
.name = req.package_name,
|
||||
.version_spec = req.version_spec,
|
||||
.components = req.components,
|
||||
}},
|
||||
.build = {},
|
||||
};
|
||||
if (auto r = manifest::write(m, proj_root / "Cargoxx.toml"); !r) {
|
||||
auto db = linkdb::Database::open(req.overlay_path);
|
||||
if (db) {
|
||||
(void)db->abort_provisional(req.package_name, req.version_spec,
|
||||
req.source);
|
||||
}
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
|
||||
// Empty main — exercises find_package + target + linker without
|
||||
// requiring per-package symbol knowledge.
|
||||
if (auto r = write_text(proj_root / "src" / "main.cpp", "int main() {}\n"); !r) {
|
||||
auto db = linkdb::Database::open(req.overlay_path);
|
||||
if (db) {
|
||||
(void)db->abort_provisional(req.package_name, req.version_spec,
|
||||
req.source);
|
||||
}
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
|
||||
auto build_result = build_fn(proj_root);
|
||||
|
||||
// Always re-open the db to flip the provisional row's fate.
|
||||
auto db = linkdb::Database::open(req.overlay_path);
|
||||
if (!db) {
|
||||
return std::unexpected(db.error());
|
||||
}
|
||||
if (build_result) {
|
||||
if (auto r = db->confirm_provisional(req.package_name, req.version_spec,
|
||||
req.source);
|
||||
!r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
(void)db->abort_provisional(req.package_name, req.version_spec, req.source);
|
||||
return std::unexpected(build_result.error());
|
||||
}
|
||||
|
||||
} // namespace cargoxx::resolver
|
||||
@@ -32,3 +32,4 @@ cargoxx_add_test(conan_probe_parse)
|
||||
cargoxx_add_test(conan_probe_live)
|
||||
cargoxx_add_test(vcpkg_probe_parse)
|
||||
cargoxx_add_test(vcpkg_probe_live)
|
||||
cargoxx_add_test(verify_link_unit)
|
||||
|
||||
118
tests/verify_link_unit.cpp
Normal file
118
tests/verify_link_unit.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
// Unit tests for resolver::verify_link with a mocked BuildFn that never
|
||||
// invokes nix or cmake. Covers the provisional-row lifecycle and the
|
||||
// scratch-project scaffolding without paying the cost of a real build.
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
import cargoxx.resolver;
|
||||
import cargoxx.linkdb;
|
||||
import cargoxx.manifest;
|
||||
import cargoxx.util;
|
||||
import std;
|
||||
|
||||
using cargoxx::resolver::verify_link;
|
||||
using cargoxx::resolver::VerifyLinkRequest;
|
||||
using cargoxx::linkdb::Database;
|
||||
using cargoxx::linkdb::Recipe;
|
||||
|
||||
namespace {
|
||||
|
||||
auto fresh_dir() -> std::filesystem::path {
|
||||
auto d = std::filesystem::temp_directory_path() /
|
||||
std::format("cargoxx-verify-link-test-{}", std::random_device{}());
|
||||
std::filesystem::create_directories(d);
|
||||
return d;
|
||||
}
|
||||
|
||||
auto sample_recipe() -> Recipe {
|
||||
return Recipe{
|
||||
.nixpkgs_attr = "fmt",
|
||||
.find_package = "fmt CONFIG REQUIRED",
|
||||
.targets = {"fmt::fmt"},
|
||||
.source = "conan",
|
||||
};
|
||||
}
|
||||
|
||||
auto make_request(const std::filesystem::path& parent) -> VerifyLinkRequest {
|
||||
return VerifyLinkRequest{
|
||||
.candidate = sample_recipe(),
|
||||
.source = "conan",
|
||||
.package_name = "fmt",
|
||||
.version_spec = "*",
|
||||
.components = {},
|
||||
.overlay_path = parent / "overlay.sqlite",
|
||||
.scratch_root = parent / "scratch",
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("verify_link confirms the provisional row when the build succeeds",
|
||||
"[resolver][verify_link]") {
|
||||
auto parent = fresh_dir();
|
||||
auto req = make_request(parent);
|
||||
|
||||
bool build_called = false;
|
||||
auto r = verify_link(req, [&](const std::filesystem::path& root) {
|
||||
build_called = true;
|
||||
// Sanity-check the scaffold: the manifest and main.cpp must exist
|
||||
// in the dir handed to the build function.
|
||||
REQUIRE(std::filesystem::exists(root / "Cargoxx.toml"));
|
||||
REQUIRE(std::filesystem::exists(root / "src" / "main.cpp"));
|
||||
auto m = cargoxx::manifest::parse(root / "Cargoxx.toml");
|
||||
REQUIRE(m.has_value());
|
||||
REQUIRE(m->dependencies.size() == 1);
|
||||
REQUIRE(m->dependencies[0].name == "fmt");
|
||||
return cargoxx::util::Result<void>{};
|
||||
});
|
||||
REQUIRE(build_called);
|
||||
REQUIRE(r.has_value());
|
||||
|
||||
// Overlay row should now be confirmed (verified_at != 0). The simplest
|
||||
// way to check is to resolve through the Database — confirmed rows
|
||||
// satisfy `overlay_is_fresh` and surface as the matching recipe.
|
||||
auto db = Database::open(req.overlay_path);
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("fmt", "*", {});
|
||||
REQUIRE(rec.has_value());
|
||||
REQUIRE(rec->source == "conan");
|
||||
REQUIRE(rec->find_package == "fmt CONFIG REQUIRED");
|
||||
}
|
||||
|
||||
TEST_CASE("verify_link rolls the provisional row back when the build fails",
|
||||
"[resolver][verify_link]") {
|
||||
auto parent = fresh_dir();
|
||||
auto req = make_request(parent);
|
||||
|
||||
auto r = verify_link(req, [&](const std::filesystem::path&) {
|
||||
return cargoxx::util::Result<void>{std::unexpected(cargoxx::util::Error{
|
||||
cargoxx::util::ErrorCode::BuildCmakeFailed,
|
||||
"fake build failure",
|
||||
"", std::nullopt, std::nullopt,
|
||||
})};
|
||||
});
|
||||
REQUIRE_FALSE(r.has_value());
|
||||
REQUIRE(r.error().code == cargoxx::util::ErrorCode::BuildCmakeFailed);
|
||||
|
||||
// The conan-source row must be gone; resolve falls through to the
|
||||
// curated linkdb (which has its own fmt recipe with source = "curated").
|
||||
auto db = Database::open(req.overlay_path);
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("fmt", "10.2.0", {});
|
||||
REQUIRE(rec.has_value());
|
||||
REQUIRE(rec->source == "curated");
|
||||
}
|
||||
|
||||
TEST_CASE("verify_link cleans up its scratch project", "[resolver][verify_link]") {
|
||||
auto parent = fresh_dir();
|
||||
auto req = make_request(parent);
|
||||
std::filesystem::path captured;
|
||||
|
||||
(void)verify_link(req, [&](const std::filesystem::path& root) {
|
||||
captured = root;
|
||||
return cargoxx::util::Result<void>{};
|
||||
});
|
||||
|
||||
REQUIRE_FALSE(captured.empty());
|
||||
REQUIRE_FALSE(std::filesystem::exists(captured));
|
||||
}
|
||||
Reference in New Issue
Block a user