feat: support archive listing

This commit is contained in:
2026-03-12 02:26:59 +02:00
parent 81aeb56ba3
commit a09ad7617d
28 changed files with 474 additions and 108 deletions

View File

@@ -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
------------------------------------------

View File

@@ -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
--------------------------------------------

View File

@@ -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

View File

@@ -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:
"""

View File

@@ -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 <http://www.gnu.org/licenses/>.
#
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]

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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:

View File

@@ -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]

View File

@@ -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()`

View File

@@ -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 <http://www.gnu.org/licenses/>.
#
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])

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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())