[M2] add SQLite overlay + add_manual
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -57,3 +57,16 @@ All notable changes to cargoxx will be documented in this file.
|
||||
are deferred to the M2 follow-up commit.
|
||||
`tests/linkdb_lookup.cpp` covers 13 cases including a smoke test that
|
||||
resolves all 25 curated packages.
|
||||
- SQLite overlay: `Database::open(overlay_path)` now opens (and creates,
|
||||
if missing) a per-user `linkdb.sqlite` cache, applying the schema from
|
||||
`SPEC.md` §9 idempotently. Default path is
|
||||
`$XDG_CACHE_HOME/cargoxx/linkdb.sqlite` (falling back to
|
||||
`$HOME/.cache/cargoxx/...`); tests pass an explicit temp path so they
|
||||
never touch the user cache. `Database::add_manual(pkg, range, recipe)`
|
||||
inserts a row with `source = 'manual'` and `verified_at = now()`;
|
||||
`resolve()` consults the overlay first and falls back to curated when
|
||||
no overlay row matches the requested version range. Manual entries
|
||||
never expire; `nix-probe` (v0.2) entries respect a 30-day freshness
|
||||
window. `tests/linkdb_overlay.cpp` covers 7 cases (insert/persist,
|
||||
override-curated, version-range gating, components rejection,
|
||||
move semantics).
|
||||
|
||||
@@ -29,6 +29,8 @@ if(CARGOXX_WERROR)
|
||||
add_compile_options(-Werror)
|
||||
endif()
|
||||
|
||||
find_package(SQLite3 REQUIRED)
|
||||
|
||||
# ----- cargoxx library: module units + implementation units -----
|
||||
add_library(cargoxx STATIC)
|
||||
target_include_directories(cargoxx SYSTEM PRIVATE third_party)
|
||||
@@ -44,6 +46,7 @@ target_sources(cargoxx
|
||||
src/layout/layout.cpp
|
||||
src/linkdb/recipe.cpp
|
||||
src/linkdb/curated.cpp
|
||||
src/linkdb/overlay.cpp
|
||||
src/cli/cmd_new.cpp
|
||||
src/cli/run.cpp
|
||||
PUBLIC
|
||||
@@ -60,6 +63,8 @@ target_sources(cargoxx
|
||||
src/cli/cli.cppm
|
||||
)
|
||||
|
||||
target_link_libraries(cargoxx PRIVATE SQLite::SQLite3)
|
||||
|
||||
# ----- cargoxx binary -----
|
||||
add_executable(cargoxx_bin src/main.cpp)
|
||||
set_target_properties(cargoxx_bin PROPERTIES OUTPUT_NAME cargoxx)
|
||||
|
||||
@@ -71,22 +71,22 @@ auto load_curated(const std::filesystem::path& path)
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto Database::open() -> util::Result<Database> {
|
||||
Database db;
|
||||
auto curated = load_curated(CARGOXX_LINKDB_DEFAULT_PATH);
|
||||
if (!curated) {
|
||||
return std::unexpected(curated.error());
|
||||
auto default_overlay_path() -> std::filesystem::path {
|
||||
namespace fs = std::filesystem;
|
||||
if (auto* xdg = std::getenv("XDG_CACHE_HOME"); xdg && *xdg) {
|
||||
return fs::path{xdg} / "cargoxx" / "linkdb.sqlite";
|
||||
}
|
||||
db.curated_ = std::move(*curated);
|
||||
return db;
|
||||
if (auto* home = std::getenv("HOME"); home && *home) {
|
||||
return fs::path{home} / ".cache" / "cargoxx" / "linkdb.sqlite";
|
||||
}
|
||||
return fs::current_path() / ".cargoxx-linkdb.sqlite";
|
||||
}
|
||||
|
||||
auto Database::resolve(const std::string& package, const std::string& version,
|
||||
const std::vector<std::string>& components) -> util::Result<Recipe> {
|
||||
auto it = curated_.find(package);
|
||||
if (it == curated_.end() || it->second.empty()) {
|
||||
auto resolve_curated(const std::map<std::string, std::vector<detail::CuratedRecipe>>& curated,
|
||||
const std::string& package, const std::string& version,
|
||||
const std::vector<std::string>& components) -> util::Result<Recipe> {
|
||||
auto it = curated.find(package);
|
||||
if (it == curated.end() || it->second.empty()) {
|
||||
return std::unexpected(util::Error{
|
||||
util::ErrorCode::LinkdbUnknownPackage,
|
||||
std::format("package '{}' has no known CMake link recipe", package),
|
||||
@@ -126,22 +126,69 @@ auto Database::resolve(const std::string& package, const std::string& version,
|
||||
});
|
||||
}
|
||||
|
||||
Recipe out{
|
||||
return Recipe{
|
||||
.nixpkgs_attr = match->nixpkgs_attr,
|
||||
.find_package = substitute_components(match->find_package, components),
|
||||
.targets = expand_targets(match->targets, components),
|
||||
.source = "curated",
|
||||
};
|
||||
return out;
|
||||
}
|
||||
|
||||
auto Database::add_manual(const std::string&, const std::string&, const Recipe&)
|
||||
-> util::Result<void> {
|
||||
return std::unexpected(util::Error{
|
||||
util::ErrorCode::NotImplemented,
|
||||
"manual link recipes are not implemented in this milestone",
|
||||
"", std::nullopt, std::nullopt,
|
||||
});
|
||||
} // namespace
|
||||
|
||||
auto Database::open(std::optional<std::filesystem::path> overlay_path) -> util::Result<Database> {
|
||||
Database db;
|
||||
|
||||
auto curated = load_curated(CARGOXX_LINKDB_DEFAULT_PATH);
|
||||
if (!curated) {
|
||||
return std::unexpected(curated.error());
|
||||
}
|
||||
db.curated_ = std::move(*curated);
|
||||
|
||||
auto path = overlay_path.value_or(default_overlay_path());
|
||||
auto handle = detail::overlay_open(path);
|
||||
if (!handle) {
|
||||
return std::unexpected(handle.error());
|
||||
}
|
||||
db.overlay_ = std::move(*handle);
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
auto Database::resolve(const std::string& package, const std::string& version,
|
||||
const std::vector<std::string>& components) -> util::Result<Recipe> {
|
||||
if (overlay_) {
|
||||
auto rows = detail::overlay_query(*overlay_, package);
|
||||
if (!rows) {
|
||||
return std::unexpected(rows.error());
|
||||
}
|
||||
const auto now = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
for (const auto& row : *rows) {
|
||||
if (!detail::overlay_is_fresh(row, now)) {
|
||||
continue;
|
||||
}
|
||||
if (!util::satisfies(version, row.version_range)) {
|
||||
continue;
|
||||
}
|
||||
if (!components.empty()) {
|
||||
return std::unexpected(util::Error{
|
||||
util::ErrorCode::LinkdbComponentNotSupported,
|
||||
std::format("overlay recipe for '{}' does not support components", package),
|
||||
"", std::nullopt, std::nullopt,
|
||||
});
|
||||
}
|
||||
return Recipe{
|
||||
.nixpkgs_attr = row.nixpkgs_attr,
|
||||
.find_package = row.find_package,
|
||||
.targets = row.targets,
|
||||
.source = row.source,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return resolve_curated(curated_, package, version, components);
|
||||
}
|
||||
|
||||
} // namespace cargoxx::linkdb
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
module;
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
export module cargoxx.linkdb;
|
||||
|
||||
import std;
|
||||
@@ -26,13 +30,60 @@ struct CuratedRecipe {
|
||||
bool components_supported = false;
|
||||
};
|
||||
|
||||
struct OverlayRow {
|
||||
std::string version_range;
|
||||
std::string nixpkgs_attr;
|
||||
std::string find_package;
|
||||
std::vector<std::string> targets;
|
||||
std::string source;
|
||||
std::int64_t verified_at = 0;
|
||||
};
|
||||
|
||||
// RAII wrapper for an open sqlite3 connection used by the overlay database.
|
||||
// Move-only would be redundant — the holder (Database::overlay_) is a
|
||||
// std::unique_ptr<OverlayState>, so we delete copy and move outright.
|
||||
class OverlayState {
|
||||
public:
|
||||
OverlayState() = default;
|
||||
explicit OverlayState(sqlite3* handle) noexcept : db_(handle) {}
|
||||
~OverlayState() {
|
||||
if (db_) {
|
||||
sqlite3_close(db_);
|
||||
}
|
||||
}
|
||||
|
||||
OverlayState(const OverlayState&) = delete;
|
||||
OverlayState& operator=(const OverlayState&) = delete;
|
||||
OverlayState(OverlayState&&) = delete;
|
||||
OverlayState& operator=(OverlayState&&) = delete;
|
||||
|
||||
[[nodiscard]] auto handle() const noexcept -> sqlite3* { return db_; }
|
||||
|
||||
private:
|
||||
sqlite3* db_ = nullptr;
|
||||
};
|
||||
|
||||
auto overlay_open(const std::filesystem::path& path)
|
||||
-> cargoxx::util::Result<std::unique_ptr<OverlayState>>;
|
||||
|
||||
auto overlay_insert_manual(OverlayState& state, const std::string& package,
|
||||
const std::string& version_range,
|
||||
const cargoxx::linkdb::Recipe& r)
|
||||
-> cargoxx::util::Result<void>;
|
||||
|
||||
auto overlay_query(OverlayState& state, const std::string& package)
|
||||
-> cargoxx::util::Result<std::vector<OverlayRow>>;
|
||||
|
||||
auto overlay_is_fresh(const OverlayRow& row, std::int64_t now_epoch_seconds) -> bool;
|
||||
|
||||
} // namespace cargoxx::linkdb::detail
|
||||
|
||||
export namespace cargoxx::linkdb {
|
||||
|
||||
class Database {
|
||||
public:
|
||||
static auto open() -> util::Result<Database>;
|
||||
static auto open(std::optional<std::filesystem::path> overlay_path = std::nullopt)
|
||||
-> util::Result<Database>;
|
||||
|
||||
auto resolve(const std::string& package, const std::string& version,
|
||||
const std::vector<std::string>& components = {})
|
||||
@@ -44,6 +95,7 @@ class Database {
|
||||
private:
|
||||
Database() = default;
|
||||
std::map<std::string, std::vector<detail::CuratedRecipe>> curated_;
|
||||
std::unique_ptr<detail::OverlayState> overlay_;
|
||||
};
|
||||
|
||||
// Pure helpers exported for unit testing.
|
||||
|
||||
195
src/linkdb/overlay.cpp
Normal file
195
src/linkdb/overlay.cpp
Normal file
@@ -0,0 +1,195 @@
|
||||
module;
|
||||
|
||||
#include <json.hpp>
|
||||
#include <sqlite3.h>
|
||||
|
||||
module cargoxx.linkdb;
|
||||
|
||||
import std;
|
||||
import cargoxx.util;
|
||||
|
||||
namespace cargoxx::linkdb {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* SCHEMA_SQL = R"(
|
||||
CREATE TABLE IF NOT EXISTS recipes (
|
||||
package TEXT NOT NULL,
|
||||
version_range TEXT NOT NULL,
|
||||
nixpkgs_attr TEXT NOT NULL,
|
||||
find_package TEXT NOT NULL,
|
||||
targets TEXT NOT NULL,
|
||||
components TEXT,
|
||||
source TEXT NOT NULL,
|
||||
verified_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (package, version_range, source)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS resolution_failures (
|
||||
package TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
last_attempt INTEGER NOT NULL,
|
||||
error TEXT NOT NULL,
|
||||
PRIMARY KEY (package, version)
|
||||
);
|
||||
)";
|
||||
|
||||
constexpr std::int64_t THIRTY_DAYS_SECONDS = 30LL * 24 * 60 * 60;
|
||||
|
||||
auto sqlite_error(sqlite3* db, std::string_view ctx) -> util::Error {
|
||||
return util::Error{
|
||||
util::ErrorCode::LinkdbCorrupt,
|
||||
std::format("{}: {}", ctx, sqlite3_errmsg(db)),
|
||||
"", std::nullopt, std::nullopt,
|
||||
};
|
||||
}
|
||||
|
||||
auto column_text(sqlite3_stmt* stmt, int idx) -> std::string {
|
||||
const auto* p = sqlite3_column_text(stmt, idx);
|
||||
if (!p) {
|
||||
return {};
|
||||
}
|
||||
return std::string{reinterpret_cast<const char*>(p)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace detail {
|
||||
|
||||
auto overlay_open(const std::filesystem::path& path)
|
||||
-> util::Result<std::unique_ptr<OverlayState>> {
|
||||
std::error_code ec;
|
||||
if (path.has_parent_path()) {
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
// ec is non-fatal; sqlite3_open will report a clearer error if it matters
|
||||
}
|
||||
|
||||
sqlite3* db = nullptr;
|
||||
auto rc = sqlite3_open(path.string().c_str(), &db);
|
||||
if (rc != SQLITE_OK) {
|
||||
std::string msg = std::format("cannot open overlay '{}': {}", path.string(),
|
||||
db ? sqlite3_errmsg(db) : sqlite3_errstr(rc));
|
||||
if (db) {
|
||||
sqlite3_close(db);
|
||||
}
|
||||
return std::unexpected(util::Error{
|
||||
util::ErrorCode::LinkdbCorrupt, std::move(msg), "", path, std::nullopt,
|
||||
});
|
||||
}
|
||||
|
||||
auto state = std::make_unique<OverlayState>(db);
|
||||
|
||||
char* errmsg = nullptr;
|
||||
rc = sqlite3_exec(state->handle(), SCHEMA_SQL, nullptr, nullptr, &errmsg);
|
||||
if (rc != SQLITE_OK) {
|
||||
std::string msg =
|
||||
std::format("cannot create overlay schema: {}", errmsg ? errmsg : "?");
|
||||
sqlite3_free(errmsg);
|
||||
return std::unexpected(util::Error{
|
||||
util::ErrorCode::LinkdbCorrupt, std::move(msg), "", path, std::nullopt,
|
||||
});
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
auto overlay_insert_manual(OverlayState& state, const std::string& package,
|
||||
const std::string& version_range, const Recipe& r)
|
||||
-> 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, 'manual', ?)";
|
||||
|
||||
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();
|
||||
auto now = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
|
||||
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_int64(stmt, 6, now);
|
||||
|
||||
auto rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
return std::unexpected(sqlite_error(db, "step insert"));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto overlay_query(OverlayState& state, const std::string& package)
|
||||
-> util::Result<std::vector<OverlayRow>> {
|
||||
constexpr const char* SQL =
|
||||
"SELECT version_range, nixpkgs_attr, find_package, targets, source, verified_at "
|
||||
"FROM recipes WHERE package = ?";
|
||||
|
||||
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 select"));
|
||||
}
|
||||
sqlite3_bind_text(stmt, 1, package.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
std::vector<OverlayRow> out;
|
||||
int rc;
|
||||
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
|
||||
OverlayRow row;
|
||||
row.version_range = column_text(stmt, 0);
|
||||
row.nixpkgs_attr = column_text(stmt, 1);
|
||||
row.find_package = column_text(stmt, 2);
|
||||
auto targets_text = column_text(stmt, 3);
|
||||
try {
|
||||
row.targets =
|
||||
nlohmann::json::parse(targets_text).get<std::vector<std::string>>();
|
||||
} catch (const nlohmann::json::exception&) {
|
||||
sqlite3_finalize(stmt);
|
||||
return std::unexpected(util::Error{
|
||||
util::ErrorCode::LinkdbCorrupt,
|
||||
std::format("overlay row for '{}' has malformed targets JSON", package),
|
||||
"", std::nullopt, std::nullopt,
|
||||
});
|
||||
}
|
||||
row.source = column_text(stmt, 4);
|
||||
row.verified_at = sqlite3_column_int64(stmt, 5);
|
||||
out.push_back(std::move(row));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
return std::unexpected(sqlite_error(db, "step select"));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
auto overlay_is_fresh(const OverlayRow& row, std::int64_t now) -> bool {
|
||||
if (row.source == "manual" || row.source == "curated") {
|
||||
return true;
|
||||
}
|
||||
return (now - row.verified_at) < THIRTY_DAYS_SECONDS;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
auto Database::add_manual(const std::string& package, const std::string& version_range,
|
||||
const Recipe& r) -> util::Result<void> {
|
||||
if (!overlay_) {
|
||||
return std::unexpected(util::Error{
|
||||
util::ErrorCode::Internal,
|
||||
"no overlay database is open",
|
||||
"", std::nullopt, std::nullopt,
|
||||
});
|
||||
}
|
||||
return detail::overlay_insert_manual(*overlay_, package, version_range, r);
|
||||
}
|
||||
|
||||
} // namespace cargoxx::linkdb
|
||||
@@ -13,4 +13,5 @@ cargoxx_add_test(manifest_parse)
|
||||
cargoxx_add_test(manifest_write)
|
||||
cargoxx_add_test(layout_discovery)
|
||||
cargoxx_add_test(linkdb_lookup)
|
||||
cargoxx_add_test(linkdb_overlay)
|
||||
cargoxx_add_test(cmd_new)
|
||||
|
||||
@@ -10,15 +10,31 @@ using cargoxx::linkdb::expand_targets;
|
||||
using cargoxx::linkdb::substitute_components;
|
||||
using cargoxx::util::ErrorCode;
|
||||
|
||||
TEST_CASE("Database::open loads the curated linkdb", "[linkdb]") {
|
||||
auto r = Database::open();
|
||||
namespace {
|
||||
|
||||
auto fresh_overlay() -> std::filesystem::path {
|
||||
auto d = std::filesystem::temp_directory_path() /
|
||||
std::format("cargoxx-linkdb-test-{}", std::random_device{}());
|
||||
std::filesystem::create_directories(d);
|
||||
return d / "overlay.sqlite";
|
||||
}
|
||||
|
||||
auto open_db() -> Database {
|
||||
auto r = Database::open(fresh_overlay());
|
||||
REQUIRE(r.has_value());
|
||||
return std::move(*r);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Database::open loads the curated linkdb", "[linkdb]") {
|
||||
auto db = open_db();
|
||||
(void)db;
|
||||
}
|
||||
|
||||
TEST_CASE("resolve returns the curated recipe for fmt 10", "[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("fmt", "10.2.0");
|
||||
auto db = open_db();
|
||||
auto rec = db.resolve("fmt", "10.2.0");
|
||||
REQUIRE(rec.has_value());
|
||||
REQUIRE(rec->nixpkgs_attr == "fmt_10");
|
||||
REQUIRE(rec->find_package == "fmt CONFIG REQUIRED");
|
||||
@@ -27,25 +43,22 @@ TEST_CASE("resolve returns the curated recipe for fmt 10", "[linkdb]") {
|
||||
}
|
||||
|
||||
TEST_CASE("resolve returns the older fmt recipe for fmt 8", "[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("fmt", "8.1.0");
|
||||
auto db = open_db();
|
||||
auto rec = db.resolve("fmt", "8.1.0");
|
||||
REQUIRE(rec.has_value());
|
||||
REQUIRE(rec->nixpkgs_attr == "fmt_8");
|
||||
}
|
||||
|
||||
TEST_CASE("resolve fails for an unknown package", "[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("obscurelib", "0.0.1");
|
||||
auto db = open_db();
|
||||
auto rec = db.resolve("obscurelib", "0.0.1");
|
||||
REQUIRE_FALSE(rec.has_value());
|
||||
REQUIRE(rec.error().code == ErrorCode::LinkdbUnknownPackage);
|
||||
}
|
||||
|
||||
TEST_CASE("resolve substitutes boost components", "[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("boost", "1.84.0", {"filesystem", "system"});
|
||||
auto db = open_db();
|
||||
auto rec = db.resolve("boost", "1.84.0", {"filesystem", "system"});
|
||||
REQUIRE(rec.has_value());
|
||||
REQUIRE(rec->find_package == "Boost REQUIRED COMPONENTS filesystem system");
|
||||
REQUIRE(rec->targets ==
|
||||
@@ -54,26 +67,23 @@ TEST_CASE("resolve substitutes boost components", "[linkdb]") {
|
||||
|
||||
TEST_CASE("resolve fails when a componentized package gets no components",
|
||||
"[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("boost", "1.84.0");
|
||||
auto db = open_db();
|
||||
auto rec = db.resolve("boost", "1.84.0");
|
||||
REQUIRE_FALSE(rec.has_value());
|
||||
REQUIRE(rec.error().code == ErrorCode::LinkdbComponentNotSupported);
|
||||
}
|
||||
|
||||
TEST_CASE("resolve fails when components are passed to a non-componentized package",
|
||||
"[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("fmt", "10.2.0", {"core"});
|
||||
auto db = open_db();
|
||||
auto rec = db.resolve("fmt", "10.2.0", {"core"});
|
||||
REQUIRE_FALSE(rec.has_value());
|
||||
REQUIRE(rec.error().code == ErrorCode::LinkdbComponentNotSupported);
|
||||
}
|
||||
|
||||
TEST_CASE("resolve handles wildcard versions", "[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("openssl", "3.2.0");
|
||||
auto db = open_db();
|
||||
auto rec = db.resolve("openssl", "3.2.0");
|
||||
REQUIRE(rec.has_value());
|
||||
REQUIRE(rec->find_package == "OpenSSL REQUIRED");
|
||||
REQUIRE(rec->targets ==
|
||||
@@ -81,8 +91,7 @@ TEST_CASE("resolve handles wildcard versions", "[linkdb]") {
|
||||
}
|
||||
|
||||
TEST_CASE("resolve covers all 25 curated packages", "[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
auto db = open_db();
|
||||
|
||||
struct Sample {
|
||||
std::string name;
|
||||
@@ -118,7 +127,7 @@ TEST_CASE("resolve covers all 25 curated packages", "[linkdb]") {
|
||||
};
|
||||
|
||||
for (const auto& s : samples) {
|
||||
auto rec = db->resolve(s.name, s.version, s.components);
|
||||
auto rec = db.resolve(s.name, s.version, s.components);
|
||||
INFO("resolving " << s.name);
|
||||
REQUIRE(rec.has_value());
|
||||
REQUIRE_FALSE(rec->nixpkgs_attr.empty());
|
||||
|
||||
153
tests/linkdb_overlay.cpp
Normal file
153
tests/linkdb_overlay.cpp
Normal file
@@ -0,0 +1,153 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
import cargoxx.linkdb;
|
||||
import cargoxx.util;
|
||||
import std;
|
||||
|
||||
using cargoxx::linkdb::Database;
|
||||
using cargoxx::linkdb::Recipe;
|
||||
using cargoxx::util::ErrorCode;
|
||||
|
||||
namespace {
|
||||
|
||||
auto fresh_overlay() -> std::filesystem::path {
|
||||
auto d = std::filesystem::temp_directory_path() /
|
||||
std::format("cargoxx-overlay-test-{}", std::random_device{}());
|
||||
std::filesystem::create_directories(d);
|
||||
return d / "overlay.sqlite";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("open creates the overlay schema and file", "[linkdb][overlay]") {
|
||||
auto path = fresh_overlay();
|
||||
auto db = Database::open(path);
|
||||
REQUIRE(db.has_value());
|
||||
REQUIRE(std::filesystem::exists(path));
|
||||
}
|
||||
|
||||
TEST_CASE("add_manual then resolve returns the manual recipe",
|
||||
"[linkdb][overlay]") {
|
||||
auto db = Database::open(fresh_overlay());
|
||||
REQUIRE(db.has_value());
|
||||
|
||||
Recipe r{
|
||||
.nixpkgs_attr = "obscurelib",
|
||||
.find_package = "obscurelib CONFIG REQUIRED",
|
||||
.targets = {"obscurelib::obscurelib"},
|
||||
.source = "manual",
|
||||
};
|
||||
REQUIRE(db->add_manual("obscurelib", "*", r).has_value());
|
||||
|
||||
auto got = db->resolve("obscurelib", "1.0.0");
|
||||
REQUIRE(got.has_value());
|
||||
REQUIRE(got->nixpkgs_attr == "obscurelib");
|
||||
REQUIRE(got->find_package == "obscurelib CONFIG REQUIRED");
|
||||
REQUIRE(got->targets == std::vector<std::string>{"obscurelib::obscurelib"});
|
||||
REQUIRE(got->source == "manual");
|
||||
}
|
||||
|
||||
TEST_CASE("manual entry overrides curated for the same package",
|
||||
"[linkdb][overlay]") {
|
||||
auto db = Database::open(fresh_overlay());
|
||||
REQUIRE(db.has_value());
|
||||
|
||||
Recipe override_r{
|
||||
.nixpkgs_attr = "fmt_pinned",
|
||||
.find_package = "fmt CONFIG REQUIRED",
|
||||
.targets = {"fmt::fmt"},
|
||||
.source = "manual",
|
||||
};
|
||||
REQUIRE(db->add_manual("fmt", ">=10.0.0", override_r).has_value());
|
||||
|
||||
auto got = db->resolve("fmt", "10.2.0");
|
||||
REQUIRE(got.has_value());
|
||||
REQUIRE(got->nixpkgs_attr == "fmt_pinned");
|
||||
REQUIRE(got->source == "manual");
|
||||
}
|
||||
|
||||
TEST_CASE("manual entry is constrained by version_range",
|
||||
"[linkdb][overlay]") {
|
||||
auto db = Database::open(fresh_overlay());
|
||||
REQUIRE(db.has_value());
|
||||
|
||||
Recipe r{
|
||||
.nixpkgs_attr = "fmt_v11_only",
|
||||
.find_package = "fmt CONFIG REQUIRED",
|
||||
.targets = {"fmt::fmt"},
|
||||
.source = "manual",
|
||||
};
|
||||
REQUIRE(db->add_manual("fmt", ">=11.0.0", r).has_value());
|
||||
|
||||
// 10.x falls outside the manual range and falls through to curated
|
||||
auto curated = db->resolve("fmt", "10.2.0");
|
||||
REQUIRE(curated.has_value());
|
||||
REQUIRE(curated->source == "curated");
|
||||
REQUIRE(curated->nixpkgs_attr == "fmt_10");
|
||||
|
||||
// 11.x matches the manual range
|
||||
auto manual = db->resolve("fmt", "11.0.0");
|
||||
REQUIRE(manual.has_value());
|
||||
REQUIRE(manual->source == "manual");
|
||||
REQUIRE(manual->nixpkgs_attr == "fmt_v11_only");
|
||||
}
|
||||
|
||||
TEST_CASE("manual recipes persist across reopen", "[linkdb][overlay]") {
|
||||
auto path = fresh_overlay();
|
||||
|
||||
{
|
||||
auto db = Database::open(path);
|
||||
REQUIRE(db.has_value());
|
||||
Recipe r{
|
||||
.nixpkgs_attr = "persistlib",
|
||||
.find_package = "persistlib CONFIG REQUIRED",
|
||||
.targets = {"persistlib::persistlib"},
|
||||
.source = "manual",
|
||||
};
|
||||
REQUIRE(db->add_manual("persistlib", "*", r).has_value());
|
||||
}
|
||||
|
||||
auto db = Database::open(path);
|
||||
REQUIRE(db.has_value());
|
||||
auto got = db->resolve("persistlib", "0.0.1");
|
||||
REQUIRE(got.has_value());
|
||||
REQUIRE(got->nixpkgs_attr == "persistlib");
|
||||
REQUIRE(got->source == "manual");
|
||||
}
|
||||
|
||||
TEST_CASE("resolve with components on a manual recipe is rejected",
|
||||
"[linkdb][overlay]") {
|
||||
auto db = Database::open(fresh_overlay());
|
||||
REQUIRE(db.has_value());
|
||||
|
||||
Recipe r{
|
||||
.nixpkgs_attr = "weirdlib",
|
||||
.find_package = "weirdlib CONFIG REQUIRED",
|
||||
.targets = {"weirdlib::weirdlib"},
|
||||
.source = "manual",
|
||||
};
|
||||
REQUIRE(db->add_manual("weirdlib", "*", r).has_value());
|
||||
|
||||
auto got = db->resolve("weirdlib", "1.0.0", {"some_component"});
|
||||
REQUIRE_FALSE(got.has_value());
|
||||
REQUIRE(got.error().code == ErrorCode::LinkdbComponentNotSupported);
|
||||
}
|
||||
|
||||
TEST_CASE("Database is move-constructible without leaking the handle",
|
||||
"[linkdb][overlay]") {
|
||||
auto db = Database::open(fresh_overlay());
|
||||
REQUIRE(db.has_value());
|
||||
Database moved = std::move(*db);
|
||||
|
||||
Recipe r{
|
||||
.nixpkgs_attr = "movelib",
|
||||
.find_package = "movelib CONFIG REQUIRED",
|
||||
.targets = {"movelib::movelib"},
|
||||
.source = "manual",
|
||||
};
|
||||
REQUIRE(moved.add_manual("movelib", "*", r).has_value());
|
||||
|
||||
auto got = moved.resolve("movelib", "1.0.0");
|
||||
REQUIRE(got.has_value());
|
||||
REQUIRE(got->source == "manual");
|
||||
}
|
||||
Reference in New Issue
Block a user