From a09ad7617dce7200fde00af23402b1cecbbdf1d3 Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Thu, 12 Mar 2026 02:26:59 +0200 Subject: [PATCH] feat: support archive listing --- docs/ahriman.application.handlers.rst | 8 ++ docs/ahriman.web.views.v1.packages.rst | 8 ++ .../application/application/application.py | 4 +- .../application/application_repository.py | 2 +- src/ahriman/application/handlers/archives.py | 81 ++++++++++++++++++ src/ahriman/application/handlers/change.py | 3 +- src/ahriman/application/handlers/pkgbuild.py | 3 +- src/ahriman/application/handlers/reload.py | 3 +- src/ahriman/application/handlers/status.py | 2 +- .../application/handlers/status_update.py | 3 +- src/ahriman/core/repository/executor.py | 11 ++- src/ahriman/core/repository/package_info.py | 5 ++ .../core/repository/repository_properties.py | 22 +---- src/ahriman/core/repository/update_handler.py | 2 +- src/ahriman/core/status/watcher.py | 18 +++- src/ahriman/models/package.py | 25 +++--- src/ahriman/web/views/v1/packages/archives.py | 65 ++++++++++++++ src/ahriman/web/web.py | 45 +++++++--- .../handlers/test_handler_archives.py | 84 +++++++++++++++++++ tests/ahriman/application/test_ahriman.py | 16 ++++ tests/ahriman/conftest.py | 4 +- .../ahriman/core/repository/test_executor.py | 5 +- .../core/repository/test_package_info.py | 31 ++++--- .../repository/test_repository_properties.py | 14 ---- tests/ahriman/core/status/test_watcher.py | 12 +++ tests/ahriman/models/test_package.py | 38 +++++---- tests/ahriman/web/test_web.py | 16 +++- .../test_view_v1_packages_archives.py | 52 ++++++++++++ 28 files changed, 474 insertions(+), 108 deletions(-) create mode 100644 src/ahriman/application/handlers/archives.py create mode 100644 src/ahriman/web/views/v1/packages/archives.py create mode 100644 tests/ahriman/application/handlers/test_handler_archives.py create mode 100644 tests/ahriman/web/views/v1/packages/test_view_v1_packages_archives.py diff --git a/docs/ahriman.application.handlers.rst b/docs/ahriman.application.handlers.rst index 342280a6..f45d6887 100644 --- a/docs/ahriman.application.handlers.rst +++ b/docs/ahriman.application.handlers.rst @@ -12,6 +12,14 @@ ahriman.application.handlers.add module :no-undoc-members: :show-inheritance: +ahriman.application.handlers.archives module +-------------------------------------------- + +.. automodule:: ahriman.application.handlers.archives + :members: + :no-undoc-members: + :show-inheritance: + ahriman.application.handlers.backup module ------------------------------------------ diff --git a/docs/ahriman.web.views.v1.packages.rst b/docs/ahriman.web.views.v1.packages.rst index 48f2b916..71c0655b 100644 --- a/docs/ahriman.web.views.v1.packages.rst +++ b/docs/ahriman.web.views.v1.packages.rst @@ -4,6 +4,14 @@ ahriman.web.views.v1.packages package Submodules ---------- +ahriman.web.views.v1.packages.archives module +--------------------------------------------- + +.. automodule:: ahriman.web.views.v1.packages.archives + :members: + :no-undoc-members: + :show-inheritance: + ahriman.web.views.v1.packages.changes module -------------------------------------------- diff --git a/src/ahriman/application/application/application.py b/src/ahriman/application/application/application.py index e9e5d706..cdc5ad98 100644 --- a/src/ahriman/application/application/application.py +++ b/src/ahriman/application/application/application.py @@ -154,13 +154,13 @@ class Application(ApplicationPackages, ApplicationRepository): for package_name, packager in missing.items(): if (source_dir := self.repository.paths.cache_for(package_name)).is_dir(): # there is local cache, load package from it - leaf = Package.from_build(source_dir, self.repository.architecture, packager) + leaf = Package.from_build(source_dir, self.repository.repository_id.architecture, packager) else: leaf = Package.from_aur(package_name, packager, include_provides=True) portion[leaf.base] = leaf # register package in the database - self.repository.reporter.set_unknown(leaf) + self.reporter.set_unknown(leaf) return portion diff --git a/src/ahriman/application/application/application_repository.py b/src/ahriman/application/application/application_repository.py index 27435356..400ace68 100644 --- a/src/ahriman/application/application/application_repository.py +++ b/src/ahriman/application/application/application_repository.py @@ -46,7 +46,7 @@ class ApplicationRepository(ApplicationProperties): continue # skip check in case if we can't calculate diff if (changes := self.repository.package_changes(package, last_commit_sha)) is not None: - self.repository.reporter.package_changes_update(package.base, changes) + self.reporter.package_changes_update(package.base, changes) def clean(self, *, cache: bool, chroot: bool, manual: bool, packages: bool, pacman: bool) -> None: """ diff --git a/src/ahriman/application/handlers/archives.py b/src/ahriman/application/handlers/archives.py new file mode 100644 index 00000000..61c3104f --- /dev/null +++ b/src/ahriman/application/handlers/archives.py @@ -0,0 +1,81 @@ +# +# Copyright (c) 2021-2026 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 ahriman.application.application import Application +from ahriman.application.handlers.handler import Handler, SubParserAction +from ahriman.core.configuration import Configuration +from ahriman.core.formatters import PackagePrinter +from ahriman.models.action import Action +from ahriman.models.build_status import BuildStatus, BuildStatusEnum +from ahriman.models.repository_id import RepositoryId + + +class Archives(Handler): + """ + package archives handler + """ + + ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io + + @classmethod + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: + """ + callback for command line + + Args: + args(argparse.Namespace): command line args + repository_id(RepositoryId): repository unique identifier + configuration(Configuration): configuration instance + report(bool): force enable or disable reporting + """ + application = Application(repository_id, configuration, report=True) + + match args.action: + case Action.List: + archives = application.repository.package_archives(args.package) + for package in archives: + PackagePrinter(package, BuildStatus(BuildStatusEnum.Success))(verbose=args.info) + + Archives.check_status(args.exit_code, bool(archives)) + + @staticmethod + def _set_package_archives_parser(root: SubParserAction) -> argparse.ArgumentParser: + """ + add parser for package archives subcommand + + Args: + root(SubParserAction): subparsers for the commands + + Returns: + argparse.ArgumentParser: created argument parser + """ + parser = root.add_parser("package-archives", help="list package archive versions", + description="list available archive versions for the package") + parser.add_argument("package", help="package base") + parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", + action="store_true") + parser.add_argument("--info", help="show additional package information", + action=argparse.BooleanOptionalAction, default=False) + parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True) + return parser + + arguments = [_set_package_archives_parser] diff --git a/src/ahriman/application/handlers/change.py b/src/ahriman/application/handlers/change.py index 89396ce9..2371d4aa 100644 --- a/src/ahriman/application/handlers/change.py +++ b/src/ahriman/application/handlers/change.py @@ -47,8 +47,7 @@ class Change(Handler): configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(repository_id, configuration, report=True) - client = application.repository.reporter + client = Application(repository_id, configuration, report=True).reporter match args.action: case Action.List: diff --git a/src/ahriman/application/handlers/pkgbuild.py b/src/ahriman/application/handlers/pkgbuild.py index 9f42244f..0350f713 100644 --- a/src/ahriman/application/handlers/pkgbuild.py +++ b/src/ahriman/application/handlers/pkgbuild.py @@ -48,8 +48,7 @@ class Pkgbuild(Handler): configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(repository_id, configuration, report=True) - client = application.repository.reporter + client = Application(repository_id, configuration, report=True).reporter match args.action: case Action.List: diff --git a/src/ahriman/application/handlers/reload.py b/src/ahriman/application/handlers/reload.py index a7319578..f20e6801 100644 --- a/src/ahriman/application/handlers/reload.py +++ b/src/ahriman/application/handlers/reload.py @@ -44,8 +44,7 @@ class Reload(Handler): configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(repository_id, configuration, report=True) - client = application.repository.reporter + client = Application(repository_id, configuration, report=True).reporter client.configuration_reload() @staticmethod diff --git a/src/ahriman/application/handlers/status.py b/src/ahriman/application/handlers/status.py index 0f045b0d..25151833 100644 --- a/src/ahriman/application/handlers/status.py +++ b/src/ahriman/application/handlers/status.py @@ -52,7 +52,7 @@ class Status(Handler): report(bool): force enable or disable reporting """ # we are using reporter here - client = Application(repository_id, configuration, report=True).repository.reporter + client = Application(repository_id, configuration, report=True).reporter if args.ahriman: service_status = client.status_get() StatusPrinter(service_status.status)(verbose=args.info) diff --git a/src/ahriman/application/handlers/status_update.py b/src/ahriman/application/handlers/status_update.py index 5f732c88..a06eb5d3 100644 --- a/src/ahriman/application/handlers/status_update.py +++ b/src/ahriman/application/handlers/status_update.py @@ -47,8 +47,7 @@ class StatusUpdate(Handler): configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(repository_id, configuration, report=True) - client = application.repository.reporter + client = Application(repository_id, configuration, report=True).reporter match args.action: case Action.Update if args.package: diff --git a/src/ahriman/core/repository/executor.py b/src/ahriman/core/repository/executor.py index ca77709a..0ade2895 100644 --- a/src/ahriman/core/repository/executor.py +++ b/src/ahriman/core/repository/executor.py @@ -61,12 +61,11 @@ class Executor(PackageInfo, Cleaner): if built.version != package.version: continue - packages = built.packages.values() # all packages must be either any or same architecture - if not all(single.architecture in ("any", self.architecture) for single in packages): + if not built.supports_architecture(self.repository_id.architecture): continue - return list_flatmap(packages, lambda single: archive.glob(f"{single.filename}*")) + return list_flatmap(built.packages.values(), lambda single: archive.glob(f"{single.filename}*")) return [] @@ -102,11 +101,11 @@ class Executor(PackageInfo, Cleaner): """ self.reporter.set_building(package.base) - task = Task(package, self.configuration, self.architecture, self.paths) + task = Task(package, self.configuration, self.repository_id.architecture, self.paths) patches = self.reporter.package_patches_get(package.base, None) commit_sha = task.init(path, patches, local_version) - loaded_package = Package.from_build(path, self.architecture, None) + loaded_package = Package.from_build(path, self.repository_id.architecture, None) if prebuilt := list(self._archive_lookup(loaded_package)): self.logger.info("using prebuilt packages for %s-%s", loaded_package.base, loaded_package.version) built = [] @@ -218,7 +217,7 @@ class Executor(PackageInfo, Cleaner): except Exception: self.reporter.set_failed(single.base) result.add_failed(single) - self.logger.exception("%s (%s) build exception", single.base, self.architecture) + self.logger.exception("%s (%s) build exception", single.base, self.repository_id.architecture) return result diff --git a/src/ahriman/core/repository/package_info.py b/src/ahriman/core/repository/package_info.py index 80652ceb..d7f882dc 100644 --- a/src/ahriman/core/repository/package_info.py +++ b/src/ahriman/core/repository/package_info.py @@ -33,6 +33,7 @@ from ahriman.core.status import Client from ahriman.core.utils import package_like from ahriman.models.changes import Changes from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId class PackageInfo(LazyLogging): @@ -43,11 +44,13 @@ class PackageInfo(LazyLogging): configuration(Configuration): configuration instance pacman(Pacman): alpm wrapper instance reporter(Client): build status reporter instance + repository_id(RepositoryId): repository unique identifier """ configuration: Configuration pacman: Pacman reporter: Client + repository_id: RepositoryId def full_depends(self, package: Package, packages: Iterable[Package]) -> list[str]: """ @@ -133,6 +136,8 @@ class PackageInfo(LazyLogging): # we can't use here load_archives, because it ignores versions for full_path in filter(package_like, paths.archive_for(package_base).iterdir()): local = Package.from_archive(full_path) + if not local.supports_architecture(self.repository_id.architecture): + continue packages.setdefault((local.base, local.version), local).packages.update(local.packages) comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version) diff --git a/src/ahriman/core/repository/repository_properties.py b/src/ahriman/core/repository/repository_properties.py index 9e678341..dacd6887 100644 --- a/src/ahriman/core/repository/repository_properties.py +++ b/src/ahriman/core/repository/repository_properties.py @@ -72,32 +72,12 @@ class RepositoryProperties(EventLogger, LazyLogging): self.ignore_list = configuration.getlist("build", "ignore_packages", fallback=[]) self.pacman = Pacman(repository_id, configuration, refresh_database=refresh_pacman_database) self.sign = GPG(configuration) - self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args) + self.repo = Repo(self.repository_id.name, self.paths, self.sign.repository_sign_args) self.reporter = Client.load(repository_id, configuration, database, report=report) self.triggers = TriggerLoader.load(repository_id, configuration) self.scan_paths = ScanPaths(configuration.getlist("build", "scan_paths", fallback=[])) - @property - def architecture(self) -> str: - """ - repository architecture for backward compatibility - - Returns: - str: repository architecture - """ - return self.repository_id.architecture - - @property - def name(self) -> str: - """ - repository name for backward compatibility - - Returns: - str: repository name - """ - return self.repository_id.name - def packager(self, packagers: Packagers, package_base: str) -> User: """ extract packager from configuration having username diff --git a/src/ahriman/core/repository/update_handler.py b/src/ahriman/core/repository/update_handler.py index 953be782..b1ef75bb 100644 --- a/src/ahriman/core/repository/update_handler.py +++ b/src/ahriman/core/repository/update_handler.py @@ -150,7 +150,7 @@ class UpdateHandler(PackageInfo, Cleaner): ) Sources.fetch(cache_dir, source) - remote = Package.from_build(cache_dir, self.architecture, None) + remote = Package.from_build(cache_dir, self.repository_id.architecture, None) local = packages.get(remote.base) if local is None: diff --git a/src/ahriman/core/status/watcher.py b/src/ahriman/core/status/watcher.py index e341b2a0..8c9b0955 100644 --- a/src/ahriman/core/status/watcher.py +++ b/src/ahriman/core/status/watcher.py @@ -23,6 +23,7 @@ from typing import Any, Self from ahriman.core.exceptions import UnknownPackageError from ahriman.core.log import LazyLogging +from ahriman.core.repository.package_info import PackageInfo from ahriman.core.status import Client from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.changes import Changes @@ -39,15 +40,18 @@ class Watcher(LazyLogging): Attributes: client(Client): reporter instance + package_info(PackageInfo): package info instance status(BuildStatus): daemon status """ - def __init__(self, client: Client) -> None: + def __init__(self, client: Client, package_info: PackageInfo) -> None: """ Args: client(Client): reporter instance + package_info(PackageInfo): package info instance """ self.client = client + self.package_info = package_info self._lock = Lock() self._known: dict[str, tuple[Package, BuildStatus]] = {} @@ -80,6 +84,18 @@ class Watcher(LazyLogging): logs_rotate: Callable[[int], None] + def package_archives(self, package_base: str) -> list[Package]: + """ + get known package archives + + Args: + package_base(str): package base + + Returns: + list[Package]: list of built package for this package base + """ + return self.package_info.package_archives(package_base) + package_changes_get: Callable[[str], Changes] package_changes_update: Callable[[str, Changes], None] diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py index b2157e99..7d2d86c0 100644 --- a/src/ahriman/models/package.py +++ b/src/ahriman/models/package.py @@ -137,16 +137,6 @@ class Package(LazyLogging): """ return list_flatmap(self.packages.values(), lambda package: package.groups) - @property - def is_single_package(self) -> bool: - """ - is it possible to transform package base to single package or not - - Returns: - bool: true in case if this base has only one package with the same name - """ - return self.base in self.packages and len(self.packages) == 1 - @property def is_vcs(self) -> bool: """ @@ -375,9 +365,22 @@ class Package(LazyLogging): Returns: str: print-friendly string """ - details = "" if self.is_single_package else f" ({" ".join(sorted(self.packages.keys()))})" + is_single_package = self.base in self.packages and len(self.packages) == 1 + details = "" if is_single_package else f" ({" ".join(sorted(self.packages.keys()))})" return f"{self.base}{details}" + def supports_architecture(self, architecture: str) -> bool: + """ + helper to check if the package belongs to the specified architecture + + Args: + architecture(str): probe repository architecture + + Returns: + bool: ``True`` if all packages are same architecture or any + """ + return all(single.architecture in ("any", architecture) for single in self.packages.values()) + def vercmp(self, version: str) -> int: """ typed wrapper around :func:`pyalpm.vercmp()` diff --git a/src/ahriman/web/views/v1/packages/archives.py b/src/ahriman/web/views/v1/packages/archives.py new file mode 100644 index 00000000..8c577bf5 --- /dev/null +++ b/src/ahriman/web/views/v1/packages/archives.py @@ -0,0 +1,65 @@ +# +# Copyright (c) 2021-2026 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 aiohttp.web import Response +from typing import ClassVar + +from ahriman.models.user_access import UserAccess +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import PackageNameSchema, PackageSchema, RepositoryIdSchema +from ahriman.web.views.base import BaseView +from ahriman.web.views.status_view_guard import StatusViewGuard + + +class Archives(StatusViewGuard, BaseView): + """ + package archives web view + + Attributes: + GET_PERMISSION(UserAccess): (class attribute) get permissions of self + """ + + GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter + ROUTES = ["/api/v1/packages/{package}/archives"] + + @apidocs( + tags=["Packages"], + summary="Get package archives", + description="Retrieve built package archives for the base", + permission=GET_PERMISSION, + error_404_description="Package base and/or repository are unknown", + schema=PackageSchema(many=True), + match_schema=PackageNameSchema, + query_schema=RepositoryIdSchema, + ) + async def get(self) -> Response: + """ + get package archives + + Returns: + Response: 200 with package archives on success + + Raises: + HTTPNotFound: if no package was found + """ + package_base = self.request.match_info["package"] + + archives = self.service(package_base=package_base).package_archives(package_base) + + return self.json_response([archive.view() for archive in archives]) diff --git a/src/ahriman/web/web.py b/src/ahriman/web/web.py index 348ab66f..28434561 100644 --- a/src/ahriman/web/web.py +++ b/src/ahriman/web/web.py @@ -23,12 +23,14 @@ import logging import socket from aiohttp.web import Application, normalize_path_middleware, run_app +from pathlib import Path from ahriman.core.auth import Auth from ahriman.core.configuration import Configuration from ahriman.core.database import SQLite from ahriman.core.distributed import WorkersCache from ahriman.core.exceptions import InitializeError +from ahriman.core.repository.package_info import PackageInfo from ahriman.core.spawn import Spawn from ahriman.core.status import Client from ahriman.core.status.watcher import Watcher @@ -78,6 +80,34 @@ def _create_socket(configuration: Configuration, application: Application) -> so return sock +def _create_watcher(path: Path, repository_id: RepositoryId) -> Watcher: + """ + build watcher for selected repository + + Args: + path(Path): path to configuration file + repository_id(RepositoryId): repository unique identifier + + Returns: + Watcher: watcher instance + """ + logging.getLogger(__name__).info("load repository %s", repository_id) + # load settings explicitly for architecture if any + configuration = Configuration.from_path(path, repository_id) + + # load database instance, because it holds identifier + database = SQLite.load(configuration) + # explicitly load local client + client = Client.load(repository_id, configuration, database, report=False) + + # load package info wrapper + package_info = PackageInfo() + package_info.configuration = configuration + package_info.repository_id = repository_id + + return Watcher(client, package_info) + + async def _on_shutdown(application: Application) -> None: """ web application shutdown handler @@ -168,18 +198,11 @@ def setup_server(configuration: Configuration, spawner: Spawn, repositories: lis # package cache if not repositories: raise InitializeError("No repositories configured, exiting") - watchers: dict[RepositoryId, Watcher] = {} configuration_path, _ = configuration.check_loaded() - for repository_id in repositories: - application.logger.info("load repository %s", repository_id) - # load settings explicitly for architecture if any - repository_configuration = Configuration.from_path(configuration_path, repository_id) - # load database instance, because it holds identifier - database = SQLite.load(repository_configuration) - # explicitly load local client - client = Client.load(repository_id, repository_configuration, database, report=False) - watchers[repository_id] = Watcher(client) - application[WatcherKey] = watchers + application[WatcherKey] = { + repository_id: _create_watcher(configuration_path, repository_id) + for repository_id in repositories + } # workers cache application[WorkersKey] = WorkersCache(configuration) # process spawner diff --git a/tests/ahriman/application/handlers/test_handler_archives.py b/tests/ahriman/application/handlers/test_handler_archives.py new file mode 100644 index 00000000..b584eafa --- /dev/null +++ b/tests/ahriman/application/handlers/test_handler_archives.py @@ -0,0 +1,84 @@ +import argparse +import pytest + +from pytest_mock import MockerFixture + +from ahriman.application.handlers.archives import Archives +from ahriman.core.configuration import Configuration +from ahriman.core.database import SQLite +from ahriman.core.repository import Repository +from ahriman.models.action import Action +from ahriman.models.package import Package + + +def _default_args(args: argparse.Namespace) -> argparse.Namespace: + """ + default arguments for these test cases + + Args: + args(argparse.Namespace): command line arguments fixture + + Returns: + argparse.Namespace: generated arguments for these test cases + """ + args.action = Action.List + args.exit_code = False + args.info = False + args.package = "package" + return args + + +def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository, + package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must run command + """ + args = _default_args(args) + mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) + application_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", + return_value=[package_ahriman]) + check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status") + print_mock = mocker.patch("ahriman.core.formatters.Printer.print") + + _, repository_id = configuration.check_loaded() + Archives.run(args, repository_id, configuration, report=False) + application_mock.assert_called_once_with(args.package) + check_mock.assert_called_once_with(False, True) + print_mock.assert_called_once_with(verbose=False, log_fn=pytest.helpers.anyvar(int), separator=": ") + + +def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, repository: Repository, + mocker: MockerFixture) -> None: + """ + must raise ExitCode exception on empty archives result + """ + args = _default_args(args) + args.exit_code = True + mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) + mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=[]) + check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status") + + _, repository_id = configuration.check_loaded() + Archives.run(args, repository_id, configuration, report=False) + check_mock.assert_called_once_with(True, False) + + +def test_imply_with_report(args: argparse.Namespace, configuration: Configuration, database: SQLite, + mocker: MockerFixture) -> None: + """ + must create application object with native reporting + """ + args = _default_args(args) + mocker.patch("ahriman.core.database.SQLite.load", return_value=database) + load_mock = mocker.patch("ahriman.core.repository.Repository.load") + + _, repository_id = configuration.check_loaded() + Archives.run(args, repository_id, configuration, report=False) + load_mock.assert_called_once_with(repository_id, configuration, database, report=True, refresh_pacman_database=0) + + +def test_disallow_multi_architecture_run() -> None: + """ + must not allow multi architecture run + """ + assert not Archives.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py index 8e7e1010..25587b8b 100644 --- a/tests/ahriman/application/test_ahriman.py +++ b/tests/ahriman/application/test_ahriman.py @@ -271,6 +271,22 @@ def test_subparsers_package_add_option_variable_multiple(parser: argparse.Argume assert args.variable == ["var1", "var2"] +def test_subparsers_package_archives(parser: argparse.ArgumentParser) -> None: + """ + package-archives command must imply action, exit code, info, lock, quiet, report and unsafe + """ + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-archives", "ahriman"]) + assert args.action == Action.List + assert args.architecture == "x86_64" + assert not args.exit_code + assert not args.info + assert args.lock is None + assert args.quiet + assert not args.report + assert args.repository == "repo" + assert args.unsafe + + def test_subparsers_package_changes(parser: argparse.ArgumentParser) -> None: """ package-changes command must imply action, exit code, lock, quiet, report and unsafe diff --git a/tests/ahriman/conftest.py b/tests/ahriman/conftest.py index 6c2c0975..56fadf19 100644 --- a/tests/ahriman/conftest.py +++ b/tests/ahriman/conftest.py @@ -16,6 +16,7 @@ from ahriman.core.database import SQLite from ahriman.core.database.migrations import Migrations from ahriman.core.log.log_loader import LogLoader from ahriman.core.repository import Repository +from ahriman.core.repository.package_info import PackageInfo from ahriman.core.spawn import Spawn from ahriman.core.status import Client from ahriman.core.status.watcher import Watcher @@ -688,4 +689,5 @@ def watcher(local_client: Client) -> Watcher: Returns: Watcher: package status watcher test instance """ - return Watcher(local_client) + package_info = PackageInfo() + return Watcher(local_client, package_info) diff --git a/tests/ahriman/core/repository/test_executor.py b/tests/ahriman/core/repository/test_executor.py index 3e4caafa..4189f2f9 100644 --- a/tests/ahriman/core/repository/test_executor.py +++ b/tests/ahriman/core/repository/test_executor.py @@ -11,6 +11,7 @@ from ahriman.models.changes import Changes from ahriman.models.dependencies import Dependencies from ahriman.models.package import Package from ahriman.models.packagers import Packagers +from ahriman.models.repository_id import RepositoryId from ahriman.models.user import User @@ -56,7 +57,7 @@ def test_archive_lookup_architecture_mismatch(executor: Executor, package_ahrima """ package_ahriman.packages[package_ahriman.base].architecture = "x86_64" mocker.patch("pathlib.Path.is_dir", return_value=True) - mocker.patch("ahriman.core.repository.executor.Executor.architecture", return_value="i686") + executor.repository_id = RepositoryId("i686", executor.repository_id.name) mocker.patch("pathlib.Path.iterdir", return_value=[ Path("1.pkg.tar.zst"), ]) @@ -116,7 +117,7 @@ def test_package_build(executor: Executor, package_ahriman: Package, mocker: Moc assert executor._package_build(package_ahriman, Path("local"), "packager", None) == "sha" status_client_mock.assert_called_once_with(package_ahriman.base) init_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), None) - package_mock.assert_called_once_with(Path("local"), executor.architecture, None) + package_mock.assert_called_once_with(Path("local"), executor.repository_id.architecture, None) lookup_mock.assert_called_once_with(package_ahriman) with_packages_mock.assert_called_once_with([Path(package_ahriman.base)]) rename_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base) diff --git a/tests/ahriman/core/repository/test_package_info.py b/tests/ahriman/core/repository/test_package_info.py index cb97e55d..69502629 100644 --- a/tests/ahriman/core/repository/test_package_info.py +++ b/tests/ahriman/core/repository/test_package_info.py @@ -1,5 +1,6 @@ import pytest +from dataclasses import replace from pathlib import Path from pytest_mock import MockerFixture from unittest.mock import MagicMock @@ -95,26 +96,30 @@ def test_package_archives(repository: Repository, package_ahriman: Package, mock """ must load package archives sorted by version """ - from dataclasses import replace - from typing import Any - - def package(version: Any, *args: Any, **kwargs: Any) -> Package: - generated = replace(package_ahriman, version=str(version)) - generated.packages = { - key: replace(value, filename=str(version)) - for key, value in generated.packages.items() - } - return generated - mocker.patch("ahriman.core.repository.package_info.package_like", return_value=True) - mocker.patch("pathlib.Path.iterdir", return_value=[Path(str(i)) for i in range(5)]) - mocker.patch("ahriman.models.package.Package.from_archive", side_effect=package) + mocker.patch("pathlib.Path.iterdir", return_value=[str(i) for i in range(5)]) + mocker.patch("ahriman.models.package.Package.from_archive", + side_effect=lambda version: replace(package_ahriman, version=version)) result = repository.package_archives(package_ahriman.base) assert len(result) == 5 assert [p.version for p in result] == [str(i) for i in range(5)] +def test_package_archives_architecture_mismatch(repository: Repository, package_ahriman: Package, + mocker: MockerFixture) -> None: + """ + must skip packages with mismatched architecture + """ + package_ahriman.packages[package_ahriman.base].architecture = "i686" + + mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.packages[package_ahriman.base].filepath]) + mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman) + + result = repository.package_archives(package_ahriman.base) + assert len(result) == 0 + + def test_package_changes(repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None: """ must load package changes diff --git a/tests/ahriman/core/repository/test_repository_properties.py b/tests/ahriman/core/repository/test_repository_properties.py index 2f6e6bf3..694e8467 100644 --- a/tests/ahriman/core/repository/test_repository_properties.py +++ b/tests/ahriman/core/repository/test_repository_properties.py @@ -6,20 +6,6 @@ from ahriman.models.user import User from ahriman.models.user_access import UserAccess -def test_architecture(repository: RepositoryProperties) -> None: - """ - must provide repository architecture for backward compatibility - """ - assert repository.architecture == repository.repository_id.architecture - - -def test_name(repository: RepositoryProperties) -> None: - """ - must provide repository name for backward compatibility - """ - assert repository.name == repository.repository_id.name - - def test_packager(repository: RepositoryProperties, mocker: MockerFixture) -> None: """ must extract packager diff --git a/tests/ahriman/core/status/test_watcher.py b/tests/ahriman/core/status/test_watcher.py index ff99d3da..7a0f4434 100644 --- a/tests/ahriman/core/status/test_watcher.py +++ b/tests/ahriman/core/status/test_watcher.py @@ -45,6 +45,18 @@ def test_load_known(watcher: Watcher, package_ahriman: Package, mocker: MockerFi assert status.status == BuildStatusEnum.Success +def test_package_archives(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must return package archives from package info + """ + archives_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", + return_value=[package_ahriman]) + + result = watcher.package_archives(package_ahriman.base) + assert result == [package_ahriman] + archives_mock.assert_called_once_with(package_ahriman.base) + + def test_package_get(watcher: Watcher, package_ahriman: Package) -> None: """ must return package status diff --git a/tests/ahriman/models/test_package.py b/tests/ahriman/models/test_package.py index c7629e47..0c1d46f5 100644 --- a/tests/ahriman/models/test_package.py +++ b/tests/ahriman/models/test_package.py @@ -101,20 +101,6 @@ def test_groups(package_ahriman: Package) -> None: assert sorted(package_ahriman.groups) == package_ahriman.groups -def test_is_single_package_false(package_python_schedule: Package) -> None: - """ - python-schedule must not be single package - """ - assert not package_python_schedule.is_single_package - - -def test_is_single_package_true(package_ahriman: Package) -> None: - """ - ahriman must be single package - """ - assert package_ahriman.is_single_package - - def test_is_vcs_false(package_ahriman: Package) -> None: """ ahriman must not be VCS package @@ -353,6 +339,30 @@ def test_build_status_pretty_print(package_ahriman: Package) -> None: assert isinstance(package_ahriman.pretty_print(), str) +def test_supports_architecture(package_ahriman: Package) -> None: + """ + must check if package supports architecture + """ + package_ahriman.packages[package_ahriman.base].architecture = "x86_64" + assert package_ahriman.supports_architecture("x86_64") + + +def test_supports_architecture_any(package_ahriman: Package) -> None: + """ + must support any architecture + """ + package_ahriman.packages[package_ahriman.base].architecture = "any" + assert package_ahriman.supports_architecture("x86_64") + + +def test_supports_architecture_mismatch(package_ahriman: Package) -> None: + """ + must not support mismatched architecture + """ + package_ahriman.packages[package_ahriman.base].architecture = "i686" + assert not package_ahriman.supports_architecture("x86_64") + + def test_vercmp(package_ahriman: Package, mocker: MockerFixture) -> None: """ must call vercmp diff --git a/tests/ahriman/web/test_web.py b/tests/ahriman/web/test_web.py index bbdc76a2..3bc04267 100644 --- a/tests/ahriman/web/test_web.py +++ b/tests/ahriman/web/test_web.py @@ -10,7 +10,7 @@ from ahriman.core.exceptions import InitializeError from ahriman.core.spawn import Spawn from ahriman.core.status.watcher import Watcher from ahriman.web.keys import ConfigurationKey -from ahriman.web.web import _create_socket, _on_shutdown, _on_startup, run_server, setup_server +from ahriman.web.web import _create_socket, _create_watcher, _on_shutdown, _on_startup, run_server, setup_server async def test_create_socket(application: Application, mocker: MockerFixture) -> None: @@ -139,6 +139,20 @@ def test_run_with_socket(application: Application, mocker: MockerFixture) -> Non ) +def test_create_watcher(configuration: Configuration, mocker: MockerFixture) -> None: + """ + must create watcher for repository + """ + database_mock = mocker.patch("ahriman.core.database.SQLite.load") + client_mock = mocker.patch("ahriman.core.status.Client.load") + configuration_path, repository_id = configuration.check_loaded() + + result = _create_watcher(configuration_path, repository_id) + assert isinstance(result, Watcher) + database_mock.assert_called_once() + client_mock.assert_called_once() + + def test_setup_no_repositories(configuration: Configuration, spawner: Spawn) -> None: """ must raise InitializeError if no repositories set diff --git a/tests/ahriman/web/views/v1/packages/test_view_v1_packages_archives.py b/tests/ahriman/web/views/v1/packages/test_view_v1_packages_archives.py new file mode 100644 index 00000000..fa7f6b62 --- /dev/null +++ b/tests/ahriman/web/views/v1/packages/test_view_v1_packages_archives.py @@ -0,0 +1,52 @@ +import pytest + +from aiohttp.test_utils import TestClient +from pytest_mock import MockerFixture + +from ahriman.models.build_status import BuildStatusEnum +from ahriman.models.package import Package +from ahriman.models.user_access import UserAccess +from ahriman.web.views.v1.packages.archives import Archives + + +async def test_get_permission() -> None: + """ + must return correct permission for the request + """ + for method in ("GET",): + request = pytest.helpers.request("", "", method) + assert await Archives.get_permission(request) == UserAccess.Reporter + + +def test_routes() -> None: + """ + must return correct routes + """ + assert Archives.ROUTES == ["/api/v1/packages/{package}/archives"] + + +async def test_get(client: TestClient, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must get archives for package + """ + await client.post(f"/api/v1/packages/{package_ahriman.base}", + json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}) + mocker.patch("ahriman.core.status.watcher.Watcher.package_archives", return_value=[package_ahriman]) + response_schema = pytest.helpers.schema_response(Archives.get) + + response = await client.get(f"/api/v1/packages/{package_ahriman.base}/archives") + assert response.status == 200 + + archives = await response.json() + assert not response_schema.validate(archives) + + +async def test_get_not_found(client: TestClient, package_ahriman: Package) -> None: + """ + must return not found for missing package + """ + response_schema = pytest.helpers.schema_response(Archives.get, code=404) + + response = await client.get(f"/api/v1/packages/{package_ahriman.base}/archives") + assert response.status == 404 + assert not response_schema.validate(await response.json())