Move problem graph creation to MSolver (#2515)

Move problem graph creation to MSolver and remove JupyterLab SAT error case as no long unsolvable
This commit is contained in:
Antoine Prouvost 2023-05-23 14:03:00 +02:00 committed by GitHub
parent 6792f50cae
commit 83706d5f9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 544 additions and 601 deletions

View File

@ -94,8 +94,6 @@ namespace mamba
using node_id = graph_t::node_id;
using conflicts_t = conflict_map<node_id>;
static ProblemsGraph from_solver(const MSolver& solver, const MPool& pool);
ProblemsGraph(graph_t graph, conflicts_t conflicts, node_id root_node);
const graph_t& graph() const noexcept;

View File

@ -21,6 +21,7 @@
#include "mamba/core/package_info.hpp"
#include "mamba/core/pool.hpp"
#include "mamba/core/satisfiability_error.hpp"
#include "match_spec.hpp"
@ -65,8 +66,8 @@ namespace mamba
MSolver(const MSolver&) = delete;
MSolver& operator=(const MSolver&) = delete;
MSolver(MSolver&&) = delete;
MSolver& operator=(MSolver&&) = delete;
MSolver(MSolver&&);
MSolver& operator=(MSolver&&);
void add_global_job(int job_flag);
void add_jobs(const std::vector<std::string>& jobs, int job_flag);
@ -82,6 +83,7 @@ namespace mamba
[[nodiscard]] std::string problems_to_str() const;
[[nodiscard]] std::vector<std::string> all_problems() const;
[[nodiscard]] std::vector<MSolverProblem> all_problems_structured() const;
[[nodiscard]] ProblemsGraph problems_graph() const;
[[nodiscard]] std::string all_problems_to_str() const;
std::ostream& explain_problems(std::ostream& out) const;
[[nodiscard]] std::string explain_problems() const;

View File

@ -18,291 +18,14 @@
#include <fmt/color.h>
#include <fmt/format.h>
#include <fmt/ostream.h>
#include <solv/pool.h>
#include "mamba/core/output.hpp"
#include "mamba/core/package_info.hpp"
#include "mamba/core/pool.hpp"
#include "mamba/core/satisfiability_error.hpp"
#include "mamba/core/solver.hpp"
#include "mamba/core/util_string.hpp"
namespace mamba
{
namespace
{
void warn_unexpected_problem(const MSolverProblem& problem)
{
// TODO: Once the new error message are not experimental, we should consider
// reducing this level since it is not somethig the user has control over.
LOG_WARNING << "Unexpected empty optionals for problem type "
<< solver_ruleinfo_name(problem.type);
}
class ProblemsGraphCreator
{
public:
using SolvId = Id; // Unscoped from libsolv
using graph_t = ProblemsGraph::graph_t;
using RootNode = ProblemsGraph::RootNode;
using PackageNode = ProblemsGraph::PackageNode;
using UnresolvedDependencyNode = ProblemsGraph::UnresolvedDependencyNode;
using ConstraintNode = ProblemsGraph::ConstraintNode;
using node_t = ProblemsGraph::node_t;
using node_id = ProblemsGraph::node_id;
using edge_t = ProblemsGraph::edge_t;
using conflicts_t = ProblemsGraph::conflicts_t;
ProblemsGraphCreator(const MSolver& solver, const MPool& pool);
ProblemsGraph problem_graph() &&;
private:
const MSolver& m_solver;
const MPool& m_pool;
graph_t m_graph;
conflicts_t m_conflicts;
std::map<SolvId, node_id> m_solv2node;
node_id m_root_node;
/**
* Add a node and return the node id.
*
* If the node is already present and ``update`` is false then the current
* node is left as it is, otherwise the new value is inserted.
*/
node_id add_solvable(SolvId solv_id, node_t&& pkg_info, bool update = true);
void add_conflict(node_id n1, node_id n2);
[[nodiscard]] bool
add_expanded_deps_edges(node_id from_id, SolvId dep_id, const edge_t& edge);
void parse_problems();
};
ProblemsGraphCreator::ProblemsGraphCreator(const MSolver& solver, const MPool& pool)
: m_solver{ solver }
, m_pool{ pool }
{
m_root_node = m_graph.add_node(RootNode());
parse_problems();
}
ProblemsGraph ProblemsGraphCreator::problem_graph() &&
{
return { std::move(m_graph), std::move(m_conflicts), m_root_node };
}
auto ProblemsGraphCreator::add_solvable(SolvId solv_id, node_t&& node, bool update) -> node_id
{
if (const auto iter = m_solv2node.find(solv_id); iter != m_solv2node.end())
{
const node_id id = iter->second;
if (update)
{
m_graph.node(id) = std::move(node);
}
return id;
}
const node_id id = m_graph.add_node(std::move(node));
m_solv2node[solv_id] = id;
return id;
};
void ProblemsGraphCreator::add_conflict(node_id n1, node_id n2)
{
m_conflicts.add(n1, n2);
}
bool
ProblemsGraphCreator::add_expanded_deps_edges(node_id from_id, SolvId dep_id, const edge_t& edge)
{
bool added = false;
for (const auto& solv_id : m_pool.select_solvables(dep_id))
{
added = true;
auto pkg_info = m_pool.id2pkginfo(solv_id);
assert(pkg_info.has_value());
node_id to_id = add_solvable(solv_id, PackageNode{ std::move(pkg_info).value() }, false);
m_graph.add_edge(from_id, to_id, edge);
}
return added;
}
void ProblemsGraphCreator::parse_problems()
{
for (auto& problem : m_solver.all_problems_structured())
{
std::optional<PackageInfo>& source = problem.source;
std::optional<PackageInfo>& target = problem.target;
std::optional<std::string>& dep = problem.dep;
SolverRuleinfo type = problem.type;
switch (type)
{
case SOLVER_RULE_PKG_CONSTRAINS:
{
// A constraint (run_constrained) on source is conflicting with target.
// SOLVER_RULE_PKG_CONSTRAINS has a dep, but it can resolve to nothing.
// The constraint conflict is actually expressed between the target and
// a constrains node child of the source.
if (!source || !target || !dep)
{
warn_unexpected_problem(problem);
break;
}
auto src_id = add_solvable(
problem.source_id,
PackageNode{ std::move(source).value() }
);
node_id tgt_id = add_solvable(
problem.target_id,
PackageNode{ std::move(target).value() }
);
node_id cons_id = add_solvable(
problem.dep_id,
ConstraintNode{ { dep.value(), m_pool.channel_context() } }
);
MatchSpec edge(dep.value(), m_pool.channel_context());
m_graph.add_edge(src_id, cons_id, std::move(edge));
add_conflict(cons_id, tgt_id);
break;
}
case SOLVER_RULE_PKG_REQUIRES:
{
// Express a dependency on source that is involved in explaining the
// problem.
// Not all dependency of package will appear, only enough to explain the
// problem. It is not a problem in itself, only a part of the graph.
if (!dep || !source)
{
warn_unexpected_problem(problem);
break;
}
auto src_id = add_solvable(
problem.source_id,
PackageNode{ std::move(source).value() }
);
MatchSpec edge{ dep.value(), m_pool.channel_context() };
bool added = add_expanded_deps_edges(src_id, problem.dep_id, edge);
if (!added)
{
LOG_WARNING << "Added empty dependency for problem type "
<< solver_ruleinfo_name(type);
}
break;
}
case SOLVER_RULE_JOB:
case SOLVER_RULE_PKG:
{
// A top level requirement.
// The difference between JOB and PKG is unknown (possibly unused).
if (!dep)
{
warn_unexpected_problem(problem);
break;
}
MatchSpec edge(dep.value(), m_pool.channel_context());
bool added = add_expanded_deps_edges(m_root_node, problem.dep_id, edge);
if (!added)
{
LOG_WARNING << "Added empty dependency for problem type "
<< solver_ruleinfo_name(type);
}
break;
}
case SOLVER_RULE_JOB_NOTHING_PROVIDES_DEP:
case SOLVER_RULE_JOB_UNKNOWN_PACKAGE:
{
// A top level dependency does not exist.
// Could be a wrong name or missing channel.
if (!dep)
{
warn_unexpected_problem(problem);
break;
}
MatchSpec edge(dep.value(), m_pool.channel_context());
node_id dep_id = add_solvable(
problem.dep_id,
UnresolvedDependencyNode{
{ std::move(dep).value(), m_pool.channel_context() } }
);
m_graph.add_edge(m_root_node, dep_id, std::move(edge));
break;
}
case SOLVER_RULE_PKG_NOTHING_PROVIDES_DEP:
{
// A package dependency does not exist.
// Could be a wrong name or missing channel.
// This is a partial exaplanation of why a specific solvable (could be any
// of the parent) cannot be installed.
if (!source || !dep)
{
warn_unexpected_problem(problem);
break;
}
MatchSpec edge(dep.value(), m_pool.channel_context());
node_id src_id = add_solvable(
problem.source_id,
PackageNode{ std::move(source).value() }
);
node_id dep_id = add_solvable(
problem.dep_id,
UnresolvedDependencyNode{
{ std::move(dep).value(), m_pool.channel_context() } }
);
m_graph.add_edge(src_id, dep_id, std::move(edge));
break;
}
case SOLVER_RULE_PKG_CONFLICTS:
case SOLVER_RULE_PKG_SAME_NAME:
{
// Looking for a valid solution to the installation satisfiability expand to
// two solvables of same package that cannot be installed together. This is
// a partial exaplanation of why one of the solvables (could be any of the
// parent) cannot be installed.
if (!source || !target)
{
warn_unexpected_problem(problem);
break;
}
node_id src_id = add_solvable(
problem.source_id,
PackageNode{ std::move(source).value() }
);
node_id tgt_id = add_solvable(
problem.target_id,
PackageNode{ std::move(target).value() }
);
add_conflict(src_id, tgt_id);
break;
}
case SOLVER_RULE_UPDATE:
{
// Encounterd in the problems list from libsolv but unknown.
// Explicitly ignored until we do something with it.
break;
}
default:
{
// Many more SolverRuleinfo that heve not been encountered.
LOG_WARNING << "Problem type not implemented " << solver_ruleinfo_name(type);
break;
}
}
}
}
}
auto ProblemsGraph::from_solver(const MSolver& solver, const MPool& pool) -> ProblemsGraph
{
return ProblemsGraphCreator(solver, pool).problem_graph();
}
ProblemsGraph::ProblemsGraph(graph_t graph, conflicts_t conflicts, node_id root_node)
: m_graph(std::move(graph))
, m_conflicts(std::move(conflicts))

View File

@ -210,6 +210,20 @@ namespace mamba
MSolver::~MSolver() = default;
MSolver::MSolver(MSolver&&) = default;
MSolver& MSolver::operator=(MSolver&&) = default;
MSolver::operator const Solver*() const
{
return m_solver.get();
}
MSolver::operator Solver*()
{
return m_solver.get();
}
void MSolver::add_global_job(int job_flag)
{
m_jobs->push_back(job_flag, 0);
@ -568,7 +582,7 @@ namespace mamba
{
const auto& ctx = Context::instance();
out << "Could not solve for environment specs\n";
const auto pbs = ProblemsGraph::from_solver(*this, pool());
const auto pbs = problems_graph();
const auto pbs_simplified = simplify_conflicts(pbs);
const auto cp_pbs = CompressedProblemsGraph::from_problems_graph(pbs_simplified);
print_problem_tree_msg(
@ -614,13 +628,277 @@ namespace mamba
return problems;
}
MSolver::operator const Solver*() const
namespace
{
return m_solver.get();
void warn_unexpected_problem(const MSolverProblem& problem)
{
// TODO: Once the new error message are not experimental, we should consider
// reducing this level since it is not somethig the user has control over.
LOG_WARNING << "Unexpected empty optionals for problem type "
<< solver_ruleinfo_name(problem.type);
}
class ProblemsGraphCreator
{
public:
using SolvId = Id; // Unscoped from libsolv
using graph_t = ProblemsGraph::graph_t;
using RootNode = ProblemsGraph::RootNode;
using PackageNode = ProblemsGraph::PackageNode;
using UnresolvedDependencyNode = ProblemsGraph::UnresolvedDependencyNode;
using ConstraintNode = ProblemsGraph::ConstraintNode;
using node_t = ProblemsGraph::node_t;
using node_id = ProblemsGraph::node_id;
using edge_t = ProblemsGraph::edge_t;
using conflicts_t = ProblemsGraph::conflicts_t;
ProblemsGraphCreator(const MSolver& solver, const MPool& pool);
ProblemsGraph problem_graph() &&;
private:
const MSolver& m_solver;
const MPool& m_pool;
graph_t m_graph;
conflicts_t m_conflicts;
std::map<SolvId, node_id> m_solv2node;
node_id m_root_node;
/**
* Add a node and return the node id.
*
* If the node is already present and ``update`` is false then the current
* node is left as it is, otherwise the new value is inserted.
*/
node_id add_solvable(SolvId solv_id, node_t&& pkg_info, bool update = true);
void add_conflict(node_id n1, node_id n2);
[[nodiscard]] bool
add_expanded_deps_edges(node_id from_id, SolvId dep_id, const edge_t& edge);
void parse_problems();
};
ProblemsGraphCreator::ProblemsGraphCreator(const MSolver& solver, const MPool& pool)
: m_solver{ solver }
, m_pool{ pool }
{
m_root_node = m_graph.add_node(RootNode());
parse_problems();
}
ProblemsGraph ProblemsGraphCreator::problem_graph() &&
{
return { std::move(m_graph), std::move(m_conflicts), m_root_node };
}
auto ProblemsGraphCreator::add_solvable(SolvId solv_id, node_t&& node, bool update) -> node_id
{
if (const auto iter = m_solv2node.find(solv_id); iter != m_solv2node.end())
{
const node_id id = iter->second;
if (update)
{
m_graph.node(id) = std::move(node);
}
return id;
}
const node_id id = m_graph.add_node(std::move(node));
m_solv2node[solv_id] = id;
return id;
};
void ProblemsGraphCreator::add_conflict(node_id n1, node_id n2)
{
m_conflicts.add(n1, n2);
}
bool
ProblemsGraphCreator::add_expanded_deps_edges(node_id from_id, SolvId dep_id, const edge_t& edge)
{
bool added = false;
for (const auto& solv_id : m_pool.select_solvables(dep_id))
{
added = true;
auto pkg_info = m_pool.id2pkginfo(solv_id);
assert(pkg_info.has_value());
node_id to_id = add_solvable(solv_id, PackageNode{ std::move(pkg_info).value() }, false);
m_graph.add_edge(from_id, to_id, edge);
}
return added;
}
void ProblemsGraphCreator::parse_problems()
{
auto& channel_context = m_pool.channel_context();
for (auto& problem : m_solver.all_problems_structured())
{
std::optional<PackageInfo>& source = problem.source;
std::optional<PackageInfo>& target = problem.target;
std::optional<std::string>& dep = problem.dep;
SolverRuleinfo type = problem.type;
switch (type)
{
case SOLVER_RULE_PKG_CONSTRAINS:
{
// A constraint (run_constrained) on source is conflicting with target.
// SOLVER_RULE_PKG_CONSTRAINS has a dep, but it can resolve to nothing.
// The constraint conflict is actually expressed between the target and
// a constrains node child of the source.
if (!source || !target || !dep)
{
warn_unexpected_problem(problem);
break;
}
auto src_id = add_solvable(
problem.source_id,
PackageNode{ std::move(source).value() }
);
node_id tgt_id = add_solvable(
problem.target_id,
PackageNode{ std::move(target).value() }
);
node_id cons_id = add_solvable(
problem.dep_id,
ConstraintNode{ { dep.value(), channel_context } }
);
MatchSpec edge(dep.value(), channel_context);
m_graph.add_edge(src_id, cons_id, std::move(edge));
add_conflict(cons_id, tgt_id);
break;
}
case SOLVER_RULE_PKG_REQUIRES:
{
// Express a dependency on source that is involved in explaining the
// problem.
// Not all dependency of package will appear, only enough to explain the
// problem. It is not a problem in itself, only a part of the graph.
if (!dep || !source)
{
warn_unexpected_problem(problem);
break;
}
auto src_id = add_solvable(
problem.source_id,
PackageNode{ std::move(source).value() }
);
MatchSpec edge(dep.value(), channel_context);
bool added = add_expanded_deps_edges(src_id, problem.dep_id, edge);
if (!added)
{
LOG_WARNING << "Added empty dependency for problem type "
<< solver_ruleinfo_name(type);
}
break;
}
case SOLVER_RULE_JOB:
case SOLVER_RULE_PKG:
{
// A top level requirement.
// The difference between JOB and PKG is unknown (possibly unused).
if (!dep)
{
warn_unexpected_problem(problem);
break;
}
MatchSpec edge(dep.value(), channel_context);
bool added = add_expanded_deps_edges(m_root_node, problem.dep_id, edge);
if (!added)
{
LOG_WARNING << "Added empty dependency for problem type "
<< solver_ruleinfo_name(type);
}
break;
}
case SOLVER_RULE_JOB_NOTHING_PROVIDES_DEP:
case SOLVER_RULE_JOB_UNKNOWN_PACKAGE:
{
// A top level dependency does not exist.
// Could be a wrong name or missing channel.
if (!dep)
{
warn_unexpected_problem(problem);
break;
}
MatchSpec edge(dep.value(), channel_context);
node_id dep_id = add_solvable(
problem.dep_id,
UnresolvedDependencyNode{ { std::move(dep).value(), channel_context } }
);
m_graph.add_edge(m_root_node, dep_id, std::move(edge));
break;
}
case SOLVER_RULE_PKG_NOTHING_PROVIDES_DEP:
{
// A package dependency does not exist.
// Could be a wrong name or missing channel.
// This is a partial exaplanation of why a specific solvable (could be any
// of the parent) cannot be installed.
if (!source || !dep)
{
warn_unexpected_problem(problem);
break;
}
MatchSpec edge(dep.value(), channel_context);
node_id src_id = add_solvable(
problem.source_id,
PackageNode{ std::move(source).value() }
);
node_id dep_id = add_solvable(
problem.dep_id,
UnresolvedDependencyNode{ { std::move(dep).value(), channel_context } }
);
m_graph.add_edge(src_id, dep_id, std::move(edge));
break;
}
case SOLVER_RULE_PKG_CONFLICTS:
case SOLVER_RULE_PKG_SAME_NAME:
{
// Looking for a valid solution to the installation satisfiability expand to
// two solvables of same package that cannot be installed together. This is
// a partial exaplanation of why one of the solvables (could be any of the
// parent) cannot be installed.
if (!source || !target)
{
warn_unexpected_problem(problem);
break;
}
node_id src_id = add_solvable(
problem.source_id,
PackageNode{ std::move(source).value() }
);
node_id tgt_id = add_solvable(
problem.target_id,
PackageNode{ std::move(target).value() }
);
add_conflict(src_id, tgt_id);
break;
}
case SOLVER_RULE_UPDATE:
{
// Encounterd in the problems list from libsolv but unknown.
// Explicitly ignored until we do something with it.
break;
}
default:
{
// Many more SolverRuleinfo that heve not been encountered.
LOG_WARNING << "Problem type not implemented " << solver_ruleinfo_name(type);
break;
}
}
}
}
}
MSolver::operator Solver*()
auto MSolver::problems_graph() const -> ProblemsGraph
{
return m_solver.get();
return ProblemsGraphCreator(*this, m_pool).problem_graph();
}
} // namespace mamba

View File

@ -27,53 +27,57 @@
#include "mamba/core/util_random.hpp"
#include "mamba/core/util_string.hpp"
namespace mamba
using namespace mamba;
TEST_SUITE("conflict_map")
{
TEST_SUITE("conflict_map")
TEST_CASE("symetric")
{
TEST_CASE("symetric")
{
auto c = conflict_map<std::size_t>();
CHECK_EQ(c.size(), 0);
CHECK_FALSE(c.has_conflict(0));
CHECK_FALSE(c.in_conflict(0, 1));
CHECK(c.add(0, 1));
CHECK(c.add(1, 2));
CHECK_FALSE(c.add(1, 2));
CHECK(c.has_conflict(0));
CHECK(c.in_conflict(0, 1));
CHECK(c.in_conflict(1, 2));
CHECK(c.has_conflict(2));
CHECK_FALSE(c.in_conflict(0, 2));
// With same
CHECK(c.add(5, 5));
CHECK(c.has_conflict(5));
CHECK(c.in_conflict(5, 5));
}
TEST_CASE("remove")
{
auto c = conflict_map<std::size_t>({ { 1, 1 }, { 1, 2 }, { 1, 3 }, { 2, 4 } });
REQUIRE_EQ(c.size(), 4);
REQUIRE(c.in_conflict(2, 4));
REQUIRE(c.in_conflict(4, 2));
CHECK(c.remove(2, 4));
CHECK_FALSE(c.in_conflict(4, 2));
CHECK_FALSE(c.in_conflict(2, 4));
CHECK(c.has_conflict(2));
CHECK_FALSE(c.has_conflict(4));
CHECK_FALSE(c.remove(2, 4));
CHECK(c.remove(1));
CHECK_FALSE(c.has_conflict(1));
CHECK_FALSE(c.in_conflict(1, 1));
CHECK_FALSE(c.in_conflict(1, 2));
CHECK_FALSE(c.in_conflict(3, 1));
}
auto c = conflict_map<std::size_t>();
CHECK_EQ(c.size(), 0);
CHECK_FALSE(c.has_conflict(0));
CHECK_FALSE(c.in_conflict(0, 1));
CHECK(c.add(0, 1));
CHECK(c.add(1, 2));
CHECK_FALSE(c.add(1, 2));
CHECK(c.has_conflict(0));
CHECK(c.in_conflict(0, 1));
CHECK(c.in_conflict(1, 2));
CHECK(c.has_conflict(2));
CHECK_FALSE(c.in_conflict(0, 2));
// With same
CHECK(c.add(5, 5));
CHECK(c.has_conflict(5));
CHECK(c.in_conflict(5, 5));
}
TEST_CASE("remove")
{
auto c = conflict_map<std::size_t>({ { 1, 1 }, { 1, 2 }, { 1, 3 }, { 2, 4 } });
REQUIRE_EQ(c.size(), 4);
REQUIRE(c.in_conflict(2, 4));
REQUIRE(c.in_conflict(4, 2));
CHECK(c.remove(2, 4));
CHECK_FALSE(c.in_conflict(4, 2));
CHECK_FALSE(c.in_conflict(2, 4));
CHECK(c.has_conflict(2));
CHECK_FALSE(c.has_conflict(4));
CHECK_FALSE(c.remove(2, 4));
CHECK(c.remove(1));
CHECK_FALSE(c.has_conflict(1));
CHECK_FALSE(c.in_conflict(1, 1));
CHECK_FALSE(c.in_conflict(1, 2));
CHECK_FALSE(c.in_conflict(3, 1));
}
}
TEST_SUITE_BEGIN("satifiability_error");
namespace
{
/**
* A RAII object to ensure a path exists only for the lifetime of the guard.
*/
@ -136,12 +140,9 @@ namespace mamba
* The underlying packages do not exist, we are onl interested in the conflict.
*/
template <typename PkgRange>
auto create_problem(
const PkgRange& packages,
const std::vector<std::string>& specs,
ChannelContext& channel_context
)
auto create_problem(const PkgRange& packages, const std::vector<std::string>& specs)
{
ChannelContext channel_context = {};
const auto tmp_dir = dir_guard(
fs::temp_directory_path() / "mamba/tests" / generate_random_alphanumeric_string(20)
);
@ -149,46 +150,35 @@ namespace mamba
auto pool = MPool{ channel_context };
MRepo(pool, "some-name", repodata_f, RepoMetadata{ /* .url= */ "some-url" });
auto solver = std::make_unique<MSolver>(
auto solver = MSolver(
std::move(pool),
std::vector{ std::pair{ SOLVER_FLAG_ALLOW_DOWNGRADE, 1 } }
);
solver->add_jobs(specs, SOLVER_INSTALL);
solver.add_jobs(specs, SOLVER_INSTALL);
return solver;
}
}
/**
* Test the test utility function.
*/
TEST_SUITE("satifiability_error")
{
TEST_CASE("create_problem")
{
ChannelContext channel_context;
auto solver = create_problem(
std::array{ mkpkg("foo", "0.1.0", {}) },
{ "foo" },
channel_context
);
const auto solved = solver->try_solve();
REQUIRE(solved);
}
}
TEST_CASE("Test create_problem utility")
{
auto solver = create_problem(std::array{ mkpkg("foo", "0.1.0", {}) }, { "foo" });
const auto solved = solver.try_solve();
REQUIRE(solved);
}
auto create_basic_conflict() -> MSolver&
namespace
{
auto create_basic_conflict() -> MSolver
{
ChannelContext channel_context;
static auto solver = create_problem(
return create_problem(
std::array{
mkpkg("A", "0.1.0"),
mkpkg("A", "0.2.0"),
mkpkg("A", "0.3.0"),
},
{ "A=0.4.0" },
channel_context
{ "A=0.4.0" }
);
return *solver;
}
/**
@ -197,10 +187,9 @@ namespace mamba
* The example given by Natalie Weizenbaum
* (credits https://nex3.medium.com/pubgrub-2fb6470504f).
*/
auto create_pubgrub() -> MSolver&
auto create_pubgrub() -> MSolver
{
ChannelContext channel_context;
static auto solver = create_problem(
return create_problem(
std::array{
mkpkg("menu", "1.5.0", { "dropdown=2.*" }),
mkpkg("menu", "1.4.0", { "dropdown=2.*" }),
@ -219,13 +208,11 @@ namespace mamba
mkpkg("intl", "4.0.0"),
mkpkg("intl", "3.0.0"),
},
{ "menu", "icons=1.*", "intl=5.*" },
channel_context
{ "menu", "icons=1.*", "intl=5.*" }
);
return *solver;
}
auto create_pubgrub_hard_(bool missing_package, mamba::ChannelContext& channel_context)
auto create_pubgrub_hard_(bool missing_package) -> MSolver
{
auto packages = std::vector{
mkpkg("menu", "2.1.0", { "dropdown>=2.1", "emoji" }),
@ -276,29 +263,24 @@ namespace mamba
}
return create_problem(
packages,
{ "menu", "pyicons=1.*", "intl=5.*", "intl-mod", "pretty>=1.0" },
channel_context
{ "menu", "pyicons=1.*", "intl=5.*", "intl-mod", "pretty>=1.0" }
);
}
/**
* A harder version of ``create_pubgrub``.
*/
auto create_pubgrub_hard() -> MSolver&
auto create_pubgrub_hard() -> MSolver
{
static auto channel_context = mamba::ChannelContext{};
static auto solver = create_pubgrub_hard_(false, channel_context);
return *solver;
return create_pubgrub_hard_(false);
}
/**
* The hard version of the alternate PubGrub with missing packages.
*/
auto create_pubgrub_missing() -> MSolver&
auto create_pubgrub_missing() -> MSolver
{
static auto channel_context = mamba::ChannelContext{};
static auto solver = create_pubgrub_hard_(true, channel_context);
return *solver;
return create_pubgrub_hard_(true);
}
template <typename T, typename E>
@ -349,13 +331,13 @@ namespace mamba
* Create a solver and a pool of a conflict from conda-forge packages.
*/
auto create_conda_forge(
ChannelContext& channel_context,
std::vector<std::string>&& specs,
const std::vector<PackageInfo>& virtual_packages = { mkpkg("__glibc", "2.17.0") },
std::vector<std::string>&& channels = { "conda-forge" },
const std::vector<std::string>& platforms = { "linux-64", "noarch" }
)
) -> MSolver
{
ChannelContext channel_context = {};
// Reusing the cache for all invocation of this funciton for speedup
static const auto tmp_dir = dir_guard(
fs::temp_directory_path() / "mamba/tests" / generate_random_alphanumeric_string(20)
@ -377,101 +359,71 @@ namespace mamba
load_channels(pool, cache, make_platform_channels(std::move(channels), platforms));
Context::instance().graphics_params.no_progress_bars = prev_progress_bars_value;
auto solver = std::make_unique<MSolver>(
auto solver = MSolver(
std::move(pool),
std::vector{ std::pair{ SOLVER_FLAG_ALLOW_DOWNGRADE, 1 } }
);
solver->add_jobs(specs, SOLVER_INSTALL);
solver.add_jobs(specs, SOLVER_INSTALL);
return solver;
}
}
/**
* Test the test utility function.
*/
TEST_SUITE("satifiability_error")
TEST_CASE("Test create_conda_forge utility ")
{
auto solver = create_conda_forge({ "xtensor>=0.7" });
const auto solved = solver.try_solve();
REQUIRE(solved);
}
namespace
{
auto create_pytorch_cpu() -> MSolver
{
TEST_CASE("create_conda_forge")
{
ChannelContext channel_context;
auto solver = create_conda_forge(channel_context, { "xtensor>=0.7" });
const auto solved = solver->try_solve();
REQUIRE(solved);
}
return create_conda_forge({ "python=2.7", "pytorch=1.12" });
}
auto create_pytorch_cpu() -> MSolver&
auto create_pytorch_cuda() -> MSolver
{
static ChannelContext channel_context;
static auto solver = create_conda_forge(channel_context, { "python=2.7", "pytorch=1.12" });
return *solver;
}
auto create_pytorch_cuda() -> MSolver&
{
static ChannelContext channel_context;
static auto solver = create_conda_forge(
channel_context,
return create_conda_forge(
{ "python=2.7", "pytorch=1.12" },
{ mkpkg("__glibc", "2.17.0"), mkpkg("__cuda", "10.2.0") }
);
return *solver;
}
auto create_cudatoolkit() -> MSolver&
auto create_cudatoolkit() -> MSolver
{
static ChannelContext channel_context;
static auto solver = create_conda_forge(
channel_context,
return create_conda_forge(
{ "python=3.7", "cudatoolkit=11.1", "cudnn=8.0", "pytorch=1.8", "torchvision=0.9=*py37_cu111*" },
{ mkpkg("__glibc", "2.17.0"), mkpkg("__cuda", "11.1") }
);
return *solver;
}
auto create_jpeg9b() -> MSolver&
auto create_jpeg9b() -> MSolver
{
static ChannelContext channel_context;
static auto solver = create_conda_forge(channel_context, { "python=3.7", "jpeg=9b" });
return *solver;
return create_conda_forge({ "python=3.7", "jpeg=9b" });
}
auto create_r_base() -> MSolver&
auto create_r_base() -> MSolver
{
static ChannelContext channel_context;
static auto solver = create_conda_forge(
channel_context,
return create_conda_forge(
{ "r-base=3.5.* ", "pandas=0", "numpy<1.20.0", "matplotlib=2", "r-matchit=4.*" }
);
return *solver;
}
auto create_scip() -> MSolver&
auto create_scip() -> MSolver
{
static ChannelContext channel_context;
static auto solver = create_conda_forge(channel_context, { "scip=8.*", "pyscipopt<4.0" });
return *solver;
return create_conda_forge({ "scip=8.*", "pyscipopt<4.0" });
}
auto create_jupyterlab() -> MSolver&
auto create_double_python() -> MSolver
{
static ChannelContext channel_context;
static auto solver = create_conda_forge(channel_context, { "jupyterlab=3.4", "openssl=3.0.0" });
return *solver;
return create_conda_forge({ "python=3.9.*", "python=3.10.*" });
}
auto create_double_python() -> MSolver&
auto create_numba() -> MSolver
{
static ChannelContext channel_context;
static auto solver = create_conda_forge(channel_context, { "python=3.9.*", "python=3.10.*" });
return *solver;
}
auto create_numba() -> MSolver&
{
static ChannelContext channel_context;
static auto solver = create_conda_forge(channel_context, { "python=3.11", "numba<0.56" });
return *solver;
return create_conda_forge({ "python=3.11", "numba<0.56" });
}
template <typename NodeVariant>
@ -490,165 +442,155 @@ namespace mamba
node
);
};
}
namespace
TEST_CASE("NamedList")
{
auto l = CompressedProblemsGraph::PackageListNode();
static constexpr std::size_t n_packages = 9;
for (std::size_t minor = 1; minor <= n_packages; ++minor)
{
std::vector<decltype(&create_basic_conflict)> pb_values = {
create_basic_conflict, create_pubgrub, create_pubgrub_hard, create_pubgrub_missing,
create_pytorch_cpu, create_pytorch_cuda, create_cudatoolkit, create_jpeg9b,
create_r_base, create_scip, create_jupyterlab, create_double_python,
create_numba
};
l.insert({ mkpkg("pkg", fmt::format("0.{}.0", minor)) });
}
TEST_SUITE("satifiability_error")
CHECK_EQ(l.size(), n_packages);
CHECK_EQ(l.name(), "pkg");
{
TEST_CASE("NamedList")
{
auto l = CompressedProblemsGraph::PackageListNode();
static constexpr std::size_t n_packages = 9;
for (std::size_t minor = 1; minor <= n_packages; ++minor)
{
l.insert({ mkpkg("pkg", fmt::format("0.{}.0", minor)) });
}
CHECK_EQ(l.size(), n_packages);
CHECK_EQ(l.name(), "pkg");
{
auto [str, size] = l.versions_trunc(", ", "...", 5);
CHECK_EQ(size, 9);
CHECK_EQ(str, "0.1.0, 0.2.0, ..., 0.9.0");
}
{
auto [str, size] = l.build_strings_trunc(", ", "...", 5, false);
CHECK_EQ(size, 9);
CHECK_EQ(str, "bld, bld, ..., bld");
}
{
auto [str, size] = l.build_strings_trunc(", ", "...", 5, true);
CHECK_EQ(size, 1);
CHECK_EQ(str, "bld");
}
{
auto [str, size] = l.versions_and_build_strings_trunc("|", "---", 5);
CHECK_EQ(size, 9);
CHECK_EQ(str, "0.1.0 bld|0.2.0 bld|---|0.9.0 bld");
}
}
auto [str, size] = l.versions_trunc(", ", "...", 5);
CHECK_EQ(size, 9);
CHECK_EQ(str, "0.1.0, 0.2.0, ..., 0.9.0");
}
{
auto [str, size] = l.build_strings_trunc(", ", "...", 5, false);
CHECK_EQ(size, 9);
CHECK_EQ(str, "bld, bld, ..., bld");
}
{
auto [str, size] = l.build_strings_trunc(", ", "...", 5, true);
CHECK_EQ(size, 1);
CHECK_EQ(str, "bld");
}
{
auto [str, size] = l.versions_and_build_strings_trunc("|", "---", 5);
CHECK_EQ(size, 9);
CHECK_EQ(str, "0.1.0 bld|0.2.0 bld|---|0.9.0 bld");
}
}
TEST_CASE("constructor")
{
for (auto& p : pb_values)
TEST_CASE("Create problem graph")
{
using PbGr = ProblemsGraph;
using CpPbGr = CompressedProblemsGraph;
const auto issues = std::array{
std::pair{ "Basic conflict", &create_basic_conflict },
std::pair{ "PubGrub example", &create_pubgrub },
std::pair{ "Harder PubGrub example", &create_pubgrub_hard },
std::pair{ "PubGrub example with missing packages", &create_pubgrub_missing },
std::pair{ "PyTorch CPU", &create_pytorch_cpu },
std::pair{ "PyTorch Cuda", &create_pytorch_cuda },
std::pair{ "Cuda Toolkit", &create_cudatoolkit },
std::pair{ "Jpeg", &create_jpeg9b },
std::pair{ "R base", &create_r_base },
std::pair{ "SCIP", &create_scip },
std::pair{ "Two different Python", &create_double_python },
std::pair{ "Numba", &create_numba },
};
for (const auto& [name, factory] : issues)
{
// Somehow the capture does not work directly on ``name``
std::string_view name_copy = name;
CAPTURE(name_copy);
auto solver = factory();
const auto solved = solver.try_solve();
REQUIRE_FALSE(solved);
const auto pbs_init = solver.problems_graph();
const auto& graph_init = pbs_init.graph();
REQUIRE_GE(graph_init.number_of_nodes(), 1);
graph_init.for_each_node_id(
[&](auto id)
{
CAPTURE(p);
auto& solver = p();
const auto solved = solver.try_solve();
REQUIRE_FALSE(solved);
const auto pbs = ProblemsGraph::from_solver(solver, solver.pool());
const auto& g = pbs.graph();
REQUIRE_GE(g.number_of_nodes(), 1);
g.for_each_node_id(
[&](auto id)
{
const auto& node = g.node(id);
// Currently we do not make assumption about virtual package since
// we are not sure we are including them the same way than they would be in
// practice
if (!is_virtual_package(node))
{
if (g.in_degree(id) == 0)
{
// Only one root node
CHECK_EQ(id, pbs.root_node());
CHECK(std::holds_alternative<ProblemsGraph::RootNode>(node));
}
else if (g.out_degree(id) == 0)
{
CHECK_FALSE(std::holds_alternative<ProblemsGraph::RootNode>(node));
}
else
{
CHECK(std::holds_alternative<ProblemsGraph::PackageNode>(node));
}
// All nodes reachable from the root
CHECK(is_reachable(pbs.graph(), pbs.root_node(), id));
}
}
);
const auto& conflicts = pbs.conflicts();
for (const auto& [n, _] : conflicts)
const auto& node = graph_init.node(id);
// Currently we do not make assumption about virtual package since
// we are not sure we are including them the same way than they would be in
// practice
if (!is_virtual_package(node))
{
bool tmp = std::holds_alternative<ProblemsGraph::PackageNode>(g.node(n))
|| std::holds_alternative<ProblemsGraph::ConstraintNode>(g.node(n));
CHECK(tmp);
if (graph_init.in_degree(id) == 0)
{
// Only one root node
CHECK_EQ(id, pbs_init.root_node());
CHECK(std::holds_alternative<PbGr::RootNode>(node));
}
else if (graph_init.out_degree(id) == 0)
{
CHECK_FALSE(std::holds_alternative<PbGr::RootNode>(node));
}
else
{
CHECK(std::holds_alternative<PbGr::PackageNode>(node));
}
// All nodes reachable from the root
CHECK(is_reachable(pbs_init.graph(), pbs_init.root_node(), id));
}
}
);
const auto& conflicts_init = pbs_init.conflicts();
for (const auto& [n, _] : conflicts_init)
{
bool tmp = std::holds_alternative<PbGr::PackageNode>(graph_init.node(n))
|| std::holds_alternative<PbGr::ConstraintNode>(graph_init.node(n));
CHECK(tmp);
}
TEST_CASE("simplify_conflicts")
SUBCASE("Simplify conflicts")
{
for (auto& p : pb_values)
const auto& pbs_simplified = simplify_conflicts(pbs_init);
const auto& graph_simplified = pbs_simplified.graph();
REQUIRE_GE(graph_simplified.number_of_nodes(), 1);
REQUIRE_LE(graph_simplified.number_of_nodes(), pbs_init.graph().number_of_nodes());
for (const auto& [id, _] : pbs_simplified.conflicts())
{
CAPTURE(p);
auto& solver = p();
const auto solved = solver.try_solve();
REQUIRE_FALSE(solved);
const auto& pbs = ProblemsGraph::from_solver(solver, solver.pool());
const auto& pbs_simplified = simplify_conflicts(pbs);
const auto& graph_simplified = pbs_simplified.graph();
REQUIRE_GE(graph_simplified.number_of_nodes(), 1);
REQUIRE_LE(graph_simplified.number_of_nodes(), pbs.graph().number_of_nodes());
for (const auto& [id, _] : pbs_simplified.conflicts())
const auto& node = graph_simplified.node(id);
// Currently we do not make assumption about virtual package since
// we are not sure we are including them the same way than they would be in
// practice
if (!is_virtual_package(node))
{
const auto& node = graph_simplified.node(id);
// Currently we do not make assumption about virtual package since
// we are not sure we are including them the same way than they would be in
// practice
if (!is_virtual_package(node))
{
CHECK(graph_simplified.has_node(id));
// Unfortunately not all conflicts are on leaves
// CHECK_EQ(graph_simplified.out_degree(id), 0);
CHECK(is_reachable(graph_simplified, pbs_simplified.root_node(), id));
}
CHECK(graph_simplified.has_node(id));
// Unfortunately not all conflicts are on leaves
// CHECK_EQ(graph_simplified.out_degree(id), 0);
CHECK(is_reachable(graph_simplified, pbs_simplified.root_node(), id));
}
}
}
TEST_CASE("compression")
{
using CpPbGr = CompressedProblemsGraph;
for (auto& p : pb_values)
SUBCASE("Compress graph")
{
CAPTURE(p);
auto& solver = p();
const auto solved = solver.try_solve();
REQUIRE_FALSE(solved);
const auto pbs = ProblemsGraph::from_solver(solver, solver.pool());
const auto cp_pbs = CpPbGr::from_problems_graph(simplify_conflicts(pbs));
const auto& cp_g = cp_pbs.graph();
const auto pbs_comp = CpPbGr::from_problems_graph(pbs_simplified);
const auto& graph_comp = pbs_comp.graph();
REQUIRE_GE(pbs.graph().number_of_nodes(), cp_g.number_of_nodes());
REQUIRE_GE(cp_g.number_of_nodes(), 1);
cp_g.for_each_node_id(
REQUIRE_GE(pbs_init.graph().number_of_nodes(), graph_comp.number_of_nodes());
REQUIRE_GE(graph_comp.number_of_nodes(), 1);
graph_comp.for_each_node_id(
[&](auto id)
{
const auto& node = cp_g.node(id);
const auto& node = graph_comp.node(id);
// Currently we do not make assumption about virtual package since
// we are not sure we are including them the same way than they would be in
// we are not sure we are including them the same way than they
// would be in
if (!is_virtual_package(node))
{
if (cp_g.in_degree(id) == 0)
if (graph_comp.in_degree(id) == 0)
{
// Only one root node
CHECK_EQ(id, pbs.root_node());
CHECK_EQ(id, pbs_init.root_node());
CHECK(std::holds_alternative<CpPbGr::RootNode>(node));
}
else if (cp_g.out_degree(id) == 0)
else if (graph_comp.out_degree(id) == 0)
{
CHECK_FALSE(std::holds_alternative<CpPbGr::RootNode>(node));
}
@ -657,51 +599,43 @@ namespace mamba
CHECK(std::holds_alternative<CpPbGr::PackageListNode>(node));
}
// All nodes reachable from the root
CHECK(is_reachable(cp_g, cp_pbs.root_node(), id));
CHECK(is_reachable(graph_comp, pbs_comp.root_node(), id));
}
}
);
const auto& conflicts = cp_pbs.conflicts();
for (const auto& [n, _] : conflicts)
const auto& conflicts_comp = pbs_comp.conflicts();
for (const auto& [n, _] : conflicts_comp)
{
bool tmp = std::holds_alternative<CpPbGr::PackageListNode>(cp_g.node(n))
|| std::holds_alternative<CpPbGr::ConstraintListNode>(cp_g.node(n));
bool tmp = std::holds_alternative<CpPbGr::PackageListNode>(graph_comp.node(n))
|| std::holds_alternative<CpPbGr::ConstraintListNode>(graph_comp.node(n
));
CHECK(tmp);
}
}
}
TEST_CASE("problem_tree_str")
{
using CpPbGr = CompressedProblemsGraph;
for (auto& p : pb_values)
{
CAPTURE(p);
auto& solver = p();
const auto solved = solver.try_solve();
REQUIRE_FALSE(solved);
const auto pbs = ProblemsGraph::from_solver(solver, solver.pool());
const auto cp_pbs = CpPbGr::from_problems_graph(simplify_conflicts(pbs));
const auto message = problem_tree_msg(cp_pbs);
auto message_contains = [&message](const auto& node)
SUBCASE("Compose error message")
{
using Node = std::remove_cv_t<std::remove_reference_t<decltype(node)>>;
if constexpr (!std::is_same_v<Node, CpPbGr::RootNode>)
{
CHECK(contains(message, node.name()));
}
};
const auto message = problem_tree_msg(pbs_comp);
cp_pbs.graph().for_each_node_id(
[&message_contains, &g = cp_pbs.graph()](auto id)
auto message_contains = [&message](const auto& node)
{
std::visit(message_contains, g.node(id)); //
}
);
using Node = std::remove_cv_t<std::remove_reference_t<decltype(node)>>;
if constexpr (!std::is_same_v<Node, CpPbGr::RootNode>)
{
CHECK(contains(message, node.name()));
}
};
pbs_comp.graph().for_each_node_id(
[&message_contains, &g = pbs_comp.graph()](auto id)
{
std::visit(message_contains, g.node(id)); //
}
);
}
}
}
}
}
TEST_SUITE_END();

View File

@ -278,7 +278,15 @@ PYBIND11_MODULE(bindings, m)
.def("clear", [](PbGraph::conflicts_t& self) { return self.clear(); })
.def("add", &PbGraph::conflicts_t::add);
pyPbGraph.def_static("from_solver", &PbGraph::from_solver)
pyPbGraph
.def_static(
"from_solver",
[](const MSolver& solver, const MPool& /* pool */)
{
deprecated("Use Solver.problems_graph() instead");
return solver.problems_graph();
}
)
.def("root_node", &PbGraph::root_node)
.def("conflicts", &PbGraph::conflicts)
.def(