feat: archive package tree implementation (#153)

* store built packages in archive tree instead of repository

* write tests to support new changes

* implement atomic_move method, move files only with lock

* use generic packages tree for all repos

* lookup through archive packages before build

* add archive trigger

* add archive trigger

* regenerate docs

* gpg loader fix

* support requires repostory flag

* drop excess REQUIRES_REPOSITORY

* simplify symlionk creation

* remove generators

* fix sttyle

* add separate function for symlinks creation

* fix rebase

* add note about slicing

* smol refactoring of archive_tree class

* remove duplicate code

* fix typos

* few review fixes

* monor fixes and typos

* clean empty directories

* remove side effect from getter

* drop recursive remove

* ensure_exists now accepts only argument

* add package like guard to symlinks fix

* speedup archive_lookup processing by iterrupting cycle

* remove custom filelock

* fix naming

* remove remove flag from repo

* review fixes

* restore wrapper around filelock

* extract repository explorer to separate class

* docs update

* fix ide findings
This commit is contained in:
2026-02-16 00:12:51 +02:00
committed by GitHub
parent 6a2454548d
commit 2d6d42f969
71 changed files with 1876 additions and 363 deletions

View File

@@ -145,63 +145,11 @@ def test_repositories_extract(args: argparse.Namespace, configuration: Configura
args.configuration = configuration.path
args.repository = "repo"
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
extract_mock = mocker.patch("ahriman.core.repository.Explorer.repositories_extract",
return_value=[RepositoryId("arch", "repo")])
assert Handler.repositories_extract(args) == [RepositoryId("arch", "repo")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_not_called()
def test_repositories_extract_repository(args: argparse.Namespace, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must generate list of available repositories based on flags and tree
"""
args.architecture = "arch"
args.configuration = configuration.path
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
return_value={"repo"})
assert Handler.repositories_extract(args) == [RepositoryId("arch", "repo")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_called_once_with(configuration.repository_paths.root)
def test_repositories_extract_repository_legacy(args: argparse.Namespace, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must generate list of available repositories based on flags and tree (legacy mode)
"""
args.architecture = "arch"
args.configuration = configuration.path
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
return_value=set())
assert Handler.repositories_extract(args) == [RepositoryId("arch", "aur")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_called_once_with(configuration.repository_paths.root)
def test_repositories_extract_architecture(args: argparse.Namespace, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must read repository name from config
"""
args.configuration = configuration.path
args.repository = "repo"
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures",
return_value={"arch"})
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
assert Handler.repositories_extract(args) == [RepositoryId("arch", "repo")]
known_architectures_mock.assert_called_once_with(configuration.repository_paths.root, "repo")
known_repositories_mock.assert_not_called()
extract_mock.assert_called_once_with(pytest.helpers.anyvar(Configuration, True), args.repository, args.architecture)
def test_repositories_extract_empty(args: argparse.Namespace, configuration: Configuration,
@@ -212,8 +160,7 @@ def test_repositories_extract_empty(args: argparse.Namespace, configuration: Con
args.command = "config"
args.configuration = configuration.path
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures", return_value=set())
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories", return_value=set())
mocker.patch("ahriman.core.repository.Explorer.repositories_extract", return_value=[])
with pytest.raises(MissingArchitectureError):
Handler.repositories_extract(args)
@@ -227,12 +174,11 @@ def test_repositories_extract_systemd(args: argparse.Namespace, configuration: C
args.configuration = configuration.path
args.repository_id = "i686/some/repo/name"
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
extract_mock = mocker.patch("ahriman.core.repository.Explorer.repositories_extract",
return_value=[RepositoryId("i686", "some-repo-name")])
assert Handler.repositories_extract(args) == [RepositoryId("i686", "some-repo-name")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_not_called()
extract_mock.assert_called_once_with(pytest.helpers.anyvar(Configuration, True), "some-repo-name", "i686")
def test_repositories_extract_systemd_with_dash(args: argparse.Namespace, configuration: Configuration,
@@ -243,12 +189,11 @@ def test_repositories_extract_systemd_with_dash(args: argparse.Namespace, config
args.configuration = configuration.path
args.repository_id = "i686-some-repo-name"
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
extract_mock = mocker.patch("ahriman.core.repository.Explorer.repositories_extract",
return_value=[RepositoryId("i686", "some-repo-name")])
assert Handler.repositories_extract(args) == [RepositoryId("i686", "some-repo-name")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_not_called()
extract_mock.assert_called_once_with(pytest.helpers.anyvar(Configuration, True), "some-repo-name", "i686")
def test_repositories_extract_systemd_legacy(args: argparse.Namespace, configuration: Configuration,
@@ -259,10 +204,8 @@ def test_repositories_extract_systemd_legacy(args: argparse.Namespace, configura
args.configuration = configuration.path
args.repository_id = "i686"
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
return_value=set())
extract_mock = mocker.patch("ahriman.core.repository.Explorer.repositories_extract",
return_value=[RepositoryId("i686", "aur")])
assert Handler.repositories_extract(args) == [RepositoryId("i686", "aur")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_called_once_with(configuration.repository_paths.root)
extract_mock.assert_called_once_with(pytest.helpers.anyvar(Configuration, True), None, "i686")

View File

@@ -6,6 +6,7 @@ from unittest.mock import call as MockCall
from ahriman.application.handlers.tree_migrate import TreeMigrate
from ahriman.core.configuration import Configuration
from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
@@ -16,6 +17,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
"""
tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.application.handlers.tree_migrate.TreeMigrate.tree_move")
symlinks_mock = mocker.patch("ahriman.application.handlers.tree_migrate.TreeMigrate.symlinks_fix")
_, repository_id = configuration.check_loaded()
old_paths = configuration.repository_paths
new_paths = RepositoryPaths(old_paths.root, old_paths.repository_id, _force_current_tree=True)
@@ -23,6 +25,36 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
TreeMigrate.run(args, repository_id, configuration, report=False)
tree_create_mock.assert_called_once_with()
application_mock.assert_called_once_with(old_paths, new_paths)
symlinks_mock.assert_called_once_with(new_paths)
def test_symlinks_fix(repository_paths: RepositoryPaths, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must replace symlinks during migration
"""
mocker.patch("ahriman.application.handlers.tree_migrate.walk", side_effect=[
[
repository_paths.archive_for(package_ahriman.base) / "file",
repository_paths.archive_for(package_ahriman.base) / "symlink",
],
[
repository_paths.repository / "file",
repository_paths.repository / "symlink",
],
])
mocker.patch("pathlib.Path.exists", autospec=True, side_effect=lambda p: p.name == "file")
unlink_mock = mocker.patch("pathlib.Path.unlink")
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
TreeMigrate.symlinks_fix(repository_paths)
unlink_mock.assert_called_once_with()
symlink_mock.assert_called_once_with(
Path("..") /
".." /
".." /
repository_paths.archive_for(package_ahriman.base).relative_to(repository_paths.root) /
"symlink"
)
def test_move_tree(mocker: MockerFixture) -> None:

View File

@@ -79,7 +79,7 @@ def test_run_repo_specific_triggers(args: argparse.Namespace, configuration: Con
_, repository_id = configuration.check_loaded()
# remove unused sections
for section in ("customs3", "github:x86_64", "logs-rotation", "mirrorlist"):
for section in ("archive", "customs3", "github:x86_64", "logs-rotation", "mirrorlist"):
configuration.remove_section(section)
configuration.set_option("report", "target", "test")

View File

@@ -1,8 +1,10 @@
import datetime
import pytest
from dataclasses import replace
from pathlib import Path
from pytest_mock import MockerFixture
from sqlite3 import Cursor
from typing import Any, TypeVar
from unittest.mock import MagicMock, PropertyMock
@@ -11,12 +13,14 @@ from ahriman.core.alpm.remote import AUR
from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.database.migrations import Migrations
from ahriman.core.repository import Repository
from ahriman.core.spawn import Spawn
from ahriman.core.status import Client
from ahriman.core.status.watcher import Watcher
from ahriman.models.aur_package import AURPackage
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.migration import Migration
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource
@@ -48,7 +52,9 @@ def anyvar(cls: type[T], strict: bool = False) -> T:
T: any wrapper
"""
class AnyVar(cls):
"""any value wrapper"""
"""
any value wrapper
"""
def __eq__(self, other: Any) -> bool:
"""
@@ -271,16 +277,23 @@ def configuration(repository_id: RepositoryId, tmp_path: Path, resource_path_roo
@pytest.fixture
def database(configuration: Configuration) -> SQLite:
def database(configuration: Configuration, mocker: MockerFixture) -> SQLite:
"""
database fixture
Args:
configuration(Configuration): configuration fixture
mocker(MockerFixture): mocker object
Returns:
SQLite: database test instance
"""
original_method = Migrations.perform_migration
def perform_migration(self: Migrations, cursor: Cursor, migration: Migration) -> None:
original_method(self, cursor, replace(migration, migrate_data=lambda *args: None))
mocker.patch.object(Migrations, "perform_migration", autospec=True, side_effect=perform_migration)
return SQLite.load(configuration)

View File

@@ -4,6 +4,16 @@ from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.alpm.repo import Repo
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
def test_root(repository_paths: RepositoryPaths) -> None:
"""
must correctly define repository root
"""
assert Repo(repository_paths.repository_id.name, repository_paths, []).root == repository_paths.repository
assert Repo(repository_paths.repository_id.name, repository_paths, [], Path("path")).root == Path("path")
def test_repo_path(repo: Repo) -> None:
@@ -22,6 +32,7 @@ def test_repo_add(repo: Repo, mocker: MockerFixture) -> None:
repo.add(Path("path"))
check_output_mock.assert_called_once() # it will be checked later
assert check_output_mock.call_args[0][0] == "repo-add"
assert "--remove" in check_output_mock.call_args[0]
def test_repo_init(repo: Repo, mocker: MockerFixture) -> None:
@@ -35,21 +46,23 @@ def test_repo_init(repo: Repo, mocker: MockerFixture) -> None:
assert check_output_mock.call_args[0][0] == "repo-add"
def test_repo_remove(repo: Repo, mocker: MockerFixture) -> None:
def test_repo_remove(repo: Repo, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must call repo-remove on package addition
must call repo-remove on package removal
"""
filepath = package_ahriman.packages[package_ahriman.base].filepath
mocker.patch("pathlib.Path.glob", return_value=[])
check_output_mock = mocker.patch("ahriman.core.alpm.repo.check_output")
repo.remove("package", Path("package.pkg.tar.xz"))
repo.remove(package_ahriman.base, filepath)
check_output_mock.assert_called_once() # it will be checked later
assert check_output_mock.call_args[0][0] == "repo-remove"
assert package_ahriman.base in check_output_mock.call_args[0]
def test_repo_remove_fail_no_file(repo: Repo, mocker: MockerFixture) -> None:
"""
must fail on missing file
must fail removal on missing file
"""
mocker.patch("pathlib.Path.glob", return_value=[Path("package.pkg.tar.xz")])
mocker.patch("pathlib.Path.unlink", side_effect=FileNotFoundError)

View File

@@ -0,0 +1,34 @@
import pytest
from ahriman.core.archive import ArchiveTrigger
from ahriman.core.archive.archive_tree import ArchiveTree
from ahriman.core.configuration import Configuration
@pytest.fixture
def archive_tree(configuration: Configuration) -> ArchiveTree:
"""
archive tree fixture
Args:
configuration(Configuration): configuration fixture
Returns:
ArchiveTree: archive tree test instance
"""
return ArchiveTree(configuration.repository_paths, [])
@pytest.fixture
def archive_trigger(configuration: Configuration) -> ArchiveTrigger:
"""
archive trigger fixture
Args:
configuration(Configuration): configuration fixture
Returns:
ArchiveTrigger: archive trigger test instance
"""
_, repository_id = configuration.check_loaded()
return ArchiveTrigger(repository_id, configuration)

View File

@@ -0,0 +1,176 @@
from pathlib import Path
from pytest_mock import MockerFixture
from unittest.mock import call as MockCall
from ahriman.core.archive.archive_tree import ArchiveTree
from ahriman.core.utils import utcnow
from ahriman.models.package import Package
def test_repo(archive_tree: ArchiveTree) -> None:
"""
must return correct repository object
"""
local = Path("local")
repo = archive_tree._repo(local)
assert repo.sign_args == archive_tree.sign_args
assert repo.name == archive_tree.repository_id.name
assert repo.root == local
def test_repository_for(archive_tree: ArchiveTree) -> None:
"""
must correctly generate path to repository
"""
path = archive_tree.repository_for()
assert path.is_relative_to(archive_tree.paths.archive / "repos")
assert (archive_tree.repository_id.name, archive_tree.repository_id.architecture) == path.parts[-2:]
assert set(map("{:02d}".format, utcnow().timetuple()[:3])).issubset(path.parts)
def test_directories_fix(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
"""
must remove empty directories recursively
"""
root = archive_tree.paths.archive / "repos"
(root / "a" / "b").mkdir(parents=True, exist_ok=True)
(root / "a" / "b" / "file").touch()
(root / "a" / "b" / "c" / "d").mkdir(parents=True, exist_ok=True)
_original_rmdir = Path.rmdir
rmdir_mock = mocker.patch("pathlib.Path.rmdir", autospec=True, side_effect=_original_rmdir)
archive_tree.directories_fix({Path("a") / "b" / "c" / "d"})
rmdir_mock.assert_has_calls([
MockCall(root / "a" / "b" / "c" / "d"),
MockCall(root / "a" / "b" / "c"),
])
def test_symlinks_create(archive_tree: ArchiveTree, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must create symlinks
"""
_original_exists = Path.exists
symlinks_mock = mocker.patch("pathlib.Path.symlink_to", side_effect=(None, FileExistsError, FileExistsError))
add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
mocker.patch("pathlib.Path.glob", autospec=True, side_effect=lambda path, name: [path / name[:-1]])
archive_tree.symlinks_create([package_ahriman, package_python_schedule])
symlinks_mock.assert_has_calls([
MockCall(Path("..") /
".." /
".." /
".." /
".." /
".." /
archive_tree.paths.archive_for(package.base)
.relative_to(archive_tree.paths.root)
.relative_to("archive") /
single.filename
)
for package in (package_ahriman, package_python_schedule)
for single in package.packages.values()
])
add_mock.assert_called_once_with(
archive_tree.repository_for() / package_ahriman.packages[package_ahriman.base].filename
)
def test_symlinks_create_empty_filename(archive_tree: ArchiveTree, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must skip symlinks creation if filename is not set
"""
package_ahriman.packages[package_ahriman.base].filename = None
symlinks_mock = mocker.patch("pathlib.Path.symlink_to")
archive_tree.symlinks_create([package_ahriman])
symlinks_mock.assert_not_called()
def test_symlinks_fix(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
"""
must fix broken symlinks
"""
_original_exists = Path.exists
def exists_mock(path: Path) -> bool:
if path.name.startswith("symlink"):
return True
return _original_exists(path)
mocker.patch("pathlib.Path.is_symlink", side_effect=[True, True, False])
mocker.patch("pathlib.Path.exists", autospec=True, side_effect=exists_mock)
walk_mock = mocker.patch("ahriman.core.archive.archive_tree.walk", return_value=[
archive_tree.repository_for() / filename
for filename in (
"symlink-1.0.0-1-x86_64.pkg.tar.zst",
"symlink-1.0.0-1-x86_64.pkg.tar.zst.sig",
"broken_symlink-1.0.0-1-x86_64.pkg.tar.zst",
"file-1.0.0-1-x86_64.pkg.tar.zst",
)
])
remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
assert list(archive_tree.symlinks_fix()) == [
archive_tree.repository_for().relative_to(archive_tree.paths.archive / "repos"),
]
walk_mock.assert_called_once_with(archive_tree.paths.archive / "repos")
remove_mock.assert_called_once_with(
"broken_symlink", archive_tree.repository_for() / "broken_symlink-1.0.0-1-x86_64.pkg.tar.zst")
def test_symlinks_fix_foreign_repository(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
"""
must skip symlinks check if repository name or architecture doesn't match
"""
_original_exists = Path.exists
def exists_mock(path: Path) -> bool:
if path.name.startswith("symlink"):
return True
return _original_exists(path)
mocker.patch("pathlib.Path.is_symlink", side_effect=[True, True, False])
mocker.patch("pathlib.Path.exists", autospec=True, side_effect=exists_mock)
mocker.patch("ahriman.core.archive.archive_tree.walk", return_value=[
archive_tree.repository_for().with_name("i686") / filename
for filename in (
"symlink-1.0.0-1-x86_64.pkg.tar.zst",
"broken_symlink-1.0.0-1-x86_64.pkg.tar.zst",
"file-1.0.0-1-x86_64.pkg.tar.zst",
)
])
remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
assert list(archive_tree.symlinks_fix()) == []
remove_mock.assert_not_called()
def test_tree_create(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
"""
must create repository root if not exists
"""
owner_guard_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner")
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init")
archive_tree.tree_create()
owner_guard_mock.assert_called_once_with()
mkdir_mock.assert_called_once_with(0o755, parents=True)
init_mock.assert_called_once_with()
def test_tree_create_exists(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
"""
must skip directory creation if already exists
"""
mocker.patch("pathlib.Path.exists", return_value=True)
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
archive_tree.tree_create()
mkdir_mock.assert_not_called()

View File

@@ -0,0 +1,37 @@
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.archive import ArchiveTrigger
from ahriman.models.package import Package
from ahriman.models.result import Result
def test_on_result(archive_trigger: ArchiveTrigger, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must create symlinks for actual repository
"""
symlinks_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.symlinks_create")
archive_trigger.on_result(Result(), [package_ahriman])
symlinks_mock.assert_called_once_with([package_ahriman])
def test_on_start(archive_trigger: ArchiveTrigger, mocker: MockerFixture) -> None:
"""
must create repository tree on load
"""
tree_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.tree_create")
archive_trigger.on_start()
tree_mock.assert_called_once_with()
def test_on_stop(archive_trigger: ArchiveTrigger, mocker: MockerFixture) -> None:
"""
must fix broken symlinks on stop
"""
local = Path("local")
symlinks_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.symlinks_fix", return_value=[local])
directories_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.directories_fix")
archive_trigger.on_stop()
symlinks_mock.assert_called_once_with()
directories_mock.assert_called_once_with({local})

View File

@@ -0,0 +1,78 @@
import pytest
from dataclasses import replace
from pathlib import Path
from pytest_mock import MockerFixture
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
from ahriman.models.repository_paths import RepositoryPaths
def test_migrate_data(connection: Connection, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must perform data migration
"""
_, repository_id = configuration.check_loaded()
repositories = [
repository_id,
replace(repository_id, architecture="i686"),
]
mocker.patch("ahriman.core.repository.Explorer.repositories_extract", return_value=repositories)
migration_mock = mocker.patch("ahriman.core.database.migrations.m016_archive.move_packages")
migrate_data(connection, configuration)
migration_mock.assert_has_calls([
MockCall(replace(configuration.repository_paths, repository_id=repository), pytest.helpers.anyvar(int))
for repository in repositories
])
def test_move_packages(repository_paths: RepositoryPaths, pacman: Pacman, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must move packages to the archive directory
"""
def is_file(self: Path, *args: Any, **kwargs: Any) -> bool:
return "file" in self.name
mocker.patch("pathlib.Path.iterdir", return_value=[
repository_paths.repository / ".hidden-file.pkg.tar.xz",
repository_paths.repository / "directory",
repository_paths.repository / "file.pkg.tar.xz",
repository_paths.repository / "file.pkg.tar.xz.sig",
repository_paths.repository / "file2.pkg.tar.xz",
repository_paths.repository / "symlink.pkg.tar.xz",
])
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=is_file)
mocker.patch("pathlib.Path.exists", return_value=True)
archive_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
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)
archive_mock.assert_has_calls([
MockCall(repository_paths.repository / filename, pacman)
for filename in ("file.pkg.tar.xz", "file2.pkg.tar.xz")
])
move_mock.assert_has_calls([
MockCall(repository_paths.repository / filename, repository_paths.archive_for(package_ahriman.base) / filename)
for filename in ("file.pkg.tar.xz", "file.pkg.tar.xz.sig", "file2.pkg.tar.xz")
])
symlink_mock.assert_has_calls([
MockCall(
Path("..") /
".." /
".." /
repository_paths.archive_for(package_ahriman.base).relative_to(repository_paths.root) /
filename
)
for filename in ("file.pkg.tar.xz", "file.pkg.tar.xz.sig", "file2.pkg.tar.xz")
])

View File

@@ -16,7 +16,7 @@ def test_load(configuration: Configuration, mocker: MockerFixture) -> None:
init_mock.assert_called_once_with()
def test_init(database: SQLite, configuration: Configuration, mocker: MockerFixture) -> None:
def test_init(database: SQLite, mocker: MockerFixture) -> None:
"""
must run migrations on init
"""

View File

@@ -1,13 +1,28 @@
import pytest
from ahriman.core.configuration import Configuration
from ahriman.core.housekeeping import LogsRotationTrigger
from ahriman.core.housekeeping import ArchiveRotationTrigger, LogsRotationTrigger
@pytest.fixture
def archive_rotation_trigger(configuration: Configuration) -> ArchiveRotationTrigger:
"""
archive rotation trigger fixture
Args:
configuration(Configuration): configuration fixture
Returns:
ArchiveRotationTrigger: archive rotation trigger test instance
"""
_, repository_id = configuration.check_loaded()
return ArchiveRotationTrigger(repository_id, configuration)
@pytest.fixture
def logs_rotation_trigger(configuration: Configuration) -> LogsRotationTrigger:
"""
logs roration trigger fixture
logs rotation trigger fixture
Args:
configuration(Configuration): configuration fixture

View File

@@ -0,0 +1,83 @@
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.models.package import Package
from ahriman.models.result import Result
def test_configuration_sections(configuration: Configuration) -> None:
"""
must correctly parse target list
"""
assert ArchiveRotationTrigger.configuration_sections(configuration) == ["archive"]
def test_archives_remove(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,
pacman: Pacman, mocker: MockerFixture) -> None:
"""
must remove older packages
"""
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)
archive_rotation_trigger.archives_remove(package_ahriman, pacman)
unlink_mock.assert_has_calls([
MockCall(Path("0")),
MockCall(Path("1")),
])
def test_archives_remove_keep(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,
pacman: Pacman, 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)
archive_rotation_trigger.keep_built_packages = 0
archive_rotation_trigger.archives_remove(package_ahriman, pacman)
unlink_mock.assert_not_called()
def test_on_result(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,
package_python_schedule: Package, mocker: MockerFixture) -> None:
"""
must rotate archives
"""
mocker.patch("ahriman.core._Context.get")
remove_mock = mocker.patch("ahriman.core.housekeeping.ArchiveRotationTrigger.archives_remove")
archive_rotation_trigger.on_result(Result(added=[package_ahriman], failed=[package_python_schedule]), [])
remove_mock.assert_called_once_with(package_ahriman, pytest.helpers.anyvar(int))

View File

@@ -7,13 +7,6 @@ from ahriman.core.status import Client
from ahriman.models.result import Result
def test_requires_repository() -> None:
"""
must require repository identifier to be set to start
"""
assert LogsRotationTrigger.REQUIRES_REPOSITORY
def test_configuration_sections(configuration: Configuration) -> None:
"""
must correctly parse target list
@@ -21,7 +14,7 @@ def test_configuration_sections(configuration: Configuration) -> None:
assert LogsRotationTrigger.configuration_sections(configuration) == ["logs-rotation"]
def test_rotate(logs_rotation_trigger: LogsRotationTrigger, mocker: MockerFixture) -> None:
def test_on_result(logs_rotation_trigger: LogsRotationTrigger, mocker: MockerFixture) -> None:
"""
must rotate logs
"""

View File

@@ -2,7 +2,6 @@ import logging
import pytest
from pytest_mock import MockerFixture
from unittest.mock import call as MockCall
from ahriman.core.alpm.repo import Repo
from ahriman.core.build_tools.task import Task

View File

@@ -5,13 +5,6 @@ from ahriman.core.report import ReportTrigger
from ahriman.models.result import Result
def test_requires_repository() -> None:
"""
must require repository identifier to be set to start
"""
assert ReportTrigger.REQUIRES_REPOSITORY
def test_configuration_sections(configuration: Configuration) -> None:
"""
must correctly parse target list

View File

@@ -1,5 +1,6 @@
import pytest
from dataclasses import replace
from pathlib import Path
from pytest_mock import MockerFixture
from typing import Any
@@ -13,34 +14,223 @@ from ahriman.models.packagers import Packagers
from ahriman.models.user import User
def test_archive_lookup(executor: Executor, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must existing packages which match the version
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.iterdir", return_value=[
Path("1.pkg.tar.zst"),
Path("2.pkg.tar.zst"),
Path("3.pkg.tar.zst"),
])
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=[
package_ahriman,
package_python_schedule,
replace(package_ahriman, version="1"),
])
glob_mock = mocker.patch("pathlib.Path.glob", return_value=[Path("1.pkg.tar.xz")])
assert list(executor._archive_lookup(package_ahriman)) == [Path("1.pkg.tar.xz")]
glob_mock.assert_called_once_with(f"{package_ahriman.packages[package_ahriman.base].filename}*")
def test_archive_lookup_version_mismatch(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return nothing if no packages found with the same version
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.iterdir", return_value=[
Path("1.pkg.tar.zst"),
])
mocker.patch("ahriman.models.package.Package.from_archive", return_value=replace(package_ahriman, version="1"))
assert list(executor._archive_lookup(package_ahriman)) == []
def test_archive_lookup_architecture_mismatch(executor: Executor, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must return nothing if architecture doesn't match
"""
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")
mocker.patch("pathlib.Path.iterdir", return_value=[
Path("1.pkg.tar.zst"),
])
mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
assert list(executor._archive_lookup(package_ahriman)) == []
def test_archive_lookup_no_archive_directory(
executor: Executor,
package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must return nothing if no archive directory found
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
assert list(executor._archive_lookup(package_ahriman)) == []
def test_archive_rename(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must correctly remove package archive
"""
path = "gconf-3.2.6+11+g07808097-10-x86_64.pkg.tar.zst"
safe_path = "gconf-3.2.6-11-g07808097-10-x86_64.pkg.tar.zst"
package_ahriman.packages[package_ahriman.base].filename = path
rename_mock = mocker.patch("ahriman.core.repository.executor.atomic_move")
executor._archive_rename(package_ahriman.packages[package_ahriman.base], package_ahriman.base)
rename_mock.assert_called_once_with(executor.paths.packages / path, executor.paths.packages / safe_path)
assert package_ahriman.packages[package_ahriman.base].filename == safe_path
def test_archive_rename_empty_filename(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must skip renaming if filename is not set
"""
package_ahriman.packages[package_ahriman.base].filename = None
rename_mock = mocker.patch("ahriman.core.repository.executor.atomic_move")
executor._archive_rename(package_ahriman.packages[package_ahriman.base], package_ahriman.base)
rename_mock.assert_not_called()
def test_package_build(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must build single package
"""
mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)])
status_client_mock = mocker.patch("ahriman.core.status.Client.set_building")
init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init", return_value="sha")
package_mock = mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman)
lookup_mock = mocker.patch("ahriman.core.repository.executor.Executor._archive_lookup", return_value=[])
with_packages_mock = mocker.patch("ahriman.models.package.Package.with_packages")
rename_mock = mocker.patch("ahriman.core.repository.executor.atomic_move")
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)
lookup_mock.assert_called_once_with(package_ahriman)
with_packages_mock.assert_called_once_with([Path(package_ahriman.base)], executor.pacman)
rename_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base)
def test_package_build_copy(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must copy package from archive if there are already built ones
"""
path = package_ahriman.packages[package_ahriman.base].filepath
mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)])
mocker.patch("ahriman.core.build_tools.task.Task.init")
mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman)
mocker.patch("ahriman.core.repository.executor.Executor._archive_lookup", return_value=[path])
mocker.patch("ahriman.core.repository.executor.atomic_move")
mocker.patch("ahriman.models.package.Package.with_packages")
copy_mock = mocker.patch("shutil.copy")
rename_mock = mocker.patch("ahriman.core.repository.executor.atomic_move")
executor._package_build(package_ahriman, Path("local"), "packager", None)
copy_mock.assert_called_once_with(path, Path("local"))
rename_mock.assert_called_once_with(Path("local") / path, executor.paths.packages / path)
def test_package_remove(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run remove for packages
"""
repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
executor._package_remove(package_ahriman.base, package_ahriman.packages[package_ahriman.base].filepath)
repo_remove_mock.assert_called_once_with(
package_ahriman.base, package_ahriman.packages[package_ahriman.base].filepath)
def test_package_remove_failed(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress errors during archive removal
"""
mocker.patch("ahriman.core.alpm.repo.Repo.remove", side_effect=Exception)
executor._package_remove(package_ahriman.base, package_ahriman.packages[package_ahriman.base].filepath)
def test_package_remove_base(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run remove base from status client
"""
status_client_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_remove")
executor._package_remove_base(package_ahriman.base)
status_client_mock.assert_called_once_with(package_ahriman.base)
def test_package_remove_base_failed(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress errors during base removal
"""
mocker.patch("ahriman.core.status.local_client.LocalClient.package_remove", side_effect=Exception)
executor._package_remove_base(package_ahriman.base)
def test_package_update(executor: Executor, package_ahriman: Package, user: User, mocker: MockerFixture) -> None:
"""
must update built package in repository
"""
rename_mock = mocker.patch("ahriman.core.repository.executor.atomic_move")
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
sign_package_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_package", side_effect=lambda fn, _: [fn])
filepath = next(package.filepath for package in package_ahriman.packages.values())
executor._package_update(filepath, package_ahriman.base, user.key)
# must move files (once)
rename_mock.assert_called_once_with(
executor.paths.packages / filepath, executor.paths.archive_for(package_ahriman.base) / filepath)
# must sign package
sign_package_mock.assert_called_once_with(executor.paths.packages / filepath, user.key)
# symlink to the archive
symlink_mock.assert_called_once_with(
Path("..") /
".." /
".." /
executor.paths.archive_for(package_ahriman.base).relative_to(executor.paths.root) /
filepath)
# must add package
repo_add_mock.assert_called_once_with(executor.paths.repository / filepath)
def test_package_update_empty_filename(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must skip update for package which does not have path
"""
rename_mock = mocker.patch("ahriman.core.repository.executor.atomic_move")
executor._package_update(None, package_ahriman.base, None)
rename_mock.assert_not_called()
def test_process_build(executor: Executor, package_ahriman: Package, passwd: Any, mocker: MockerFixture) -> None:
"""
must run build process
"""
mocker.patch("ahriman.models.repository_paths.getpwuid", return_value=passwd)
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)])
init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init", return_value="sha")
move_mock = mocker.patch("shutil.move")
status_client_mock = mocker.patch("ahriman.core.status.Client.set_building")
changes_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get",
return_value=Changes("commit", "change"))
commit_sha_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
depends_on_mock = mocker.patch("ahriman.core.build_tools.package_archive.PackageArchive.depends_on",
return_value=Dependencies())
dependencies_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_dependencies_update")
with_packages_mock = mocker.patch("ahriman.models.package.Package.with_packages")
build_mock = mocker.patch("ahriman.core.repository.executor.Executor._package_build", return_value="sha")
executor.process_build([package_ahriman], Packagers("packager"), bump_pkgrel=False)
init_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), None)
with_packages_mock.assert_called_once_with([Path(package_ahriman.base)], executor.pacman)
changes_mock.assert_called_once_with(package_ahriman.base)
build_mock.assert_called_once_with(package_ahriman, pytest.helpers.anyvar(Path, strict=True), None, None)
depends_on_mock.assert_called_once_with()
dependencies_mock.assert_called_once_with(package_ahriman.base, Dependencies())
# must move files (once)
move_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base)
# must update status
status_client_mock.assert_called_once_with(package_ahriman.base)
commit_sha_mock.assert_called_once_with(package_ahriman.base, Changes("sha", "change"))
@@ -50,7 +240,7 @@ def test_process_build_bump_pkgrel(executor: Executor, package_ahriman: Package,
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)])
mocker.patch("shutil.move")
mocker.patch("ahriman.core.repository.executor.atomic_move")
init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init")
executor.process_build([package_ahriman], Packagers("packager"), bump_pkgrel=True)
@@ -67,7 +257,7 @@ def test_process_build_failure(executor: Executor, package_ahriman: Package, moc
mocker.patch("ahriman.core.repository.executor.Executor.packages_built")
mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)])
mocker.patch("ahriman.core.build_tools.task.Task.init")
mocker.patch("shutil.move", side_effect=Exception)
mocker.patch("ahriman.core.repository.executor.atomic_move", side_effect=Exception)
status_client_mock = mocker.patch("ahriman.core.status.Client.set_failed")
executor.process_build([package_ahriman])
@@ -79,15 +269,15 @@ def test_process_remove_base(executor: Executor, package_ahriman: Package, mocke
must run remove process for whole base
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
status_client_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_remove")
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor._package_remove")
base_remove_mock = mocker.patch("ahriman.core.repository.executor.Executor._package_remove_base")
executor.process_remove([package_ahriman.base])
# must remove via alpm wrapper
repo_remove_mock.assert_called_once_with(
remove_mock.assert_called_once_with(
package_ahriman.base, package_ahriman.packages[package_ahriman.base].filepath)
# must update status and remove package files
status_client_mock.assert_called_once_with(package_ahriman.base)
base_remove_mock.assert_called_once_with(package_ahriman.base)
def test_process_remove_with_debug(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
@@ -99,12 +289,12 @@ def test_process_remove_with_debug(executor: Executor, package_ahriman: Package,
f"{package_ahriman.base}-debug": package_ahriman.packages[package_ahriman.base],
}
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
mocker.patch("ahriman.core.status.local_client.LocalClient.package_remove")
repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
mocker.patch("ahriman.core.repository.executor.Executor._package_remove_base")
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor._package_remove")
executor.process_remove([package_ahriman.base])
# must remove via alpm wrapper
repo_remove_mock.assert_has_calls([
remove_mock.assert_has_calls([
MockCall(package_ahriman.base, package_ahriman.packages[package_ahriman.base].filepath),
MockCall(f"{package_ahriman.base}-debug", package_ahriman.packages[package_ahriman.base].filepath),
])
@@ -116,12 +306,12 @@ def test_process_remove_base_multiple(executor: Executor, package_python_schedul
must run remove process for whole base with multiple packages
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_python_schedule])
repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
status_client_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_remove")
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor._package_remove")
status_client_mock = mocker.patch("ahriman.core.repository.executor.Executor._package_remove_base")
executor.process_remove([package_python_schedule.base])
# must remove via alpm wrapper
repo_remove_mock.assert_has_calls([
remove_mock.assert_has_calls([
MockCall(package, props.filepath)
for package, props in package_python_schedule.packages.items()
], any_order=True)
@@ -135,45 +325,27 @@ def test_process_remove_base_single(executor: Executor, package_python_schedule:
must run remove process for single package in base
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_python_schedule])
repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
status_client_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_remove")
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor._package_remove")
status_client_mock = mocker.patch("ahriman.core.repository.executor.Executor._package_remove_base")
executor.process_remove(["python2-schedule"])
# must remove via alpm wrapper
repo_remove_mock.assert_called_once_with(
remove_mock.assert_called_once_with(
"python2-schedule", package_python_schedule.packages["python2-schedule"].filepath)
# must not update status
status_client_mock.assert_not_called()
def test_process_remove_failed(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress tree clear errors during package base removal
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
mocker.patch("ahriman.core.status.local_client.LocalClient.package_remove", side_effect=Exception)
executor.process_remove([package_ahriman.base])
def test_process_remove_tree_clear_failed(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress remove errors
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
mocker.patch("ahriman.core.alpm.repo.Repo.remove", side_effect=Exception)
executor.process_remove([package_ahriman.base])
def test_process_remove_nothing(executor: Executor, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must not remove anything if it was not requested
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor._package_remove")
executor.process_remove([package_python_schedule.base])
repo_remove_mock.assert_not_called()
remove_mock.assert_not_called()
def test_process_remove_unknown(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
@@ -181,11 +353,11 @@ def test_process_remove_unknown(executor: Executor, package_ahriman: Package, mo
must remove unknown package base
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[])
repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
status_client_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_remove")
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor._package_remove")
status_client_mock = mocker.patch("ahriman.core.repository.executor.Executor._package_remove_base")
executor.process_remove([package_ahriman.base])
repo_remove_mock.assert_not_called()
remove_mock.assert_not_called()
status_client_mock.assert_called_once_with(package_ahriman.base)
@@ -195,9 +367,8 @@ def test_process_update(executor: Executor, package_ahriman: Package, user: User
"""
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[package_ahriman])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
move_mock = mocker.patch("shutil.move")
repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
sign_package_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_package", side_effect=lambda fn, _: [fn])
rename_mock = mocker.patch("ahriman.core.repository.executor.Executor._archive_rename")
update_mock = mocker.patch("ahriman.core.repository.executor.Executor._package_update")
status_client_mock = mocker.patch("ahriman.core.status.Client.set_success")
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")
packager_mock = mocker.patch("ahriman.core.repository.executor.Executor.packager", return_value=user)
@@ -206,12 +377,8 @@ def test_process_update(executor: Executor, package_ahriman: Package, user: User
# must return complete
assert executor.process_update([filepath], Packagers("packager"))
packager_mock.assert_called_once_with(Packagers("packager"), "ahriman")
# must move files (once)
move_mock.assert_called_once_with(executor.paths.packages / filepath, executor.paths.repository / filepath)
# must sign package
sign_package_mock.assert_called_once_with(executor.paths.packages / filepath, user.key)
# must add package
repo_add_mock.assert_called_once_with(executor.paths.repository / filepath)
rename_mock.assert_called_once_with(package_ahriman.packages[package_ahriman.base], package_ahriman.base)
update_mock.assert_called_once_with(filepath.name, package_ahriman.base, user.key)
# must update status
status_client_mock.assert_called_once_with(package_ahriman)
# must clear directory
@@ -226,58 +393,26 @@ def test_process_update_group(executor: Executor, package_python_schedule: Packa
"""
must group single packages under one base
"""
mocker.patch("shutil.move")
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[package_python_schedule])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_python_schedule])
repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
update_mock = mocker.patch("ahriman.core.repository.executor.Executor._package_update")
status_client_mock = mocker.patch("ahriman.core.status.Client.set_success")
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")
executor.process_update([package.filepath for package in package_python_schedule.packages.values()])
repo_add_mock.assert_has_calls([
MockCall(executor.paths.repository / package.filepath)
update_mock.assert_has_calls([
MockCall(package.filename, package_python_schedule.base, None)
for package in package_python_schedule.packages.values()
], any_order=True)
status_client_mock.assert_called_once_with(package_python_schedule)
remove_mock.assert_called_once_with([])
def test_process_update_unsafe(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must encode file name
"""
path = "gconf-3.2.6+11+g07808097-10-x86_64.pkg.tar.zst"
safe_path = "gconf-3.2.6-11-g07808097-10-x86_64.pkg.tar.zst"
package_ahriman.packages[package_ahriman.base].filename = path
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[package_ahriman])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
move_mock = mocker.patch("shutil.move")
repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
executor.process_update([Path(path)])
move_mock.assert_has_calls([
MockCall(executor.paths.packages / path, executor.paths.packages / safe_path),
MockCall(executor.paths.packages / safe_path, executor.paths.repository / safe_path)
])
repo_add_mock.assert_called_once_with(executor.paths.repository / safe_path)
def test_process_empty_filename(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must skip update for package which does not have path
"""
package_ahriman.packages[package_ahriman.base].filename = None
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[package_ahriman])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
executor.process_update([package.filepath for package in package_ahriman.packages.values()])
def test_process_update_failed(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must process update for failed package
"""
mocker.patch("shutil.move", side_effect=Exception)
mocker.patch("ahriman.core.repository.executor.Executor._package_update", side_effect=Exception)
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[package_ahriman])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
status_client_mock = mocker.patch("ahriman.core.status.Client.set_failed")
@@ -294,8 +429,7 @@ def test_process_update_removed_package(executor: Executor, package_python_sched
without_python2 = Package.from_json(package_python_schedule.view())
del without_python2.packages["python2-schedule"]
mocker.patch("shutil.move")
mocker.patch("ahriman.core.alpm.repo.Repo.add")
mocker.patch("ahriman.core.repository.executor.Executor._package_update")
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[without_python2])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_python_schedule])
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")

View File

@@ -0,0 +1,56 @@
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Explorer
from ahriman.models.repository_id import RepositoryId
def test_repositories_extract(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must generate list of available repositories based on arguments
"""
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
assert Explorer.repositories_extract(configuration, "repo", "arch") == [RepositoryId("arch", "repo")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_not_called()
def test_repositories_extract_repository(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must generate list of available repositories based on arguments and tree
"""
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
return_value={"repo"})
assert Explorer.repositories_extract(configuration, architecture="arch") == [RepositoryId("arch", "repo")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_called_once_with(configuration.repository_paths.root)
def test_repositories_extract_repository_legacy(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must generate list of available repositories based on arguments and tree (legacy mode)
"""
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
return_value=set())
assert Explorer.repositories_extract(configuration, architecture="arch") == [RepositoryId("arch", "aur")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_called_once_with(configuration.repository_paths.root)
def test_repositories_extract_architecture(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must read repository name from config
"""
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures",
return_value={"arch"})
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
assert Explorer.repositories_extract(configuration, repository="repo") == [RepositoryId("arch", "repo")]
known_architectures_mock.assert_called_once_with(configuration.repository_paths.root, "repo")
known_repositories_mock.assert_not_called()

View File

@@ -35,7 +35,7 @@ def test_event_get(local_client: LocalClient, package_ahriman: Package, mocker:
local_client.repository_id)
def test_logs_rotate(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None:
def test_logs_rotate(local_client: LocalClient, mocker: MockerFixture) -> None:
"""
must rotate logs
"""

View File

@@ -7,13 +7,6 @@ from ahriman.core.sign.gpg import GPG
from ahriman.core.support import KeyringTrigger
def test_requires_repository() -> None:
"""
must require repository identifier to be set to start
"""
assert KeyringTrigger.REQUIRES_REPOSITORY
def test_configuration_sections(configuration: Configuration) -> None:
"""
must correctly parse target list

View File

@@ -4,13 +4,6 @@ from ahriman.core.configuration import Configuration
from ahriman.core.support import MirrorlistTrigger
def test_requires_repository() -> None:
"""
must require repository identifier to be set to start
"""
assert MirrorlistTrigger.REQUIRES_REPOSITORY
def test_configuration_sections(configuration: Configuration) -> None:
"""
must correctly parse target list

View File

@@ -16,6 +16,18 @@ from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
def test_atomic_move(mocker: MockerFixture) -> None:
"""
must move file with locking
"""
filelock_mock = mocker.patch("ahriman.core.utils.filelock")
move_mock = mocker.patch("shutil.move")
atomic_move(Path("source"), Path("destination"))
filelock_mock.assert_called_once_with(Path("destination"))
move_mock.assert_called_once_with(Path("source"), Path("destination"))
def test_check_output(mocker: MockerFixture) -> None:
"""
must run command and log result
@@ -235,6 +247,30 @@ def test_extract_user() -> None:
assert extract_user() == "doas"
def test_filelock(tmp_path: Path) -> None:
"""
must acquire lock and remove lock file after
"""
local = tmp_path / "local"
lock = local.with_name(f".{local.name}.lock")
with filelock(local):
assert lock.exists()
assert not lock.exists()
def test_filelock_cleanup_on_missing(tmp_path: Path) -> None:
"""
must not fail if lock file is already removed
"""
local = tmp_path / "local"
lock = local.with_name(f".{local.name}.lock")
with filelock(local):
lock.unlink(missing_ok=True)
assert not lock.exists()
def test_filter_json(package_ahriman: Package) -> None:
"""
must filter fields by known list
@@ -470,6 +506,23 @@ def test_srcinfo_property_list() -> None:
assert srcinfo_property_list("key", {"key_x86_64": ["overrides"]}, {}, architecture="x86_64") == ["overrides"]
def test_symlink_relative(mocker: MockerFixture) -> None:
"""
must create symlinks with relative paths
"""
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
symlink_relative(Path("a"), Path("b"))
symlink_relative(Path("root/a"), Path("root/c"))
symlink_relative(Path("root/sub/a"), Path("root/c"))
symlink_mock.assert_has_calls([
MockCall(Path("b")),
MockCall(Path("c")),
MockCall(Path("../c")),
])
def test_trim_package() -> None:
"""
must trim package version

View File

@@ -58,7 +58,7 @@ def test_configuration_schema_no_schema(configuration: Configuration) -> None:
assert ReportTrigger.configuration_schema(configuration) == {}
def test_configuration_schema_empty(configuration: Configuration) -> None:
def test_configuration_schema_empty() -> None:
"""
must return default schema if no configuration set
"""

View File

@@ -5,13 +5,6 @@ from ahriman.core.upload import UploadTrigger
from ahriman.models.result import Result
def test_requires_repository() -> None:
"""
must require repository identifier to be set to start
"""
assert UploadTrigger.REQUIRES_REPOSITORY
def test_configuration_sections(configuration: Configuration) -> None:
"""
must correctly parse target list

View File

@@ -4,16 +4,12 @@ from pathlib import Path
from unittest.mock import MagicMock, PropertyMock
from ahriman import __version__
from ahriman.core.alpm.remote import AUR
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.counters import Counters
from ahriman.models.filesystem_package import FilesystemPackage
from ahriman.models.internal_status import InternalStatus
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource
from ahriman.models.pkgbuild import Pkgbuild
from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_stats import RepositoryStats

View File

@@ -353,6 +353,15 @@ def test_build_status_pretty_print(package_ahriman: Package) -> None:
assert isinstance(package_ahriman.pretty_print(), str)
def test_vercmp(package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must call vercmp
"""
vercmp_mock = mocker.patch("ahriman.models.package.vercmp")
package_ahriman.vercmp("version")
vercmp_mock.assert_called_once_with(package_ahriman.version, "version")
def test_with_packages(package_ahriman: Package, package_python_schedule: Package, pacman: Pacman,
mocker: MockerFixture) -> None:
"""

View File

@@ -185,6 +185,14 @@ def test_known_repositories_empty(repository_paths: RepositoryPaths, mocker: Moc
iterdir_mock.assert_not_called()
def test_archive_for(repository_paths: RepositoryPaths, package_ahriman: Package) -> None:
"""
must correctly define archive path
"""
path = repository_paths.archive_for(package_ahriman.base)
assert path == repository_paths.archive / "packages" / "a" / package_ahriman.base
def test_cache_for(repository_paths: RepositoryPaths, package_ahriman: Package) -> None:
"""
must return correct path for cache directory
@@ -194,6 +202,29 @@ def test_cache_for(repository_paths: RepositoryPaths, package_ahriman: Package)
assert path.parent == repository_paths.cache
def test_ensure_exists(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
"""
must create directory if it doesn't exist
"""
owner_guard_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner")
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
repository_paths.ensure_exists(repository_paths.archive)
owner_guard_mock.assert_called_once_with()
mkdir_mock.assert_called_once_with(mode=0o755, parents=True)
def test_ensure_exists_skip(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
"""
must do not create directory if it already exists
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
repository_paths.ensure_exists(repository_paths.archive)
mkdir_mock.assert_not_called()
def test_preserve_owner(tmp_path: Path, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must preserve file owner during operations
@@ -248,8 +279,8 @@ def test_tree_clear(repository_paths: RepositoryPaths, package_ahriman: Package,
must remove any package related files
"""
paths = {
getattr(repository_paths, prop)(package_ahriman.base)
for prop in dir(repository_paths) if prop.endswith("_for")
repository_paths.cache_for(package_ahriman.base),
repository_paths.archive_for(package_ahriman.base),
}
rmtree_mock = mocker.patch("shutil.rmtree")
@@ -269,6 +300,7 @@ def test_tree_create(repository_paths: RepositoryPaths, mocker: MockerFixture) -
for prop in dir(repository_paths)
if not prop.startswith("_")
and prop not in (
"archive_for",
"build_root",
"logger_name",
"logger",
@@ -282,8 +314,12 @@ def test_tree_create(repository_paths: RepositoryPaths, mocker: MockerFixture) -
owner_guard_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner")
repository_paths.tree_create()
mkdir_mock.assert_has_calls([MockCall(mode=0o755, parents=True, exist_ok=True) for _ in paths], any_order=True)
owner_guard_mock.assert_called_once_with()
mkdir_mock.assert_has_calls([MockCall(mode=0o755, parents=True) for _ in paths], any_order=True)
owner_guard_mock.assert_has_calls([
MockCall(),
MockCall().__enter__(),
MockCall().__exit__(None, None, None)
] * len(paths))
def test_tree_create_skip(mocker: MockerFixture) -> None:

View File

@@ -66,7 +66,7 @@ async def test_delete_partially(client: TestClient, package_ahriman: Package) ->
assert json
async def test_delete_exception(client: TestClient, package_ahriman: Package) -> None:
async def test_delete_exception(client: TestClient) -> None:
"""
must raise exception on invalid payload
"""

View File

@@ -109,7 +109,7 @@ async def test_post(client: TestClient, repository_paths: RepositoryPaths, mocke
local = Path("local")
save_mock = pytest.helpers.patch_view(client.app, "save_file",
AsyncMock(return_value=("filename", local / ".filename")))
rename_mock = mocker.patch("pathlib.Path.rename")
rename_mock = mocker.patch("ahriman.web.views.v1.service.upload.atomic_move")
# no content validation here because it has invalid schema
data = FormData()
@@ -118,7 +118,7 @@ async def test_post(client: TestClient, repository_paths: RepositoryPaths, mocke
response = await client.post("/api/v1/service/upload", data=data)
assert response.ok
save_mock.assert_called_once_with(pytest.helpers.anyvar(int), repository_paths.packages, max_body_size=None)
rename_mock.assert_called_once_with(local / "filename")
rename_mock.assert_called_once_with(local / ".filename", local / "filename")
async def test_post_with_sig(client: TestClient, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
@@ -131,7 +131,7 @@ async def test_post_with_sig(client: TestClient, repository_paths: RepositoryPat
("filename", local / ".filename"),
("filename.sig", local / ".filename.sig"),
]))
rename_mock = mocker.patch("pathlib.Path.rename")
rename_mock = mocker.patch("ahriman.web.views.v1.service.upload.atomic_move")
# no content validation here because it has invalid schema
data = FormData()
@@ -145,8 +145,8 @@ async def test_post_with_sig(client: TestClient, repository_paths: RepositoryPat
MockCall(pytest.helpers.anyvar(int), repository_paths.packages, max_body_size=None),
])
rename_mock.assert_has_calls([
MockCall(local / "filename"),
MockCall(local / "filename.sig"),
MockCall(local / ".filename", local / "filename"),
MockCall(local / ".filename.sig", local / "filename.sig"),
])

View File

@@ -10,6 +10,9 @@ root = /
sync_files_database = no
use_ahriman_cache = no
[archive]
keep_built_packages = 3
[auth]
client_id = client_id
client_secret = client_secret