# Copyright (C) 2012 Anaconda, Inc # SPDX-License-Identifier: BSD-3-Clause """Helpers for testing the solver.""" from __future__ import annotations import collections import functools import json import pathlib from tempfile import TemporaryDirectory import pytest from ..base.context import context from ..core.solve import Solver from ..exceptions import ( PackagesNotFoundError, ResolvePackageNotFound, UnsatisfiableError, ) from ..models.channel import Channel from ..models.match_spec import MatchSpec from ..models.records import PackageRecord from . import helpers @functools.lru_cache def index_packages(num): """Get the index data of the ``helpers.get_index_r_*`` helpers.""" # XXX: get_index_r_X should probably be refactored to avoid loading the environment like this. get_index = getattr(helpers, f"get_index_r_{num}") index, _ = get_index(context.subdir) return list(index.values()) def package_string(record): return f"{record.channel.name}::{record.name}-{record.version}-{record.build}" def package_string_set(packages): """Transforms package container in package string set.""" return {package_string(record) for record in packages} def package_dict(packages): """Transforms package container into a dictionary.""" return {record.name: record for record in packages} class SimpleEnvironment: """Helper environment object.""" REPO_DATA_KEYS = ( "build", "build_number", "depends", "license", "md5", "name", "sha256", "size", "subdir", "timestamp", "version", "track_features", "features", ) def __init__(self, path, solver_class, subdirs=context.subdirs): self._path = pathlib.Path(path) self._prefix_path = self._path / "prefix" self._channels_path = self._path / "channels" self._solver_class = solver_class self.subdirs = subdirs self.installed_packages = [] # if repo_packages is a list, the packages will be put in a `test` channel # if it is a dictionary, it the keys are the channel name and the value # the channel packages self.repo_packages: list[str] | dict[str, list[str]] = [] def solver(self, add, remove): """Writes ``repo_packages`` to the disk and creates a solver instance.""" channels = [] self._write_installed_packages() for channel_name, packages in self._channel_packages.items(): self._write_repo_packages(channel_name, packages) channel = Channel(str(self._channels_path / channel_name)) channels.append(channel) return self._solver_class( prefix=self._prefix_path, subdirs=self.subdirs, channels=channels, specs_to_add=add, specs_to_remove=remove, ) def solver_transaction(self, add=(), remove=(), as_specs=False): packages = self.solver(add=add, remove=remove).solve_final_state() if as_specs: return packages return package_string_set(packages) def install(self, *specs, as_specs=False): return self.solver_transaction(add=specs, as_specs=as_specs) def remove(self, *specs, as_specs=False): return self.solver_transaction(remove=specs, as_specs=as_specs) @property def _channel_packages(self): """Helper that unfolds the ``repo_packages`` into a dictionary.""" if isinstance(self.repo_packages, dict): return self.repo_packages return {"test": self.repo_packages} def _package_data(self, record): """Turn record into data, to be written in the JSON environment/repo files.""" data = { key: value for key, value in vars(record).items() if key in self.REPO_DATA_KEYS } if "subdir" not in data: data["subdir"] = context.subdir return data def _write_installed_packages(self): if not self.installed_packages: return conda_meta = self._prefix_path / "conda-meta" conda_meta.mkdir(exist_ok=True, parents=True) # write record files for record in self.installed_packages: record_path = ( conda_meta / f"{record.name}-{record.version}-{record.build}.json" ) record_data = self._package_data(record) record_data["channel"] = record.channel.name record_path.write_text(json.dumps(record_data)) # write history file history_path = conda_meta / "history" history_path.write_text( "\n".join( ( "==> 2000-01-01 00:00:00 <==", *map(package_string, self.installed_packages), ) ) ) def _write_repo_packages(self, channel_name, packages): """Write packages to the channel path.""" # build package data package_data = collections.defaultdict(dict) for record in packages: package_data[record.subdir][record.fn] = self._package_data(record) # write repodata assert set(self.subdirs).issuperset(set(package_data.keys())) for subdir in self.subdirs: subdir_path = self._channels_path / channel_name / subdir subdir_path.mkdir(parents=True, exist_ok=True) subdir_path.joinpath("repodata.json").write_text( json.dumps( { "info": { "subdir": subdir, }, "packages": package_data.get(subdir, {}), } ) ) def empty_prefix(): return TemporaryDirectory(prefix="conda-test-repo-") @pytest.fixture() def temp_simple_env(solver_class=Solver) -> SimpleEnvironment: with empty_prefix() as prefix: yield SimpleEnvironment(prefix, solver_class) class SolverTests: """Tests for :py:class:`conda.core.solve.Solver` implementations.""" @property def solver_class(self) -> type[Solver]: """Class under test.""" raise NotImplementedError @property def tests_to_skip(self): return {} # skip reason -> list of tests to skip @pytest.fixture(autouse=True) def skip_tests(self, request): for reason, skip_list in self.tests_to_skip.items(): if request.node.name in skip_list: pytest.skip(reason) @pytest.fixture() def env(self): with TemporaryDirectory(prefix="conda-test-repo-") as tmpdir: self.env = SimpleEnvironment(tmpdir, self.solver_class) yield self.env self.env = None def find_package_in_list(self, packages, **kwargs): for record in packages: if all(getattr(record, key) == value for key, value in kwargs.items()): return record def find_package(self, **kwargs): if isinstance(self.env.repo_packages, dict): if "channel" not in kwargs: raise ValueError( "Repo has multiple channels, the `channel` argument must be specified" ) packages = self.env.repo_packages[kwargs["channel"]] else: packages = self.env.repo_packages return self.find_package_in_list(packages, **kwargs) def assert_unsatisfiable(self, exc_info, entries): """Helper to assert that a :py:class:`conda.exceptions.UnsatisfiableError` instance as a the specified set of unsatisfiable specifications. """ assert issubclass(exc_info.type, UnsatisfiableError) if exc_info.type is UnsatisfiableError: assert ( sorted( tuple(map(str, entries)) for entries in exc_info.value.unsatisfiable ) == entries ) def test_empty(self, env): env.repo_packages = index_packages(1) assert env.install() == set() def test_iopro_mkl(self, env): env.repo_packages = index_packages(1) assert env.install("iopro 1.4*", "python 2.7*", "numpy 1.7*") == { "test::iopro-1.4.3-np17py27_p0", "test::numpy-1.7.1-py27_0", "test::openssl-1.0.1c-0", "test::python-2.7.5-0", "test::readline-6.2-0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::unixodbc-2.3.1-0", "test::zlib-1.2.7-0", "test::distribute-0.6.36-py27_1", "test::pip-1.3.1-py27_1", } def test_iopro_nomkl(self, env): env.repo_packages = index_packages(1) assert env.install( "iopro 1.4*", "python 2.7*", "numpy 1.7*", MatchSpec(track_features="mkl") ) == { "test::iopro-1.4.3-np17py27_p0", "test::mkl-rt-11.0-p0", "test::numpy-1.7.1-py27_p0", "test::openssl-1.0.1c-0", "test::python-2.7.5-0", "test::readline-6.2-0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::unixodbc-2.3.1-0", "test::zlib-1.2.7-0", "test::distribute-0.6.36-py27_1", "test::pip-1.3.1-py27_1", } def test_mkl(self, env): env.repo_packages = index_packages(1) assert env.install("mkl") == env.install( "mkl 11*", MatchSpec(track_features="mkl") ) def test_accelerate(self, env): env.repo_packages = index_packages(1) assert env.install("accelerate") == env.install( "accelerate", MatchSpec(track_features="mkl") ) def test_scipy_mkl(self, env): env.repo_packages = index_packages(1) records = env.install( "scipy", "python 2.7*", "numpy 1.7*", MatchSpec(track_features="mkl"), as_specs=True, ) for record in records: if record.name in ("numpy", "scipy"): assert "mkl" in record.features assert "test::numpy-1.7.1-py27_p0" in package_string_set(records) assert "test::scipy-0.12.0-np17py27_p0" in package_string_set(records) def test_anaconda_nomkl(self, env): env.repo_packages = index_packages(1) records = env.install("anaconda 1.5.0", "python 2.7*", "numpy 1.7*") assert len(records) == 107 assert "test::scipy-0.12.0-np17py27_0" in records def test_pseudo_boolean(self, env): env.repo_packages = index_packages(1) # The latest version of iopro, 1.5.0, was not built against numpy 1.5 assert env.install("iopro", "python 2.7*", "numpy 1.5*") == { "test::iopro-1.4.3-np15py27_p0", "test::numpy-1.5.1-py27_4", "test::openssl-1.0.1c-0", "test::python-2.7.5-0", "test::readline-6.2-0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::unixodbc-2.3.1-0", "test::zlib-1.2.7-0", "test::distribute-0.6.36-py27_1", "test::pip-1.3.1-py27_1", } assert env.install( "iopro", "python 2.7*", "numpy 1.5*", MatchSpec(track_features="mkl") ) == { "test::iopro-1.4.3-np15py27_p0", "test::mkl-rt-11.0-p0", "test::numpy-1.5.1-py27_p4", "test::openssl-1.0.1c-0", "test::python-2.7.5-0", "test::readline-6.2-0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::unixodbc-2.3.1-0", "test::zlib-1.2.7-0", "test::distribute-0.6.36-py27_1", "test::pip-1.3.1-py27_1", } def test_unsat_from_r1(self, env): env.repo_packages = index_packages(1) with pytest.raises(UnsatisfiableError) as exc_info: env.install("numpy 1.5*", "scipy 0.12.0b1") self.assert_unsatisfiable( exc_info, [ ("numpy=1.5",), ("scipy==0.12.0b1", "numpy[version='1.6.*|1.7.*']"), ], ) with pytest.raises(UnsatisfiableError) as exc_info: env.install("numpy 1.5*", "python 3*") self.assert_unsatisfiable( exc_info, [ ("numpy=1.5", "nose", "python=3.3"), ("numpy=1.5", "python[version='2.6.*|2.7.*']"), ("python=3",), ], ) with pytest.raises((ResolvePackageNotFound, PackagesNotFoundError)) as exc_info: env.install("numpy 1.5*", "numpy 1.6*") if exc_info.type is ResolvePackageNotFound: assert sorted(map(str, exc_info.value.bad_deps)) == [ "numpy[version='1.5.*,1.6.*']", ] def test_unsat_simple(self, env): env.repo_packages = [ helpers.record(name="a", depends=["c >=1,<2"]), helpers.record(name="b", depends=["c >=2,<3"]), helpers.record(name="c", version="1.0"), helpers.record(name="c", version="2.0"), ] with pytest.raises(UnsatisfiableError) as exc_info: env.install("a", "b") self.assert_unsatisfiable( exc_info, [ ("a", "c[version='>=1,<2']"), ("b", "c[version='>=2,<3']"), ], ) def test_get_dists(self, env): env.repo_packages = index_packages(1) records = env.install("anaconda 1.4.0") assert "test::anaconda-1.4.0-np17py33_0" in records assert "test::freetype-2.4.10-0" in records def test_unsat_shortest_chain_1(self, env): env.repo_packages = [ helpers.record(name="a", depends=["d", "c <1.3.0"]), helpers.record(name="b", depends=["c"]), helpers.record( name="c", version="1.3.6", ), helpers.record( name="c", version="1.2.8", ), helpers.record(name="d", depends=["c >=0.8.0"]), ] with pytest.raises(UnsatisfiableError) as exc_info: env.install("c=1.3.6", "a", "b") self.assert_unsatisfiable( exc_info, [ ("a", "c[version='<1.3.0']"), ("a", "d", "c[version='>=0.8.0']"), ("b", "c"), ("c=1.3.6",), ], ) def test_unsat_shortest_chain_2(self, env): env.repo_packages = [ helpers.record(name="a", depends=["d", "c >=0.8.0"]), helpers.record(name="b", depends=["c"]), helpers.record( name="c", version="1.3.6", ), helpers.record( name="c", version="1.2.8", ), helpers.record(name="d", depends=["c <1.3.0"]), ] with pytest.raises(UnsatisfiableError) as exc_info: env.install("c=1.3.6", "a", "b") self.assert_unsatisfiable( exc_info, [ ("a", "c[version='>=0.8.0']"), ("a", "d", "c[version='<1.3.0']"), ("b", "c"), ("c=1.3.6",), ], ) def test_unsat_shortest_chain_3(self, env): env.repo_packages = [ helpers.record(name="a", depends=["f", "e"]), helpers.record(name="b", depends=["c"]), helpers.record( name="c", version="1.3.6", ), helpers.record( name="c", version="1.2.8", ), helpers.record(name="d", depends=["c >=0.8.0"]), helpers.record(name="e", depends=["c <1.3.0"]), helpers.record(name="f", depends=["d"]), ] with pytest.raises(UnsatisfiableError) as exc_info: env.install("c=1.3.6", "a", "b") self.assert_unsatisfiable( exc_info, [ ("a", "e", "c[version='<1.3.0']"), ("b", "c"), ("c=1.3.6",), ], ) def test_unsat_shortest_chain_4(self, env): env.repo_packages = [ helpers.record(name="a", depends=["py =3.7.1"]), helpers.record(name="py_req_1"), helpers.record(name="py_req_2"), helpers.record( name="py", version="3.7.1", depends=["py_req_1", "py_req_2"] ), helpers.record( name="py", version="3.6.1", depends=["py_req_1", "py_req_2"] ), ] with pytest.raises(UnsatisfiableError) as exc_info: env.install("a", "py=3.6.1") self.assert_unsatisfiable( exc_info, [ ("a", "py=3.7.1"), ("py=3.6.1",), ], ) def test_unsat_chain(self, env): # a -> b -> c=1.x -> d=1.x # e -> c=2.x -> d=2.x env.repo_packages = [ helpers.record(name="a", depends=["b"]), helpers.record(name="b", depends=["c >=1,<2"]), helpers.record(name="c", version="1.0", depends=["d >=1,<2"]), helpers.record(name="d", version="1.0"), helpers.record(name="e", depends=["c >=2,<3"]), helpers.record(name="c", version="2.0", depends=["d >=2,<3"]), helpers.record(name="d", version="2.0"), ] with pytest.raises(UnsatisfiableError) as exc_info: env.install("a", "e") self.assert_unsatisfiable( exc_info, [ ("a", "b", "c[version='>=1,<2']"), ("e", "c[version='>=2,<3']"), ], ) def test_unsat_any_two_not_three(self, env): # can install any two of a, b and c but not all three env.repo_packages = [ helpers.record(name="a", version="1.0", depends=["d >=1,<2"]), helpers.record(name="a", version="2.0", depends=["d >=2,<3"]), helpers.record(name="b", version="1.0", depends=["d >=1,<2"]), helpers.record(name="b", version="2.0", depends=["d >=3,<4"]), helpers.record(name="c", version="1.0", depends=["d >=2,<3"]), helpers.record(name="c", version="2.0", depends=["d >=3,<4"]), helpers.record(name="d", version="1.0"), helpers.record(name="d", version="2.0"), helpers.record(name="d", version="3.0"), ] # a and b can be installed installed = env.install("a", "b", as_specs=True) assert any(k.name == "a" and k.version == "1.0" for k in installed) assert any(k.name == "b" and k.version == "1.0" for k in installed) # a and c can be installed installed = env.install("a", "c", as_specs=True) assert any(k.name == "a" and k.version == "2.0" for k in installed) assert any(k.name == "c" and k.version == "1.0" for k in installed) # b and c can be installed installed = env.install("b", "c", as_specs=True) assert any(k.name == "b" and k.version == "2.0" for k in installed) assert any(k.name == "c" and k.version == "2.0" for k in installed) # a, b and c cannot be installed with pytest.raises(UnsatisfiableError) as exc_info: env.install("a", "b", "c") self.assert_unsatisfiable( exc_info, [ ("a", "d[version='>=1,<2|>=2,<3']"), ("b", "d[version='>=1,<2|>=3,<4']"), ("c", "d[version='>=2,<3|>=3,<4']"), ], ) def test_unsat_expand_single(self, env): env.repo_packages = [ helpers.record(name="a", depends=["b", "c"]), helpers.record(name="b", depends=["d >=1,<2"]), helpers.record(name="c", depends=["d >=2,<3"]), helpers.record(name="d", version="1.0"), helpers.record(name="d", version="2.0"), ] with pytest.raises(UnsatisfiableError) as exc_info: env.install("a") self.assert_unsatisfiable( exc_info, [ ("b", "d[version='>=1,<2']"), ("c", "d[version='>=2,<3']"), ], ) def test_unsat_missing_dep(self, env): env.repo_packages = [ helpers.record(name="a", depends=["b", "c"]), helpers.record(name="b", depends=["c >=2,<3"]), helpers.record(name="c", version="1.0"), ] with pytest.raises(UnsatisfiableError) as exc_info: env.install("a", "b") self.assert_unsatisfiable( exc_info, [ ("a", "b"), ("b",), ], ) def test_nonexistent(self, env): with pytest.raises((ResolvePackageNotFound, PackagesNotFoundError)): env.install("notarealpackage 2.0*") with pytest.raises((ResolvePackageNotFound, PackagesNotFoundError)): env.install("numpy 1.5") def test_timestamps_and_deps(self, env): env.repo_packages = index_packages(1) + [ helpers.record( name="mypackage", version="1.0", build="hash12_0", timestamp=1, depends=["libpng 1.2.*"], ), helpers.record( name="mypackage", version="1.0", build="hash15_0", timestamp=0, depends=["libpng 1.5.*"], ), ] # libpng 1.2 records_12 = env.install("libpng 1.2.*", "mypackage") assert "test::libpng-1.2.50-0" in records_12 assert "test::mypackage-1.0-hash12_0" in records_12 # libpng 1.5 records_15 = env.install("libpng 1.5.*", "mypackage") assert "test::libpng-1.5.13-1" in records_15 assert "test::mypackage-1.0-hash15_0" in records_15 # this is testing that previously installed reqs are not disrupted # by newer timestamps. regression test of sorts for # https://github.com/conda/conda/issues/6271 assert ( env.install("mypackage", *env.install("libpng 1.2.*", as_specs=True)) == records_12 ) assert ( env.install("mypackage", *env.install("libpng 1.5.*", as_specs=True)) == records_15 ) # unspecified python version should maximize libpng (v1.5), # even though it has a lower timestamp assert env.install("mypackage") == records_15 def test_nonexistent_deps(self, env): env.repo_packages = index_packages(1) + [ helpers.record( name="mypackage", version="1.0", depends=["nose", "python 3.3*", "notarealpackage 2.0*"], ), helpers.record( name="mypackage", version="1.1", depends=["nose", "python 3.3*"], ), helpers.record( name="anotherpackage", version="1.0", depends=["nose", "mypackage 1.1"], ), helpers.record( name="anotherpackage", version="2.0", depends=["nose", "mypackage"], ), ] # XXX: missing find_matches and reduced_index assert env.install("mypackage") == { "test::mypackage-1.1-0", "test::nose-1.3.0-py33_0", "test::openssl-1.0.1c-0", "test::python-3.3.2-0", "test::readline-6.2-0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::zlib-1.2.7-0", "test::distribute-0.6.36-py33_1", "test::pip-1.3.1-py33_1", } assert env.install("anotherpackage 1.0") == { "test::anotherpackage-1.0-0", "test::mypackage-1.1-0", "test::nose-1.3.0-py33_0", "test::openssl-1.0.1c-0", "test::python-3.3.2-0", "test::readline-6.2-0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::zlib-1.2.7-0", "test::distribute-0.6.36-py33_1", "test::pip-1.3.1-py33_1", } assert env.install("anotherpackage") == { "test::anotherpackage-2.0-0", "test::mypackage-1.1-0", "test::nose-1.3.0-py33_0", "test::openssl-1.0.1c-0", "test::python-3.3.2-0", "test::readline-6.2-0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::zlib-1.2.7-0", "test::distribute-0.6.36-py33_1", "test::pip-1.3.1-py33_1", } # This time, the latest version is messed up env.repo_packages = index_packages(1) + [ helpers.record( name="mypackage", version="1.0", depends=["nose", "python 3.3*"], ), helpers.record( name="mypackage", version="1.1", depends=["nose", "python 3.3*", "notarealpackage 2.0*"], ), helpers.record( name="anotherpackage", version="1.0", depends=["nose", "mypackage 1.0"], ), helpers.record( name="anotherpackage", version="2.0", depends=["nose", "mypackage"], ), ] # XXX: missing find_matches and reduced_index assert env.install("mypackage") == { "test::mypackage-1.0-0", "test::nose-1.3.0-py33_0", "test::openssl-1.0.1c-0", "test::python-3.3.2-0", "test::readline-6.2-0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::zlib-1.2.7-0", "test::distribute-0.6.36-py33_1", "test::pip-1.3.1-py33_1", } # TODO: We need UnsatisfiableError here because mamba does not # have more granular exceptions yet. with pytest.raises((ResolvePackageNotFound, UnsatisfiableError)): env.install("mypackage 1.1") assert env.install("anotherpackage 1.0") == { "test::anotherpackage-1.0-0", "test::mypackage-1.0-0", "test::nose-1.3.0-py33_0", "test::openssl-1.0.1c-0", "test::python-3.3.2-0", "test::readline-6.2-0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::zlib-1.2.7-0", "test::distribute-0.6.36-py33_1", "test::pip-1.3.1-py33_1", } # If recursive checking is working correctly, this will give # anotherpackage 2.0, not anotherpackage 1.0 assert env.install("anotherpackage") == { "test::anotherpackage-2.0-0", "test::mypackage-1.0-0", "test::nose-1.3.0-py33_0", "test::openssl-1.0.1c-0", "test::python-3.3.2-0", "test::readline-6.2-0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::zlib-1.2.7-0", "test::distribute-0.6.36-py33_1", "test::pip-1.3.1-py33_1", } def test_install_package_with_feature(self, env): env.repo_packages = index_packages(1) + [ helpers.record( name="mypackage", version="1.0", depends=["python 3.3*"], features="feature", ), helpers.record( name="feature", version="1.0", depends=["python 3.3*"], track_features="feature", ), ] # should not raise env.install("mypackage", "feature 1.0") def test_unintentional_feature_downgrade(self, env): # See https://github.com/conda/conda/issues/6765 # With the bug in place, this bad build of scipy # will be selected for install instead of a later # build of scipy 0.11.0. good_rec_match = MatchSpec("channel-1::scipy==0.11.0=np17py33_3") good_rec = next( prec for prec in index_packages(1) if good_rec_match.match(prec) ) bad_deps = tuple(d for d in good_rec.depends if not d.startswith("numpy")) bad_rec = PackageRecord.from_objects( good_rec, channel="test", build=good_rec.build.replace("_3", "_x0"), build_number=0, depends=bad_deps, fn=good_rec.fn.replace("_3", "_x0"), url=good_rec.url.replace("_3", "_x0"), ) env.repo_packages = index_packages(1) + [bad_rec] records = env.install("scipy 0.11.0") assert "test::scipy-0.11.0-np17py33_x0" not in records assert "test::scipy-0.11.0-np17py33_3" in records def test_circular_dependencies(self, env): env.repo_packages = index_packages(1) + [ helpers.record( name="package1", depends=["package2"], ), helpers.record( name="package2", depends=["package1"], ), ] assert ( env.install("package1", "package2") == env.install("package1") == env.install("package2") ) def test_irrational_version(self, env): env.repo_packages = index_packages(1) assert env.install("pytz 2012d", "python 3*") == { "test::distribute-0.6.36-py33_1", "test::openssl-1.0.1c-0", "test::pip-1.3.1-py33_1", "test::python-3.3.2-0", "test::pytz-2012d-py33_0", "test::readline-6.2-0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::zlib-1.2.7-0", } def test_no_features(self, env): env.repo_packages = index_packages(1) assert env.install("python 2.6*", "numpy 1.6*", "scipy 0.11*") == { "test::distribute-0.6.36-py26_1", "test::numpy-1.6.2-py26_4", "test::openssl-1.0.1c-0", "test::pip-1.3.1-py26_1", "test::python-2.6.8-6", "test::readline-6.2-0", "test::scipy-0.11.0-np16py26_3", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::zlib-1.2.7-0", } assert env.install( "python 2.6*", "numpy 1.6*", "scipy 0.11*", MatchSpec(track_features="mkl") ) == { "test::distribute-0.6.36-py26_1", "test::mkl-rt-11.0-p0", "test::numpy-1.6.2-py26_p4", "test::openssl-1.0.1c-0", "test::pip-1.3.1-py26_1", "test::python-2.6.8-6", "test::readline-6.2-0", "test::scipy-0.11.0-np16py26_p3", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::zlib-1.2.7-0", } env.repo_packages += [ helpers.record( name="pandas", version="0.12.0", build="np16py27_0", depends=[ "dateutil", "numpy 1.6*", "python 2.7*", "pytz", ], ), helpers.record( name="numpy", version="1.6.2", build="py27_p5", build_number=0, depends=[ "mkl-rt 11.0", "python 2.7", ], features="mkl", ), ] assert env.install("pandas 0.12.0 np16py27_0", "python 2.7*") == { "test::dateutil-2.1-py27_1", "test::distribute-0.6.36-py27_1", "test::numpy-1.6.2-py27_4", "test::openssl-1.0.1c-0", "test::pandas-0.12.0-np16py27_0", "test::pip-1.3.1-py27_1", "test::python-2.7.5-0", "test::pytz-2013b-py27_0", "test::readline-6.2-0", "test::six-1.3.0-py27_0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::zlib-1.2.7-0", } assert env.install( "pandas 0.12.0 np16py27_0", "python 2.7*", MatchSpec(track_features="mkl") ) == { "test::dateutil-2.1-py27_1", "test::distribute-0.6.36-py27_1", "test::mkl-rt-11.0-p0", "test::numpy-1.6.2-py27_p4", "test::openssl-1.0.1c-0", "test::pandas-0.12.0-np16py27_0", "test::pip-1.3.1-py27_1", "test::python-2.7.5-0", "test::pytz-2013b-py27_0", "test::readline-6.2-0", "test::six-1.3.0-py27_0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::zlib-1.2.7-0", } @pytest.mark.xfail(reason="CONDA_CHANNEL_PRIORITY does not seem to have any effect") def test_channel_priority_1(self, monkeypatch, env): # XXX: Test is skipped because CONDA_CHANNEL_PRIORITY does not seems to # have any effect. I have also tried conda.common.io.env_var like # the other tests but no luck. env.repo_packages = {} env.repo_packages["channel-A"] = [] env.repo_packages["channel-1"] = index_packages(1) pandas_0 = self.find_package( channel="channel-1", name="pandas", version="0.10.1", build="np17py27_0", ) env.repo_packages["channel-A"].append(pandas_0) # channel-1 has pandas np17py27_1, channel-A only has np17py27_0 # when priority is set, it channel-A should take precedence and # np17py27_0 be installed, otherwise np17py27_1 should be installed as # it has a higher build version monkeypatch.setenv("CONDA_CHANNEL_PRIORITY", "True") assert "channel-A::pandas-0.11.0-np16py27_0" in env.install( "pandas", "python 2.7*", "numpy 1.6*" ) monkeypatch.setenv("CONDA_CHANNEL_PRIORITY", "False") assert "channel-1::pandas-0.11.0-np16py27_1" in env.install( "pandas", "python 2.7*", "numpy 1.6*" ) # now lets revert the channels env.repo_packages = dict(reversed(env.repo_packages.items())) monkeypatch.setenv("CONDA_CHANNEL_PRIORITY", "True") assert "channel-1::pandas-0.11.0-np16py27_1" in env.install( "pandas", "python 2.7*", "numpy 1.6*" ) @pytest.mark.xfail(reason="CONDA_CHANNEL_PRIORITY does not seem to have any effect") def test_unsat_channel_priority(self, monkeypatch, env): # XXX: Test is skipped because CONDA_CHANNEL_PRIORITY does not seems to # have any effect. I have also tried conda.common.io.env_var like # the other tests but no luck. env.repo_packages = {} # higher priority env.repo_packages["channel-1"] = [ helpers.record( name="a", version="1.0", depends=["c"], ), helpers.record( name="b", version="1.0", depends=["c >=2,<3"], ), helpers.record( name="c", version="1.0", ), ] # lower priority, missing c 2.0 env.repo_packages["channel-2"] = [ helpers.record( name="a", version="2.0", depends=["c"], ), helpers.record( name="b", version="2.0", depends=["c >=2,<3"], ), helpers.record( name="c", version="1.0", ), helpers.record( name="c", version="2.0", ), ] monkeypatch.setenv("CONDA_CHANNEL_PRIORITY", "True") records = env.install("a", "b", as_specs=True) # channel-1 a and b packages (1.0) installed assert any(k.name == "a" and k.version == "1.0" for k in records) assert any(k.name == "b" and k.version == "1.0" for k in records) monkeypatch.setenv("CONDA_CHANNEL_PRIORITY", "False") records = env.install("a", "b", as_specs=True) # no channel priority, largest version of a and b (2.0) installed assert any(k.name == "a" and k.version == "2.0" for k in records) assert any(k.name == "b" and k.version == "2.0" for k in records) monkeypatch.setenv("CONDA_CHANNEL_PRIORITY", "True") with pytest.raises(UnsatisfiableError) as exc_info: env.install("a", "b") self.assert_unsatisfiable(exc_info, [("b", "c[version='>=2,<3']")]) @pytest.mark.xfail( reason="There is some weird global state making " "this test fail when the whole test suite is run" ) def test_remove(self, env): env.repo_packages = index_packages(1) records = env.install("pandas", "python 2.7*", as_specs=True) assert package_string_set(records) == { "test::dateutil-2.1-py27_1", "test::distribute-0.6.36-py27_1", "test::numpy-1.7.1-py27_0", "test::openssl-1.0.1c-0", "test::pandas-0.11.0-np17py27_1", "test::pip-1.3.1-py27_1", "test::python-2.7.5-0", "test::pytz-2013b-py27_0", "test::readline-6.2-0", "test::scipy-0.12.0-np17py27_0", "test::six-1.3.0-py27_0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::zlib-1.2.7-0", } env.installed_packages = records assert env.remove("pandas") == { "test::dateutil-2.1-py27_1", "test::distribute-0.6.36-py27_1", "test::numpy-1.7.1-py27_0", "test::openssl-1.0.1c-0", "test::pip-1.3.1-py27_1", "test::python-2.7.5-0", "test::pytz-2013b-py27_0", "test::readline-6.2-0", "test::scipy-0.12.0-np17py27_0", "test::six-1.3.0-py27_0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::zlib-1.2.7-0", } assert env.remove("numpy") == { "test::dateutil-2.1-py27_1", "test::distribute-0.6.36-py27_1", "test::openssl-1.0.1c-0", "test::pip-1.3.1-py27_1", "test::python-2.7.5-0", "test::pytz-2013b-py27_0", "test::readline-6.2-0", "test::six-1.3.0-py27_0", "test::sqlite-3.7.13-0", "test::system-5.8-1", "test::tk-8.5.13-0", "test::zlib-1.2.7-0", } def test_surplus_features_1(self, env): env.repo_packages += [ helpers.record( name="feature", track_features="feature", ), helpers.record( name="package1", features="feature", ), helpers.record( name="package2", version="1.0", features="feature", depends=["package1"], ), helpers.record( name="package2", version="2.0", features="feature", ), ] assert env.install("package2", "feature") == { "test::package2-2.0-0", "test::feature-1.0-0", } def test_surplus_features_2(self, env): env.repo_packages += [ helpers.record( name="feature", track_features="feature", ), helpers.record( name="package1", features="feature", ), helpers.record( name="package2", version="1.0", build_number=0, features="feature", depends=["package1"], ), helpers.record( name="package2", version="1.0", build_number=1, features="feature", ), ] assert env.install("package2", "feature") == { "test::package2-1.0-0", "test::feature-1.0-0", } def test_get_reduced_index_broadening_with_unsatisfiable_early_dep(self, env): # Test that spec broadening reduction doesn't kill valid solutions # In other words, the order of packages in the index should not affect the # overall result of the reduced index. # see discussion at https://github.com/conda/conda/pull/8117#discussion_r249249815 env.repo_packages += [ helpers.record( name="a", version="1.0", # not satisfiable. This record should come first, so that its c==2 # constraint tries to mess up the inclusion of the c record below, # which should be included as part of b's deps, but which is # broader than this dep. depends=["b", "c==2"], ), helpers.record( name="a", version="2.0", depends=["b"], ), helpers.record( name="b", depends=["c"], ), helpers.record( name="c", ), ] assert env.install("a") == { "test::a-2.0-0", "test::b-1.0-0", "test::c-1.0-0", } def test_get_reduced_index_broadening_preferred_solution(self, env): # test that order of index reduction does not eliminate what should be a preferred solution # https://github.com/conda/conda/pull/8117#discussion_r249216068 env.repo_packages += [ helpers.record( name="top", version="1.0", # this is the first processed record, and imposes a broadening constraint on bottom # if things are overly restricted, we'll end up with bottom 1.5 in our solution # instead of the preferred (latest) 2.5 depends=["middle", "bottom==1.5"], ), helpers.record( name="top", version="2.0", depends=["middle"], ), helpers.record( name="middle", depends=["bottom"], ), helpers.record( name="bottom", version="1.5", ), helpers.record( name="bottom", version="2.5", ), ] for record in env.install("top", as_specs=True): if record.name == "top": assert ( record.version == "2.0" ), f"top version should be 2.0, but is {record.version}" elif record.name == "bottom": assert ( record.version == "2.5" ), f"bottom version should be 2.5, but is {record.version}" def test_arch_preferred_over_noarch_when_otherwise_equal(self, env): env.repo_packages += [ helpers.record( name="package1", subdir="noarch", ), helpers.record( name="package1", ), ] records = env.install("package1", as_specs=True) assert len(records) == 1 assert records[0].subdir == context.subdir def test_noarch_preferred_over_arch_when_version_greater(self, env): env.repo_packages += [ helpers.record( name="package1", version="2.0", subdir="noarch", ), helpers.record( name="package1", version="1.0", ), ] records = env.install("package1", as_specs=True) assert len(records) == 1 assert records[0].subdir == "noarch" def test_noarch_preferred_over_arch_when_version_greater_dep(self, env): env.repo_packages += [ helpers.record( name="package1", version="1.0", ), helpers.record( name="package1", version="2.0", subdir="noarch", ), helpers.record( name="package2", depends=["package1"], ), ] records = env.install("package2", as_specs=True) package1 = self.find_package_in_list(records, name="package1") assert package1.subdir == "noarch" def test_noarch_preferred_over_arch_when_build_greater(self, env): env.repo_packages += [ helpers.record( name="package1", build_number=0, ), helpers.record( name="package1", build_number=1, subdir="noarch", ), ] records = env.install("package1", as_specs=True) assert len(records) == 1 assert records[0].subdir == "noarch" def test_noarch_preferred_over_arch_when_build_greater_dep(self, env): env.repo_packages += [ helpers.record( name="package1", build_number=0, ), helpers.record( name="package1", build_number=1, subdir="noarch", ), helpers.record( name="package2", depends=["package1"], ), ] records = env.install("package2", as_specs=True) package1 = self.find_package_in_list(records, name="package1") assert package1.subdir == "noarch"