Compare commits

..

3 Commits

22 changed files with 273 additions and 129 deletions

View File

@@ -172,6 +172,14 @@ ahriman.models.packagers module
:no-undoc-members:
:show-inheritance:
ahriman.models.pacman\_handle module
------------------------------------
.. automodule:: ahriman.models.pacman_handle
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.pacman\_synchronization module
---------------------------------------------

View File

@@ -24,13 +24,14 @@ import tarfile
from collections.abc import Iterable, Iterator
from functools import cached_property
from pathlib import Path
from pyalpm import DB, Handle, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
from pyalpm import DB, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
from string import Template
from ahriman.core.alpm.pacman_database import PacmanDatabase
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
from ahriman.core.utils import trim_package
from ahriman.models.pacman_handle import PacmanHandle
from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.repository_id import RepositoryId
@@ -61,16 +62,16 @@ class Pacman(LazyLogging):
self.refresh_database = refresh_database
@cached_property
def handle(self) -> Handle:
def handle(self) -> PacmanHandle:
"""
pyalpm handle
Returns:
Handle: generated pyalpm handle instance
PacmanHandle: generated pyalpm handle instance
"""
return self.__create_handle(refresh_database=self.refresh_database)
def __create_handle(self, *, refresh_database: PacmanSynchronization) -> Handle:
def __create_handle(self, *, refresh_database: PacmanSynchronization) -> PacmanHandle:
"""
create lazy handle function
@@ -78,14 +79,14 @@ class Pacman(LazyLogging):
refresh_database(PacmanSynchronization): synchronize local cache to remote
Returns:
Handle: fully initialized pacman handle
PacmanHandle: fully initialized pacman handle
"""
pacman_root = self.configuration.getpath("alpm", "database")
use_ahriman_cache = self.configuration.getboolean("alpm", "use_ahriman_cache")
database_path = self.repository_paths.pacman if use_ahriman_cache else pacman_root
root = self.configuration.getpath("alpm", "root")
handle = Handle(str(root), str(database_path))
handle = PacmanHandle(str(root), str(database_path))
for repository in self.configuration.getlist("alpm", "repositories"):
database = self.database_init(handle, repository, self.repository_id.architecture)
@@ -99,12 +100,12 @@ class Pacman(LazyLogging):
return handle
def database_copy(self, handle: Handle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None:
def database_copy(self, handle: PacmanHandle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None:
"""
copy database from the operating system root to the ahriman local home
Args:
handle(Handle): pacman handle which will be used for database copying
handle(PacmanHandle): pacman handle which will be used for database copying
database(DB): pacman database instance to be copied
pacman_root(Path): operating system pacman root
use_ahriman_cache(bool): use local ahriman cache instead of system one
@@ -133,12 +134,12 @@ class Pacman(LazyLogging):
with self.repository_paths.preserve_owner():
shutil.copy(src, dst)
def database_init(self, handle: Handle, repository: str, architecture: str) -> DB:
def database_init(self, handle: PacmanHandle, repository: str, architecture: str) -> DB:
"""
create database instance from pacman handler and set its properties
Args:
handle(Handle): pacman handle which will be used for database initializing
handle(PacmanHandle): pacman handle which will be used for database initializing
repository(str): pacman repository name (e.g. core)
architecture(str): repository architecture
@@ -164,12 +165,12 @@ class Pacman(LazyLogging):
return database
def database_sync(self, handle: Handle, *, force: bool) -> None:
def database_sync(self, handle: PacmanHandle, *, force: bool) -> None:
"""
sync local database
Args:
handle(Handle): pacman handle which will be used for database sync
handle(PacmanHandle): pacman handle which will be used for database sync
force(bool): force database synchronization (same as ``pacman -Syy``)
"""
self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force)

View File

@@ -19,11 +19,9 @@
#
from sqlite3 import Connection
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration
from ahriman.core.utils import package_like
from ahriman.models.package import Package
from ahriman.models.pacman_synchronization import PacmanSynchronization
__all__ = ["migrate_data", "steps"]
@@ -61,12 +59,9 @@ def migrate_package_depends(connection: Connection, configuration: Configuration
if not configuration.repository_paths.repository.is_dir():
return
_, repository_id = configuration.check_loaded()
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
package_list = []
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
base = Package.from_archive(full_path, pacman)
base = Package.from_archive(full_path)
for package, description in base.packages.items():
package_list.append({
"make_depends": description.make_depends,

View File

@@ -19,11 +19,9 @@
#
from sqlite3 import Connection
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration
from ahriman.core.utils import package_like
from ahriman.models.package import Package
from ahriman.models.pacman_synchronization import PacmanSynchronization
__all__ = ["migrate_data", "steps"]
@@ -58,12 +56,9 @@ def migrate_package_check_depends(connection: Connection, configuration: Configu
if not configuration.repository_paths.repository.is_dir():
return
_, repository_id = configuration.check_loaded()
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
package_list = []
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
base = Package.from_archive(full_path, pacman)
base = Package.from_archive(full_path)
for package, description in base.packages.items():
package_list.append({
"check_depends": description.check_depends,

View File

@@ -19,11 +19,9 @@
#
from sqlite3 import Connection
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration
from ahriman.core.utils import package_like
from ahriman.models.package import Package
from ahriman.models.pacman_synchronization import PacmanSynchronization
__all__ = ["migrate_data", "steps"]
@@ -64,12 +62,9 @@ def migrate_package_base_packager(connection: Connection, configuration: Configu
if not configuration.repository_paths.repository.is_dir():
return
_, repository_id = configuration.check_loaded()
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
package_list = []
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
package = Package.from_archive(full_path, pacman)
package = Package.from_archive(full_path)
package_list.append({
"package_base": package.base,
"packager": package.packager,

View File

@@ -20,13 +20,11 @@
from dataclasses import replace
from sqlite3 import Connection
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Explorer
from ahriman.core.sign.gpg import GPG
from ahriman.core.utils import atomic_move, package_like, symlink_relative
from ahriman.models.package import Package
from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.repository_paths import RepositoryPaths
@@ -45,29 +43,27 @@ def migrate_data(connection: Connection, configuration: Configuration) -> None:
for repository_id in Explorer.repositories_extract(configuration):
paths = replace(configuration.repository_paths, repository_id=repository_id)
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
# create archive directory if required
if not paths.archive.is_dir():
with paths.preserve_owner():
paths.archive.mkdir(mode=0o755, parents=True)
move_packages(paths, pacman)
move_packages(paths)
def move_packages(repository_paths: RepositoryPaths, pacman: Pacman) -> None:
def move_packages(repository_paths: RepositoryPaths) -> None:
"""
move packages from repository to archive and create symbolic links
Args:
repository_paths(RepositoryPaths): repository paths instance
pacman(Pacman): alpm wrapper instance
"""
for archive in filter(package_like, repository_paths.repository.iterdir()):
if not archive.is_file(follow_symlinks=False):
continue # skip symbolic links if any
package = Package.from_archive(archive, pacman)
package = Package.from_archive(archive)
artifacts = [archive]
# check if there are signatures for this package and append it here too
if (signature := GPG.signature(archive)).exists():

View File

@@ -17,14 +17,10 @@
# 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 collections.abc import Callable
from functools import cmp_to_key
from ahriman.core import context
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository
from ahriman.core.triggers import Trigger
from ahriman.core.utils import package_like
from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId
from ahriman.models.result import Result
@@ -78,27 +74,20 @@ class ArchiveRotationTrigger(Trigger):
"""
return list(cls.CONFIGURATION_SCHEMA.keys())
def archives_remove(self, package: Package, pacman: Pacman) -> None:
def archives_remove(self, package: Package, repository: Repository) -> None:
"""
remove older versions of the specified package
Args:
package(Package): package which has been updated to check for older versions
pacman(Pacman): alpm wrapper instance
repository(Repository): repository instance
"""
# explicit guard to skip process in case if rotation is disabled
# this guard is supposed to speedup process
if self.keep_built_packages == 0:
return
packages: dict[tuple[str, str], Package] = {}
# we can't use here load_archives, because it ignores versions
for full_path in filter(package_like, self.paths.archive_for(package.base).iterdir()):
local = Package.from_archive(full_path, pacman)
packages.setdefault((local.base, local.version), local).packages.update(local.packages)
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
to_remove = sorted(packages.values(), key=cmp_to_key(comparator))
to_remove = repository.package_archives(package.base)
for single in to_remove[:-self.keep_built_packages]:
self.logger.info("removing version %s of package %s", single.version, single.base)
@@ -115,7 +104,7 @@ class ArchiveRotationTrigger(Trigger):
packages(list[Package]): list of all available packages
"""
ctx = context.get()
pacman = ctx.get(Pacman)
repository = ctx.get(Repository)
for package in result.success:
self.archives_remove(package, pacman)
self.archives_remove(package, repository)

View File

@@ -57,7 +57,7 @@ class Executor(PackageInfo, Cleaner):
for path in filter(package_like, archive.iterdir()):
# check if package version is the same
built = Package.from_archive(path, self.pacman)
built = Package.from_archive(path)
if built.version != package.version:
continue
@@ -117,7 +117,7 @@ class Executor(PackageInfo, Cleaner):
else:
built = task.build(path, PACKAGER=packager)
package.with_packages(built, self.pacman)
package.with_packages(built)
for src in built:
dst = self.paths.packages / src.name
atomic_move(src, dst)

View File

@@ -19,7 +19,8 @@
#
import copy
from collections.abc import Iterable
from collections.abc import Callable, Iterable
from functools import cmp_to_key
from pathlib import Path
from tempfile import TemporaryDirectory
@@ -86,7 +87,7 @@ class PackageInfo(RepositoryProperties):
# we are iterating over bases, not single packages
for full_path in packages:
try:
local = Package.from_archive(full_path, self.pacman)
local = Package.from_archive(full_path)
if (source := sources.get(local.base)) is not None: # update source with remote
local.remote = source
@@ -102,6 +103,27 @@ class PackageInfo(RepositoryProperties):
self.logger.exception("could not load package from %s", full_path)
return list(result.values())
def package_archives(self, package_base: str) -> list[Package]:
"""
load list of packages known for this package base. This method unlike
:func:`ahriman.core.repository.package_info.PackageInfo.load_archives` scans archive directory and loads all
versions available for the ``package_base``
Args:
package_base(str): package base
Returns:
list[Package]: list of packages belonging to this base, sorted by version by ascension
"""
packages: dict[tuple[str, str], Package] = {}
# we can't use here load_archives, because it ignores versions
for full_path in filter(package_like, self.paths.archive_for(package_base).iterdir()):
local = Package.from_archive(full_path)
packages.setdefault((local.base, local.version), local).packages.update(local.packages)
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
return sorted(packages.values(), key=cmp_to_key(comparator))
def package_changes(self, package: Package, last_commit_sha: str) -> Changes | None:
"""
extract package change for the package since last commit if available

View File

@@ -31,6 +31,7 @@ from ahriman.core.log import LazyLogging
from ahriman.core.utils import dataclass_view, full_version, list_flatmap, parse_version, srcinfo_property_list
from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource
from ahriman.models.pacman_handle import PacmanHandle
from ahriman.models.pkgbuild import Pkgbuild
from ahriman.models.remote_source import RemoteSource
@@ -186,18 +187,17 @@ class Package(LazyLogging):
return sorted(packages)
@classmethod
def from_archive(cls, path: Path, pacman: Pacman) -> Self:
def from_archive(cls, path: Path) -> Self:
"""
construct package properties from package archive
Args:
path(Path): path to package archive
pacman(Pacman): alpm wrapper instance
Returns:
Self: package properties
"""
package = pacman.handle.load_pkg(str(path))
package = PacmanHandle.ephemeral().package_load(path)
description = PackageDescription.from_package(package, path)
return cls(
base=package.base or package.name,
@@ -400,17 +400,16 @@ class Package(LazyLogging):
"""
return dataclass_view(self)
def with_packages(self, packages: Iterable[Path], pacman: Pacman) -> None:
def with_packages(self, packages: Iterable[Path]) -> None:
"""
replace packages descriptions with ones from archives
Args:
packages(Iterable[Path]): paths to package archives
pacman(Pacman): alpm wrapper instance
"""
self.packages = {} # reset state
for package in packages:
archive = self.from_archive(package, pacman)
archive = self.from_archive(package)
if archive.base != self.base:
continue

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/>.
#
from pathlib import Path
from pyalpm import Handle, Package # type: ignore[import-not-found]
from tempfile import TemporaryDirectory
from typing import Any, ClassVar, Self
class PacmanHandle:
"""
lightweight wrapper for pacman handle to be used for direct alpm operations (e.g. package load)
Attributes:
handle(Handle): pyalpm handle instance
"""
_ephemeral: ClassVar[Self | None] = None
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""
Args:
*args(Any): positional arguments for :class:`pyalpm.Handle`
**kwargs(Any): keyword arguments for :class:`pyalpm.Handle`
"""
self.handle = Handle(*args, **kwargs)
@classmethod
def ephemeral(cls) -> Self:
"""
create temporary instance with no access to real databases
Returns:
Self: loaded class
"""
if cls._ephemeral is None:
# handle creates alpm version file, but we don't use it
# so it is ok to just remove it
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
cls._ephemeral = cls("/", dir_name)
return cls._ephemeral
def package_load(self, path: Path) -> Package:
"""
load package from path to the archive
Args:
path(Path): path to package archive
Returns:
Package: package instance
"""
return self.handle.load_pkg(str(path))
def __getattr__(self, item: str) -> Any:
"""
proxy methods for :class:`pyalpm.Handle`, because it doesn't allow subclassing
Args:
item(str): property name
Returns:
Any: attribute by its name
"""
return self.handle.__getattribute__(item)

View File

@@ -309,6 +309,44 @@ def test_subparsers_package_changes_remove_package_changes(parser: argparse.Argu
assert dir(args) == dir(reference_args)
def test_subparsers_package_pkgbuild(parser: argparse.ArgumentParser) -> None:
"""
package-pkgbuild command must imply action, exit code, lock, quiet, report and unsafe
"""
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-pkgbuild", "ahriman"])
assert args.action == Action.List
assert args.architecture == "x86_64"
assert not args.exit_code
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == "repo"
assert args.unsafe
def test_subparsers_package_pkgbuild_remove(parser: argparse.ArgumentParser) -> None:
"""
package-pkgbuild-remove command must imply action, lock, quiet, report and unsafe
"""
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-pkgbuild-remove", "ahriman"])
assert args.action == Action.Remove
assert args.architecture == "x86_64"
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == "repo"
assert args.unsafe
def test_subparsers_package_pkgbuild_remove_package_pkgbuild(parser: argparse.ArgumentParser) -> None:
"""
package-pkgbuild-remove must have same keys as package-pkgbuild
"""
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-pkgbuild-remove", "ahriman"])
reference_args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-pkgbuild", "ahriman"])
assert dir(args) == dir(reference_args)
def test_subparsers_package_copy_option_architecture(parser: argparse.ArgumentParser) -> None:
"""
package-copy command must correctly parse architecture list

View File

@@ -34,8 +34,7 @@ def test_migrate_package_depends(connection: Connection, configuration: Configur
package_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
migrate_package_depends(connection, configuration)
package_mock.assert_called_once_with(
package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int))
package_mock.assert_called_once_with(package_ahriman.packages[package_ahriman.base].filepath)
connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{
"make_depends": package_ahriman.packages[package_ahriman.base].make_depends,
"opt_depends": package_ahriman.packages[package_ahriman.base].opt_depends,

View File

@@ -34,8 +34,7 @@ def test_migrate_package_depends(connection: Connection, configuration: Configur
package_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
migrate_package_check_depends(connection, configuration)
package_mock.assert_called_once_with(
package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int))
package_mock.assert_called_once_with(package_ahriman.packages[package_ahriman.base].filepath)
connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{
"check_depends": package_ahriman.packages[package_ahriman.base].check_depends,
"package": package_ahriman.base,

View File

@@ -34,8 +34,7 @@ def test_migrate_package_base_packager(connection: Connection, configuration: Co
package_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
migrate_package_base_packager(connection, configuration)
package_mock.assert_called_once_with(
package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int))
package_mock.assert_called_once_with(package_ahriman.packages[package_ahriman.base].filepath)
connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{
"package_base": package_ahriman.base,
"packager": package_ahriman.packager,

View File

@@ -7,7 +7,6 @@ from sqlite3 import Connection
from typing import Any
from unittest.mock import call as MockCall
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration
from ahriman.core.database.migrations.m016_archive import migrate_data, move_packages
from ahriman.models.package import Package
@@ -28,12 +27,12 @@ def test_migrate_data(connection: Connection, configuration: Configuration, mock
migrate_data(connection, configuration)
migration_mock.assert_has_calls([
MockCall(replace(configuration.repository_paths, repository_id=repository), pytest.helpers.anyvar(int))
MockCall(replace(configuration.repository_paths, repository_id=repository))
for repository in repositories
])
def test_move_packages(repository_paths: RepositoryPaths, pacman: Pacman, package_ahriman: Package,
def test_move_packages(repository_paths: RepositoryPaths, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must move packages to the archive directory
@@ -57,9 +56,9 @@ def test_move_packages(repository_paths: RepositoryPaths, pacman: Pacman, packag
move_mock = mocker.patch("ahriman.core.database.migrations.m016_archive.atomic_move")
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
move_packages(repository_paths, pacman)
move_packages(repository_paths)
archive_mock.assert_has_calls([
MockCall(repository_paths.repository / filename, pacman)
MockCall(repository_paths.repository / filename)
for filename in ("file.pkg.tar.xz", "file2.pkg.tar.xz")
])
move_mock.assert_has_calls([

View File

@@ -3,12 +3,11 @@ import pytest
from dataclasses import replace
from pathlib import Path
from pytest_mock import MockerFixture
from typing import Any
from unittest.mock import call as MockCall
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration
from ahriman.core.housekeeping import ArchiveRotationTrigger
from ahriman.core.repository import Repository
from ahriman.models.package import Package
from ahriman.models.result import Result
@@ -21,26 +20,24 @@ def test_configuration_sections(configuration: Configuration) -> None:
def test_archives_remove(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,
pacman: Pacman, mocker: MockerFixture) -> None:
repository: Repository, mocker: MockerFixture) -> None:
"""
must remove older packages
"""
def package(version: Any, *args: Any, **kwargs: Any) -> Package:
generated = replace(package_ahriman, version=str(version))
packages = []
for i in range(5):
generated = replace(package_ahriman, version=str(i))
generated.packages = {
key: replace(value, filename=str(version))
key: replace(value, filename=str(i))
for key, value in generated.packages.items()
}
return generated
packages.append(generated)
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("ahriman.core.housekeeping.archive_rotation_trigger.package_like", return_value=True)
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=packages)
mocker.patch("pathlib.Path.glob", return_value=[Path(str(i)) for i in range(5)])
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)
unlink_mock = mocker.patch("pathlib.Path.unlink", autospec=True)
archive_rotation_trigger.archives_remove(package_ahriman, pacman)
archive_rotation_trigger.archives_remove(package_ahriman, repository)
unlink_mock.assert_has_calls([
MockCall(Path("0")),
MockCall(Path("1")),
@@ -48,28 +45,15 @@ def test_archives_remove(archive_rotation_trigger: ArchiveRotationTrigger, packa
def test_archives_remove_keep(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,
pacman: Pacman, mocker: MockerFixture) -> None:
repository: Repository, mocker: MockerFixture) -> None:
"""
must keep all packages if set to
"""
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("pathlib.Path.is_dir", return_value=True)
mocker.patch("ahriman.core.housekeeping.archive_rotation_trigger.package_like", return_value=True)
mocker.patch("pathlib.Path.glob", return_value=[Path(str(i)) for i in range(5)])
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)
unlink_mock = mocker.patch("pathlib.Path.unlink", autospec=True)
archives_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives")
archive_rotation_trigger.keep_built_packages = 0
archive_rotation_trigger.archives_remove(package_ahriman, pacman)
unlink_mock.assert_not_called()
archive_rotation_trigger.archives_remove(package_ahriman, repository)
archives_mock.assert_not_called()
def test_on_result(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,

View File

@@ -118,7 +118,7 @@ def test_package_build(executor: Executor, package_ahriman: Package, mocker: Moc
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)
lookup_mock.assert_called_once_with(package_ahriman)
with_packages_mock.assert_called_once_with([Path(package_ahriman.base)], executor.pacman)
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

@@ -91,6 +91,30 @@ def test_load_archives_different_version(package_info: PackageInfo, package_pyth
assert packages[0].version == package_python_schedule.version
def test_package_archives(package_info: PackageInfo, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
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)
result = package_info.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_changes(package_info: PackageInfo, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must load package changes

View File

@@ -89,22 +89,6 @@ def pkgbuild_ahriman(resource_path_root: Path) -> Pkgbuild:
return Pkgbuild.from_file(pkgbuild)
@pytest.fixture
def pyalpm_handle(pyalpm_package_ahriman: MagicMock) -> MagicMock:
"""
mock object for pyalpm
Args:
pyalpm_package_ahriman(MagicMock): mock object for pyalpm package
Returns:
MagicMock: pyalpm mock
"""
mock = MagicMock()
mock.handle.load_pkg.return_value = pyalpm_package_ahriman
return mock
@pytest.fixture
def pyalpm_package_description_ahriman(package_description_ahriman: PackageDescription) -> MagicMock:
"""

View File

@@ -148,13 +148,14 @@ def test_packages_full(package_ahriman: Package) -> None:
assert package_ahriman.packages_full == [package_ahriman.base, f"{package_ahriman.base}-git"]
def test_from_archive(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
def test_from_archive(package_ahriman: Package, pyalpm_package_ahriman: MagicMock, mocker: MockerFixture) -> None:
"""
must construct package from alpm library
"""
mocker.patch("ahriman.models.pacman_handle.PacmanHandle.package_load", return_value=pyalpm_package_ahriman)
mocker.patch("ahriman.models.package_description.PackageDescription.from_package",
return_value=package_ahriman.packages[package_ahriman.base])
generated = Package.from_archive(Path("path"), pyalpm_handle)
generated = Package.from_archive(Path("path"))
generated.remote = package_ahriman.remote
assert generated == package_ahriman
@@ -165,13 +166,12 @@ def test_from_archive_empty_base(package_ahriman: Package, pyalpm_package_ahrima
"""
must construct package with empty base from alpm library
"""
pyalpm_handle = MagicMock()
type(pyalpm_package_ahriman).base = PropertyMock(return_value=None)
pyalpm_handle.handle.load_pkg.return_value = pyalpm_package_ahriman
mocker.patch("ahriman.models.pacman_handle.PacmanHandle.package_load", return_value=pyalpm_package_ahriman)
mocker.patch("ahriman.models.package_description.PackageDescription.from_package",
return_value=package_ahriman.packages[package_ahriman.base])
generated = Package.from_archive(Path("path"), pyalpm_handle)
generated = Package.from_archive(Path("path"))
generated.remote = package_ahriman.remote
assert generated == package_ahriman
@@ -362,7 +362,7 @@ def test_vercmp(package_ahriman: Package, mocker: MockerFixture) -> None:
vercmp_mock.assert_called_once_with(package_ahriman.version, "version")
def test_with_packages(package_ahriman: Package, package_python_schedule: Package, pacman: Pacman,
def test_with_packages(package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must correctly replace packages descriptions
@@ -375,8 +375,8 @@ def test_with_packages(package_ahriman: Package, package_python_schedule: Packag
result = copy.deepcopy(package_ahriman)
package_ahriman.packages[package_ahriman.base].architecture = "i686"
result.with_packages(paths, pacman)
result.with_packages(paths)
from_archive_mock.assert_has_calls([
MockCall(path, pacman) for path in paths
MockCall(path) for path in paths
])
assert result.packages[result.base] == package_ahriman.packages[package_ahriman.base]

View File

@@ -0,0 +1,37 @@
import pytest
from pathlib import Path
from unittest.mock import MagicMock
from ahriman.models.pacman_handle import PacmanHandle
def test_package_load() -> None:
"""
must load package from archive path
"""
local = Path("local")
instance = PacmanHandle.ephemeral()
handle_mock = instance.handle = MagicMock()
instance.package_load(local)
handle_mock.load_pkg.assert_called_once_with(str(local))
PacmanHandle._ephemeral = None
def test_getattr() -> None:
"""
must proxy attribute access to underlying handle
"""
instance = PacmanHandle.ephemeral()
assert instance.dbpath
def test_getattr_not_found() -> None:
"""
must raise AttributeError for missing handle attributes
"""
instance = PacmanHandle.ephemeral()
with pytest.raises(AttributeError):
assert instance.random_attribute