#include import cargoxx.lockfile; import cargoxx.util; import std; using cargoxx::lockfile::Lockfile; using cargoxx::lockfile::LockfilePackage; using cargoxx::lockfile::parse; using cargoxx::lockfile::write; using cargoxx::util::ErrorCode; namespace { auto tmp_path() -> std::filesystem::path { auto d = std::filesystem::temp_directory_path() / std::format("cargoxx-lockfile-test-{}", std::random_device{}()); std::filesystem::create_directories(d); return d / "Cargoxx.lock"; } auto root_pkg(std::string name, std::string version, std::vector deps = {}) -> LockfilePackage { return LockfilePackage{ .name = std::move(name), .version = std::move(version), .dependencies = std::move(deps), .nixpkgs_attr = std::nullopt, .nixpkgs_rev = std::nullopt, .linkdb_source = std::nullopt, }; } auto dep_pkg(std::string name, std::string version, std::string attr, std::string rev, std::string source = "curated") -> LockfilePackage { return LockfilePackage{ .name = std::move(name), .version = std::move(version), .dependencies = {}, .nixpkgs_attr = std::move(attr), .nixpkgs_rev = std::move(rev), .linkdb_source = std::move(source), }; } auto round_trip(const Lockfile& l) -> Lockfile { auto path = tmp_path(); REQUIRE(write(l, path).has_value()); auto parsed = parse(path); REQUIRE(parsed.has_value()); return *parsed; } } // namespace TEST_CASE("write round-trips a minimal lockfile", "[lockfile]") { Lockfile l{.version = 1, .packages = {root_pkg("my-project", "0.1.0")}}; REQUIRE(round_trip(l) == l); } TEST_CASE("write round-trips a lockfile with deps", "[lockfile]") { Lockfile l{ .version = 1, .packages = { root_pkg("my-project", "0.1.0", {"fmt 10.2.1", "spdlog 1.13.0"}), dep_pkg("fmt", "10.2.1", "fmt_10", "8a3f...c2d1"), dep_pkg("spdlog", "1.13.0", "spdlog", "8a3f...c2d1"), }, }; REQUIRE(round_trip(l) == l); } TEST_CASE("write round-trips lockfile recipe fields", "[lockfile]") { Lockfile l{ .version = 1, .packages = { LockfilePackage{ .name = "fmt", .version = "10.2.1", .dependencies = {}, .nixpkgs_attr = "fmt_10", .nixpkgs_rev = std::nullopt, .linkdb_source = "conan", .find_package = "fmt CONFIG REQUIRED", .targets = {"fmt::fmt"}, .pkg_config_module = std::nullopt, .brute_force_libs = {}, .brute_force_includes = {}, }, LockfilePackage{ .name = "sqlite", .version = "*", .dependencies = {}, .nixpkgs_attr = "sqlite", .nixpkgs_rev = std::nullopt, .linkdb_source = "pkg-config", .find_package = "PkgConfig REQUIRED", .targets = {"PkgConfig::SQLITE3"}, .pkg_config_module = "sqlite3", .brute_force_libs = {}, .brute_force_includes = {}, }, LockfilePackage{ .name = "obscure", .version = "*", .dependencies = {}, .nixpkgs_attr = "obscure", .nixpkgs_rev = std::nullopt, .linkdb_source = "brute-force", .find_package = "", .targets = {"obscure::obscure"}, .pkg_config_module = std::nullopt, .brute_force_libs = {"/nix/store/abc/lib/libobscure.a"}, .brute_force_includes = {"/nix/store/abc/include"}, }, }, }; REQUIRE(round_trip(l) == l); } TEST_CASE("write round-trips cargoxx-path source fields", "[lockfile]") { Lockfile l{ .version = 1, .packages = { LockfilePackage{ .name = "mylib", .version = "*", .dependencies = {}, .nixpkgs_attr = std::nullopt, .nixpkgs_rev = std::nullopt, .linkdb_source = "cargoxx-path", .find_package = "mylib CONFIG REQUIRED", .targets = {"mylib::mylib"}, .pkg_config_module = std::nullopt, .brute_force_libs = {}, .brute_force_includes = {}, .source_kind = "cargoxx-path", .source_path = "../mylib", }, }, }; REQUIRE(round_trip(l) == l); } TEST_CASE("write round-trips cargoxx-git source fields", "[lockfile]") { Lockfile l{ .version = 1, .packages = { LockfilePackage{ .name = "mylib", .version = "*", .dependencies = {}, .nixpkgs_attr = std::nullopt, .nixpkgs_rev = std::nullopt, .linkdb_source = "cargoxx-git", .find_package = "mylib CONFIG REQUIRED", .targets = {"mylib::mylib"}, .pkg_config_module = std::nullopt, .brute_force_libs = {}, .brute_force_includes = {}, .source_kind = "cargoxx-git", .source_path = std::nullopt, .source_git_url = "https://gitea.example/me/mylib", .source_git_commit = "0123456789012345678901234567890123456789", .source_git_sha256 = "sha256-abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123=", }, }, }; REQUIRE(round_trip(l) == l); } TEST_CASE("Lockfile::nixpkgs_rev returns the shared rev", "[lockfile]") { Lockfile l{ .version = 1, .packages = { root_pkg("p", "0.1.0", {"fmt 10.2.1"}), dep_pkg("fmt", "10.2.1", "fmt_10", "abc123"), }, }; REQUIRE(l.nixpkgs_rev() == "abc123"); } TEST_CASE("write round-trips top-level nixpkgs_rev + flake_utils_rev pins", "[lockfile]") { Lockfile l{ .version = 1, .nixpkgs_rev_pin = "549bd84d6279f9852cae6225e372cc67fb91a4c1", .flake_utils_rev_pin = "11707dc2f618dd54ca8739b309ec4fc024de578b", .packages = {root_pkg("p", "0.1.0")}, }; REQUIRE(round_trip(l) == l); } TEST_CASE("Lockfile::nixpkgs_rev is nullopt when no deps", "[lockfile]") { Lockfile l{.version = 1, .packages = {root_pkg("p", "0.1.0")}}; REQUIRE_FALSE(l.nixpkgs_rev().has_value()); } TEST_CASE("parse rejects a missing file", "[lockfile]") { auto r = parse("/nonexistent/Cargoxx.lock"); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::ManifestNotFound); } TEST_CASE("parse rejects malformed toml", "[lockfile]") { auto path = tmp_path(); std::ofstream{path} << "version = \nname = \"x\"\n"; auto r = parse(path); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::ManifestParseError); } TEST_CASE("parse rejects a package missing name", "[lockfile]") { auto path = tmp_path(); std::ofstream{path} << R"( version = 1 [[package]] version = "0.1.0" )"; auto r = parse(path); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::ManifestInvalidField); } TEST_CASE("parse rejects a package missing version", "[lockfile]") { auto path = tmp_path(); std::ofstream{path} << R"( version = 1 [[package]] name = "p" )"; auto r = parse(path); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::ManifestInvalidField); } TEST_CASE("parse defaults version to 1 when omitted", "[lockfile]") { auto path = tmp_path(); std::ofstream{path} << R"( [[package]] name = "p" version = "0.1.0" )"; auto r = parse(path); REQUIRE(r.has_value()); REQUIRE(r->version == 1); REQUIRE(r->packages.size() == 1); }