Skip to content

add broken tests for conflicting dependency extras

Cirrus CI / Tests / FreeBSD (Python 3.10) / pytest failed Oct 11, 2024 in 3m 16s

Task Summary

Instruction pytest failed in 01:32

Details

✅ 00:03 clone
✅ 01:20 bootstrap_poetry
✅ 00:18 setup_environment
❌ 01:32 pytest

E               },
E         +     {
E         +         'job': 'install',
E         +         'package': Package('a', '2.0'),
E         +         'skipped': False,
E         +     },
E           ]

/tmp/cirrus-ci-build/tests/puzzle/test_solver.py:113: AssertionError
-------------- generated xml file: /tmp/cirrus-ci-build/junit.xml --------------
=========================== short test summary info ============================
FAILED tests/installation/test_installer.py::test_run_with_conflicting_dependency_extras[extra-one-False] - poetry.puzzle.exceptions.SolverProblemError: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
 and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.
FAILED tests/installation/test_installer.py::test_run_with_conflicting_dependency_extras[extra-two-False] - poetry.puzzle.exceptions.SolverProblemError: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
 and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.
FAILED tests/installation/test_installer.py::test_run_with_conflicting_dependency_extras[None-False] - poetry.puzzle.exceptions.SolverProblemError: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
 and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.
FAILED tests/puzzle/test_solver.py::test_solver_resolves_duplicate_dependency_in_root_extra[False] - AssertionError: assert [{'job': 'ins...pped': False}] == [{'job': 'ins...pped': False}]
  
  At index 0 diff: {'job': 'install', 'package': Package('a', '1.0'), 'skipped': False} != {'job': 'install', 'package': Package('a', '2.0'), 'skipped': False}
  Left contains one more item: {'job': 'install', 'package': Package('a', '2.0'), 'skipped': False}
  
  Full diff:
    [
  +     {
  +         'job': 'install',
  +         'package': Package('a', '1.0'),
  +         'skipped': False,
  +     },
        {
            'job': 'install',
            'package': Package('a', '2.0'),
            'skipped': False,
        },
    ]
FAILED tests/puzzle/test_solver.py::test_solver_resolves_duplicate_dependency_in_root_extra[True] - AssertionError: assert [{'job': 'ins...pped': False}] == [{'job': 'ins...pped': False}]
  
  Left contains one more item: {'job': 'install', 'package': Package('a', '2.0'), 'skipped': False}
  
  Full diff:
    [
        {
            'job': 'install',
            'package': Package('a', '1.0'),
            'skipped': False,
        },
  +     {
  +         'job': 'install',
  +         'package': Package('a', '2.0'),
  +         'skipped': False,
  +     },
    ]
============= 5 failed, 1706 passed, 7 skipped in 89.45s (0:01:29) =============

Annotations

Check failure on line 161 in src/poetry/puzzle/solver.py

See this annotation in the file changed.

@cirrus-ci cirrus-ci / Tests / FreeBSD (Python 3.10) / pytest

src/poetry/puzzle/solver.py#L161

tests.installation.test_installer.test_run_with_conflicting_dependency_extras[extra-one-False]
Raw output
self = <poetry.puzzle.solver.Solver object at 0x31c7a2c0f430>

    def _solve(self) -> tuple[list[Package], list[int]]:
        if self._provider._overrides:
            self._overrides.append(self._provider._overrides)
    
        try:
>           result = resolve_version(self._package, self._provider)

/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:161: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/tmp/cirrus-ci-build/src/poetry/mixology/__init__.py:18: in resolve_version
    return solver.solve()
/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:174: in solve
    self._propagate(next)
/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:213: in _propagate
    root_cause = self._resolve_conflict(incompatibility)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <poetry.mixology.version_solver.VersionSolver object at 0x31c7a2ea1e70>
incompatibility = <Incompatibility version solving failed>

    def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility:
        """
        Given an incompatibility that's satisfied by _solution,
        The `conflict resolution`_ constructs a new incompatibility that encapsulates
        the root cause of the conflict and backtracks _solution until the new
        incompatibility will allow _propagate() to deduce new assignments.
    
        Adds the new incompatibility to _incompatibilities and returns it.
    
        .. _conflict resolution:
        https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution
        """
        self._log(f"conflict: {incompatibility}")
    
        new_incompatibility = False
        while not incompatibility.is_failure():
            # The term in incompatibility.terms that was most recently satisfied by
            # _solution.
            most_recent_term = None
    
            # The earliest assignment in _solution such that incompatibility is
            # satisfied by _solution up to and including this assignment.
            most_recent_satisfier = None
    
            # The difference between most_recent_satisfier and most_recent_term;
            # that is, the versions that are allowed by most_recent_satisfier and not
            # by most_recent_term. This is None if most_recent_satisfier totally
            # satisfies most_recent_term.
            difference = None
    
            # The decision level of the earliest assignment in _solution *before*
            # most_recent_satisfier such that incompatibility is satisfied by
            # _solution up to and including this assignment plus
            # most_recent_satisfier.
            #
            # Decision level 1 is the level where the root package was selected. It's
            # safe to go back to decision level 0, but stopping at 1 tends to produce
            # better error messages, because references to the root package end up
            # closer to the final conclusion that no solution exists.
            previous_satisfier_level = 1
    
            for term in incompatibility.terms:
                satisfier = self._solution.satisfier(term)
    
                if most_recent_satisfier is None:
                    most_recent_term = term
                    most_recent_satisfier = satisfier
                elif most_recent_satisfier.index < satisfier.index:
                    previous_satisfier_level = max(
                        previous_satisfier_level, most_recent_satisfier.decision_level
                    )
                    most_recent_term = term
                    most_recent_satisfier = satisfier
                    difference = None
                else:
                    previous_satisfier_level = max(
                        previous_satisfier_level, satisfier.decision_level
                    )
    
                if most_recent_term == term:
                    # If most_recent_satisfier doesn't satisfy most_recent_term on its
                    # own, then the next-most-recent satisfier may be the one that
                    # satisfies the remainder.
                    difference = most_recent_satisfier.difference(most_recent_term)
                    if difference is not None:
                        previous_satisfier_level = max(
                            previous_satisfier_level,
                            self._solution.satisfier(difference.inverse).decision_level,
                        )
    
            # If most_recent_identifier is the only satisfier left at its decision
            # level, or if it has no cause (indicating that it's a decision rather
            # than a derivation), then incompatibility is the root cause. We then
            # backjump to previous_satisfier_level, where incompatibility is
            # guaranteed to allow _propagate to produce more assignments.
    
            # using assert to suppress mypy [union-attr]
            assert most_recent_satisfier is not None
            if (
                previous_satisfier_level < most_recent_satisfier.decision_level
                or most_recent_satisfier.cause is None
            ):
                for level in range(
                    self._solution.decision_level, previous_satisfier_level, -1
                ):
                    if level in self._contradicted_incompatibilities_by_level:
                        self._contradicted_incompatibilities.difference_update(
                            self._contradicted_incompatibilities_by_level.pop(level),
                        )
                    self._dependency_cache.clear_level(level)
    
                self._solution.backtrack(previous_satisfier_level)
                if new_incompatibility:
                    self._add_incompatibility(incompatibility)
    
                return incompatibility
    
            # Create a new incompatibility by combining incompatibility with the
            # incompatibility that caused most_recent_satisfier to be assigned. Doing
            # this iteratively constructs an incompatibility that's guaranteed to be
            # true (that is, we know for sure no solution will satisfy the
            # incompatibility) while also approximating the intuitive notion of the
            # "root cause" of the conflict.
            new_terms = [
                term for term in incompatibility.terms if term != most_recent_term
            ]
    
            for term in most_recent_satisfier.cause.terms:
                if term.dependency != most_recent_satisfier.dependency:
                    new_terms.append(term)
    
            # The most_recent_satisfier may not satisfy most_recent_term on its own
            # if there are a collection of constraints on most_recent_term that
            # only satisfy it together. For example, if most_recent_term is
            # `foo ^1.0.0` and _solution contains `[foo >=1.0.0,
            # foo <2.0.0]`, then most_recent_satisfier will be `foo <2.0.0` even
            # though it doesn't totally satisfy `foo ^1.0.0`.
            #
            # In this case, we add `not (most_recent_satisfier \ most_recent_term)` to
            # the incompatibility as well, See the `algorithm documentation`_ for
            # details.
            #
            # .. _algorithm documentation:
            # https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution
            if difference is not None:
                inverse = difference.inverse
                if inverse.dependency != most_recent_satisfier.dependency:
                    new_terms.append(inverse)
    
            incompatibility = Incompatibility(
                new_terms, ConflictCause(incompatibility, most_recent_satisfier.cause)
            )
            new_incompatibility = True
    
            partially = "" if difference is None else " partially"
            self._log(
                f"! {most_recent_term} is{partially} satisfied by"
                f" {most_recent_satisfier}"
            )
            self._log(f'! which is caused by "{most_recent_satisfier.cause}"')
            self._log(f"! thus: {incompatibility}")
    
>       raise SolveFailure(incompatibility)
E       poetry.mixology.failure.SolveFailure: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
E        and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
E       So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.

/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:427: SolveFailure

During handling of the above exception, another exception occurred:

installer = <poetry.installation.installer.Installer object at 0x31c7a2c0dae0>
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x31c7a4420fa0>
locker = <tests.installation.test_installer.Locker object at 0x31c7a4422da0>
installed = <tests.installation.test_installer.CustomInstalledRepository object at 0x31c7a4422e00>
repo = <poetry.repositories.repository.Repository object at 0x31c7a44222f0>
config = <tests.conftest.Config object at 0x31c7a447b940>
package = Package('root', '1.0'), extra = 'extra-one', locked = False

    @pytest.mark.parametrize("locked", [False])  # TODO: lock data
    @pytest.mark.parametrize("extra", [None, "extra-one", "extra-two"])
    def test_run_with_conflicting_dependency_extras(
        installer: Installer,
        pool: RepositoryPool,
        locker: Locker,
        installed: CustomInstalledRepository,
        repo: Repository,
        config: Config,
        package: ProjectPackage,
        extra: str | None,
        locked: bool,
    ) -> None:
        """https://github.com/python-poetry/poetry/issues/834
    
        Tests resolution of extras in both root ('extra-one', 'extra-two') and transitive
        ('demo-extra-one', 'demo-extra-two') dependencies
        """
        # Demo package with two optional transitive dependencies, one for each extra
        demo_pkg = get_package("demo", "1.0.0")
        transitive_dep_one = get_package("transitive-dep", "1.1.0")
        transitive_dep_two = get_package("transitive-dep", "1.2.0")
        demo_pkg.extras = {
            canonicalize_name("demo-extra-one"): [
                get_dependency("transitive-dep", constraint={"version": "1.1.0", "optional": True})
            ],
            canonicalize_name("demo-extra-two"): [
                get_dependency("transitive-dep", constraint={"version": "1.2.0", "optional": True})
            ],
        }
        demo_pkg.add_dependency(
            Factory.create_dependency(
                "transitive-dep",
                {
                    "version": "1.1.0",
                    "markers": "extra == 'demo-extra-one' and extra != 'demo-extra-two'",
                    "optional": True,
                }
            )
        )
        demo_pkg.add_dependency(
            Factory.create_dependency(
                "transitive-dep",
                {
                    "version": "1.2.0",
                    "markers": "extra != 'demo-extra-one' and extra == 'demo-extra-two'",
                    "optional": True,
                }
            )
        )
        repo.add_package(demo_pkg)
        repo.add_package(transitive_dep_one)
        repo.add_package(transitive_dep_two)
    
        # 'demo' with extra 'demo-extra-one' when package has 'extra-one' extra
        #  and with extra 'demo-extra-two' when 'extra-two'
        extra_one_dep = Factory.create_dependency(
            "demo",
            {
                "version": "1.0.0",
                "markers": "extra == 'extra-one' and extra != 'extra-two'",
                "extras": ["demo-extra-one"],
                "optional": True,
            },
        )
        extra_two_dep = Factory.create_dependency(
            "demo",
            {
                "version": "1.0.0",
                "markers": "extra != 'extra-one' and extra == 'extra-two'",
                "extras": ["demo-extra-two"],
                "optional": True,
            },
        )
        package.add_dependency(extra_one_dep)
        package.add_dependency(extra_two_dep)
        package.extras = {
            canonicalize_name("extra-one"): [extra_one_dep],
            canonicalize_name("extra-two"): [extra_two_dep],
        }
    
        locker.locked(locked)
        if locked:
            raise ValueError("no lock data for this test yet")
            locker.mock_lock_data(dict(fixture("TODO")))
    
        if extra is not None:
            installer.extras([extra])
>       result = installer.run()

/tmp/cirrus-ci-build/tests/installation/test_installer.py:1118: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/tmp/cirrus-ci-build/src/poetry/installation/installer.py:102: in run
    return self._do_install()
/tmp/cirrus-ci-build/src/poetry/installation/installer.py:239: in _do_install
    ops = solver.solve(use_latest=self._whitelist).calculate_operations()
/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:78: in solve
    packages, depths = self._solve()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <poetry.puzzle.solver.Solver object at 0x31c7a2c0f430>

    def _solve(self) -> tuple[list[Package], list[int]]:
        if self._provider._overrides:
            self._overrides.append(self._provider._overrides)
    
        try:
            result = resolve_version(self._package, self._provider)
    
            packages = result.packages
        except OverrideNeeded as e:
            return self._solve_in_compatibility_mode(e.overrides)
        except SolveFailure as e:
>           raise SolverProblemError(e)
E           poetry.puzzle.exceptions.SolverProblemError: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
E            and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
E           So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.

/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:167: SolverProblemError

Check failure on line 161 in src/poetry/puzzle/solver.py

See this annotation in the file changed.

@cirrus-ci cirrus-ci / Tests / FreeBSD (Python 3.10) / pytest

src/poetry/puzzle/solver.py#L161

tests.installation.test_installer.test_run_with_conflicting_dependency_extras[extra-two-False]
Raw output
self = <poetry.puzzle.solver.Solver object at 0x31c7a43aae00>

    def _solve(self) -> tuple[list[Package], list[int]]:
        if self._provider._overrides:
            self._overrides.append(self._provider._overrides)
    
        try:
>           result = resolve_version(self._package, self._provider)

/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:161: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/tmp/cirrus-ci-build/src/poetry/mixology/__init__.py:18: in resolve_version
    return solver.solve()
/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:174: in solve
    self._propagate(next)
/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:213: in _propagate
    root_cause = self._resolve_conflict(incompatibility)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <poetry.mixology.version_solver.VersionSolver object at 0x31c7a43ab610>
incompatibility = <Incompatibility version solving failed>

    def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility:
        """
        Given an incompatibility that's satisfied by _solution,
        The `conflict resolution`_ constructs a new incompatibility that encapsulates
        the root cause of the conflict and backtracks _solution until the new
        incompatibility will allow _propagate() to deduce new assignments.
    
        Adds the new incompatibility to _incompatibilities and returns it.
    
        .. _conflict resolution:
        https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution
        """
        self._log(f"conflict: {incompatibility}")
    
        new_incompatibility = False
        while not incompatibility.is_failure():
            # The term in incompatibility.terms that was most recently satisfied by
            # _solution.
            most_recent_term = None
    
            # The earliest assignment in _solution such that incompatibility is
            # satisfied by _solution up to and including this assignment.
            most_recent_satisfier = None
    
            # The difference between most_recent_satisfier and most_recent_term;
            # that is, the versions that are allowed by most_recent_satisfier and not
            # by most_recent_term. This is None if most_recent_satisfier totally
            # satisfies most_recent_term.
            difference = None
    
            # The decision level of the earliest assignment in _solution *before*
            # most_recent_satisfier such that incompatibility is satisfied by
            # _solution up to and including this assignment plus
            # most_recent_satisfier.
            #
            # Decision level 1 is the level where the root package was selected. It's
            # safe to go back to decision level 0, but stopping at 1 tends to produce
            # better error messages, because references to the root package end up
            # closer to the final conclusion that no solution exists.
            previous_satisfier_level = 1
    
            for term in incompatibility.terms:
                satisfier = self._solution.satisfier(term)
    
                if most_recent_satisfier is None:
                    most_recent_term = term
                    most_recent_satisfier = satisfier
                elif most_recent_satisfier.index < satisfier.index:
                    previous_satisfier_level = max(
                        previous_satisfier_level, most_recent_satisfier.decision_level
                    )
                    most_recent_term = term
                    most_recent_satisfier = satisfier
                    difference = None
                else:
                    previous_satisfier_level = max(
                        previous_satisfier_level, satisfier.decision_level
                    )
    
                if most_recent_term == term:
                    # If most_recent_satisfier doesn't satisfy most_recent_term on its
                    # own, then the next-most-recent satisfier may be the one that
                    # satisfies the remainder.
                    difference = most_recent_satisfier.difference(most_recent_term)
                    if difference is not None:
                        previous_satisfier_level = max(
                            previous_satisfier_level,
                            self._solution.satisfier(difference.inverse).decision_level,
                        )
    
            # If most_recent_identifier is the only satisfier left at its decision
            # level, or if it has no cause (indicating that it's a decision rather
            # than a derivation), then incompatibility is the root cause. We then
            # backjump to previous_satisfier_level, where incompatibility is
            # guaranteed to allow _propagate to produce more assignments.
    
            # using assert to suppress mypy [union-attr]
            assert most_recent_satisfier is not None
            if (
                previous_satisfier_level < most_recent_satisfier.decision_level
                or most_recent_satisfier.cause is None
            ):
                for level in range(
                    self._solution.decision_level, previous_satisfier_level, -1
                ):
                    if level in self._contradicted_incompatibilities_by_level:
                        self._contradicted_incompatibilities.difference_update(
                            self._contradicted_incompatibilities_by_level.pop(level),
                        )
                    self._dependency_cache.clear_level(level)
    
                self._solution.backtrack(previous_satisfier_level)
                if new_incompatibility:
                    self._add_incompatibility(incompatibility)
    
                return incompatibility
    
            # Create a new incompatibility by combining incompatibility with the
            # incompatibility that caused most_recent_satisfier to be assigned. Doing
            # this iteratively constructs an incompatibility that's guaranteed to be
            # true (that is, we know for sure no solution will satisfy the
            # incompatibility) while also approximating the intuitive notion of the
            # "root cause" of the conflict.
            new_terms = [
                term for term in incompatibility.terms if term != most_recent_term
            ]
    
            for term in most_recent_satisfier.cause.terms:
                if term.dependency != most_recent_satisfier.dependency:
                    new_terms.append(term)
    
            # The most_recent_satisfier may not satisfy most_recent_term on its own
            # if there are a collection of constraints on most_recent_term that
            # only satisfy it together. For example, if most_recent_term is
            # `foo ^1.0.0` and _solution contains `[foo >=1.0.0,
            # foo <2.0.0]`, then most_recent_satisfier will be `foo <2.0.0` even
            # though it doesn't totally satisfy `foo ^1.0.0`.
            #
            # In this case, we add `not (most_recent_satisfier \ most_recent_term)` to
            # the incompatibility as well, See the `algorithm documentation`_ for
            # details.
            #
            # .. _algorithm documentation:
            # https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution
            if difference is not None:
                inverse = difference.inverse
                if inverse.dependency != most_recent_satisfier.dependency:
                    new_terms.append(inverse)
    
            incompatibility = Incompatibility(
                new_terms, ConflictCause(incompatibility, most_recent_satisfier.cause)
            )
            new_incompatibility = True
    
            partially = "" if difference is None else " partially"
            self._log(
                f"! {most_recent_term} is{partially} satisfied by"
                f" {most_recent_satisfier}"
            )
            self._log(f'! which is caused by "{most_recent_satisfier.cause}"')
            self._log(f"! thus: {incompatibility}")
    
>       raise SolveFailure(incompatibility)
E       poetry.mixology.failure.SolveFailure: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
E        and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
E       So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.

/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:427: SolveFailure

During handling of the above exception, another exception occurred:

installer = <poetry.installation.installer.Installer object at 0x31c7a28e0340>
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x31c7a43a4280>
locker = <tests.installation.test_installer.Locker object at 0x31c7a43a4910>
installed = <tests.installation.test_installer.CustomInstalledRepository object at 0x31c7a43a6470>
repo = <poetry.repositories.repository.Repository object at 0x31c7a43a44c0>
config = <tests.conftest.Config object at 0x31c7a4453820>
package = Package('root', '1.0'), extra = 'extra-two', locked = False

    @pytest.mark.parametrize("locked", [False])  # TODO: lock data
    @pytest.mark.parametrize("extra", [None, "extra-one", "extra-two"])
    def test_run_with_conflicting_dependency_extras(
        installer: Installer,
        pool: RepositoryPool,
        locker: Locker,
        installed: CustomInstalledRepository,
        repo: Repository,
        config: Config,
        package: ProjectPackage,
        extra: str | None,
        locked: bool,
    ) -> None:
        """https://github.com/python-poetry/poetry/issues/834
    
        Tests resolution of extras in both root ('extra-one', 'extra-two') and transitive
        ('demo-extra-one', 'demo-extra-two') dependencies
        """
        # Demo package with two optional transitive dependencies, one for each extra
        demo_pkg = get_package("demo", "1.0.0")
        transitive_dep_one = get_package("transitive-dep", "1.1.0")
        transitive_dep_two = get_package("transitive-dep", "1.2.0")
        demo_pkg.extras = {
            canonicalize_name("demo-extra-one"): [
                get_dependency("transitive-dep", constraint={"version": "1.1.0", "optional": True})
            ],
            canonicalize_name("demo-extra-two"): [
                get_dependency("transitive-dep", constraint={"version": "1.2.0", "optional": True})
            ],
        }
        demo_pkg.add_dependency(
            Factory.create_dependency(
                "transitive-dep",
                {
                    "version": "1.1.0",
                    "markers": "extra == 'demo-extra-one' and extra != 'demo-extra-two'",
                    "optional": True,
                }
            )
        )
        demo_pkg.add_dependency(
            Factory.create_dependency(
                "transitive-dep",
                {
                    "version": "1.2.0",
                    "markers": "extra != 'demo-extra-one' and extra == 'demo-extra-two'",
                    "optional": True,
                }
            )
        )
        repo.add_package(demo_pkg)
        repo.add_package(transitive_dep_one)
        repo.add_package(transitive_dep_two)
    
        # 'demo' with extra 'demo-extra-one' when package has 'extra-one' extra
        #  and with extra 'demo-extra-two' when 'extra-two'
        extra_one_dep = Factory.create_dependency(
            "demo",
            {
                "version": "1.0.0",
                "markers": "extra == 'extra-one' and extra != 'extra-two'",
                "extras": ["demo-extra-one"],
                "optional": True,
            },
        )
        extra_two_dep = Factory.create_dependency(
            "demo",
            {
                "version": "1.0.0",
                "markers": "extra != 'extra-one' and extra == 'extra-two'",
                "extras": ["demo-extra-two"],
                "optional": True,
            },
        )
        package.add_dependency(extra_one_dep)
        package.add_dependency(extra_two_dep)
        package.extras = {
            canonicalize_name("extra-one"): [extra_one_dep],
            canonicalize_name("extra-two"): [extra_two_dep],
        }
    
        locker.locked(locked)
        if locked:
            raise ValueError("no lock data for this test yet")
            locker.mock_lock_data(dict(fixture("TODO")))
    
        if extra is not None:
            installer.extras([extra])
>       result = installer.run()

/tmp/cirrus-ci-build/tests/installation/test_installer.py:1118: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/tmp/cirrus-ci-build/src/poetry/installation/installer.py:102: in run
    return self._do_install()
/tmp/cirrus-ci-build/src/poetry/installation/installer.py:239: in _do_install
    ops = solver.solve(use_latest=self._whitelist).calculate_operations()
/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:78: in solve
    packages, depths = self._solve()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <poetry.puzzle.solver.Solver object at 0x31c7a43aae00>

    def _solve(self) -> tuple[list[Package], list[int]]:
        if self._provider._overrides:
            self._overrides.append(self._provider._overrides)
    
        try:
            result = resolve_version(self._package, self._provider)
    
            packages = result.packages
        except OverrideNeeded as e:
            return self._solve_in_compatibility_mode(e.overrides)
        except SolveFailure as e:
>           raise SolverProblemError(e)
E           poetry.puzzle.exceptions.SolverProblemError: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
E            and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
E           So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.

/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:167: SolverProblemError

Check failure on line 161 in src/poetry/puzzle/solver.py

See this annotation in the file changed.

@cirrus-ci cirrus-ci / Tests / FreeBSD (Python 3.10) / pytest

src/poetry/puzzle/solver.py#L161

tests.installation.test_installer.test_run_with_conflicting_dependency_extras[None-False]
Raw output
self = <poetry.puzzle.solver.Solver object at 0x31c7a43de0b0>

    def _solve(self) -> tuple[list[Package], list[int]]:
        if self._provider._overrides:
            self._overrides.append(self._provider._overrides)
    
        try:
>           result = resolve_version(self._package, self._provider)

/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:161: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/tmp/cirrus-ci-build/src/poetry/mixology/__init__.py:18: in resolve_version
    return solver.solve()
/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:174: in solve
    self._propagate(next)
/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:213: in _propagate
    root_cause = self._resolve_conflict(incompatibility)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <poetry.mixology.version_solver.VersionSolver object at 0x31c7a43de110>
incompatibility = <Incompatibility version solving failed>

    def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility:
        """
        Given an incompatibility that's satisfied by _solution,
        The `conflict resolution`_ constructs a new incompatibility that encapsulates
        the root cause of the conflict and backtracks _solution until the new
        incompatibility will allow _propagate() to deduce new assignments.
    
        Adds the new incompatibility to _incompatibilities and returns it.
    
        .. _conflict resolution:
        https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution
        """
        self._log(f"conflict: {incompatibility}")
    
        new_incompatibility = False
        while not incompatibility.is_failure():
            # The term in incompatibility.terms that was most recently satisfied by
            # _solution.
            most_recent_term = None
    
            # The earliest assignment in _solution such that incompatibility is
            # satisfied by _solution up to and including this assignment.
            most_recent_satisfier = None
    
            # The difference between most_recent_satisfier and most_recent_term;
            # that is, the versions that are allowed by most_recent_satisfier and not
            # by most_recent_term. This is None if most_recent_satisfier totally
            # satisfies most_recent_term.
            difference = None
    
            # The decision level of the earliest assignment in _solution *before*
            # most_recent_satisfier such that incompatibility is satisfied by
            # _solution up to and including this assignment plus
            # most_recent_satisfier.
            #
            # Decision level 1 is the level where the root package was selected. It's
            # safe to go back to decision level 0, but stopping at 1 tends to produce
            # better error messages, because references to the root package end up
            # closer to the final conclusion that no solution exists.
            previous_satisfier_level = 1
    
            for term in incompatibility.terms:
                satisfier = self._solution.satisfier(term)
    
                if most_recent_satisfier is None:
                    most_recent_term = term
                    most_recent_satisfier = satisfier
                elif most_recent_satisfier.index < satisfier.index:
                    previous_satisfier_level = max(
                        previous_satisfier_level, most_recent_satisfier.decision_level
                    )
                    most_recent_term = term
                    most_recent_satisfier = satisfier
                    difference = None
                else:
                    previous_satisfier_level = max(
                        previous_satisfier_level, satisfier.decision_level
                    )
    
                if most_recent_term == term:
                    # If most_recent_satisfier doesn't satisfy most_recent_term on its
                    # own, then the next-most-recent satisfier may be the one that
                    # satisfies the remainder.
                    difference = most_recent_satisfier.difference(most_recent_term)
                    if difference is not None:
                        previous_satisfier_level = max(
                            previous_satisfier_level,
                            self._solution.satisfier(difference.inverse).decision_level,
                        )
    
            # If most_recent_identifier is the only satisfier left at its decision
            # level, or if it has no cause (indicating that it's a decision rather
            # than a derivation), then incompatibility is the root cause. We then
            # backjump to previous_satisfier_level, where incompatibility is
            # guaranteed to allow _propagate to produce more assignments.
    
            # using assert to suppress mypy [union-attr]
            assert most_recent_satisfier is not None
            if (
                previous_satisfier_level < most_recent_satisfier.decision_level
                or most_recent_satisfier.cause is None
            ):
                for level in range(
                    self._solution.decision_level, previous_satisfier_level, -1
                ):
                    if level in self._contradicted_incompatibilities_by_level:
                        self._contradicted_incompatibilities.difference_update(
                            self._contradicted_incompatibilities_by_level.pop(level),
                        )
                    self._dependency_cache.clear_level(level)
    
                self._solution.backtrack(previous_satisfier_level)
                if new_incompatibility:
                    self._add_incompatibility(incompatibility)
    
                return incompatibility
    
            # Create a new incompatibility by combining incompatibility with the
            # incompatibility that caused most_recent_satisfier to be assigned. Doing
            # this iteratively constructs an incompatibility that's guaranteed to be
            # true (that is, we know for sure no solution will satisfy the
            # incompatibility) while also approximating the intuitive notion of the
            # "root cause" of the conflict.
            new_terms = [
                term for term in incompatibility.terms if term != most_recent_term
            ]
    
            for term in most_recent_satisfier.cause.terms:
                if term.dependency != most_recent_satisfier.dependency:
                    new_terms.append(term)
    
            # The most_recent_satisfier may not satisfy most_recent_term on its own
            # if there are a collection of constraints on most_recent_term that
            # only satisfy it together. For example, if most_recent_term is
            # `foo ^1.0.0` and _solution contains `[foo >=1.0.0,
            # foo <2.0.0]`, then most_recent_satisfier will be `foo <2.0.0` even
            # though it doesn't totally satisfy `foo ^1.0.0`.
            #
            # In this case, we add `not (most_recent_satisfier \ most_recent_term)` to
            # the incompatibility as well, See the `algorithm documentation`_ for
            # details.
            #
            # .. _algorithm documentation:
            # https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution
            if difference is not None:
                inverse = difference.inverse
                if inverse.dependency != most_recent_satisfier.dependency:
                    new_terms.append(inverse)
    
            incompatibility = Incompatibility(
                new_terms, ConflictCause(incompatibility, most_recent_satisfier.cause)
            )
            new_incompatibility = True
    
            partially = "" if difference is None else " partially"
            self._log(
                f"! {most_recent_term} is{partially} satisfied by"
                f" {most_recent_satisfier}"
            )
            self._log(f'! which is caused by "{most_recent_satisfier.cause}"')
            self._log(f"! thus: {incompatibility}")
    
>       raise SolveFailure(incompatibility)
E       poetry.mixology.failure.SolveFailure: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
E        and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
E       So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.

/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:427: SolveFailure

During handling of the above exception, another exception occurred:

installer = <poetry.installation.installer.Installer object at 0x31c7a43dfb80>
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x31c7a43dd030>
locker = <tests.installation.test_installer.Locker object at 0x31c7a43ddc90>
installed = <tests.installation.test_installer.CustomInstalledRepository object at 0x31c7a43dcfa0>
repo = <poetry.repositories.repository.Repository object at 0x31c7a43dd180>
config = <tests.conftest.Config object at 0x31c7a43ec370>
package = Package('root', '1.0'), extra = None, locked = False

    @pytest.mark.parametrize("locked", [False])  # TODO: lock data
    @pytest.mark.parametrize("extra", [None, "extra-one", "extra-two"])
    def test_run_with_conflicting_dependency_extras(
        installer: Installer,
        pool: RepositoryPool,
        locker: Locker,
        installed: CustomInstalledRepository,
        repo: Repository,
        config: Config,
        package: ProjectPackage,
        extra: str | None,
        locked: bool,
    ) -> None:
        """https://github.com/python-poetry/poetry/issues/834
    
        Tests resolution of extras in both root ('extra-one', 'extra-two') and transitive
        ('demo-extra-one', 'demo-extra-two') dependencies
        """
        # Demo package with two optional transitive dependencies, one for each extra
        demo_pkg = get_package("demo", "1.0.0")
        transitive_dep_one = get_package("transitive-dep", "1.1.0")
        transitive_dep_two = get_package("transitive-dep", "1.2.0")
        demo_pkg.extras = {
            canonicalize_name("demo-extra-one"): [
                get_dependency("transitive-dep", constraint={"version": "1.1.0", "optional": True})
            ],
            canonicalize_name("demo-extra-two"): [
                get_dependency("transitive-dep", constraint={"version": "1.2.0", "optional": True})
            ],
        }
        demo_pkg.add_dependency(
            Factory.create_dependency(
                "transitive-dep",
                {
                    "version": "1.1.0",
                    "markers": "extra == 'demo-extra-one' and extra != 'demo-extra-two'",
                    "optional": True,
                }
            )
        )
        demo_pkg.add_dependency(
            Factory.create_dependency(
                "transitive-dep",
                {
                    "version": "1.2.0",
                    "markers": "extra != 'demo-extra-one' and extra == 'demo-extra-two'",
                    "optional": True,
                }
            )
        )
        repo.add_package(demo_pkg)
        repo.add_package(transitive_dep_one)
        repo.add_package(transitive_dep_two)
    
        # 'demo' with extra 'demo-extra-one' when package has 'extra-one' extra
        #  and with extra 'demo-extra-two' when 'extra-two'
        extra_one_dep = Factory.create_dependency(
            "demo",
            {
                "version": "1.0.0",
                "markers": "extra == 'extra-one' and extra != 'extra-two'",
                "extras": ["demo-extra-one"],
                "optional": True,
            },
        )
        extra_two_dep = Factory.create_dependency(
            "demo",
            {
                "version": "1.0.0",
                "markers": "extra != 'extra-one' and extra == 'extra-two'",
                "extras": ["demo-extra-two"],
                "optional": True,
            },
        )
        package.add_dependency(extra_one_dep)
        package.add_dependency(extra_two_dep)
        package.extras = {
            canonicalize_name("extra-one"): [extra_one_dep],
            canonicalize_name("extra-two"): [extra_two_dep],
        }
    
        locker.locked(locked)
        if locked:
            raise ValueError("no lock data for this test yet")
            locker.mock_lock_data(dict(fixture("TODO")))
    
        if extra is not None:
            installer.extras([extra])
>       result = installer.run()

/tmp/cirrus-ci-build/tests/installation/test_installer.py:1118: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/tmp/cirrus-ci-build/src/poetry/installation/installer.py:102: in run
    return self._do_install()
/tmp/cirrus-ci-build/src/poetry/installation/installer.py:239: in _do_install
    ops = solver.solve(use_latest=self._whitelist).calculate_operations()
/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:78: in solve
    packages, depths = self._solve()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <poetry.puzzle.solver.Solver object at 0x31c7a43de0b0>

    def _solve(self) -> tuple[list[Package], list[int]]:
        if self._provider._overrides:
            self._overrides.append(self._provider._overrides)
    
        try:
            result = resolve_version(self._package, self._provider)
    
            packages = result.packages
        except OverrideNeeded as e:
            return self._solve_in_compatibility_mode(e.overrides)
        except SolveFailure as e:
>           raise SolverProblemError(e)
E           poetry.puzzle.exceptions.SolverProblemError: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
E            and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
E           So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.

/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:167: SolverProblemError

Check failure on line 4741 in tests/puzzle/test_solver.py

See this annotation in the file changed.

@cirrus-ci cirrus-ci / Tests / FreeBSD (Python 3.10) / pytest

tests/puzzle/test_solver.py#L4741

tests.puzzle.test_solver.test_solver_resolves_duplicate_dependency_in_root_extra[False]
Raw output
package = Package('root', '1.0')
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x235c06e10b50>
repo = <poetry.repositories.repository.Repository object at 0x235c06e10c10>
io = <cleo.io.null_io.NullIO object at 0x235c06e10f40>, with_extra = False

    @pytest.mark.parametrize("with_extra", [False, True])
    def test_solver_resolves_duplicate_dependency_in_root_extra(
        package: ProjectPackage,
        pool: RepositoryPool,
        repo: Repository,
        io: NullIO,
        with_extra: bool,
    ) -> None:
        """
        Without extras, a newer version of A can be chosen than with root extras.
        """
        constraint: dict[str, Any] = {"version": "*"}
        if with_extra:
            constraint["extras"] = ["foo"]
        package_a1 = get_package("A", "1.0")
        package_a2 = get_package("A", "2.0")
    
        dep = get_dependency("A", ">=1.0")
        package.add_dependency(dep)
    
        dep_extra = get_dependency("A", "^1.0", optional=True)
        dep_extra.marker = parse_marker("extra == 'foo'")
        package.extras = {canonicalize_name("foo"): [dep_extra]}
        package.add_dependency(dep_extra)
    
        repo.add_package(package_a1)
        repo.add_package(package_a2)
    
        solver = Solver(package, pool, [], [], io)
        transaction = solver.solve()
    
>       check_solver_result(
            transaction,
            (
                [
                    {"job": "install", "package": package_a1 if with_extra else package_a2},
                ]
            ),
        )

/tmp/cirrus-ci-build/tests/puzzle/test_solver.py:4741: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

transaction = <poetry.puzzle.transaction.Transaction object at 0x235c06ef3ee0>
expected = [{'job': 'install', 'package': Package('a', '2.0'), 'skipped': False}]
synchronize = False

    def check_solver_result(
        transaction: Transaction,
        expected: list[dict[str, Any]],
        synchronize: bool = False,
    ) -> list[Operation]:
        for e in expected:
            if "skipped" not in e:
                e["skipped"] = False
    
        result = []
        ops = transaction.calculate_operations(synchronize=synchronize)
        for op in ops:
            if op.job_type == "update":
                assert isinstance(op, Update)
                result.append(
                    {
                        "job": "update",
                        "from": op.initial_package,
                        "to": op.target_package,
                        "skipped": op.skipped,
                    }
                )
            else:
                job = "install"
                if op.job_type == "uninstall":
                    job = "remove"
    
                result.append({"job": job, "package": op.package, "skipped": op.skipped})
    
>       assert result == expected
E       AssertionError: assert [{'job': 'ins...pped': False}] == [{'job': 'ins...pped': False}]
E         
E         At index 0 diff: {'job': 'install', 'package': Package('a', '1.0'), 'skipped': False} != {'job': 'install', 'package': Package('a', '2.0'), 'skipped': False}
E         Left contains one more item: {'job': 'install', 'package': Package('a', '2.0'), 'skipped': False}
E         
E         Full diff:
E           [
E         +     {
E         +         'job': 'install',
E         +         'package': Package('a', '1.0'),
E         +         'skipped': False,
E         +     },
E               {
E                   'job': 'install',
E                   'package': Package('a', '2.0'),
E                   'skipped': False,
E               },
E           ]

/tmp/cirrus-ci-build/tests/puzzle/test_solver.py:113: AssertionError

Check failure on line 4741 in tests/puzzle/test_solver.py

See this annotation in the file changed.

@cirrus-ci cirrus-ci / Tests / FreeBSD (Python 3.10) / pytest

tests/puzzle/test_solver.py#L4741

tests.puzzle.test_solver.test_solver_resolves_duplicate_dependency_in_root_extra[True]
Raw output
package = Package('root', '1.0')
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x235c06f223b0>
repo = <poetry.repositories.repository.Repository object at 0x235c06f21ff0>
io = <cleo.io.null_io.NullIO object at 0x235c07480490>, with_extra = True

    @pytest.mark.parametrize("with_extra", [False, True])
    def test_solver_resolves_duplicate_dependency_in_root_extra(
        package: ProjectPackage,
        pool: RepositoryPool,
        repo: Repository,
        io: NullIO,
        with_extra: bool,
    ) -> None:
        """
        Without extras, a newer version of A can be chosen than with root extras.
        """
        constraint: dict[str, Any] = {"version": "*"}
        if with_extra:
            constraint["extras"] = ["foo"]
        package_a1 = get_package("A", "1.0")
        package_a2 = get_package("A", "2.0")
    
        dep = get_dependency("A", ">=1.0")
        package.add_dependency(dep)
    
        dep_extra = get_dependency("A", "^1.0", optional=True)
        dep_extra.marker = parse_marker("extra == 'foo'")
        package.extras = {canonicalize_name("foo"): [dep_extra]}
        package.add_dependency(dep_extra)
    
        repo.add_package(package_a1)
        repo.add_package(package_a2)
    
        solver = Solver(package, pool, [], [], io)
        transaction = solver.solve()
    
>       check_solver_result(
            transaction,
            (
                [
                    {"job": "install", "package": package_a1 if with_extra else package_a2},
                ]
            ),
        )

/tmp/cirrus-ci-build/tests/puzzle/test_solver.py:4741: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

transaction = <poetry.puzzle.transaction.Transaction object at 0x235c06ee39a0>
expected = [{'job': 'install', 'package': Package('a', '1.0'), 'skipped': False}]
synchronize = False

    def check_solver_result(
        transaction: Transaction,
        expected: list[dict[str, Any]],
        synchronize: bool = False,
    ) -> list[Operation]:
        for e in expected:
            if "skipped" not in e:
                e["skipped"] = False
    
        result = []
        ops = transaction.calculate_operations(synchronize=synchronize)
        for op in ops:
            if op.job_type == "update":
                assert isinstance(op, Update)
                result.append(
                    {
                        "job": "update",
                        "from": op.initial_package,
                        "to": op.target_package,
                        "skipped": op.skipped,
                    }
                )
            else:
                job = "install"
                if op.job_type == "uninstall":
                    job = "remove"
    
                result.append({"job": job, "package": op.package, "skipped": op.skipped})
    
>       assert result == expected
E       AssertionError: assert [{'job': 'ins...pped': False}] == [{'job': 'ins...pped': False}]
E         
E         Left contains one more item: {'job': 'install', 'package': Package('a', '2.0'), 'skipped': False}
E         
E         Full diff:
E           [
E               {
E                   'job': 'install',
E                   'package': Package('a', '1.0'),
E                   'skipped': False,
E               },
E         +     {
E         +         'job': 'install',
E         +         'package': Package('a', '2.0'),
E         +         'skipped': False,
E         +     },
E           ]

/tmp/cirrus-ci-build/tests/puzzle/test_solver.py:113: AssertionError