Finalized Solver bindings and add solver doc (#3195)

* Fix cross reference in docs

* Rename Request::Item > Job

* Add missing Request python test

* Add Solver usage documentation

* Add tests for ProblemsGraph

* Adjust 2.0 changes

* use mambatest directories

* Remove channel from test repodata.json

* Add simple solver tests
This commit is contained in:
Antoine Prouvost 2024-02-21 17:27:35 +01:00 committed by GitHub
parent 3a6d010e38
commit 6d535ea5ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 698 additions and 161 deletions

View File

@ -0,0 +1,9 @@
``mamba::solver``
=================
The ``mamba::solver`` namespace contains the packages solver and related objects.
This particular namespace is a generic common interface, while the subnamespace
``mamba::solver::libsolv`` contains `LibSolv <https://en.opensuse.org/openSUSE:Libzypp_satsolver>`_
specific implementations.
.. doxygennamespace:: mamba::solver

View File

@ -59,25 +59,40 @@ Changes inlcude:
- The global ``Context``, previously available through ``Context()``, must now be accessed through
``Context.instance()``.
What's more, it is required to be passed explicitly in a few more functions.
Future version of ``libmambapy`` will continue in this direction until there are no global context.In version 2, ``Context()`` will throw an exception to avoid hard to catch errors.
Future version of ``libmambapy`` will continue in this direction until there are no global context.
In version 2, ``Context()`` will throw an exception to avoid hard to catch errors.
- ``ChannelContext`` is no longer an implicit global variable.
It must be constructed with one of ``ChannelContext.make_simple`` or
``ChannelContext.make_conda_compatible`` (with ``Context.instance`` as argument in most cases)
and passed explicitly to a few functions.
- ``Channel`` has been redesigned an moved to a new ``libmambapy.specs``.
A featureful ``libmambapy.specs.CondaURL`` is used to describe channel URLs.
This module also includes ``UnresolvedChannel`` used to describe unresolved channel strings.
- ``MatchSpec`` has been redesigned and moved to ``libmambapy.specs``.
The module also includes a platform enumeration, an implementation of ordered ``Version``, and a
``VersionSpec`` to match versions.
- ``PackageInfo`` has been moved to ``libmambapy.specs``.
Some attributes have been given a more explicit name ``fn`` > ``filename``,
``url`` > ``package_url``.
- ``Repo`` has been redesigned into a lightweight ``RepoInfo`` and moved to
``libmambapy.solver.libsolv``.
The creation and modification of repos happens through the ``Pool``, with methods such as
``Pool.add_repo_from_repodata_json`` and ``Pool.add_repo_from_packages``, but also high-level
free functions such as ``load_subdir_in_pool`` and ``load_installed_packages_in_pool``.
- A new ``Context`` independent submodule ``libmambapy.specs`` has been introduced with:
- The redesign of the ``Channel`` and a new ``UnresolvedChannel`` used to describe unresolved
channel strings.
A featureful ``libmambapy.specs.CondaURL`` is used to describe channel URLs.
- The redesign``MatchSpec``.
The module also includes a platform enumeration, an implementation of ordered ``Version``,
and a ``VersionSpec`` to match versions.
- ``PackageInfo`` has been moved to this submodule.
Some attributes have been given a more explicit name ``fn`` > ``filename``,
``url`` > ``package_url``.
- A new ``Context`` independent submodule ``libmambapy.solver`` has been introduced with the
changes below.
A usage documentation page is available at
https://mamba.readthedocs.io/en/latest/usage/solver.html
- The redesign of the ``Pool``, which is now available as ``libmambapy.solver.libsolv.Database``.
The new interfaces makes it easier to create repositories without using other ``libmambapy``
objects.
- ``Repo`` has been redesigned into a lightweight ``RepoInfo`` and moved to
``libmambapy.solver.libsolv``.
The creation and modification of repos happens through the ``Database``, with methods such as
``Database.add_repo_from_repodata_json`` and ``Database.add_repo_from_packages``, but also
high-level free functions such as ``load_subdir_in_database`` and
``load_installed_packages_in_database``.
- The ``Solver`` has been moved to ``libmambapy.solver.libsolv.Solver``.
- All jobs, pins, and flags must be passed as a single ``libmambapy.solver.Request``.
- The outcome of solving the request is either a ``libmambapy.solver.Solution`` or a
``libmambapy.solver.libsolv.Unsolvable`` state from which rich error messages can be
extracted.
.. TODO include final decision for Channels as URLs.
@ -99,14 +114,19 @@ The main changes are:
- A cleanup of ``ChannelContext`` for be a light proxy and parameter holder wrapping the
``specs::Channel``.
- A new ``repodata.json`` parser using `simdjson <https://simdjson.org/>`_.
- [WIP] Creation of the ``solver::libsolv`` sub-namespace for full isolation of libsolv, and a
- The ``MPool``, ``MRepo`` and ``MSolver`` API has been completely redesigned into a ``solver``
subnamespace and works independently of the ``Context``.
The ``solver::libsolv`` sub-namespace has also been added for full isolation of libsolv, and a
solver API without ``Context``.
It currently contains:
The ``solver`` API redesign includes:
- A refactoring of the ``MPool`` as a ``DataBase``, fully isolates libsolv, and simplifies
repository creation.
- A refactoring and thinning of ``MRepo`` as a new ``RepoInfo``.
- A refactoring of ``Solver``, whose outcome is split between a ``Solution`` and an
``UnSolvable`` state.
- The ``ProblemsGraph`` reach SAT error state.
- A solver ``Request`` with all requirements to solve is the new way to specify jobs.
- A refactoring of ``Solver``.
- A solver outcome as either a ``Solution`` or an ``UnSolvable`` state.
A usage documentation (in Python) is available at
https://mamba.readthedocs.io/en/latest/usage/solver.html
- Improved downloaders.
.. TODO OCI registry

View File

@ -57,6 +57,7 @@ You can try Mamba now by visiting the installation for
python_api
usage/specs
usage/solver
.. toctree::
:caption: API REFERENCE
@ -64,6 +65,7 @@ You can try Mamba now by visiting the installation for
:hidden:
api/specs
api/solver
.. toctree::
:caption: DEVELOPER ZONE

View File

@ -0,0 +1,183 @@
Solving Package Environments
============================
.. |MatchSpec| replace:: :cpp:type:`MatchSpec <mamba::specs::MatchSpec>`
.. |PackageInfo| replace:: :cpp:type:`PackageInfo <mamba::specs::PackageInfo>`
.. |Database| replace:: :cpp:type:`Database <mamba::solver::libsolv::Database>`
.. |Request| replace:: :cpp:type:`Request <mamba::solver::Request>`
.. |Solver| replace:: :cpp:type:`Solver <mamba::solver::libsolv::Solver>`
.. |Solution| replace:: :cpp:type:`Solution <mamba::solver::Solution>`
.. |UnSolvable| replace:: :cpp:type:`UnSolvable <mamba::solver::libsolv::UnSolvable>`
The :any:`libmambapy.solver <mamba::solver>` submodule contains a generic API for solving
requirements (|MatchSpec|) into a list of packages (|PackageInfo|) with no conflicting dependencies.
This problem is hard to solve (`NP-complete <https://en.wikipedia.org/wiki/NP-completeness>`_) which
is why Mamba uses a `SAT solver <https://en.wikipedia.org/wiki/SAT_solver>`_ to do so.
.. note::
There is currently only one solver available in Mamba:
`LibSolv <https://en.opensuse.org/openSUSE:Libzypp_satsolver>`_. For this reason, the generic
interface has not been fully completed and users need to access the submodule
:any:`libmambapy.solver.libsolv <mamba::solver::libsolv>` for certain types.
Populating the Package Database
-------------------------------
The first thing needed is a |Database| of all the packages and their dependencies.
Packages are organised in repositories, described by a
:cpp:type:`RepoInfo <mamba::solver::libsolv::RepoInfo>`.
This serves to resolve explicit channel requirements or channel priority.
As such, the database constructor takes a set of
:cpp:type:`ChannelResolveParams <mamba::specs::ChannelResolveParams>`
to work with :cpp:type:`Channel <mamba::specs::Channel>` work with Channel data
internaly (see `the usage section on Channels <libmamba_usage_channel>`_ for more
information).
The first way to add a repository is from a list of |PackageInfo| using
:cpp:func:`DataBase.add_repo_from_packages <mamba::solver::libsolv::Database::add_repo_from_packages>`:
.. code:: python
import libmambapy
db = libmambapy.solver.libsolv.Database(
libmambapy.specs.ChannelResolveParams(channel_alias="https://conda.anaconda.org")
)
repo1 = db.add_repo_from_packages(
packages=[
libmambapy.specs.PackageInfo(name="python", version="3.8", ...),
libmambapy.specs.PackageInfo(name="pip", version="3.9", ...),
...,
],
name="myrepo",
)
The second way of loading packages is throuch Conda's reposoitory index format ``repodata.json``
using
:cpp:func:`DataBase.add_repo_from_repodata <mamba::solver::libsolv::Database::add_repo_from_repodata>`.
This is meant as a convenience and performant alternative to the former method, since these files
grow large.
.. code:: python
repo2 = db.add_repo_from_repodata(
path="path/to/repodata.json",
url="htts://conda.anaconda.org/conda-forge/linux-64",
)
One of the reppository can be set to have a special meaning of "installed repository".
It is used as a reference point in the solver to compute changes.
For instance if a package is required but is already available in the installed repo, the solving
result will not mention it.
The function
:cpp:func:`DataBase.set_installed_repo <mamba::solver::libsolv::Database::set_installed_repo>` is
used for that purpose.
.. code:: python
db.set_installed_repo(repo1)
Binary serialization of the database (Advanced)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The |Database| reporitories can be serialized in binary format for faster reloading.
To ensure integrity and freshness of the serialized file, metadata about the packages,
such as source url and
:cpp:type:`RepodataOrigin <mamba::solver::libsolv::RepodataOrigin>`, are stored inside the
file when calling
:cpp:func:`DataBase.native_serialize_repo <mamba::solver::libsolv::Database::native_serialize_repo>` .
Upon reading, similar parameters are expected as input to
:cpp:func:`DataBase.add_repo_from_native_serialization <mamba::solver::libsolv::Database::add_repo_from_native_serialization>`.
If they mistmatch, the loading results in an error.
A typical wokflow first tries to load a repository from such binary cache, and then quietly
fallbacks to ``repodata.json`` on failure.
Creating a solving request
--------------------------
All jobs that need to be resolved are added as part of a |Request|.
This includes installing, updating, removing packages, as well as solving cutomization parameters.
.. code:: python
Request = libmambapy.solver.Request
MatchSpec = libmambapy.specs.MatchSpec
request = Request(
jobs=[
Request.Install(MatchSpec.parse("python>=3.9")),
Request.Update(MatchSpec.parse("numpy")),
Request.Remove(MatchSpec.parse("pandas"), clean_dependencies=False),
],
flags=Request.Flags(
allow_downgrade=True,
allow_uninstall=True,
),
)
Solving the request
-------------------
The |Request| and the |Database| are the two input parameters needed to solve an environment.
This task is achieve with the :cpp:func:`Solver.solve <mamba::solver::libsolv::Solver::solve>`
method.
.. code:: python
solver = libmambapy.solver.libsolv.Solver()
outcome = solver.solve(db, request)
The outcome can be of two types, either a |Solution| listing packages (|Packageinfo|) and the
action to take on them (install, remove...), or an |UnSolvable| type when no solution exist
(because of conflict, missing pacakges...).
Examine the solution
~~~~~~~~~~~~~~~~~~~~
We can test if a valid solution exists by checking the type of the outcome.
The attribute :cpp:member:`Solution.actions <mamba::solver::Solution::actions>` contains the actions
to take on the installed repository so that it satisfies the |Request| requirements.
.. code:: python
Solution = libmambapy.solver.Solution
if isinstance(outcome, Solution):
for action in outcome.actions:
if isinstance(action, Solution.Upgrade):
my_upgrade(from_pkg=action.remove, to_pkg=action.install)
if isinstance(action, Solution.Reinstall):
...
...
Alternatively, an easy way to compute the update to the environment is to check for ``install`` and
``remove`` members, since they will populate the relevant fields for all actions:
.. code:: python
Solution = libmambapy.solver.Solution
if isinstance(outcome, Solution):
for action in outcome.actions:
if hasattr(action, "install"):
my_download_and_install(action.install)
# WARN: Do not use `elif` since actions like `Upgrade`
# are represented as an `install` and `remove` pair.
if hasattr(action, "remove"):
my_delete(action.remove)
Understand unsolvable problems
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When a problem as no |Solution|, it is inherenty hard to come up with an explanation.
In the easiest case, a requiered package is missing from the |Database|.
In the most complex, many package dependencies are incompatible without a single culprit.
In this case, packages should be rebuild with weaker requirements, or with more build variants.
The |UnSolvable| class attempts to build an explanation.
The :cpp:func:`UnSolvable.problems <mamba::solver::libsolv::UnSolvable::problems>` is a list
of problems, as defined by the solver.
It is not easy to understand without linking it to specific |MatchSpec| and |PackageInfo|.
The method
:cpp:func:`UnSolvable.problems_graph <mamba::solver::libsolv::UnSolvable::problems_graph>`
gives a more structured graph of package dependencies and incompatibilities.
This graph is the underlying mechanism used in
:cpp:func:`UnSolvable.explain_problems <mamba::solver::libsolv::UnSolvable::explain_problems>`
to build a detail unsolvability message.

View File

@ -1,7 +1,7 @@
Describing Conda Objects
========================
The ``libmambapy.specs`` submodule contains object to *describe* abstraction in the Conda ecosystem.
The :any:`libmambapy.specs <mamba::specs>` submodule contains object to *describe* abstraction in the Conda ecosystem.
They are purely functional and do not have any observable impact on the user system.
For instance :cpp:type:`libmambapy.specs.Channel <mamba::specs::Channel>` is used to describe a
channel but does not download any file.
@ -124,6 +124,7 @@ Dynamic platforms (as in not known by Mamba) can only be detected with the ``[]`
assert uc.type == specs.UnresolvedChannel.Type.Name
.. _libmamba_usage_channel:
Channel
-------
The :cpp:type:`Channel <mamba::specs::Channel>` are represented by a

View File

@ -79,17 +79,17 @@ namespace mamba::solver
specs::MatchSpec spec;
};
using Item = std::variant<Install, Remove, Update, UpdateAll, Keep, Freeze, Pin>;
using item_list = std::vector<Item>;
using Job = std::variant<Install, Remove, Update, UpdateAll, Keep, Freeze, Pin>;
using job_list = std::vector<Job>;
Flags flags = {};
item_list items = {};
job_list jobs = {};
};
template <typename... Item, typename Func>
void for_each_of(const Request& request, Func&& func)
{
for (const auto& unknown_itm : request.items)
for (const auto& unknown_job : request.jobs)
{
const auto control = std::visit(
[&](const auto& itm) -> util::LoopControl
@ -108,7 +108,7 @@ namespace mamba::solver
}
return util::LoopControl::Continue;
},
unknown_itm
unknown_job
);
if (control == util::LoopControl::Break)
{

View File

@ -406,7 +406,7 @@ namespace mamba
const auto& prefix_pkgs = prefix_data.records();
auto request = Request();
request.items.reserve(specs.size() + freeze_installed * prefix_pkgs.size());
request.jobs.reserve(specs.size() + freeze_installed * prefix_pkgs.size());
// Consider if a FreezeAll type in Request is relevant?
if (freeze_installed && !prefix_pkgs.empty())
@ -414,13 +414,13 @@ namespace mamba
LOG_INFO << "Locking environment: " << prefix_pkgs.size() << " packages freezed";
for (const auto& [name, pkg] : prefix_pkgs)
{
request.items.emplace_back(Request::Freeze{ specs::MatchSpec::parse(name) });
request.jobs.emplace_back(Request::Freeze{ specs::MatchSpec::parse(name) });
}
}
for (const auto& s : specs)
{
request.items.emplace_back(Request::Install{ specs::MatchSpec::parse(s) });
request.jobs.emplace_back(Request::Install{ specs::MatchSpec::parse(s) });
}
return request;
}
@ -436,18 +436,16 @@ namespace mamba
{
using Request = solver::Request;
request.items.reserve(
request.items.size() + (!no_pin) * ctx.pinned_packages.size() + !no_py_pin
);
request.jobs.reserve(request.jobs.size() + (!no_pin) * ctx.pinned_packages.size() + !no_py_pin);
if (!no_pin)
{
for (const auto& pin : file_pins(prefix_data.path() / "conda-meta" / "pinned"))
{
request.items.emplace_back(Request::Pin{ specs::MatchSpec::parse(pin) });
request.jobs.emplace_back(Request::Pin{ specs::MatchSpec::parse(pin) });
}
for (const auto& pin : ctx.pinned_packages)
{
request.items.emplace_back(Request::Pin{ specs::MatchSpec::parse(pin) });
request.jobs.emplace_back(Request::Pin{ specs::MatchSpec::parse(pin) });
}
}
@ -456,14 +454,14 @@ namespace mamba
auto py_pin = python_pin(prefix_data, specs);
if (!py_pin.empty())
{
request.items.emplace_back(Request::Pin{ specs::MatchSpec::parse(py_pin) });
request.jobs.emplace_back(Request::Pin{ specs::MatchSpec::parse(py_pin) });
}
}
}
void print_request_pins_to(const solver::Request& request, std::ostream& out)
{
for (const auto& req : request.items)
for (const auto& req : request.jobs)
{
bool first = true;
std::visit(

View File

@ -76,24 +76,24 @@ namespace mamba
using Request = solver::Request;
auto request = Request();
request.items.reserve(raw_specs.size());
request.jobs.reserve(raw_specs.size());
if (prune)
{
History history(ctx.prefix_params.target_prefix, channel_context);
auto hist_map = history.get_requested_specs_map();
request.items.reserve(request.items.capacity() + hist_map.size());
request.jobs.reserve(request.jobs.capacity() + hist_map.size());
for (auto& [name, spec] : hist_map)
{
request.items.emplace_back(Request::Keep{ std::move(spec) });
request.jobs.emplace_back(Request::Keep{ std::move(spec) });
}
}
for (const auto& s : raw_specs)
{
request.items.emplace_back(Request::Remove{
request.jobs.emplace_back(Request::Remove{
specs::MatchSpec::parse(s),
/* .clean_dependencies= */ prune,
});

View File

@ -39,22 +39,22 @@ namespace mamba
if (prune_deps)
{
auto hist_map = prefix_data.history().get_requested_specs_map();
request.items.reserve(hist_map.size() + 1);
request.jobs.reserve(hist_map.size() + 1);
for (auto& [name, spec] : hist_map)
{
request.items.emplace_back(Request::Keep{ std::move(spec) });
request.jobs.emplace_back(Request::Keep{ std::move(spec) });
}
request.items.emplace_back(Request::UpdateAll{ /* .clean_dependencies= */ true });
request.jobs.emplace_back(Request::UpdateAll{ /* .clean_dependencies= */ true });
}
else
{
request.items.emplace_back(Request::UpdateAll{ /* .clean_dependencies= */ false });
request.jobs.emplace_back(Request::UpdateAll{ /* .clean_dependencies= */ false });
}
}
else
{
request.items.reserve(specs.size());
request.jobs.reserve(specs.size());
if (remove_not_specified)
{
auto hist_map = prefix_data.history().get_requested_specs_map();
@ -63,7 +63,7 @@ namespace mamba
if (std::find(specs.begin(), specs.end(), it.second.name().str())
== specs.end())
{
request.items.emplace_back(Request::Remove{
request.jobs.emplace_back(Request::Remove{
specs::MatchSpec::parse(it.second.name().str()),
/* .clean_dependencies= */ true,
});
@ -73,7 +73,7 @@ namespace mamba
for (const auto& raw_ms : specs)
{
request.items.emplace_back(Request::Update{
request.jobs.emplace_back(Request::Update{
specs::MatchSpec::parse(raw_ms),
});
}

View File

@ -963,11 +963,11 @@ namespace mamba::solver::libsolv
};
auto iter = std::find_if(
request.items.cbegin(),
request.items.cend(),
request.jobs.cbegin(),
request.jobs.cend(),
[&](const auto& unknown_job) { return std::visit(job_matches, unknown_job); }
);
return iter != request.items.cend();
return iter != request.jobs.cend();
}
}
@ -1177,83 +1177,84 @@ namespace mamba::solver::libsolv
.transform([&](auto id) { jobs.push_back(install_flag, id); });
}
template <typename Item>
template <typename Job>
[[nodiscard]] auto add_job(
const Item& item,
solv::ObjQueue& jobs,
const Job& job,
solv::ObjQueue& raw_jobs,
solv::ObjPool& pool,
const specs::ChannelResolveParams& params,
bool force_reinstall
) -> expected_t<void>
{
if constexpr (std::is_same_v<Item, Request::Install>)
if constexpr (std::is_same_v<Job, Request::Install>)
{
if (force_reinstall)
{
return add_reinstall_job(jobs, pool, item.spec, params);
return add_reinstall_job(raw_jobs, pool, job.spec, params);
}
else
{
return pool_add_matchspec(pool, item.spec, params)
.transform([&](auto id)
{ jobs.push_back(SOLVER_INSTALL | SOLVER_SOLVABLE_PROVIDES, id); }
return pool_add_matchspec(pool, job.spec, params)
.transform(
[&](auto id)
{ raw_jobs.push_back(SOLVER_INSTALL | SOLVER_SOLVABLE_PROVIDES, id); }
);
}
}
if constexpr (std::is_same_v<Item, Request::Remove>)
if constexpr (std::is_same_v<Job, Request::Remove>)
{
return pool_add_matchspec(pool, item.spec, params)
return pool_add_matchspec(pool, job.spec, params)
.transform(
[&](auto id)
{
jobs.push_back(
raw_jobs.push_back(
SOLVER_ERASE | SOLVER_SOLVABLE_PROVIDES
| (item.clean_dependencies ? SOLVER_CLEANDEPS : 0),
| (job.clean_dependencies ? SOLVER_CLEANDEPS : 0),
id
);
}
);
}
if constexpr (std::is_same_v<Item, Request::Update>)
if constexpr (std::is_same_v<Job, Request::Update>)
{
return pool_add_matchspec(pool, item.spec, params)
return pool_add_matchspec(pool, job.spec, params)
.transform(
[&](auto id)
{
// TODO: ignoring update specs here for now
if (!item.spec.is_simple())
if (!job.spec.is_simple())
{
jobs.push_back(SOLVER_INSTALL | SOLVER_SOLVABLE_PROVIDES, id);
raw_jobs.push_back(SOLVER_INSTALL | SOLVER_SOLVABLE_PROVIDES, id);
}
jobs.push_back(SOLVER_UPDATE | SOLVER_SOLVABLE_PROVIDES, id);
raw_jobs.push_back(SOLVER_UPDATE | SOLVER_SOLVABLE_PROVIDES, id);
}
);
}
if constexpr (std::is_same_v<Item, Request::UpdateAll>)
if constexpr (std::is_same_v<Job, Request::UpdateAll>)
{
jobs.push_back(
raw_jobs.push_back(
SOLVER_UPDATE | SOLVER_SOLVABLE_ALL
| (item.clean_dependencies ? SOLVER_CLEANDEPS : 0),
| (job.clean_dependencies ? SOLVER_CLEANDEPS : 0),
0
);
return {};
}
if constexpr (std::is_same_v<Item, Request::Freeze>)
if constexpr (std::is_same_v<Job, Request::Freeze>)
{
return pool_add_matchspec(pool, item.spec, params)
.transform([&](auto id) { jobs.push_back(SOLVER_LOCK, id); });
return pool_add_matchspec(pool, job.spec, params)
.transform([&](auto id) { raw_jobs.push_back(SOLVER_LOCK, id); });
}
if constexpr (std::is_same_v<Item, Request::Keep>)
if constexpr (std::is_same_v<Job, Request::Keep>)
{
jobs.push_back(
raw_jobs.push_back(
SOLVER_USERINSTALLED,
pool_add_matchspec(pool, item.spec, params).value()
pool_add_matchspec(pool, job.spec, params).value()
);
return {};
}
if constexpr (std::is_same_v<Item, Request::Pin>)
if constexpr (std::is_same_v<Job, Request::Pin>)
{
return pool_add_pin(pool, item.spec, params)
return pool_add_pin(pool, job.spec, params)
.transform(
[&](solv::ObjSolvableView pin_solv)
{
@ -1261,9 +1262,9 @@ namespace mamba::solver::libsolv
// WARNING keep separate or libsolv does not understand
// Force verify the dummy solvable dependencies, as this is not the
// default for installed packages.
jobs.push_back(SOLVER_VERIFY, name_id);
raw_jobs.push_back(SOLVER_VERIFY, name_id);
// Lock the dummy solvable so that it stays install.
jobs.push_back(SOLVER_LOCK, name_id);
raw_jobs.push_back(SOLVER_LOCK, name_id);
}
);
}
@ -1282,18 +1283,18 @@ namespace mamba::solver::libsolv
auto solv_jobs = solv::ObjQueue();
auto error = expected_t<void>();
for (const auto& item : request.items)
for (const auto& unknown_job : request.jobs)
{
auto xpt = std::visit(
[&](const auto& r) -> expected_t<void>
[&](const auto& job) -> expected_t<void>
{
if constexpr (std::is_same_v<std::decay_t<decltype(r)>, Request::Pin>)
if constexpr (std::is_same_v<std::decay_t<decltype(job)>, Request::Pin>)
{
return add_job(r, solv_jobs, pool, chan_params, force_reinstall);
return add_job(job, solv_jobs, pool, chan_params, force_reinstall);
}
return {};
},
item
unknown_job
);
if (!xpt)
{
@ -1304,18 +1305,18 @@ namespace mamba::solver::libsolv
// Channel specific MatchSpec write to whatprovides and hence require it is not modified
// afterwards.
pool.create_whatprovides();
for (const auto& item : request.items)
for (const auto& unkown_job : request.jobs)
{
auto xpt = std::visit(
[&](const auto& r) -> expected_t<void>
[&](const auto& job) -> expected_t<void>
{
if constexpr (!std::is_same_v<std::decay_t<decltype(r)>, Request::Pin>)
if constexpr (!std::is_same_v<std::decay_t<decltype(job)>, Request::Pin>)
{
return add_job(r, solv_jobs, pool, chan_params, force_reinstall);
return add_job(job, solv_jobs, pool, chan_params, force_reinstall);
}
return {};
},
item
unkown_job
);
if (!xpt)
{

View File

@ -90,7 +90,7 @@ namespace mamba::solver::libsolv
{
if (request.flags.order_request)
{
std::sort(request.items.begin(), request.items.end(), make_request_cmp());
std::sort(request.jobs.begin(), request.jobs.end(), make_request_cmp());
}
return solve_impl(mpool, request);
}
@ -100,7 +100,7 @@ namespace mamba::solver::libsolv
if (request.flags.order_request)
{
auto sorted_request = request;
std::sort(sorted_request.items.begin(), sorted_request.items.end(), make_request_cmp());
std::sort(sorted_request.jobs.begin(), sorted_request.jobs.end(), make_request_cmp());
return solve_impl(mpool, sorted_request);
}
return solve_impl(mpool, request);

View File

@ -53,6 +53,7 @@ set(
src/solver/test_problems_graph.cpp
# Solver libsolv implementation tests
src/solver/libsolv/test_database.cpp
src/solver/libsolv/test_solver.cpp
# Artifacts validation
src/validation/test_tools.cpp
src/validation/test_update_framework_v0_6.cpp

View File

@ -4,7 +4,6 @@
"build": "conda_forge",
"build_number": 0,
"build_string": "conda_forge",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": null,
"depends": null,
"fn": "_libgcc_mutex-0.1-conda_forge.tar.bz2",
@ -23,7 +22,6 @@
"build": "2_gnu",
"build_number": 16,
"build_string": "2_gnu",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": [
"openmp_impl 9999"
],
@ -47,7 +45,6 @@
"build": "h7f98852_5",
"build_number": 5,
"build_string": "h7f98852_5",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": null,
"depends": [
"libgcc-ng >=9.4.0"
@ -68,7 +65,6 @@
"build": "h166bdaf_0",
"build_number": 0,
"build_string": "h166bdaf_0",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": null,
"depends": [
"libgcc-ng >=12"
@ -91,7 +87,6 @@
"build": "hd590300_5",
"build_number": 5,
"build_string": "hd590300_5",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": null,
"depends": [
"libgcc-ng >=12"
@ -112,7 +107,6 @@
"build": "hbcca054_0",
"build_number": 0,
"build_string": "hbcca054_0",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": null,
"depends": null,
"fn": "ca-certificates-2024.2.2-hbcca054_0.conda",
@ -131,7 +125,6 @@
"build": "h41732ed_0",
"build_number": 0,
"build_string": "h41732ed_0",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": [
"binutils_impl_linux-64 2.40"
],
@ -152,7 +145,6 @@
"build": "21_linux64_openblas",
"build_number": 21,
"build_string": "21_linux64_openblas",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": [
"liblapacke 3.9.0 21_linux64_openblas",
"blas * openblas",
@ -179,7 +171,6 @@
"build": "21_linux64_openblas",
"build_number": 21,
"build_string": "21_linux64_openblas",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": [
"liblapacke 3.9.0 21_linux64_openblas",
"blas * openblas",
@ -204,7 +195,6 @@
"build": "hcb278e6_1",
"build_number": 1,
"build_string": "hcb278e6_1",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": [
"expat 2.5.0.*"
],
@ -227,7 +217,6 @@
"build": "h807b86a_5",
"build_number": 5,
"build_string": "h807b86a_5",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": [
"libgomp 13.2.0 h807b86a_5"
],
@ -251,7 +240,6 @@
"build": "h69a702a_5",
"build_number": 5,
"build_string": "h69a702a_5",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": null,
"depends": [
"libgfortran5 13.2.0 ha4646dd_5"
@ -272,7 +260,6 @@
"build": "ha4646dd_5",
"build_number": 5,
"build_string": "ha4646dd_5",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": [
"libgfortran-ng 13.2.0"
],
@ -295,7 +282,6 @@
"build": "h807b86a_5",
"build_number": 5,
"build_string": "h807b86a_5",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": null,
"depends": [
"_libgcc_mutex 0.1 conda_forge"
@ -316,7 +302,6 @@
"build": "21_linux64_openblas",
"build_number": 21,
"build_string": "21_linux64_openblas",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": [
"liblapacke 3.9.0 21_linux64_openblas",
"libcblas 3.9.0 21_linux64_openblas",
@ -341,7 +326,6 @@
"build": "hd590300_0",
"build_number": 0,
"build_string": "hd590300_0",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": null,
"depends": [
"libgcc-ng >=12"
@ -362,7 +346,6 @@
"build": "pthreads_h413a1c8_0",
"build_number": 0,
"build_string": "pthreads_h413a1c8_0",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": [
"openblas >=0.3.26,<0.3.27.0a0"
],
@ -387,7 +370,6 @@
"build": "h2797004_0",
"build_number": 0,
"build_string": "h2797004_0",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": null,
"depends": [
"libgcc-ng >=12",
@ -409,7 +391,6 @@
"build": "h7e041cc_5",
"build_number": 5,
"build_string": "h7e041cc_5",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": null,
"depends": null,
"fn": "libstdcxx-ng-13.2.0-h7e041cc_5.conda",
@ -428,7 +409,6 @@
"build": "h0b41bf4_0",
"build_number": 0,
"build_string": "h0b41bf4_0",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": null,
"depends": [
"libgcc-ng >=12"
@ -449,7 +429,6 @@
"build": "hd590300_1",
"build_number": 1,
"build_string": "hd590300_1",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": null,
"depends": [
"libgcc-ng >=12"
@ -470,7 +449,6 @@
"build": "hd590300_5",
"build_number": 5,
"build_string": "hd590300_5",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": [
"zlib 1.2.13 *_5"
],
@ -493,7 +471,6 @@
"build": "h59595ed_2",
"build_number": 2,
"build_string": "h59595ed_2",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": null,
"depends": [
"libgcc-ng >=12"
@ -514,7 +491,6 @@
"build": "py312heda63a1_0",
"build_number": 0,
"build_string": "py312heda63a1_0",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": [
"numpy-base <0a0"
],
@ -543,7 +519,6 @@
"build": "hd590300_0",
"build_number": 0,
"build_string": "hd590300_0",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": [
"pyopenssl >=22.1"
],
@ -567,7 +542,6 @@
"build": "pyhd8ed1ab_0",
"build_number": 0,
"build_string": "pyhd8ed1ab_0",
"channel": "https://conda.anaconda.org/conda-forge/noarch",
"constrains": null,
"depends": [
"setuptools",
@ -591,7 +565,6 @@
"build": "hab00c5b_1_cpython",
"build_number": 1,
"build_string": "hab00c5b_1_cpython",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": [
"python_abi 3.12.* *_cp312"
],
@ -629,7 +602,6 @@
"build": "4_cp312",
"build_number": 4,
"build_string": "4_cp312",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": [
"python 3.12.* *_cpython"
],
@ -650,7 +622,6 @@
"build": "h8228510_1",
"build_number": 1,
"build_string": "h8228510_1",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": null,
"depends": [
"libgcc-ng >=12",
@ -672,7 +643,6 @@
"build": "pyhd8ed1ab_0",
"build_number": 0,
"build_string": "pyhd8ed1ab_0",
"channel": "https://conda.anaconda.org/conda-forge/noarch",
"constrains": null,
"depends": [
"python >=3.7"
@ -694,7 +664,6 @@
"build": "noxft_h4845f30_101",
"build_number": 101,
"build_string": "noxft_h4845f30_101",
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
"constrains": null,
"depends": [
"libgcc-ng >=12",
@ -716,7 +685,6 @@
"build": "h0c530f3_0",
"build_number": 0,
"build_string": "h0c530f3_0",
"channel": "https://conda.anaconda.org/conda-forge/noarch",
"constrains": null,
"depends": null,
"fn": "tzdata-2024a-h0c530f3_0.conda",

View File

@ -15,6 +15,8 @@
#include "mamba/specs/package_info.hpp"
#include "mamba/util/string.hpp"
#include "mambatests.hpp"
using namespace mamba;
using namespace mamba::solver;
@ -172,7 +174,7 @@ TEST_SUITE("solver::libsolv::database")
SUBCASE("Add repo from repodata with no extra pip")
{
const auto repodata = fs::u8path(MAMBA_TEST_DATA_DIR)
const auto repodata = mambatests::test_data_dir
/ "repodata/conda-forge-numpy-linux-64.json";
auto repo1 = db.add_repo_from_repodata_json(
repodata,
@ -200,7 +202,7 @@ TEST_SUITE("solver::libsolv::database")
SUBCASE("Add repo from repodata with extra pip")
{
const auto repodata = fs::u8path(MAMBA_TEST_DATA_DIR)
const auto repodata = mambatests::test_data_dir
/ "repodata/conda-forge-numpy-linux-64.json";
auto repo1 = db.add_repo_from_repodata_json(
repodata,
@ -230,7 +232,7 @@ TEST_SUITE("solver::libsolv::database")
SUBCASE("Add repo from repodata only .tar.bz2")
{
const auto repodata = fs::u8path(MAMBA_TEST_DATA_DIR)
const auto repodata = mambatests::test_data_dir
/ "repodata/conda-forge-numpy-linux-64.json";
auto repo1 = db.add_repo_from_repodata_json(
repodata,

View File

@ -0,0 +1,261 @@
// Copyright (c) 2024, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <type_traits>
#include <variant>
#include <vector>
#include <doctest/doctest.h>
#include "mamba/fs/filesystem.hpp"
#include "mamba/solver/libsolv/database.hpp"
#include "mamba/solver/libsolv/solver.hpp"
#include "mamba/specs/channel.hpp"
#include "mambatests.hpp"
using namespace mamba;
using namespace mamba::solver;
auto
find_actions_with_name(const Solution& solution, std::string_view name)
-> std::vector<Solution::Action>
{
auto out = std::vector<Solution::Action>();
for (const auto& action : solution.actions)
{
std::visit(
[&](const auto& act)
{
using Act = std::decay_t<decltype(act)>;
if constexpr (Solution::has_remove_v<Act>)
{
if (act.remove.name == name)
{
out.push_back(act);
}
}
else if constexpr (Solution::has_install_v<Act>)
{
if (act.install.name == name)
{
out.push_back(act);
}
}
else
{
if (act.what.name == name)
{
out.push_back(act);
}
}
},
action
);
}
return out;
}
TEST_SUITE("solver::libsolv::solver")
{
using namespace specs::match_spec_literals;
TEST_CASE("Solve a fresh environment with one repository")
{
auto db = libsolv::Database({});
// A conda-forge/linux-64 subsample with one version of numpy and pip and their dependencies
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",
libsolv::PipAsPythonDependency::No
);
REQUIRE(repo.has_value());
SUBCASE("Install numpy")
{
const auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
const auto& solution = std::get<Solution>(outcome.value());
REQUIRE_FALSE(solution.actions.empty());
// Numpy is last because of topological sort
CHECK(std::holds_alternative<Solution::Install>(solution.actions.back()));
CHECK_EQ(std::get<Solution::Install>(solution.actions.back()).install.name, "numpy");
REQUIRE_EQ(find_actions_with_name(solution, "numpy").size(), 1);
const auto python_actions = find_actions_with_name(solution, "python");
REQUIRE_EQ(python_actions.size(), 1);
CHECK(std::holds_alternative<Solution::Install>(python_actions.front()));
}
SUBCASE("Install numpy without dependencies")
{
const auto request = Request{
/* .flags= */ {
/* .keep_dependencies */ false,
/* .keep_user_specs */ true,
},
/* .jobs= */ { Request::Install{ "numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
const auto& solution = std::get<Solution>(outcome.value());
REQUIRE_FALSE(solution.actions.empty());
// Numpy is last because of topological sort
CHECK(std::holds_alternative<Solution::Install>(solution.actions.back()));
CHECK_EQ(std::get<Solution::Install>(solution.actions.back()).install.name, "numpy");
REQUIRE_EQ(find_actions_with_name(solution, "numpy").size(), 1);
const auto python_actions = find_actions_with_name(solution, "python");
REQUIRE_EQ(python_actions.size(), 1);
CHECK(std::holds_alternative<Solution::Omit>(python_actions.front()));
}
SUBCASE("Install numpy dependencies only")
{
const auto request = Request{
/* .flags= */ {
/* .keep_dependencies */ true,
/* .keep_user_specs */ false,
},
/* .jobs= */ { Request::Install{ "numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
const auto& solution = std::get<Solution>(outcome.value());
REQUIRE_FALSE(solution.actions.empty());
// Numpy is last because of topological sort
CHECK(std::holds_alternative<Solution::Omit>(solution.actions.back()));
CHECK_EQ(std::get<Solution::Omit>(solution.actions.back()).what.name, "numpy");
REQUIRE_EQ(find_actions_with_name(solution, "numpy").size(), 1);
const auto python_actions = find_actions_with_name(solution, "python");
REQUIRE_EQ(python_actions.size(), 1);
CHECK(std::holds_alternative<Solution::Install>(python_actions.front()));
// Pip is not a dependency of numpy (or python here)
CHECK(find_actions_with_name(solution, "pip").empty());
}
SUBCASE("Fail to install missing package")
{
const auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "does-not-exist"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<libsolv::UnSolvable>(outcome.value()));
}
}
TEST_CASE("Remove packages")
{
auto db = libsolv::Database({});
// 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"
);
REQUIRE(repo.has_value());
db.set_installed_repo(repo.value());
SUBCASE("Remove numpy and dependencies")
{
const auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Remove{ "numpy"_ms, true } },
};
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
const auto& solution = std::get<Solution>(outcome.value());
REQUIRE_FALSE(solution.actions.empty());
// Numpy is first because of topological sort
CHECK(std::holds_alternative<Solution::Remove>(solution.actions.front()));
CHECK_EQ(std::get<Solution::Remove>(solution.actions.front()).remove.name, "numpy");
REQUIRE_EQ(find_actions_with_name(solution, "numpy").size(), 1);
// Python is not removed because it is needed by pip which is installed
CHECK(find_actions_with_name(solution, "pip").empty());
}
SUBCASE("Remove numpy and pip and dependencies")
{
const auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Remove{ "numpy"_ms, true }, Request::Remove{ "pip"_ms, true } },
};
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
const auto& solution = std::get<Solution>(outcome.value());
const auto numpy_actions = find_actions_with_name(solution, "numpy");
REQUIRE_EQ(numpy_actions.size(), 1);
CHECK(std::holds_alternative<Solution::Remove>(numpy_actions.front()));
const auto pip_actions = find_actions_with_name(solution, "pip");
REQUIRE_EQ(pip_actions.size(), 1);
CHECK(std::holds_alternative<Solution::Remove>(pip_actions.front()));
const auto python_actions = find_actions_with_name(solution, "python");
REQUIRE_EQ(python_actions.size(), 1);
CHECK(std::holds_alternative<Solution::Remove>(python_actions.front()));
}
SUBCASE("Remove numpy without dependencies")
{
const auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Remove{ "numpy"_ms, false } },
};
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
const auto& solution = std::get<Solution>(outcome.value());
REQUIRE_EQ(solution.actions.size(), 1);
CHECK(std::holds_alternative<Solution::Remove>(solution.actions.front()));
CHECK_EQ(std::get<Solution::Remove>(solution.actions.front()).remove.name, "numpy");
REQUIRE_EQ(find_actions_with_name(solution, "numpy").size(), 1);
}
SUBCASE("Removing non-existing package is a no-op")
{
const auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Remove{ "does-not-exist"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
const auto& solution = std::get<Solution>(outcome.value());
CHECK(solution.actions.empty());
}
}
}

View File

@ -17,10 +17,10 @@
namespace mamba::solver
{
// Fix Pybind11 py::bind_vector<Request::item_list> has trouble detecting the abscence
// Fix Pybind11 py::bind_vector<Request::job_list> has trouble detecting the abscence
// of comparions operators, so we tell it explicitly.
auto operator==(const Request::Item&, const Request::Item&) -> bool = delete;
auto operator!=(const Request::Item&, const Request::Item&) -> bool = delete;
auto operator==(const Request::Job&, const Request::Job&) -> bool = delete;
auto operator!=(const Request::Job&, const Request::Job&) -> bool = delete;
// Fix Pybind11 py::bind_vector<Solution::actions> has trouble detecting the abscence
// of comparions operators, so we tell it explicitly.
@ -28,7 +28,7 @@ namespace mamba::solver
auto operator!=(const Solution::Action&, const Solution::Action&) -> bool = delete;
}
PYBIND11_MAKE_OPAQUE(mamba::solver::Request::item_list);
PYBIND11_MAKE_OPAQUE(mamba::solver::Request::job_list);
PYBIND11_MAKE_OPAQUE(mamba::solver::Solution::action_list);
namespace mambapy
@ -113,7 +113,7 @@ namespace mambapy
.def("__deepcopy__", &deepcopy<Request::Pin>, py::arg("memo"));
// Type made opaque at the top of this file
py::bind_vector<Request::item_list>(py_request, "ItemList");
py::bind_vector<Request::job_list>(py_request, "JobList");
py::class_<Request::Flags>(py_request, "Flags")
.def(
@ -159,25 +159,31 @@ namespace mambapy
.def(
// Big copy unfortunately
py::init(
[](Request::Flags flags, Request::item_list items) -> Request {
return { std::move(flags), std::move(items) };
[](Request::job_list jobs, Request::Flags flags) -> Request {
return { std::move(flags), std::move(jobs) };
}
)
),
py::arg("jobs"),
py::arg("flags") = Request::Flags()
)
.def(py::init(
[](py::iterable items) -> Request
{
auto request = Request();
request.items.reserve(py::len_hint(items));
for (py::handle itm : items)
.def(
py::init(
[](py::iterable jobs, Request::Flags flags) -> Request
{
request.items.push_back(py::cast<Request::Item>(itm));
auto request = Request{ std::move(flags) };
request.jobs.reserve(py::len_hint(jobs));
for (py::handle itm : jobs)
{
request.jobs.push_back(py::cast<Request::Job>(itm));
}
return request;
}
return request;
}
))
),
py::arg("jobs"),
py::arg("flags") = Request::Flags()
)
.def_readwrite("flags", &Request::flags)
.def_readwrite("items", &Request::items)
.def_readwrite("jobs", &Request::jobs)
.def("__copy__", &copy<Request>)
.def("__deepcopy__", &deepcopy<Request>, py::arg("memo"));
@ -335,12 +341,18 @@ namespace mambapy
py::class_<ProblemsGraph::RootNode>(py_problems_graph, "RootNode") //
.def(py::init<>());
py::class_<ProblemsGraph::PackageNode, specs::PackageInfo>(py_problems_graph, "PackageNode");
py::class_<ProblemsGraph::PackageNode, specs::PackageInfo>(py_problems_graph, "PackageNode")
.def(py::init<specs::PackageInfo>());
py::class_<ProblemsGraph::UnresolvedDependencyNode, specs::MatchSpec>(
py_problems_graph,
"UnresolvedDependencyNode"
);
py::class_<ProblemsGraph::ConstraintNode, specs::MatchSpec>(py_problems_graph, "ConstraintNode");
)
.def(py::init<specs::MatchSpec>());
py::class_<ProblemsGraph::ConstraintNode, specs::MatchSpec>(py_problems_graph, "ConstraintNode")
.def(py::init<specs::MatchSpec>());
py::class_<ProblemsGraph::conflicts_t>(py_problems_graph, "ConflictMap")
.def(py::init([]() { return ProblemsGraph::conflicts_t(); }))
@ -373,9 +385,8 @@ namespace mambapy
auto const& g = self.graph();
return std::pair(g.nodes(), g.edges());
}
);
m.def("simplify_conflicts", &solver::simplify_conflicts);
)
.def_static("simplify_conflicts", &solver::simplify_conflicts);
auto py_compressed_problems_graph = py::class_<CompressedProblemsGraph>(
m,
@ -448,11 +459,17 @@ namespace mambapy
);
py_compressed_problems_graph
.def_static("from_problems_graph", &CompressedProblemsGraph::from_problems_graph)
.def_static(
"from_problems_graph",
&CompressedProblemsGraph::from_problems_graph,
py::arg("problems_graph"),
py::arg("merge_criteria")
)
.def_static(
"from_problems_graph",
[](const ProblemsGraph& pbs)
{ return CompressedProblemsGraph::from_problems_graph(pbs); }
{ return CompressedProblemsGraph::from_problems_graph(pbs); },
py::arg("problems_graph")
)
.def("root_node", &CompressedProblemsGraph::root_node)
.def("conflicts", &CompressedProblemsGraph::conflicts)

View File

@ -21,7 +21,7 @@ def test_import_recursive():
@pytest.mark.parametrize(
"Item",
"Job",
[
libmambapy.solver.Request.Install,
libmambapy.solver.Request.Remove,
@ -31,8 +31,8 @@ def test_import_recursive():
libmambapy.solver.Request.Pin,
],
)
def test_Request_Item_spec(Item):
itm = Item(spec=libmambapy.specs.MatchSpec.parse("foo"))
def test_Request_Job_spec(Job):
itm = Job(spec=libmambapy.specs.MatchSpec.parse("foo"))
assert str(itm.spec) == "foo"
@ -47,14 +47,14 @@ def test_Request_Item_spec(Item):
@pytest.mark.parametrize(
["Item", "kwargs"],
["Job", "kwargs"],
[
(libmambapy.solver.Request.Remove, {"spec": libmambapy.specs.MatchSpec.parse("foo")}),
(libmambapy.solver.Request.UpdateAll, {}),
],
)
def test_Request_Item_clean(Item, kwargs):
itm = Item(**kwargs, clean_dependencies=False)
def test_Request_Job_clean(Job, kwargs):
itm = Job(**kwargs, clean_dependencies=False)
assert not itm.clean_dependencies
@ -94,6 +94,31 @@ def test_Request_Flags_boolean(attr):
assert getattr(flags, attr) == val
def test_Request():
Request = libmambapy.solver.Request
MatchSpec = libmambapy.specs.MatchSpec
request = Request(
jobs=[Request.Install(MatchSpec.parse("foo"))],
flags=Request.Flags(keep_dependencies=False),
)
# Getters
assert len(request.jobs) == 1
assert not request.flags.keep_dependencies
# Setters
request.jobs.append(Request.Remove(MatchSpec.parse("bar<2.0")))
assert len(request.jobs) == 2
request.flags.keep_dependencies = True
assert request.flags.keep_dependencies
# Copy
other = copy.deepcopy(request)
assert other is not request
assert len(other.jobs) == len(request.jobs)
@pytest.mark.parametrize(
"Action",
[
@ -215,6 +240,7 @@ def test_ProblemsGraph():
assert isinstance(outcome, libmambapy.solver.libsolv.UnSolvable)
pbg = outcome.problems_graph(db)
assert isinstance(pbg.root_node(), int)
# ProblemsGraph conflicts
conflicts = pbg.conflicts()
@ -239,3 +265,51 @@ def test_ProblemsGraph():
nodes, edges = pbg.graph()
assert len(nodes) > 0
assert len(edges) > 0
# Simplify conflicts
pbg = pbg.simplify_conflicts(pbg)
# CompressedProblemsGraph
cp_pbg = libmambapy.solver.CompressedProblemsGraph.from_problems_graph(pbg)
assert isinstance(cp_pbg.root_node(), int)
assert len(cp_pbg.conflicts()) == 2
nodes, edges = cp_pbg.graph()
assert len(nodes) > 0
assert len(edges) > 0
assert "is not installable" in cp_pbg.tree_message()
def test_CompressedProblemsGraph_NamedList():
ProblemsGraph = libmambapy.solver.ProblemsGraph
CompressedProblemsGraph = libmambapy.solver.CompressedProblemsGraph
PackageInfo = libmambapy.specs.PackageInfo
named_list = CompressedProblemsGraph.PackageListNode()
assert len(named_list) == 0
assert not named_list
# Add
for ver, bld in [("1.0", "bld1"), ("2.0", "bld2"), ("3.0", "bld3"), ("4.0", "bld4")]:
named_list.add(ProblemsGraph.PackageNode(PackageInfo("a", version=ver, build_string=bld)))
# Enumeration
assert len(named_list) == 4
assert named_list
assert len(list(named_list)) == len(named_list)
# Methods
assert named_list.name() == "a"
list_str, count = named_list.versions_trunc(sep=":", etc="*", threshold=2)
assert count == 4
assert list_str == "1.0:2.0:*:4.0"
list_str, count = named_list.build_strings_trunc(sep=":", etc="*", threshold=2)
assert count == 4
assert list_str == "bld1:bld2:*:bld4"
list_str, count = named_list.versions_and_build_strings_trunc(sep=":", etc="*", threshold=2)
assert count == 4
assert list_str == "1.0 bld1:2.0 bld2:*:4.0 bld4"
# Clear
named_list.clear()
assert len(named_list) == 0