diff --git a/ahriman-core/src/ahriman/core/exceptions.py b/ahriman-core/src/ahriman/core/exceptions.py index 6908e8fd..b1ad339a 100644 --- a/ahriman-core/src/ahriman/core/exceptions.py +++ b/ahriman-core/src/ahriman/core/exceptions.py @@ -83,6 +83,15 @@ Process stderr: {self.stderr}""" +class DependencyResolutionError(RuntimeError): + """ + exception raised when package dependencies cannot be resolved + """ + + def __init__(self, unprocessed: list[str]) -> None: + RuntimeError.__init__(self, f"Could not process dependencies. Unresolved packages: {unprocessed}") + + class DuplicateRunError(RuntimeError): """ exception which will be raised if there is another application instance diff --git a/ahriman-core/src/ahriman/core/tree.py b/ahriman-core/src/ahriman/core/tree.py index 4815bff7..f662607d 100644 --- a/ahriman-core/src/ahriman/core/tree.py +++ b/ahriman-core/src/ahriman/core/tree.py @@ -22,7 +22,7 @@ from __future__ import annotations from collections.abc import Iterable from functools import partial -from ahriman.core.exceptions import PartitionError +from ahriman.core.exceptions import DependencyResolutionError, PartitionError from ahriman.core.utils import minmax, partition from ahriman.models.package import Package @@ -226,6 +226,9 @@ class Tree: Returns: list[list[Package]]: sorted list of packages lists based on their dependencies + + Raises: + DependencyResolutionError: dependency cycle prevents ordering packages """ unsorted: list[list[Leaf]] = [] @@ -235,6 +238,8 @@ class Tree: # additional workaround with partial in order to hide cell-var-from-loop pylint warning predicate = partial(Leaf.is_root, packages=unprocessed) new_level, unprocessed = partition(unprocessed, predicate) + if not new_level: + raise DependencyResolutionError([leaf.package.base for leaf in unprocessed]) unsorted.append(new_level) # move leaves to the end if they are not required at the next level diff --git a/ahriman-core/tests/ahriman/core/test_tree.py b/ahriman-core/tests/ahriman/core/test_tree.py index 0d65ae78..f03687e5 100644 --- a/ahriman-core/tests/ahriman/core/test_tree.py +++ b/ahriman-core/tests/ahriman/core/test_tree.py @@ -1,6 +1,6 @@ import pytest -from ahriman.core.exceptions import PartitionError +from ahriman.core.exceptions import DependencyResolutionError, PartitionError from ahriman.core.tree import Leaf, Tree from ahriman.models.package import Package from ahriman.models.package_description import PackageDescription @@ -270,3 +270,14 @@ def test_tree_partitions() -> None: first, second = tree.partitions(count=3) assert first == [leaf1.package, leaf2.package, leaf3.package, leaf5.package] assert second == [leaf4.package] + + +def test_tree_levels_cycle(leaf_ahriman: Leaf, leaf_python_schedule: Leaf) -> None: + """ + must raise an exception if dependencies contain a cycle + """ + leaf_ahriman.dependencies = set(leaf_python_schedule.items) + leaf_python_schedule.dependencies = set(leaf_ahriman.items) + + with pytest.raises(DependencyResolutionError): + Tree([leaf_ahriman, leaf_python_schedule]).levels()