Matchspec hardening (#3907)

Co-authored-by: Julien Jerphanion <git@jjerphan.xyz>
This commit is contained in:
Antoine Prouvost 2025-05-05 21:02:58 +02:00 committed by GitHub
parent 6ad6a6afc7
commit 542241ff3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 505 additions and 194 deletions

View File

@ -17,6 +17,7 @@
#include "mamba/core/tasksync.hpp"
#include "mamba/download/mirror_map.hpp"
#include "mamba/fs/filesystem.hpp"
#include "mamba/solver/libsolv/parameters.hpp"
#include "mamba/solver/request.hpp"
#include "mamba/specs/authentication_info.hpp"
#include "mamba/specs/platform.hpp"
@ -146,6 +147,7 @@ namespace mamba
// Configurable
bool experimental = false;
bool experimental_repodata_parsing = true;
bool experimental_matchspec_parsing = false;
bool debug = false;
// TODO check writable and add other potential dirs

View File

@ -53,9 +53,18 @@ namespace mamba::solver::libsolv
{
public:
/**
* Global Database settings.
*/
struct Settings
{
MatchSpecParser matchspec_parser = MatchSpecParser::Libsolv;
};
using logger_type = std::function<void(LogLevel, std::string_view)>;
explicit Database(specs::ChannelResolveParams channel_params);
Database(specs::ChannelResolveParams channel_params, Settings settings);
Database(const Database&) = delete;
Database(Database&&);
@ -66,6 +75,8 @@ namespace mamba::solver::libsolv
[[nodiscard]] auto channel_params() const -> const specs::ChannelResolveParams&;
[[nodiscard]] auto settings() const -> const Settings&;
void set_logger(logger_type callback);
auto add_repo_from_repodata_json(
@ -75,7 +86,7 @@ namespace mamba::solver::libsolv
PipAsPythonDependency add = PipAsPythonDependency::No,
PackageTypes package_types = PackageTypes::CondaOrElseTarBz2,
VerifyPackages verify_packages = VerifyPackages::No,
RepodataParser parser = RepodataParser::Mamba
RepodataParser repo_parser = RepodataParser::Mamba
) -> expected_t<RepoInfo>;
auto add_repo_from_native_serialization(

View File

@ -8,6 +8,7 @@
#define MAMBA_SOLVER_LIBSOLV_SOLVER_HPP
#include "mamba/core/error_handling.hpp"
#include "mamba/solver/libsolv/parameters.hpp"
#include "mamba/solver/libsolv/unsolvable.hpp"
#include "mamba/solver/request.hpp"
#include "mamba/solver/solution.hpp"
@ -22,12 +23,18 @@ namespace mamba::solver::libsolv
using Outcome = std::variant<Solution, UnSolvable>;
[[nodiscard]] auto solve(Database& database, Request&& request) -> expected_t<Outcome>;
[[nodiscard]] auto solve(Database& database, const Request& request) -> expected_t<Outcome>;
[[nodiscard]] auto
solve(Database& database, Request&& request, MatchSpecParser ms_parser = MatchSpecParser::Mixed)
-> expected_t<Outcome>;
[[nodiscard]] auto
solve(Database& database, const Request& request, MatchSpecParser ms_parser = MatchSpecParser::Mixed)
-> expected_t<Outcome>;
private:
auto solve_impl(Database& database, const Request& request) -> expected_t<Outcome>;
auto solve_impl(Database& database, const Request& request, MatchSpecParser ms_parser)
-> expected_t<Outcome>;
};
}
#endif

View File

@ -1423,6 +1423,14 @@ namespace mamba
.set_env_var_names()
.set_post_merge_hook(detail::not_supported_option_hook));
insert(Configurable("experimental_matchspec_parsing", &m_context.experimental_matchspec_parsing)
.group("Basic")
.description( //
"Enable internal parsing and matching of MatchSpecs using Mamba's experimental implementation rather than Libsolv's.\n"
"This is not mean for production"
)
.set_env_var_names());
insert(Configurable("debug", &m_context.debug)
.group("Basic")
.set_env_var_names()

View File

@ -465,7 +465,13 @@ namespace mamba
LOG_WARNING << "No 'channels' specified";
}
solver::libsolv::Database db{ channel_context.params() };
solver::libsolv::Database db{
channel_context.params(),
{
ctx.experimental_matchspec_parsing ? solver::libsolv::MatchSpecParser::Mamba
: solver::libsolv::MatchSpecParser::Libsolv,
},
};
add_spdlog_logger_to_database(db);
auto exp_load = load_channels(ctx, channel_context, db, package_caches);
@ -494,7 +500,15 @@ namespace mamba
// Console stream prints on destruction
}
auto outcome = solver::libsolv::Solver().solve(db, request).value();
auto outcome = solver::libsolv::Solver()
.solve(
db,
request,
ctx.experimental_matchspec_parsing
? solver::libsolv::MatchSpecParser::Mamba
: solver::libsolv::MatchSpecParser::Mixed
)
.value();
if (auto* unsolvable = std::get_if<solver::libsolv::UnSolvable>(&outcome))
{
@ -643,7 +657,13 @@ namespace mamba
bool remove_prefix_on_failure
)
{
solver::libsolv::Database database{ channel_context.params() };
solver::libsolv::Database database{
channel_context.params(),
{
ctx.experimental_matchspec_parsing ? solver::libsolv::MatchSpecParser::Mamba
: solver::libsolv::MatchSpecParser::Libsolv,
},
};
add_spdlog_logger_to_database(database);
init_channels(ctx, channel_context);

View File

@ -136,7 +136,13 @@ namespace mamba
}
PrefixData& prefix_data = exp_prefix_data.value();
solver::libsolv::Database database{ channel_context.params() };
solver::libsolv::Database database{
channel_context.params(),
{
ctx.experimental_matchspec_parsing ? solver::libsolv::MatchSpecParser::Mamba
: solver::libsolv::MatchSpecParser::Libsolv,
},
};
add_spdlog_logger_to_database(database);
load_installed_packages_in_database(ctx, database, prefix_data);
@ -190,7 +196,15 @@ namespace mamba
/* .strict_repo_priority= */ ctx.channel_priority == ChannelPriority::Strict,
};
auto outcome = solver::libsolv::Solver().solve(database, request).value();
auto outcome = solver::libsolv::Solver()
.solve(
database,
request,
ctx.experimental_matchspec_parsing
? solver::libsolv::MatchSpecParser::Mamba
: solver::libsolv::MatchSpecParser::Mixed
)
.value();
if (auto* unsolvable = std::get_if<solver::libsolv::UnSolvable>(&outcome))
{
if (ctx.output_params.json)

View File

@ -34,7 +34,13 @@ namespace mamba
config.load();
auto channel_context = ChannelContext::make_conda_compatible(ctx);
solver::libsolv::Database db{ channel_context.params() };
solver::libsolv::Database db{
channel_context.params(),
{
ctx.experimental_matchspec_parsing ? solver::libsolv::MatchSpecParser::Mamba
: solver::libsolv::MatchSpecParser::Libsolv,
},
};
add_spdlog_logger_to_database(db);
// bool installed = (type == QueryType::kDepends) || (type == QueryType::kWhoneeds);

View File

@ -152,7 +152,13 @@ namespace mamba
populate_context_channels_from_specs(raw_update_specs, ctx);
solver::libsolv::Database db{ channel_context.params() };
solver::libsolv::Database db{
channel_context.params(),
{
ctx.experimental_matchspec_parsing ? solver::libsolv::MatchSpecParser::Mamba
: solver::libsolv::MatchSpecParser::Libsolv,
},
};
add_spdlog_logger_to_database(db);
MultiPackageCache package_caches(ctx.pkgs_dirs, ctx.validation_params);
@ -191,7 +197,15 @@ namespace mamba
// Console stream prints on destruction
}
auto outcome = solver::libsolv::Solver().solve(db, request).value();
auto outcome = solver::libsolv::Solver()
.solve(
db,
request,
ctx.experimental_matchspec_parsing
? solver::libsolv::MatchSpecParser::Mamba
: solver::libsolv::MatchSpecParser::Mixed
)
.value();
if (auto* unsolvable = std::get_if<solver::libsolv::UnSolvable>(&outcome))
{
unsolvable->explain_problems_to(

View File

@ -6,7 +6,6 @@
#include <exception>
#include <iostream>
#include <limits>
#include <string_view>
#include <fmt/format.h>
@ -30,17 +29,24 @@ namespace mamba::solver::libsolv
{
struct Database::DatabaseImpl
{
explicit DatabaseImpl(specs::ChannelResolveParams p_channel_params)
: matcher(std::move(p_channel_params))
explicit DatabaseImpl(specs::ChannelResolveParams p_channel_params, Settings settings_)
: settings(std::move(settings_))
, matcher(std::move(p_channel_params))
{
}
Settings settings;
solv::ObjPool pool = {};
Matcher matcher;
};
Database::Database(specs::ChannelResolveParams channel_params)
: m_data(std::make_unique<DatabaseImpl>(std::move(channel_params)))
: Database(channel_params, Settings{})
{
}
Database::Database(specs::ChannelResolveParams channel_params, Settings settings)
: m_data(std::make_unique<DatabaseImpl>(std::move(channel_params), std::move(settings)))
{
pool().set_disttype(DISTTYPE_CONDA);
// Ensure that debug logging never goes to stdout as to not interfere json output
@ -87,6 +93,11 @@ namespace mamba::solver::libsolv
return m_data->matcher.channel_params();
}
auto Database::settings() const -> const Settings&
{
return m_data->settings;
}
namespace
{
auto libsolv_to_log_level(int type) -> LogLevel
@ -144,7 +155,7 @@ namespace mamba::solver::libsolv
PipAsPythonDependency add,
PackageTypes package_types,
VerifyPackages verify_packages,
RepodataParser parser
RepodataParser repo_parser
) -> expected_t<RepoInfo>
{
const auto verify_artifacts = static_cast<bool>(verify_packages);
@ -161,7 +172,7 @@ namespace mamba::solver::libsolv
auto make_repo = [&]() -> expected_t<solv::ObjRepoView>
{
if (parser == RepodataParser::Mamba)
if (repo_parser == RepodataParser::Mamba)
{
return mamba_read_json(
pool(),
@ -170,10 +181,20 @@ namespace mamba::solver::libsolv
std::string(url),
channel_id,
package_types,
MatchSpecParser::Libsolv, // Backward compatibility
settings().matchspec_parser,
verify_artifacts
);
}
if (settings().matchspec_parser != MatchSpecParser::Libsolv)
{
return make_unexpected(
" Libsolv repodata parser can only be used with Libsolv MatchSpec parser."
"A Libsolv Repodata parser option been passed to this function while a"
" non-Libsolv MatchSpec parser option has been give to the Database constructor.",
mamba_error_code::incorrect_usage
);
}
return libsolv_read_json(repo, path, package_types, verify_artifacts)
.transform(
[&url, &channel_id](solv::ObjRepoView p_repo)
@ -241,12 +262,7 @@ namespace mamba::solver::libsolv
{
auto s_repo = solv::ObjRepoView(*repo.m_ptr);
auto [id, solv] = s_repo.add_solvable();
set_solvable(
pool(),
solv,
pkg,
MatchSpecParser::Libsolv // Backward compatibility
);
set_solvable(pool(), solv, pkg, settings().matchspec_parser);
}
void Database::add_repo_from_packages_impl_post(const RepoInfo& repo, PipAsPythonDependency add)
@ -330,10 +346,11 @@ namespace mamba::solver::libsolv
namespace
{
auto pool_add_matchspec_throwing(solv::ObjPool& pool, const specs::MatchSpec& ms)
auto
pool_add_matchspec_throwing(solv::ObjPool& pool, const specs::MatchSpec& ms, MatchSpecParser parser)
-> solv::DependencyId
{
return pool_add_matchspec(pool, ms, MatchSpecParser::Mixed)
return pool_add_matchspec(pool, ms, parser)
.or_else([](mamba_error&& error) { throw std::move(error); })
.value_or(0);
}
@ -344,7 +361,7 @@ namespace mamba::solver::libsolv
static_assert(std::is_same_v<std::underlying_type_t<PackageId>, solv::SolvableId>);
pool().ensure_whatprovides();
const auto ms_id = pool_add_matchspec_throwing(pool(), ms);
const auto ms_id = pool_add_matchspec_throwing(pool(), ms, settings().matchspec_parser);
auto solvables = pool().select_solvables({ SOLVER_SOLVABLE_PROVIDES, ms_id });
auto out = std::vector<PackageId>(solvables.size());
std::transform(
@ -361,7 +378,7 @@ namespace mamba::solver::libsolv
static_assert(std::is_same_v<std::underlying_type_t<PackageId>, solv::SolvableId>);
pool().ensure_whatprovides();
const auto ms_id = pool_add_matchspec_throwing(pool(), ms);
const auto ms_id = pool_add_matchspec_throwing(pool(), ms, settings().matchspec_parser);
auto solvables = pool().what_matches_dep(SOLVABLE_REQUIRES, ms_id);
auto out = std::vector<PackageId>(solvables.size());
std::transform(

View File

@ -50,17 +50,13 @@ namespace mamba::solver::libsolv
}
}
auto Solver::solve_impl(Database& mpool, const Request& request) -> expected_t<Outcome>
auto Solver::solve_impl(Database& mpool, const Request& request, MatchSpecParser ms_parser)
-> expected_t<Outcome>
{
auto& pool = Database::Impl::get(mpool);
const auto& flags = request.flags;
return solver::libsolv::request_to_decision_queue(
request,
pool,
flags.force_reinstall,
MatchSpecParser::Mixed
)
return solver::libsolv::request_to_decision_queue(request, pool, flags.force_reinstall, ms_parser)
.transform(
[&](auto&& jobs) -> Outcome
{
@ -90,24 +86,26 @@ namespace mamba::solver::libsolv
);
}
auto Solver::solve(Database& mpool, Request&& request) -> expected_t<Outcome>
auto Solver::solve(Database& mpool, Request&& request, MatchSpecParser ms_parser)
-> expected_t<Outcome>
{
if (request.flags.order_request)
{
std::sort(request.jobs.begin(), request.jobs.end(), make_request_cmp());
}
return solve_impl(mpool, request);
return solve_impl(mpool, request, ms_parser);
}
auto Solver::solve(Database& mpool, const Request& request) -> expected_t<Outcome>
auto Solver::solve(Database& mpool, const Request& request, MatchSpecParser ms_parser)
-> expected_t<Outcome>
{
if (request.flags.order_request)
{
auto sorted_request = request;
std::sort(sorted_request.jobs.begin(), sorted_request.jobs.end(), make_request_cmp());
return solve_impl(mpool, sorted_request);
return solve_impl(mpool, sorted_request, ms_parser);
}
return solve_impl(mpool, request);
return solve_impl(mpool, request, ms_parser);
}
} // namespace mamba

View File

@ -37,9 +37,16 @@ namespace
{
using PackageInfo = specs::PackageInfo;
TEST_CASE("Create a database")
TEST_CASE("Create a database", "[mamba::solver][mamba::solver::libsolv]")
{
auto db = libsolv::Database({});
const auto matchspec_parser = GENERATE(
libsolv::MatchSpecParser::Libsolv,
libsolv::MatchSpecParser::Mixed,
libsolv::MatchSpecParser::Mamba
);
CAPTURE(matchspec_parser);
auto db = libsolv::Database({}, { matchspec_parser });
REQUIRE(std::is_move_constructible_v<libsolv::Database>);
REQUIRE(db.repo_count() == 0);
@ -162,6 +169,12 @@ namespace
SECTION("Depending on a given dependency")
{
// Complex repoqueries do not work with namespace callbacks
if (matchspec_parser != libsolv::MatchSpecParser::Libsolv)
{
return;
}
std::size_t count = 0;
db.for_each_package_depending_on(
specs::MatchSpec::parse("x").value(),
@ -351,6 +364,14 @@ namespace
libsolv::VerifyPackages::Yes,
libsolv::RepodataParser::Libsolv
);
// Libsolv repodata parser only works with its own matchspec
if (matchspec_parser != libsolv::MatchSpecParser::Libsolv)
{
REQUIRE_FALSE(repo1.has_value());
return;
}
REQUIRE(repo1.has_value());
REQUIRE(repo1->package_count() == 33);
@ -420,6 +441,14 @@ namespace
libsolv::VerifyPackages::No,
libsolv::RepodataParser::Libsolv
);
// Libsolv repodata parser only works with its own matchspec
if (matchspec_parser != libsolv::MatchSpecParser::Libsolv)
{
REQUIRE_FALSE(repo1.has_value());
return;
}
REQUIRE(repo1.has_value());
REQUIRE(repo1->package_count() == 33);

View File

@ -67,16 +67,25 @@ namespace
{
using namespace specs::match_spec_literals;
TEST_CASE("Solve a fresh environment with one repository")
TEST_CASE("Solve a fresh environment with one repository", "[mamba::solver][mamba::solver::libsolv]")
{
auto db = libsolv::Database({});
const auto matchspec_parser = GENERATE(
libsolv::MatchSpecParser::Libsolv,
libsolv::MatchSpecParser::Mixed,
libsolv::MatchSpecParser::Mamba
);
auto db = libsolv::Database({}, { matchspec_parser });
// A conda-forge/linux-64 subsample with one version of numpy and pip and their dependencies
const auto repo = db.add_repo_from_repodata_json(
mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json",
"https://conda.anaconda.org/conda-forge/linux-64",
"conda-forge",
libsolv::PipAsPythonDependency::No
libsolv::PipAsPythonDependency::No,
libsolv::PackageTypes::CondaOrElseTarBz2,
libsolv::VerifyPackages::No,
libsolv::RepodataParser::Mamba
);
REQUIRE(repo.has_value());
@ -86,7 +95,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -111,7 +120,7 @@ namespace
/* .flags= */ std::move(flags),
/* .jobs= */ { Request::Install{ "numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -137,7 +146,7 @@ namespace
},
/* .jobs= */ { Request::Install{ "numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -163,7 +172,7 @@ namespace
},
/* .jobs= */ { Request::Install{ "numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -189,7 +198,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "does-not-exist"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<libsolv::UnSolvable>(outcome.value()));
@ -205,22 +214,32 @@ namespace
},
/* .jobs= */ { Request::Install{ "numpy"_ms }, Request::Install{ "python=2.7"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<libsolv::UnSolvable>(outcome.value()));
}
}
TEST_CASE("Remove packages")
TEST_CASE("Remove packages", "[mamba::solver][mamba::solver::libsolv]")
{
auto db = libsolv::Database({});
const auto matchspec_parser = GENERATE(
libsolv::MatchSpecParser::Libsolv,
libsolv::MatchSpecParser::Mixed,
libsolv::MatchSpecParser::Mamba
);
auto db = libsolv::Database({}, { matchspec_parser });
// A conda-forge/linux-64 subsample with one version of numpy and pip and their dependencies
const auto repo = db.add_repo_from_repodata_json(
mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json",
"https://conda.anaconda.org/conda-forge/linux-64",
"conda-forge"
"conda-forge",
libsolv::PipAsPythonDependency::No,
libsolv::PackageTypes::CondaOrElseTarBz2,
libsolv::VerifyPackages::No,
libsolv::RepodataParser::Mamba
);
REQUIRE(repo.has_value());
db.set_installed_repo(repo.value());
@ -231,7 +250,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Remove{ "numpy"_ms, true } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -253,7 +272,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Remove{ "numpy"_ms, true }, Request::Remove{ "pip"_ms, true } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -278,7 +297,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Remove{ "numpy"_ms, false } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -296,7 +315,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Remove{ "does-not-exist"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -306,22 +325,36 @@ namespace
}
}
TEST_CASE("Reinstall packages")
TEST_CASE("Reinstall packages", "[mamba::solver][mamba::solver::libsolv]")
{
auto db = libsolv::Database({});
const auto matchspec_parser = GENERATE(
libsolv::MatchSpecParser::Libsolv,
libsolv::MatchSpecParser::Mixed,
libsolv::MatchSpecParser::Mamba
);
auto db = libsolv::Database({}, { matchspec_parser });
// A conda-forge/linux-64 subsample with one version of numpy and pip and their dependencies
const auto repo_installed = db.add_repo_from_repodata_json(
mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json",
"installed",
"installed"
"installed",
libsolv::PipAsPythonDependency::No,
libsolv::PackageTypes::CondaOrElseTarBz2,
libsolv::VerifyPackages::No,
libsolv::RepodataParser::Mamba
);
REQUIRE(repo_installed.has_value());
db.set_installed_repo(repo_installed.value());
const auto repo = db.add_repo_from_repodata_json(
mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json",
"https://conda.anaconda.org/conda-forge/linux-64",
"conda-forge"
"conda-forge",
libsolv::PipAsPythonDependency::No,
libsolv::PackageTypes::CondaOrElseTarBz2,
libsolv::VerifyPackages::No,
libsolv::RepodataParser::Mamba
);
REQUIRE(repo.has_value());
@ -333,7 +366,7 @@ namespace
/* .flags= */ std::move(flags),
/* .jobs= */ { Request::Install{ "numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -345,24 +378,36 @@ namespace
}
}
TEST_CASE("Solve a existing environment with one repository")
TEST_CASE("Solve a existing environment with one repository", "[mamba::solver][mamba::solver::libsolv]")
{
auto db = libsolv::Database({});
const auto matchspec_parser = GENERATE(
libsolv::MatchSpecParser::Libsolv,
libsolv::MatchSpecParser::Mixed,
libsolv::MatchSpecParser::Mamba
);
auto db = libsolv::Database({}, { matchspec_parser });
// A conda-forge/linux-64 subsample with one version of numpy and pip and their dependencies
const auto repo = db.add_repo_from_repodata_json(
mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json",
"https://conda.anaconda.org/conda-forge/linux-64",
"conda-forge",
libsolv::PipAsPythonDependency::No
libsolv::PipAsPythonDependency::No,
libsolv::PackageTypes::CondaOrElseTarBz2,
libsolv::VerifyPackages::No,
libsolv::RepodataParser::Mamba
);
REQUIRE(repo.has_value());
SECTION("numpy 1.0 is installed")
{
const auto installed = db.add_repo_from_packages(std::array{
specs::PackageInfo("numpy", "1.0.0", "phony", 0),
});
const auto installed = db.add_repo_from_packages(
std::array{
specs::PackageInfo("numpy", "1.0.0", "phony", 0),
},
"installed",
libsolv::PipAsPythonDependency::No
);
db.set_installed_repo(installed);
SECTION("Installing numpy does not upgrade")
@ -371,7 +416,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -385,7 +430,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Update{ "numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -413,7 +458,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Update{ "numpy<=1.1"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -426,11 +471,15 @@ namespace
{
auto pkg_numpy = specs::PackageInfo("numpy", "1.0.0", "phony", 0);
pkg_numpy.dependencies = { "python=2.0", "foo" };
const auto installed = db.add_repo_from_packages(std::array{
pkg_numpy,
specs::PackageInfo("python", "2.0.0", "phony", 0),
specs::PackageInfo("foo"),
});
const auto installed = db.add_repo_from_packages(
std::array{
pkg_numpy,
specs::PackageInfo("python", "2.0.0", "phony", 0),
specs::PackageInfo("foo"),
},
"installed",
libsolv::PipAsPythonDependency::No
);
db.set_installed_repo(installed);
SECTION("numpy is upgraded with cleaning dependencies")
@ -439,7 +488,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Update{ "numpy"_ms, true } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -470,7 +519,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Update{ "numpy"_ms, true }, Request::Keep{ "foo"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -500,7 +549,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Update{ "numpy"_ms, false } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -529,7 +578,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Update{ "python"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -559,11 +608,11 @@ namespace
pkg_numpy.dependencies = { "python=4.0", "foo" };
auto pkg_foo = specs::PackageInfo("foo", "1.0.0", "phony", 0);
pkg_foo.constrains = { "numpy=1.0.0", "foo" };
const auto installed = db.add_repo_from_packages(std::array{
pkg_numpy,
pkg_foo,
specs::PackageInfo("python", "4.0.0", "phony", 0),
});
const auto installed = db.add_repo_from_packages(
std::array{ pkg_numpy, pkg_foo, specs::PackageInfo("python", "4.0.0", "phony", 0) },
"installed",
libsolv::PipAsPythonDependency::No
);
db.set_installed_repo(installed);
SECTION("numpy upgrade lead to allowed python downgrade")
@ -578,7 +627,7 @@ namespace
},
/* .jobs= */ { Request::Update{ "numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -613,7 +662,7 @@ namespace
},
/* .jobs= */ { Request::Update{ "numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -625,16 +674,25 @@ namespace
}
}
TEST_CASE("Solve a fresh environment with multiple repositories")
TEST_CASE("Solve a fresh environment with multiple repositories", "[mamba::solver][mamba::solver::libsolv]")
{
auto db = libsolv::Database({});
const auto matchspec_parser = GENERATE(
libsolv::MatchSpecParser::Libsolv,
libsolv::MatchSpecParser::Mixed,
libsolv::MatchSpecParser::Mamba
);
auto db = libsolv::Database({}, { matchspec_parser });
const auto repo1 = db.add_repo_from_packages(std::array{
specs::PackageInfo("numpy", "1.0.0", "repo1", 0),
});
const auto repo2 = db.add_repo_from_packages(std::array{
specs::PackageInfo("numpy", "2.0.0", "repo2", 0),
});
const auto repo1 = db.add_repo_from_packages(
std::array{ specs::PackageInfo("numpy", "1.0.0", "repo1", 0) },
"repo1",
libsolv::PipAsPythonDependency::No
);
const auto repo2 = db.add_repo_from_packages(
std::array{ specs::PackageInfo("numpy", "2.0.0", "repo2", 0) },
"repo2",
libsolv::PipAsPythonDependency::No
);
db.set_repo_priority(repo1, { 2, 0 });
db.set_repo_priority(repo2, { 1, 0 });
@ -645,7 +703,7 @@ namespace
/* .jobs= */ { Request::Install{ "numpy>=2.0"_ms } },
};
request.flags.strict_repo_priority = false;
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -664,16 +722,22 @@ namespace
/* .jobs= */ { Request::Install{ "numpy>=2.0"_ms } },
};
request.flags.strict_repo_priority = true;
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<libsolv::UnSolvable>(outcome.value()));
}
}
TEST_CASE("Install highest priority package")
TEST_CASE("Install highest priority package", "[mamba::solver][mamba::solver::libsolv]")
{
auto db = libsolv::Database({});
const auto matchspec_parser = GENERATE(
libsolv::MatchSpecParser::Libsolv,
libsolv::MatchSpecParser::Mixed,
libsolv::MatchSpecParser::Mamba
);
auto db = libsolv::Database({}, { matchspec_parser });
auto mkfoo = [](std::string version,
std::size_t build_number = 0,
@ -690,16 +754,20 @@ namespace
SECTION("Pins are respected")
{
db.add_repo_from_packages(std::array{
mkfoo("1.0.0", 0, { "feat" }, 0),
mkfoo("2.0.0", 1, {}, 1),
});
db.add_repo_from_packages(
std::array{
mkfoo("1.0.0", 0, { "feat" }, 0),
mkfoo("2.0.0", 1, {}, 1),
},
"repo",
libsolv::PipAsPythonDependency::No
);
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "foo"_ms }, Request::Pin{ "foo==1.0"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -713,15 +781,19 @@ namespace
SECTION("Track features has highest priority")
{
db.add_repo_from_packages(std::array{
mkfoo("1.0.0", 0, {}, 0),
mkfoo("2.0.0", 1, { "feat" }, 1),
});
db.add_repo_from_packages(
std::array{
mkfoo("1.0.0", 0, {}, 0),
mkfoo("2.0.0", 1, { "feat" }, 1),
},
"repo",
libsolv::PipAsPythonDependency::No
);
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "foo"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -735,15 +807,19 @@ namespace
SECTION("Version has second highest priority")
{
db.add_repo_from_packages(std::array{
mkfoo("2.0.0", 0, {}, 0),
mkfoo("1.0.0", 1, {}, 1),
});
db.add_repo_from_packages(
std::array{
mkfoo("2.0.0", 0, {}, 0),
mkfoo("1.0.0", 1, {}, 1),
},
"repo",
libsolv::PipAsPythonDependency::No
);
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "foo"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -757,15 +833,19 @@ namespace
SECTION("Build number has third highest priority")
{
db.add_repo_from_packages(std::array{
mkfoo("2.0.0", 1, {}, 0),
mkfoo("2.0.0", 0, {}, 1),
});
db.add_repo_from_packages(
std::array{
mkfoo("2.0.0", 1, {}, 0),
mkfoo("2.0.0", 0, {}, 1),
},
"repo",
libsolv::PipAsPythonDependency::No
);
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "foo"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -779,15 +859,19 @@ namespace
SECTION("Timestamp has lowest priority")
{
db.add_repo_from_packages(std::array{
mkfoo("2.0.0", 0, {}, 0),
mkfoo("2.0.0", 0, {}, 1),
});
db.add_repo_from_packages(
std::array{
mkfoo("2.0.0", 0, {}, 0),
mkfoo("2.0.0", 0, {}, 1),
},
"repo",
libsolv::PipAsPythonDependency::No
);
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "foo"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -800,21 +884,30 @@ namespace
}
}
TEST_CASE("Respect channel-specific MatchSpec")
TEST_CASE("Respect channel-specific MatchSpec", "[mamba::solver][mamba::solver::libsolv]")
{
auto db = libsolv::Database({
/* .platforms= */ { "linux-64", "noarch" },
/* .channel_alias= */ specs::CondaURL::parse("https://conda.anaconda.org/").value(),
});
// Libsolv MatchSpec parser is not able to handle channels
const auto matchspec_parser = GENERATE(
libsolv::MatchSpecParser::Mixed,
libsolv::MatchSpecParser::Mamba
);
auto db = libsolv::Database(
{
/* .platforms= */ { "linux-64", "noarch" },
/* .channel_alias= */ specs::CondaURL::parse("https://conda.anaconda.org/").value(),
},
{ matchspec_parser }
);
SECTION("Different channels")
{
auto pkg1 = specs::PackageInfo("foo", "1.0.0", "conda", 0);
pkg1.package_url = "https://conda.anaconda.org/conda-forge/linux-64/foo-1.0.0-phony.conda";
db.add_repo_from_packages(std::array{ pkg1 });
db.add_repo_from_packages(std::array{ pkg1 }, "repo1", libsolv::PipAsPythonDependency::No);
auto pkg2 = specs::PackageInfo("foo", "1.0.0", "mamba", 0);
pkg2.package_url = "https://conda.anaconda.org/mamba-forge/linux-64/foo-1.0.0-phony.conda";
db.add_repo_from_packages(std::array{ pkg2 });
db.add_repo_from_packages(std::array{ pkg2 }, "repo2", libsolv::PipAsPythonDependency::No);
SECTION("conda-forge::foo")
{
@ -822,7 +915,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "conda-forge::foo"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -840,7 +933,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "mamba-forge::foo"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -859,7 +952,7 @@ namespace
/* .jobs= */ { Request::Install{ "pixi-forge::foo"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<libsolv::UnSolvable>(outcome.value()));
@ -871,7 +964,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "https://conda.anaconda.org/mamba-forge::foo"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -890,7 +983,10 @@ namespace
mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json",
"https://conda.anaconda.org/conda-forge/linux-64",
"conda-forge",
libsolv::PipAsPythonDependency::No
libsolv::PipAsPythonDependency::No,
libsolv::PackageTypes::CondaOrElseTarBz2,
libsolv::VerifyPackages::No,
libsolv::RepodataParser::Mamba
);
REQUIRE(repo_linux.has_value());
@ -901,7 +997,10 @@ namespace
mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json",
"https://conda.anaconda.org/conda-forge/noarch",
"conda-forge",
libsolv::PipAsPythonDependency::No
libsolv::PipAsPythonDependency::No,
libsolv::PackageTypes::CondaOrElseTarBz2,
libsolv::VerifyPackages::No,
libsolv::RepodataParser::Mamba
);
REQUIRE(repo_noarch.has_value());
@ -911,7 +1010,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "conda-forge/win-64::numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<libsolv::UnSolvable>(outcome.value()));
@ -923,7 +1022,7 @@ namespace
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "conda-forge::numpy[subdir=linux-64]"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -940,11 +1039,17 @@ namespace
}
}
TEST_CASE("Respect pins")
TEST_CASE("Respect pins", "[mamba::solver][mamba::solver::libsolv]")
{
using PackageInfo = specs::PackageInfo;
auto db = libsolv::Database({});
const auto matchspec_parser = GENERATE(
libsolv::MatchSpecParser::Libsolv,
libsolv::MatchSpecParser::Mixed,
libsolv::MatchSpecParser::Mamba
);
auto db = libsolv::Database({}, { matchspec_parser });
SECTION("Respect pins through direct dependencies")
{
@ -953,13 +1058,17 @@ namespace
auto pkg2 = PackageInfo("foo");
pkg2.version = "2.0";
db.add_repo_from_packages(std::array{ pkg1, pkg2 });
db.add_repo_from_packages(
std::array{ pkg1, pkg2 },
"repo",
libsolv::PipAsPythonDependency::No
);
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Pin{ "foo=1.0"_ms }, Request::Install{ "foo"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -984,13 +1093,17 @@ namespace
pkg4.version = "2.0";
pkg4.dependencies = { "foo=2.0" };
db.add_repo_from_packages(std::array{ pkg1, pkg2, pkg3, pkg4 });
db.add_repo_from_packages(
std::array{ pkg1, pkg2, pkg3, pkg4 },
"repo",
libsolv::PipAsPythonDependency::No
);
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Pin{ "foo=1.0"_ms }, Request::Install{ "bar"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -1014,13 +1127,17 @@ namespace
auto pkg2 = PackageInfo("bar");
pkg2.version = "1.0";
db.add_repo_from_packages(std::array{ pkg1, pkg2 });
db.add_repo_from_packages(
std::array{ pkg1, pkg2 },
"repo",
libsolv::PipAsPythonDependency::No
);
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Pin{ "foo=1.0"_ms }, Request::Install{ "bar"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -1038,13 +1155,13 @@ namespace
auto pkg = PackageInfo("bar");
pkg.version = "1.0";
db.add_repo_from_packages(std::array{ pkg });
db.add_repo_from_packages(std::array{ pkg }, "repo", libsolv::PipAsPythonDependency::No);
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Pin{ "foo=1.0"_ms }, Request::Install{ "bar"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -1058,11 +1175,17 @@ namespace
}
}
TEST_CASE("Handle complex matchspecs")
TEST_CASE("Handle complex matchspecs", "[mamba::solver][mamba::solver::libsolv]")
{
using PackageInfo = specs::PackageInfo;
auto db = libsolv::Database({});
// Libsolv MatchSpec parser cannot handle complex specs
const auto matchspec_parser = GENERATE(
libsolv::MatchSpecParser::Mixed,
libsolv::MatchSpecParser::Mamba
);
auto db = libsolv::Database({}, { matchspec_parser });
SECTION("*[md5=0bab699354cbd66959550eb9b9866620]")
{
@ -1071,13 +1194,17 @@ namespace
auto pkg2 = PackageInfo("foo");
pkg2.md5 = "bad";
db.add_repo_from_packages(std::array{ pkg1, pkg2 });
db.add_repo_from_packages(
std::array{ pkg1, pkg2 },
"repo",
libsolv::PipAsPythonDependency::No
);
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "*[md5=0bab699354cbd66959550eb9b9866620]"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -1096,13 +1223,13 @@ namespace
auto pkg1 = PackageInfo("foo");
pkg1.md5 = "0bab699354cbd66959550eb9b9866620";
db.add_repo_from_packages(std::array{ pkg1 });
db.add_repo_from_packages(std::array{ pkg1 }, "repo", libsolv::PipAsPythonDependency::No);
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "foo[md5=notreallymd5]"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<libsolv::UnSolvable>(outcome.value()));
@ -1115,13 +1242,17 @@ namespace
auto pkg2 = PackageInfo("foo");
pkg2.build_string = "bld";
db.add_repo_from_packages(std::array{ pkg1, pkg2 });
db.add_repo_from_packages(
std::array{ pkg1, pkg2 },
"repo",
libsolv::PipAsPythonDependency::No
);
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "foo[build=bld]"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -1146,13 +1277,17 @@ namespace
pkg3.build_string = "bld";
pkg3.build_number = 4;
db.add_repo_from_packages(std::array{ pkg1, pkg2, pkg3 });
db.add_repo_from_packages(
std::array{ pkg1, pkg2, pkg3 },
"repo",
libsolv::PipAsPythonDependency::No
);
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "foo[build=bld]"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
@ -1172,13 +1307,13 @@ namespace
pkg.version = "=*,=*";
pkg.build_string = "pyhd*";
db.add_repo_from_packages(std::array{ pkg });
db.add_repo_from_packages(std::array{ pkg }, "repo", libsolv::PipAsPythonDependency::No);
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "foo[version='=*,=*', build='pyhd*']"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
const auto outcome = libsolv::Solver().solve(db, request, matchspec_parser);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<libsolv::UnSolvable>(outcome.value()));

View File

@ -18,7 +18,7 @@ namespace
{
using namespace specs::match_spec_literals;
TEST_CASE("Create a request")
TEST_CASE("Create a request", "[mamba::solver]")
{
auto request = Request{
{},

View File

@ -17,7 +17,7 @@ namespace
{
using PackageInfo = specs::PackageInfo;
TEST_CASE("Create a Solution")
TEST_CASE("Create a Solution", "[mamba::solver]")
{
auto solution = Solution{ {
Solution::Omit{ PackageInfo("omit") },

View File

@ -33,6 +33,13 @@ namespace mambapy
.def(py::init(&enum_from_str<RepodataParser>));
py::implicitly_convertible<py::str, RepodataParser>();
py::enum_<MatchSpecParser>(m, "MatchSpecParser")
.value("Mixed", MatchSpecParser::Mixed)
.value("Mamba", MatchSpecParser::Mamba)
.value("Libsolv", MatchSpecParser::Libsolv)
.def(py::init(&enum_from_str<MatchSpecParser>));
py::implicitly_convertible<py::str, MatchSpecParser>();
py::enum_<PipAsPythonDependency>(m, "PipAsPythonDependency")
.value("No", PipAsPythonDependency::No)
.value("Yes", PipAsPythonDependency::Yes)
@ -117,7 +124,21 @@ namespace mambapy
.def("__deepcopy__", &deepcopy<RepoInfo>, py::arg("memo"));
py::class_<Database>(m, "Database")
.def(py::init<specs::ChannelResolveParams>(), py::arg("channel_params"))
.def(
py::init(
[](specs::ChannelResolveParams channel_params, MatchSpecParser matchspec_parser)
{
return Database(
channel_params,
Database::Settings{
matchspec_parser,
}
);
}
),
py::arg("channel_params"),
py::arg("matchspec_parser") = MatchSpecParser::Libsolv
)
.def("set_logger", &Database::set_logger, py::call_guard<py::gil_scoped_acquire>())
.def(
"add_repo_from_repodata_json",
@ -235,12 +256,15 @@ namespace mambapy
constexpr auto solver_job_v2_migrator = [](Solver&, py::args, py::kwargs)
{ throw std::runtime_error("All jobs need to be passed in the libmambapy.solver.Request."); };
py::class_<Solver>(m, "Solver") //
py::class_<Solver>(m, "Solver")
.def(py::init())
.def(
"solve",
[](Solver& self, Database& database, const solver::Request& request)
{ return self.solve(database, request); }
[](Solver& self, Database& database, const solver::Request& request, MatchSpecParser ms_parser
) { return self.solve(database, request, ms_parser); },
py::arg("database"),
py::arg("request"),
py::arg("matchspec_parser") = MatchSpecParser::Mixed
)
.def("add_jobs", solver_job_v2_migrator)
.def("add_global_job", solver_job_v2_migrator)

View File

@ -3,14 +3,14 @@ import libmambapy
def test_context_instance_scoped():
ctx = libmambapy.Context() # Initialize and then terminate libmamba internals
return ctx
assert ctx is not None
def test_context_no_log_nor_signal_handling():
ctx = libmambapy.Context(
libmambapy.ContextOptions(enable_logging=False, enable_signal_handling=False)
)
return ctx
assert ctx is not None
def test_channel_context():

View File

@ -1,6 +1,5 @@
import copy
import json
import itertools
import pytest
@ -32,6 +31,17 @@ def test_RepodataParser():
libsolv.RepodataParser("NoParser")
def test_MatchSpecParser():
assert libsolv.MatchSpecParser.Mamba.name == "Mamba"
assert libsolv.MatchSpecParser.Libsolv.name == "Libsolv"
assert libsolv.MatchSpecParser.Mixed.name == "Mixed"
assert libsolv.MatchSpecParser("Libsolv") == libsolv.MatchSpecParser.Libsolv
with pytest.raises(KeyError):
libsolv.MatchSpecParser("NoParser")
def test_PipASPythonDependency():
assert libsolv.PipAsPythonDependency.No.name == "No"
assert libsolv.PipAsPythonDependency.Yes.name == "Yes"
@ -129,8 +139,12 @@ def test_Database_logger():
@pytest.mark.parametrize("add_pip_as_python_dependency", [True, False])
def test_Database_RepoInfo_from_packages(add_pip_as_python_dependency):
db = libsolv.Database(libmambapy.specs.ChannelResolveParams())
@pytest.mark.parametrize("matchspec_parser", ["Mixed", "Mamba", "Libsolv"])
def test_Database_RepoInfo_from_packages(add_pip_as_python_dependency, matchspec_parser):
db = libsolv.Database(
libmambapy.specs.ChannelResolveParams(),
matchspec_parser=matchspec_parser,
)
assert db.repo_count() == 0
assert db.installed_repo() is None
assert db.package_count() == 0
@ -193,31 +207,43 @@ def tmp_repodata_json(tmp_path):
return file
@pytest.mark.parametrize(
["add_pip_as_python_dependency", "package_types", "repodata_parser"],
itertools.product(
[True, False],
["TarBz2Only", "CondaOrElseTarBz2"],
["Mamba", "Libsolv"],
),
)
@pytest.mark.parametrize("add_pip_as_python_dependency", [True, False])
@pytest.mark.parametrize("package_types", ["TarBz2Only", "CondaOrElseTarBz2"])
@pytest.mark.parametrize("repodata_parser", ["Mamba", "Libsolv"])
@pytest.mark.parametrize("matchspec_parser", ["Mixed", "Mamba", "Libsolv"])
def test_Database_RepoInfo_from_repodata(
tmp_path, tmp_repodata_json, add_pip_as_python_dependency, package_types, repodata_parser
tmp_path,
tmp_repodata_json,
add_pip_as_python_dependency,
package_types,
repodata_parser,
matchspec_parser,
):
db = libsolv.Database(libmambapy.specs.ChannelResolveParams())
db = libsolv.Database(
libmambapy.specs.ChannelResolveParams(),
matchspec_parser=matchspec_parser,
)
url = "https://repo.mamba.pm"
channel_id = "conda-forge"
# Json
repo = db.add_repo_from_repodata_json(
path=tmp_repodata_json,
url=url,
channel_id=channel_id,
add_pip_as_python_dependency=add_pip_as_python_dependency,
package_types=package_types,
repodata_parser=repodata_parser,
)
def add_repo_json():
return db.add_repo_from_repodata_json(
path=tmp_repodata_json,
url=url,
channel_id=channel_id,
add_pip_as_python_dependency=add_pip_as_python_dependency,
package_types=package_types,
repodata_parser=repodata_parser,
)
if (repodata_parser == "Libsolv") and (matchspec_parser != "Libsolv"):
with pytest.raises(libmambapy.MambaNativeException) as e:
add_repo_json()
assert "Libsolv repodata parser can only be used with Libsolv MatchSpec parser" in e
return
repo = add_repo_json()
db.set_installed_repo(repo)
assert repo.package_count() == 1 if package_types in ["TarBz2Only", "CondaOnly"] else 2
@ -248,7 +274,7 @@ def test_Database_RepoInfo_from_repodata(
assert repo_loaded.package_count() == 1 if package_types in ["TarBz2Only", "CondaOnly"] else 2
def test_Database_RepoInfo_from_repodata_error():
def test_Database_RepoInfo_from_repodata_missing():
db = libsolv.Database(libmambapy.specs.ChannelResolveParams())
channel_id = "conda-forge"