From bfca7e41ab41325649f3d6bb0f5adb461c96810f Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Wed, 22 Dec 2021 19:35:09 +0300 Subject: [PATCH] handle dependencies recursively --- src/ahriman/application/ahriman.py | 2 + src/ahriman/application/handlers/rebuild.py | 14 ++++--- src/ahriman/core/repository/repository.py | 19 ++++++++- src/ahriman/models/package.py | 33 ++++++++++++++- .../handlers/test_handler_rebuild.py | 42 ++++++++++++------- .../core/repository/test_repository.py | 20 +++++++++ tests/ahriman/models/conftest.py | 2 + tests/ahriman/models/test_package.py | 18 ++++++++ 8 files changed, 126 insertions(+), 24 deletions(-) diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index d9e69348..16ca210f 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -346,6 +346,8 @@ def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser: parser = root.add_parser("repo-rebuild", aliases=["rebuild"], help="rebuild repository", description="force rebuild whole repository", formatter_class=_formatter) parser.add_argument("--depends-on", help="only rebuild packages that depend on specified package", action="append") + parser.add_argument("--dry-run", help="just perform check for packages without rebuild process itself", + action="store_true") parser.set_defaults(handler=handlers.Rebuild) return parser diff --git a/src/ahriman/application/handlers/rebuild.py b/src/ahriman/application/handlers/rebuild.py index b318964a..fc293923 100644 --- a/src/ahriman/application/handlers/rebuild.py +++ b/src/ahriman/application/handlers/rebuild.py @@ -22,6 +22,7 @@ import argparse from typing import Type from ahriman.application.application import Application +from ahriman.application.formatters.update_printer import UpdatePrinter from ahriman.application.handlers.handler import Handler from ahriman.core.configuration import Configuration @@ -44,9 +45,10 @@ class Rebuild(Handler): depends_on = set(args.depends_on) if args.depends_on else None application = Application(architecture, configuration, no_report) - packages = [ - package - for package in application.repository.packages() - if depends_on is None or depends_on.intersection(package.depends) - ] # we have to use explicit list here for testing purpose - application.update(packages) + updates = application.repository.packages_depends_on(depends_on) + if args.dry_run: + for package in updates: + UpdatePrinter(package, package.version).print(verbose=True) + return + + application.update(updates) diff --git a/src/ahriman/core/repository/repository.py b/src/ahriman/core/repository/repository.py index 33c76b86..94400cb6 100644 --- a/src/ahriman/core/repository/repository.py +++ b/src/ahriman/core/repository/repository.py @@ -18,7 +18,7 @@ # along with this program. If not, see . # from pathlib import Path -from typing import Dict, Iterable, List +from typing import Dict, Iterable, List, Optional from ahriman.core.repository.executor import Executor from ahriman.core.repository.update_handler import UpdateHandler @@ -68,3 +68,20 @@ class Repository(Executor, UpdateHandler): :return: list of filenames from the directory """ return list(filter(package_like, self.paths.packages.iterdir())) + + def packages_depends_on(self, depends_on: Optional[Iterable[str]]) -> List[Package]: + """ + extract list of packages which depends on specified package + :param: depends_on: dependencies of the packages + :return: list of repository packages which depend on specified packages + """ + packages = self.packages() + if depends_on is None: + return packages # no list provided extract everything by default + depends_on = set(depends_on) + + return [ + package + for package in packages + if depends_on is None or depends_on.intersection(package.full_depends(self.pacman, packages)) + ] diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py index 9c5145ca..6a38c9be 100644 --- a/src/ahriman/models/package.py +++ b/src/ahriman/models/package.py @@ -20,13 +20,14 @@ from __future__ import annotations import aur # type: ignore +import copy import logging from dataclasses import asdict, dataclass from pathlib import Path from pyalpm import vercmp # type: ignore from srcinfo.parse import parse_srcinfo # type: ignore -from typing import Any, Dict, List, Optional, Set, Type +from typing import Any, Dict, Iterable, List, Optional, Set, Type from ahriman.core.alpm.pacman import Pacman from ahriman.core.exceptions import InvalidPackageInfo @@ -257,6 +258,36 @@ class Package: return self.version + def full_depends(self, pacman: Pacman, packages: Iterable[Package]) -> List[str]: + """ + generate full dependencies list including transitive dependencies + :param pacman: alpm wrapper instance + :param packages: repository package list + :return: all dependencies of the package + """ + dependencies = {} + # load own package dependencies + for package_base in packages: + for name, repo_package in package_base.packages.items(): + dependencies[name] = repo_package.depends + for provides in repo_package.provides: + dependencies[provides] = repo_package.depends + # load repository dependencies + for database in pacman.handle.get_syncdbs(): + for pacman_package in database.pkgcache: + dependencies[pacman_package.name] = pacman_package.depends + for provides in pacman_package.provides: + dependencies[provides] = pacman_package.depends + + result = set(self.depends) + current_depends: Set[str] = set() + while result != current_depends: + current_depends = copy.deepcopy(result) + for package in current_depends: + result.update(dependencies.get(package, [])) + + return sorted(result) + def is_outdated(self, remote: Package, paths: RepositoryPaths, calculate_version: bool = True) -> bool: """ check if package is out-of-dated diff --git a/tests/ahriman/application/handlers/test_handler_rebuild.py b/tests/ahriman/application/handlers/test_handler_rebuild.py index 6c759415..577d0b63 100644 --- a/tests/ahriman/application/handlers/test_handler_rebuild.py +++ b/tests/ahriman/application/handlers/test_handler_rebuild.py @@ -14,6 +14,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace: :return: generated arguments for these test cases """ args.depends_on = [] + args.dry_run = False return args @@ -23,7 +24,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc """ args = _default_args(args) mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") - application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages") + application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages_depends_on") application_mock = mocker.patch("ahriman.application.application.Application.update") Rebuild.run(args, "x86_64", configuration, True) @@ -31,34 +32,43 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc application_mock.assert_called_once() -def test_run_filter(args: argparse.Namespace, configuration: Configuration, - package_ahriman: Package, package_python_schedule: Package, - mocker: MockerFixture) -> None: +def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, + package_ahriman: Package, mocker: MockerFixture) -> None: """ - must run command with depends filter + must run command without update itself """ args = _default_args(args) - args.depends_on = ["python-aur"] - mocker.patch("ahriman.core.repository.repository.Repository.packages", - return_value=[package_ahriman, package_python_schedule]) + args.dry_run = True mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") + mocker.patch("ahriman.core.repository.repository.Repository.packages_depends_on", return_value=[package_ahriman]) application_mock = mocker.patch("ahriman.application.application.Application.update") Rebuild.run(args, "x86_64", configuration, True) - application_mock.assert_called_once_with([package_ahriman]) + application_mock.assert_not_called() -def test_run_without_filter(args: argparse.Namespace, configuration: Configuration, - package_ahriman: Package, package_python_schedule: Package, - mocker: MockerFixture) -> None: +def test_run_filter(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must run command with depends on filter + """ + args = _default_args(args) + args.depends_on = ["python-aur"] + mocker.patch("ahriman.application.application.Application.update") + mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") + application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages_depends_on") + + Rebuild.run(args, "x86_64", configuration, True) + application_packages_mock.assert_called_once_with({"python-aur"}) + + +def test_run_without_filter(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: """ must run command for all packages if no filter supplied """ args = _default_args(args) - mocker.patch("ahriman.core.repository.repository.Repository.packages", - return_value=[package_ahriman, package_python_schedule]) + mocker.patch("ahriman.application.application.Application.update") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") - application_mock = mocker.patch("ahriman.application.application.Application.update") + application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages_depends_on") Rebuild.run(args, "x86_64", configuration, True) - application_mock.assert_called_once_with([package_ahriman, package_python_schedule]) + application_packages_mock.assert_called_once_with(None) diff --git a/tests/ahriman/core/repository/test_repository.py b/tests/ahriman/core/repository/test_repository.py index e43e668c..048e9c4b 100644 --- a/tests/ahriman/core/repository/test_repository.py +++ b/tests/ahriman/core/repository/test_repository.py @@ -80,3 +80,23 @@ def test_packages_built(repository: Repository, mocker: MockerFixture) -> None: """ mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.tar.xz"), Path("b.pkg.tar.xz")]) assert repository.packages_built() == [Path("b.pkg.tar.xz")] + + +def test_packages_depends_on(repository: Repository, package_ahriman: Package, package_python_schedule: Package, + mocker: MockerFixture) -> None: + """ + must filter packages by depends list + """ + mocker.patch("ahriman.core.repository.repository.Repository.packages", + return_value=[package_ahriman, package_python_schedule]) + assert repository.packages_depends_on(["python-aur"]) == [package_ahriman] + + +def test_packages_depends_on_empty(repository: Repository, package_ahriman: Package, package_python_schedule: Package, + mocker: MockerFixture) -> None: + """ + must return all packages in case if no filter is provided + """ + mocker.patch("ahriman.core.repository.repository.Repository.packages", + return_value=[package_ahriman, package_python_schedule]) + assert repository.packages_depends_on(None) == [package_ahriman, package_python_schedule] diff --git a/tests/ahriman/models/conftest.py b/tests/ahriman/models/conftest.py index edfdaf00..da18af31 100644 --- a/tests/ahriman/models/conftest.py +++ b/tests/ahriman/models/conftest.py @@ -82,7 +82,9 @@ def pyalpm_package_ahriman(package_ahriman: Package) -> MagicMock: """ mock = MagicMock() type(mock).base = PropertyMock(return_value=package_ahriman.base) + type(mock).depends = PropertyMock(return_value=["python-aur"]) type(mock).name = PropertyMock(return_value=package_ahriman.base) + type(mock).provides = PropertyMock(return_value=["python-ahriman"]) type(mock).version = PropertyMock(return_value=package_ahriman.version) return mock diff --git a/tests/ahriman/models/test_package.py b/tests/ahriman/models/test_package.py index ea618aa3..110c8cc8 100644 --- a/tests/ahriman/models/test_package.py +++ b/tests/ahriman/models/test_package.py @@ -294,6 +294,24 @@ def test_actual_version_vcs_failed(package_tpacpi_bat_git: Package, repository_p assert package_tpacpi_bat_git.actual_version(repository_paths) == package_tpacpi_bat_git.version +def test_full_depends(package_ahriman: Package, package_python_schedule: Package, pyalpm_package_ahriman: MagicMock, + pyalpm_handle: MagicMock, mocker: MockerFixture) -> None: + """ + must extract all dependencies from the package + """ + package_python_schedule.packages[package_python_schedule.base].provides = ["python3-schedule"] + + database_mock = MagicMock() + database_mock.pkgcache = [pyalpm_package_ahriman] + pyalpm_handle.handle.get_syncdbs.return_value = [database_mock] + + assert package_ahriman.full_depends(pyalpm_handle, [package_python_schedule]) == package_ahriman.depends + + package_python_schedule.packages[package_python_schedule.base].depends = [package_ahriman.base] + expected = sorted(set(package_python_schedule.depends + ["python-aur"])) + assert package_python_schedule.full_depends(pyalpm_handle, [package_python_schedule]) == expected + + def test_is_outdated_false(package_ahriman: Package, repository_paths: RepositoryPaths) -> None: """ must be not outdated for the same package