diff --git a/docs/ahriman.1 b/docs/ahriman.1 index c8b17115..541b69d9 100644 --- a/docs/ahriman.1 +++ b/docs/ahriman.1 @@ -1,9 +1,9 @@ -.TH AHRIMAN "1" "2022\-12\-11" "ahriman" "Generated Python Manual" +.TH AHRIMAN "1" "2022\-12\-27" "ahriman" "Generated Python Manual" .SH NAME ahriman .SH SYNOPSIS .B ahriman -[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--report | --no-report] [-q] [--unsafe] [-V] {aur-search,search,daemon,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,shell,user-add,user-list,user-remove,version,web} ... +[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--report | --no-report] [-q] [--unsafe] [-V] {aur-search,search,daemon,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-tree,repo-triggers,repo-update,update,shell,user-add,user-list,user-remove,version,web} ... .SH DESCRIPTION ArcH linux ReposItory MANager @@ -121,6 +121,9 @@ update repository status \fBahriman\fR \fI\,repo\-sync\/\fR sync repository .TP +\fBahriman\fR \fI\,repo\-tree\/\fR +dump repository tree +.TP \fBahriman\fR \fI\,repo\-triggers\/\fR run triggers .TP @@ -583,6 +586,11 @@ usage: ahriman repo\-sync [\-h] sync repository files to remote server according to current settings +.SH COMMAND \fI\,'ahriman repo\-tree'\/\fR +usage: ahriman repo\-tree [\-h] + +dump repository tree based on packages dependencies + .SH COMMAND \fI\,'ahriman repo\-triggers'\/\fR usage: ahriman repo\-triggers [\-h] [trigger ...] diff --git a/docs/ahriman.application.handlers.rst b/docs/ahriman.application.handlers.rst index 4e4c6d24..be647e54 100644 --- a/docs/ahriman.application.handlers.rst +++ b/docs/ahriman.application.handlers.rst @@ -156,6 +156,14 @@ ahriman.application.handlers.status\_update module :no-undoc-members: :show-inheritance: +ahriman.application.handlers.structure module +--------------------------------------------- + +.. automodule:: ahriman.application.handlers.structure + :members: + :no-undoc-members: + :show-inheritance: + ahriman.application.handlers.triggers module -------------------------------------------- diff --git a/docs/ahriman.core.formatters.rst b/docs/ahriman.core.formatters.rst index d3b2a849..59821916 100644 --- a/docs/ahriman.core.formatters.rst +++ b/docs/ahriman.core.formatters.rst @@ -68,6 +68,14 @@ ahriman.core.formatters.string\_printer module :no-undoc-members: :show-inheritance: +ahriman.core.formatters.tree\_printer module +-------------------------------------------- + +.. automodule:: ahriman.core.formatters.tree_printer + :members: + :no-undoc-members: + :show-inheritance: + ahriman.core.formatters.update\_printer module ---------------------------------------------- diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index 7aca9abb..54018293 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -109,6 +109,7 @@ def _parser() -> argparse.ArgumentParser: _set_repo_sign_parser(subparsers) _set_repo_status_update_parser(subparsers) _set_repo_sync_parser(subparsers) + _set_repo_tree_parser(subparsers) _set_repo_triggers_parser(subparsers) _set_repo_update_parser(subparsers) _set_shell_parser(subparsers) @@ -703,6 +704,23 @@ def _set_repo_sync_parser(root: SubParserAction) -> argparse.ArgumentParser: return parser +def _set_repo_tree_parser(root: SubParserAction) -> argparse.ArgumentParser: + """ + add parser for repository tree subcommand + + Args: + root(SubParserAction): subparsers for the commands + + Returns: + argparse.ArgumentParser: created argument parser + """ + parser = root.add_parser("repo-tree", help="dump repository tree", + description="dump repository tree based on packages dependencies", + formatter_class=_formatter) + parser.set_defaults(handler=handlers.Structure, lock=None, report=False, quiet=True) + return parser + + def _set_repo_triggers_parser(root: SubParserAction) -> argparse.ArgumentParser: """ add parser for repository triggers subcommand diff --git a/src/ahriman/application/application/application_repository.py b/src/ahriman/application/application/application_repository.py index 42ae7641..61afa100 100644 --- a/src/ahriman/application/application/application_repository.py +++ b/src/ahriman/application/application/application_repository.py @@ -145,8 +145,8 @@ class ApplicationRepository(ApplicationProperties): process_update(packages, build_result) # process manual packages - tree = Tree.load(updates, self.repository.paths, self.database) - for num, level in enumerate(tree.levels()): + tree = Tree.resolve(updates, self.repository.paths, self.database) + for num, level in enumerate(tree): self.logger.info("processing level #%i %s", num, [package.base for package in level]) build_result = self.repository.process_build(level) packages = self.repository.packages_built() @@ -181,8 +181,12 @@ class ApplicationRepository(ApplicationProperties): local_versions = {package.base: package.version for package in self.repository.packages()} updated_packages = [package for _, package in sorted(updates.items())] - for package in updated_packages: - UpdatePrinter(package, local_versions.get(package.base)).print( - verbose=True, log_fn=log_fn, separator=" -> ") + + # reorder updates according to the dependency tree + tree = Tree.resolve(updated_packages, self.repository.paths, self.database) + for level in tree: + for package in level: + UpdatePrinter(package, local_versions.get(package.base)).print( + verbose=True, log_fn=log_fn, separator=" -> ") return updated_packages diff --git a/src/ahriman/application/handlers/__init__.py b/src/ahriman/application/handlers/__init__.py index ebba3604..2ab5f5fb 100644 --- a/src/ahriman/application/handlers/__init__.py +++ b/src/ahriman/application/handlers/__init__.py @@ -37,6 +37,7 @@ from ahriman.application.handlers.shell import Shell from ahriman.application.handlers.sign import Sign from ahriman.application.handlers.status import Status from ahriman.application.handlers.status_update import StatusUpdate +from ahriman.application.handlers.structure import Structure from ahriman.application.handlers.triggers import Triggers from ahriman.application.handlers.unsafe_commands import UnsafeCommands from ahriman.application.handlers.update import Update diff --git a/src/ahriman/application/handlers/structure.py b/src/ahriman/application/handlers/structure.py new file mode 100644 index 00000000..ba0b292c --- /dev/null +++ b/src/ahriman/application/handlers/structure.py @@ -0,0 +1,56 @@ +# +# Copyright (c) 2021-2022 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +import argparse + +from typing import Type + +from ahriman.application.application import Application +from ahriman.application.handlers import Handler +from ahriman.core.configuration import Configuration +from ahriman.core.formatters import TreePrinter +from ahriman.core.tree import Tree + + +class Structure(Handler): + """ + dump repository structure handler + """ + + ALLOW_AUTO_ARCHITECTURE_RUN = False + + @classmethod + def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, + report: bool, unsafe: bool) -> None: + """ + callback for command line + + Args: + args(argparse.Namespace): command line args + architecture(str): repository architecture + configuration(Configuration): configuration instance + report(bool): force enable or disable reporting + unsafe(bool): if set no user check will be performed before path creation + """ + application = Application(architecture, configuration, report=report, unsafe=unsafe) + packages = application.repository.packages() + + tree = Tree.resolve(packages, application.repository.paths, application.database) + for num, level in enumerate(tree): + TreePrinter(num, level).print(verbose=True, separator=" ") diff --git a/src/ahriman/core/formatters/__init__.py b/src/ahriman/core/formatters/__init__.py index a53d8d87..cdbd1960 100644 --- a/src/ahriman/core/formatters/__init__.py +++ b/src/ahriman/core/formatters/__init__.py @@ -26,6 +26,7 @@ from ahriman.core.formatters.configuration_printer import ConfigurationPrinter from ahriman.core.formatters.package_printer import PackagePrinter from ahriman.core.formatters.patch_printer import PatchPrinter from ahriman.core.formatters.status_printer import StatusPrinter +from ahriman.core.formatters.tree_printer import TreePrinter from ahriman.core.formatters.update_printer import UpdatePrinter from ahriman.core.formatters.user_printer import UserPrinter from ahriman.core.formatters.version_printer import VersionPrinter diff --git a/src/ahriman/core/formatters/tree_printer.py b/src/ahriman/core/formatters/tree_printer.py new file mode 100644 index 00000000..5bf86d3c --- /dev/null +++ b/src/ahriman/core/formatters/tree_printer.py @@ -0,0 +1,53 @@ +# +# Copyright (c) 2021-2022 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from typing import Iterable, List + +from ahriman.core.formatters import StringPrinter +from ahriman.models.package import Package +from ahriman.models.property import Property + + +class TreePrinter(StringPrinter): + """ + print content of the package tree level + + Attributes: + packages(Iterable[Package]): packages which belong to this level + """ + + def __init__(self, level: int, packages: Iterable[Package]) -> None: + """ + default constructor + + Args: + level(int): dependencies tree level + packages(Iterable[Package]): packages which belong to this level + """ + StringPrinter.__init__(self, f"level {level}") + self.packages = packages + + def properties(self) -> List[Property]: + """ + convert content into printable data + + Returns: + List[Property]: list of content properties + """ + return [Property(package.base, package.version, is_required=True) for package in self.packages] diff --git a/src/ahriman/core/tree.py b/src/ahriman/core/tree.py index 209f5c30..75724960 100644 --- a/src/ahriman/core/tree.py +++ b/src/ahriman/core/tree.py @@ -19,9 +19,11 @@ # from __future__ import annotations +import itertools + from pathlib import Path from tempfile import TemporaryDirectory -from typing import Iterable, List, Set, Type +from typing import Callable, Iterable, List, Set, Tuple, Type from ahriman.core.build_tools.sources import Sources from ahriman.core.database import SQLite @@ -77,6 +79,21 @@ class Leaf: dependencies = Package.dependencies(clone_dir) return cls(package, dependencies) + def is_dependency(self, packages: Iterable[Leaf]) -> bool: + """ + check if the package is dependency of any other package from list or not + + Args: + packages(Iterable[Leaf]): list of known leaves + + Returns: + bool: True in case if package is dependency of others and False otherwise + """ + for leaf in packages: + if leaf.dependencies.intersection(self.items): + return True + return False + def is_root(self, packages: Iterable[Leaf]) -> bool: """ check if package depends on any other package from list of not @@ -113,8 +130,8 @@ class Tree: >>> repository = Repository.load("x86_64", configuration, database, report=True, unsafe=False) >>> packages = repository.packages() >>> - >>> tree = Tree.load(packages, configuration.repository_paths, database) - >>> for tree_level in tree.levels(): + >>> tree = Tree.resolve(packages, configuration.repository_paths, database) + >>> for tree_level in tree: >>> for package in tree_level: >>> print(package.base) >>> print() @@ -141,9 +158,10 @@ class Tree: self.leaves = leaves @classmethod - def load(cls: Type[Tree], packages: Iterable[Package], paths: RepositoryPaths, database: SQLite) -> Tree: + def resolve(cls: Type[Tree], packages: Iterable[Package], paths: RepositoryPaths, + database: SQLite) -> List[List[Package]]: """ - load tree from packages + resolve dependency tree Args: packages(Iterable[Package]): packages list @@ -151,22 +169,45 @@ class Tree: database(SQLite): database instance Returns: - Tree: loaded class + List[List[Package]]: list of packages lists based on their dependencies """ - return cls([Leaf.load(package, paths, database) for package in packages]) + leaves = [Leaf.load(package, paths, database) for package in packages] + tree = cls(leaves) + return tree.levels() def levels(self) -> List[List[Package]]: """ get build levels starting from the packages which do not require any other package to build Returns: - List[List[Package]]: list of packages lists + List[List[Package]]: sorted list of packages lists based on their dependencies """ - result: List[List[Package]] = [] + # https://docs.python.org/dev/library/itertools.html#itertools-recipes + def partition(source: List[Leaf]) -> Tuple[List[Leaf], Iterable[Leaf]]: + first_iter, second_iter = itertools.tee(source) + filter_fn: Callable[[Leaf], bool] = lambda leaf: leaf.is_dependency(next_level) + # materialize first list and leave second as iterator + return list(filter(filter_fn, first_iter)), itertools.filterfalse(filter_fn, second_iter) + unsorted: List[List[Leaf]] = [] + + # build initial tree unprocessed = self.leaves[:] while unprocessed: - result.append([leaf.package for leaf in unprocessed if leaf.is_root(unprocessed)]) + unsorted.append([leaf for leaf in unprocessed if leaf.is_root(unprocessed)]) unprocessed = [leaf for leaf in unprocessed if not leaf.is_root(unprocessed)] - return result + # move leaves to the end if they are not required at the next level + for current_num, current_level in enumerate(unsorted[:-1]): + next_num = current_num + 1 + next_level = unsorted[next_num] + + # change lists inside the collection + unsorted[current_num], to_be_moved = partition(current_level) + unsorted[next_num].extend(to_be_moved) + + comparator: Callable[[Package], str] = lambda package: package.base + return [ + sorted([leaf.package for leaf in level], key=comparator) + for level in unsorted if level + ] diff --git a/tests/ahriman/application/application/test_application_repository.py b/tests/ahriman/application/application/test_application_repository.py index b8dd29e7..54aa9bf5 100644 --- a/tests/ahriman/application/application/test_application_repository.py +++ b/tests/ahriman/application/application/test_application_repository.py @@ -163,7 +163,7 @@ def test_update(application_repository: ApplicationRepository, package_ahriman: paths = [package.filepath for package in package_ahriman.packages.values()] tree = Tree([Leaf(package_ahriman, set())]) - mocker.patch("ahriman.core.tree.Tree.load", return_value=tree) + mocker.patch("ahriman.core.tree.Tree.resolve", return_value=tree.levels()) mocker.patch("ahriman.core.repository.repository.Repository.packages_built", return_value=paths) build_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_build", return_value=result) update_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_update", return_value=result) @@ -183,7 +183,7 @@ def test_update_empty(application_repository: ApplicationRepository, package_ahr """ tree = Tree([Leaf(package_ahriman, set())]) - mocker.patch("ahriman.core.tree.Tree.load", return_value=tree) + mocker.patch("ahriman.core.tree.Tree.resolve", return_value=tree.levels()) mocker.patch("ahriman.core.repository.repository.Repository.packages_built", return_value=[]) mocker.patch("ahriman.core.repository.executor.Executor.process_build") update_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_update") @@ -197,6 +197,9 @@ def test_updates_all(application_repository: ApplicationRepository, package_ahri """ must get updates for all """ + tree = Tree([Leaf(package_ahriman, set())]) + + mocker.patch("ahriman.core.tree.Tree.resolve", return_value=tree.levels()) mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[]) updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur", return_value=[package_ahriman]) diff --git a/tests/ahriman/application/handlers/test_handler_structure.py b/tests/ahriman/application/handlers/test_handler_structure.py new file mode 100644 index 00000000..08b37205 --- /dev/null +++ b/tests/ahriman/application/handlers/test_handler_structure.py @@ -0,0 +1,31 @@ +import argparse +import pytest + +from pytest_mock import MockerFixture + +from ahriman.application.handlers import Structure +from ahriman.core.configuration import Configuration +from ahriman.core.repository import Repository +from ahriman.models.package import Package + + +def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository, + package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must run command + """ + mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) + mocker.patch("ahriman.core.repository.Repository.packages", return_value=[package_ahriman]) + application_mock = mocker.patch("ahriman.core.tree.Tree.resolve", return_value=[[package_ahriman]]) + print_mock = mocker.patch("ahriman.core.formatters.Printer.print") + + Structure.run(args, "x86_64", configuration, report=False, unsafe=False) + application_mock.assert_called_once_with([package_ahriman], repository.paths, pytest.helpers.anyvar(int)) + print_mock.assert_called_once_with(verbose=True, separator=" ") + + +def test_disallow_auto_architecture_run() -> None: + """ + must not allow multi architecture run + """ + assert not Structure.ALLOW_AUTO_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py index 6c6e480c..d2f7d0c0 100644 --- a/tests/ahriman/application/test_ahriman.py +++ b/tests/ahriman/application/test_ahriman.py @@ -550,9 +550,29 @@ def test_subparsers_repo_sync_architecture(parser: argparse.ArgumentParser) -> N """ repo-sync command must correctly parse architecture list """ - args = parser.parse_args(["repo-report"]) + args = parser.parse_args(["repo-sync"]) assert args.architecture is None - args = parser.parse_args(["-a", "x86_64", "repo-report"]) + args = parser.parse_args(["-a", "x86_64", "repo-sync"]) + assert args.architecture == ["x86_64"] + + +def test_subparsers_repo_tree(parser: argparse.ArgumentParser) -> None: + """ + repo-tree command must imply lock, report and quiet + """ + args = parser.parse_args(["repo-tree"]) + assert args.lock is None + assert not args.report + assert args.quiet + + +def test_subparsers_repo_tree_architecture(parser: argparse.ArgumentParser) -> None: + """ + repo-tree command must correctly parse architecture list + """ + args = parser.parse_args(["repo-tree"]) + assert args.architecture is None + args = parser.parse_args(["-a", "x86_64", "repo-tree"]) assert args.architecture == ["x86_64"] diff --git a/tests/ahriman/core/formatters/conftest.py b/tests/ahriman/core/formatters/conftest.py index a274db52..f3d4b37f 100644 --- a/tests/ahriman/core/formatters/conftest.py +++ b/tests/ahriman/core/formatters/conftest.py @@ -1,7 +1,7 @@ import pytest from ahriman.core.formatters import AurPrinter, ConfigurationPrinter, PackagePrinter, PatchPrinter, StatusPrinter, \ - StringPrinter, UpdatePrinter, UserPrinter, VersionPrinter + StringPrinter, TreePrinter, UpdatePrinter, UserPrinter, VersionPrinter from ahriman.models.aur_package import AURPackage from ahriman.models.build_status import BuildStatus from ahriman.models.package import Package @@ -85,15 +85,29 @@ def string_printer() -> StringPrinter: @pytest.fixture -def update_printer(package_ahriman: Package) -> UpdatePrinter: +def tree_printer(package_ahriman: Package) -> TreePrinter: """ - fixture for build status printer + fixture for tree printer Args: package_ahriman(Package): package fixture Returns: - UpdatePrinter: build status printer test instance + TreePrinter: tree printer test instance + """ + return TreePrinter(0, [package_ahriman]) + + +@pytest.fixture +def update_printer(package_ahriman: Package) -> UpdatePrinter: + """ + fixture for update printer + + Args: + package_ahriman(Package): package fixture + + Returns: + UpdatePrinter: udpate printer test instance """ return UpdatePrinter(package_ahriman, None) diff --git a/tests/ahriman/core/formatters/test_tree_printer.py b/tests/ahriman/core/formatters/test_tree_printer.py new file mode 100644 index 00000000..1e61b120 --- /dev/null +++ b/tests/ahriman/core/formatters/test_tree_printer.py @@ -0,0 +1,15 @@ +from ahriman.core.formatters import TreePrinter + + +def test_properties(tree_printer: TreePrinter) -> None: + """ + must return non-empty properties list + """ + assert tree_printer.properties() + + +def test_title(tree_printer: TreePrinter) -> None: + """ + must return non-empty title + """ + assert tree_printer.title() is not None diff --git a/tests/ahriman/core/formatters/test_update_printer.py b/tests/ahriman/core/formatters/test_update_printer.py index 0f88d9b1..286d655b 100644 --- a/tests/ahriman/core/formatters/test_update_printer.py +++ b/tests/ahriman/core/formatters/test_update_printer.py @@ -3,7 +3,7 @@ from ahriman.core.formatters import UpdatePrinter def test_properties(update_printer: UpdatePrinter) -> None: """ - must return empty properties list + must return non-empty properties list """ assert update_printer.properties() diff --git a/tests/ahriman/core/test_tree.py b/tests/ahriman/core/test_tree.py index 5382ba32..d1ff6c32 100644 --- a/tests/ahriman/core/test_tree.py +++ b/tests/ahriman/core/test_tree.py @@ -5,6 +5,7 @@ from pytest_mock import MockerFixture from ahriman.core.database import SQLite from ahriman.core.tree import Leaf, Tree from ahriman.models.package import Package +from ahriman.models.package_description import PackageDescription from ahriman.models.repository_paths import RepositoryPaths @@ -53,6 +54,18 @@ def test_leaf_load(package_ahriman: Package, repository_paths: RepositoryPaths, dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int)) +def test_tree_resolve(package_ahriman: Package, package_python_schedule: Package, repository_paths: RepositoryPaths, + database: SQLite, mocker: MockerFixture) -> None: + """ + must resolve denendecnies tree + """ + mocker.patch("ahriman.core.tree.Leaf.load", side_effect=lambda package, p, d: Leaf(package, set(package.depends))) + + tree = Tree.resolve([package_ahriman, package_python_schedule], repository_paths, database) + assert len(tree) == 1 + assert len(tree[0]) == 2 + + def test_tree_levels(leaf_ahriman: Leaf, leaf_python_schedule: Leaf) -> None: """ must generate correct levels in the simples case @@ -60,21 +73,54 @@ def test_tree_levels(leaf_ahriman: Leaf, leaf_python_schedule: Leaf) -> None: leaf_ahriman.dependencies = set(leaf_python_schedule.package.packages.keys()) tree = Tree([leaf_ahriman, leaf_python_schedule]) - assert len(tree.levels()) == 2 first, second = tree.levels() assert first == [leaf_python_schedule.package] assert second == [leaf_ahriman.package] -def test_tree_load(package_ahriman: Package, package_python_schedule: Package, repository_paths: RepositoryPaths, - database: SQLite, mocker: MockerFixture) -> None: +def test_tree_levels_sorted() -> None: """ - must package list + must reorder tree, moving packages which are not required for the next level further """ - mocker.patch("tempfile.mkdtemp") - mocker.patch("ahriman.core.build_tools.sources.Sources.load") - mocker.patch("ahriman.models.package.Package.dependencies") - mocker.patch("shutil.rmtree") + leaf1 = Leaf( + Package( + base="package1", + version="1.0.0", + remote=None, + packages={"package1": PackageDescription()} + ), + dependencies=set() + ) + leaf2 = Leaf( + Package( + base="package2", + version="1.0.0", + remote=None, + packages={"package2": PackageDescription()} + ), + dependencies={"package1"} + ) + leaf3 = Leaf( + Package( + base="package3", + version="1.0.0", + remote=None, + packages={"package3": PackageDescription()} + ), + dependencies={"package1"} + ) + leaf4 = Leaf( + Package( + base="package4", + version="1.0.0", + remote=None, + packages={"package4": PackageDescription()} + ), + dependencies={"package3"} + ) - tree = Tree.load([package_ahriman, package_python_schedule], repository_paths, database) - assert len(tree.leaves) == 2 + tree = Tree([leaf1, leaf2, leaf3, leaf4]) + first, second, third = tree.levels() + assert first == [leaf1.package] + assert second == [leaf3.package] + assert third == [leaf2.package, leaf4.package]