diff --git a/src/ahriman/application/handlers/tree_migrate.py b/src/ahriman/application/handlers/tree_migrate.py index 628bda0f..9672f75c 100644 --- a/src/ahriman/application/handlers/tree_migrate.py +++ b/src/ahriman/application/handlers/tree_migrate.py @@ -50,7 +50,7 @@ class TreeMigrate(Handler): target_tree.tree_create() # perform migration TreeMigrate.tree_move(current_tree, target_tree) - TreeMigrate.fix_symlinks(current_tree) + TreeMigrate.fix_symlinks(target_tree) @staticmethod def _set_service_tree_migrate_parser(root: SubParserAction) -> argparse.ArgumentParser: diff --git a/src/ahriman/core/alpm/repo.py b/src/ahriman/core/alpm/repo.py index fa657458..3d8c1839 100644 --- a/src/ahriman/core/alpm/repo.py +++ b/src/ahriman/core/alpm/repo.py @@ -59,7 +59,7 @@ class Repo(LazyLogging): """ return self.root / f"{self.name}.db.tar.gz" - def add(self, path: Path, remove: bool = True) -> None: + def add(self, path: Path, *, remove: bool = True) -> None: """ add new package to repository @@ -97,7 +97,7 @@ class Repo(LazyLogging): filename(Path): package filename to remove """ # remove package and signature (if any) from filesystem - for full_path in self.root.glob(f"**/{filename}*"): + for full_path in self.root.glob(f"**/{filename.name}*"): full_path.unlink() # remove package from registry diff --git a/src/ahriman/core/repository/executor.py b/src/ahriman/core/repository/executor.py index 27535d4a..dc2197d4 100644 --- a/src/ahriman/core/repository/executor.py +++ b/src/ahriman/core/repository/executor.py @@ -41,7 +41,7 @@ class Executor(PackageInfo, Cleaner): trait for common repository update processes """ - def _archive_remove(self, description: PackageDescription, package_base: str) -> None: + def _archive_rename(self, description: PackageDescription, package_base: str) -> None: """ rename package archive removing special symbols @@ -259,7 +259,7 @@ class Executor(PackageInfo, Cleaner): packager = self.packager(packagers, local.base) for description in local.packages.values(): - self._archive_remove(description, local.base) + self._archive_rename(description, local.base) self._package_update(description.filename, local.base, packager.key) self.reporter.set_success(local) result.add_updated(local) diff --git a/src/ahriman/models/repository_paths.py b/src/ahriman/models/repository_paths.py index c14f57a4..b4f42cc1 100644 --- a/src/ahriman/models/repository_paths.py +++ b/src/ahriman/models/repository_paths.py @@ -309,6 +309,9 @@ class RepositoryPaths(LazyLogging): path = path or self.root def walk(root: Path) -> Generator[Path, None, None]: + if not root.exists(): + return + # basically walk, but skipping some content for child in root.iterdir(): yield child diff --git a/tests/ahriman/application/handlers/test_handler_tree_migrate.py b/tests/ahriman/application/handlers/test_handler_tree_migrate.py index af45aaba..3f161f8e 100644 --- a/tests/ahriman/application/handlers/test_handler_tree_migrate.py +++ b/tests/ahriman/application/handlers/test_handler_tree_migrate.py @@ -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.fix_symlinks") _, 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,37 @@ 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_fix_symlinks(repository_paths: RepositoryPaths, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must replace symlinks during migration + """ + mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner") + 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.fix_symlinks(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: @@ -36,6 +69,7 @@ def test_move_tree(mocker: MockerFixture) -> None: TreeMigrate.tree_move(from_paths, to_paths) rename_mock.assert_has_calls([ + MockCall(from_paths.archive, to_paths.archive), MockCall(from_paths.packages, to_paths.packages), MockCall(from_paths.pacman, to_paths.pacman), MockCall(from_paths.repository, to_paths.repository), diff --git a/tests/ahriman/conftest.py b/tests/ahriman/conftest.py index 2911fe3b..701c9a75 100644 --- a/tests/ahriman/conftest.py +++ b/tests/ahriman/conftest.py @@ -293,7 +293,7 @@ def database(configuration: Configuration, mocker: MockerFixture) -> SQLite: 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", side_effect=perform_migration, autospec=True) + mocker.patch.object(Migrations, "perform_migration", autospec=True, side_effect=perform_migration) return SQLite.load(configuration) diff --git a/tests/ahriman/core/alpm/test_repo.py b/tests/ahriman/core/alpm/test_repo.py index 8cc87036..22f27997 100644 --- a/tests/ahriman/core/alpm/test_repo.py +++ b/tests/ahriman/core/alpm/test_repo.py @@ -4,6 +4,15 @@ from pathlib import Path from pytest_mock import MockerFixture from ahriman.core.alpm.repo import Repo +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 +31,18 @@ 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_add_no_remove(repo: Repo, mocker: MockerFixture) -> None: + """ + must call repo-add without remove flag + """ + check_output_mock = mocker.patch("ahriman.core.alpm.repo.check_output") + + repo.add(Path("path"), remove=False) + check_output_mock.assert_called_once() # it will be checked later + assert "--remove" not in check_output_mock.call_args[0] def test_repo_init(repo: Repo, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/core/database/migrations/test_m016_archive.py b/tests/ahriman/core/database/migrations/test_m016_archive.py new file mode 100644 index 00000000..12f71b30 --- /dev/null +++ b/tests/ahriman/core/database/migrations/test_m016_archive.py @@ -0,0 +1,82 @@ +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.application.handlers.handler.Handler.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 / "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) + archive_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman) + rename_mock = mocker.patch("pathlib.Path.rename") + symlink_mock = mocker.patch("pathlib.Path.symlink_to") + + move_packages(repository_paths, pacman) + archive_mock.assert_has_calls([ + MockCall(repository_paths.repository / "file.pkg.tar.xz", pacman), + MockCall(repository_paths.repository / "file.pkg.tar.xz.sig", pacman), + ]) + rename_mock.assert_has_calls([ + MockCall(repository_paths.archive_for(package_ahriman.base) / "file.pkg.tar.xz"), + MockCall(repository_paths.archive_for(package_ahriman.base) / "file.pkg.tar.xz.sig"), + ]) + symlink_mock.assert_has_calls([ + MockCall( + Path("..") / + ".." / + ".." / + repository_paths.archive_for(package_ahriman.base).relative_to(repository_paths.root) / + "file.pkg.tar.xz" + ), + MockCall( + Path("..") / + ".." / + ".." / + repository_paths.archive_for(package_ahriman.base).relative_to(repository_paths.root) / + "file.pkg.tar.xz.sig" + ), + ]) diff --git a/tests/ahriman/core/housekeeping/conftest.py b/tests/ahriman/core/housekeeping/conftest.py index f1f3a0cb..c809f3d4 100644 --- a/tests/ahriman/core/housekeeping/conftest.py +++ b/tests/ahriman/core/housekeeping/conftest.py @@ -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 diff --git a/tests/ahriman/core/housekeeping/test_archive_rotation_trigger.py b/tests/ahriman/core/housekeeping/test_archive_rotation_trigger.py new file mode 100644 index 00000000..ed2977c4 --- /dev/null +++ b/tests/ahriman/core/housekeeping/test_archive_rotation_trigger.py @@ -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)) diff --git a/tests/ahriman/core/housekeeping/test_logs_rotation_trigger.py b/tests/ahriman/core/housekeeping/test_logs_rotation_trigger.py index d32c04ae..52443434 100644 --- a/tests/ahriman/core/housekeeping/test_logs_rotation_trigger.py +++ b/tests/ahriman/core/housekeeping/test_logs_rotation_trigger.py @@ -14,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 """ diff --git a/tests/ahriman/core/repository/test_executor.py b/tests/ahriman/core/repository/test_executor.py index 45e12d07..9b015ba2 100644 --- a/tests/ahriman/core/repository/test_executor.py +++ b/tests/ahriman/core/repository/test_executor.py @@ -13,34 +13,139 @@ from ahriman.models.packagers import Packagers from ahriman.models.user import User +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("pathlib.Path.rename") + + executor._archive_rename(package_ahriman.packages[package_ahriman.base], package_ahriman.base) + rename_mock.assert_called_once_with(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("pathlib.Path.rename") + + 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") + with_packages_mock = mocker.patch("ahriman.models.package.Package.with_packages") + move_mock = mocker.patch("shutil.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) + with_packages_mock.assert_called_once_with([Path(package_ahriman.base)], executor.pacman) + move_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base) + + +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("pathlib.Path.rename") + 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.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("pathlib.Path.rename") + 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")) @@ -79,15 +184,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 +204,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 +221,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 +240,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 +268,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 +282,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 +292,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 +308,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 +344,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") diff --git a/tests/ahriman/models/test_package.py b/tests/ahriman/models/test_package.py index 6762302d..a9932c94 100644 --- a/tests/ahriman/models/test_package.py +++ b/tests/ahriman/models/test_package.py @@ -516,6 +516,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: """ diff --git a/tests/ahriman/models/test_repository_paths.py b/tests/ahriman/models/test_repository_paths.py index 4100d720..62f30beb 100644 --- a/tests/ahriman/models/test_repository_paths.py +++ b/tests/ahriman/models/test_repository_paths.py @@ -248,6 +248,28 @@ def test_chown_invalid_path(repository_paths: RepositoryPaths) -> None: repository_paths._chown(repository_paths.root.parent) +def test_archive_for(repository_paths: RepositoryPaths, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must correctly define archive path + """ + mocker.patch("pathlib.Path.is_dir", return_value=True) + path = repository_paths.archive_for(package_ahriman.base) + assert path == repository_paths.archive / "packages" / "a" / package_ahriman.base + + +def test_archive_for_create_tree(repository_paths: RepositoryPaths, package_ahriman: Package, + mocker: MockerFixture) -> None: + """ + must create archive directory if it doesn't exist + """ + owner_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner") + mkdir_mock = mocker.patch("pathlib.Path.mkdir") + + repository_paths.archive_for(package_ahriman.base) + owner_mock.assert_called_once_with(repository_paths.archive) + mkdir_mock.assert_called_once_with(mode=0o755, parents=True) + + def test_cache_for(repository_paths: RepositoryPaths, package_ahriman: Package) -> None: """ must return correct path for cache directory @@ -287,13 +309,24 @@ def test_preserve_owner_specific(tmp_path: Path, repository_id: RepositoryId, mo chown_mock.assert_has_calls([MockCall(repository_paths.root / "content" / "created2")]) +def test_preserve_owner_no_directory(tmp_path: Path, repository_id: RepositoryId, mocker: MockerFixture) -> None: + """ + must skip directory scan if it does not exist + """ + repository_paths = RepositoryPaths(tmp_path, repository_id) + chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths._chown") + + with repository_paths.preserve_owner(Path("empty")): + (repository_paths.root / "created1").touch() + chown_mock.assert_not_called() + + def test_tree_clear(repository_paths: RepositoryPaths, package_ahriman: Package, mocker: MockerFixture) -> None: """ 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), } rmtree_mock = mocker.patch("shutil.rmtree") @@ -313,6 +346,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", diff --git a/tests/testresources/core/ahriman.ini b/tests/testresources/core/ahriman.ini index 522e85dc..36715f8f 100644 --- a/tests/testresources/core/ahriman.ini +++ b/tests/testresources/core/ahriman.ini @@ -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