diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index 8b3dfde0..4f5f303b 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -137,9 +137,16 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser: """ parser = root.add_parser("package-add", aliases=["add"], help="add package", description="add package", epilog="This subcommand should be used for new package addition. It also supports flag " - "--now in case if you would like to build the package immediately.", + "--now in case if you would like to build the package immediately. " + "You can add new package from one of supported sources: " + "1) if it is already built package you can specify the path to the archive; " + "2) you can also add built packages from the directory (e.g. during the migration " + "from another repository source); " + "3) it is also possible to add package from local PKGBUILD, but in this case it " + "will be ignored during the next automatic updates; " + "4) and finally you can add package from AUR.", formatter_class=_formatter) - parser.add_argument("package", help="package base/name or archive path", nargs="+") + parser.add_argument("package", help="package base/name or path to local files", nargs="+") parser.add_argument("-n", "--now", help="run update function after", action="store_true") parser.add_argument("-s", "--source", help="package source", type=PackageSource, choices=PackageSource, default=PackageSource.Auto) diff --git a/src/ahriman/application/application.py b/src/ahriman/application/application.py index 241f473a..c766693b 100644 --- a/src/ahriman/application/application.py +++ b/src/ahriman/application/application.py @@ -105,21 +105,30 @@ class Application: :param without_dependencies: if set, dependency check will be disabled """ known_packages = self._known_packages() + aur_url = self.configuration.get("alpm", "aur_url") + + def add_archive(src: Path) -> None: + dst = self.repository.paths.packages / src.name + shutil.move(src, dst) def add_directory(path: Path) -> None: for full_path in filter(package_like, path.iterdir()): add_archive(full_path) - def add_manual(src: str) -> Path: - package = Package.load(src, self.repository.pacman, self.configuration.get("alpm", "aur_url")) + def add_local(path: Path) -> Path: + package = Package.load(path, self.repository.pacman, aur_url) + cache_dir = self.repository.paths.cache_for(package.base) + shutil.copytree(path, cache_dir) # copy package to store in caches + Sources.init(cache_dir) # we need to run init command in directory where we do have permissions + shutil.copytree(cache_dir, self.repository.paths.manual_for(package.base)) # copy package for the build + return self.repository.paths.manual_for(package.base) + + def add_remote(src: str) -> Path: + package = Package.load(src, self.repository.pacman, aur_url) Sources.load(self.repository.paths.manual_for(package.base), package.git_url, self.repository.paths.patches_for(package.base)) return self.repository.paths.manual_for(package.base) - def add_archive(src: Path) -> None: - dst = self.repository.paths.packages / src.name - shutil.move(src, dst) - def process_dependencies(path: Path) -> None: if without_dependencies: return @@ -128,12 +137,15 @@ class Application: def process_single(src: str) -> None: resolved_source = source.resolve(src) - if resolved_source == PackageSource.Directory: - add_directory(Path(src)) - elif resolved_source == PackageSource.Archive: + if resolved_source == PackageSource.Archive: add_archive(Path(src)) - else: - path = add_manual(src) + elif resolved_source == PackageSource.AUR: + path = add_remote(src) + process_dependencies(path) + elif resolved_source == PackageSource.Directory: + add_directory(Path(src)) + elif resolved_source == PackageSource.Local: + path = add_local(Path(src)) process_dependencies(path) for name in names: @@ -213,13 +225,22 @@ class Application: get packages which were not found in AUR :return: unknown package list """ - packages = [] - for base in self.repository.packages(): + def has_aur(package_base: str, aur_url: str) -> bool: try: - _ = Package.from_aur(base.base, base.aur_url) + _ = Package.from_aur(package_base, aur_url) except Exception: - packages.append(base) - return packages + return False + return True + + def has_local(package_base: str) -> bool: + cache_dir = self.repository.paths.cache_for(package_base) + return cache_dir.is_dir() and not Sources.has_remotes(cache_dir) + + return [ + package + for package in self.repository.packages() + if not has_aur(package.base, package.aur_url) and not has_local(package.base) + ] def update(self, updates: Iterable[Package]) -> None: """ diff --git a/src/ahriman/core/build_tools/sources.py b/src/ahriman/core/build_tools/sources.py index 604b3c23..eb7abade 100644 --- a/src/ahriman/core/build_tools/sources.py +++ b/src/ahriman/core/build_tools/sources.py @@ -33,72 +33,98 @@ class Sources: logger = logging.getLogger("build_details") + _branch = "master" # in case if BLM would like to change it _check_output = check_output @staticmethod - def add(local_path: Path, *pattern: str) -> None: + def add(sources_dir: Path, *pattern: str) -> None: """ track found files via git - :param local_path: local path to git repository + :param sources_dir: local path to git repository :param pattern: glob patterns """ # glob directory to find files which match the specified patterns found_files: List[Path] = [] for glob in pattern: - found_files.extend(local_path.glob(glob)) + found_files.extend(sources_dir.glob(glob)) Sources.logger.info("found matching files %s", found_files) # add them to index - Sources._check_output("git", "add", "--intent-to-add", *[str(fn.relative_to(local_path)) for fn in found_files], - exception=None, cwd=local_path, logger=Sources.logger) + Sources._check_output("git", "add", "--intent-to-add", + *[str(fn.relative_to(sources_dir)) for fn in found_files], + exception=None, cwd=sources_dir, logger=Sources.logger) @staticmethod - def diff(local_path: Path, patch_path: Path) -> None: + def diff(sources_dir: Path, patch_path: Path) -> None: """ generate diff from the current version and write it to the output file - :param local_path: local path to git repository + :param sources_dir: local path to git repository :param patch_path: path to result patch """ - patch = Sources._check_output("git", "diff", exception=None, cwd=local_path, logger=Sources.logger) + patch = Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=Sources.logger) patch_path.write_text(patch) @staticmethod - def fetch(local_path: Path, remote: str, branch: str = "master") -> None: + def fetch(sources_dir: Path, remote: str) -> None: """ either clone repository or update it to origin/`branch` - :param local_path: local path to fetch + :param sources_dir: local path to fetch :param remote: remote target (from where to fetch) - :param branch: branch name to checkout, master by default """ # local directory exists and there is .git directory - if (local_path / ".git").is_dir(): - Sources.logger.info("update HEAD to remote to %s", local_path) - Sources._check_output("git", "fetch", "origin", branch, - exception=None, cwd=local_path, logger=Sources.logger) + is_initialized_git = (sources_dir / ".git").is_dir() + if is_initialized_git and not Sources.has_remotes(sources_dir): + # there is git repository, but no remote configured so far + Sources.logger.info("skip update at %s because there are no branches configured", sources_dir) + return + + if is_initialized_git: + Sources.logger.info("update HEAD to remote at %s", sources_dir) + Sources._check_output("git", "fetch", "origin", Sources._branch, + exception=None, cwd=sources_dir, logger=Sources.logger) else: - Sources.logger.info("clone remote %s to %s", remote, local_path) - Sources._check_output("git", "clone", remote, str(local_path), exception=None, logger=Sources.logger) + Sources.logger.info("clone remote %s to %s", remote, sources_dir) + Sources._check_output("git", "clone", remote, str(sources_dir), exception=None, logger=Sources.logger) # and now force reset to our branch - Sources._check_output("git", "checkout", "--force", branch, - exception=None, cwd=local_path, logger=Sources.logger) - Sources._check_output("git", "reset", "--hard", f"origin/{branch}", - exception=None, cwd=local_path, logger=Sources.logger) + Sources._check_output("git", "checkout", "--force", Sources._branch, + exception=None, cwd=sources_dir, logger=Sources.logger) + Sources._check_output("git", "reset", "--hard", f"origin/{Sources._branch}", + exception=None, cwd=sources_dir, logger=Sources.logger) @staticmethod - def load(local_path: Path, remote: str, patch_dir: Path) -> None: + def has_remotes(sources_dir: Path) -> bool: + """ + check if there are remotes for the repository + :param sources_dir: local path to git repository + :return: True in case if there is any remote and false otherwise + """ + remotes = Sources._check_output("git", "remote", exception=None, cwd=sources_dir, logger=Sources.logger) + return bool(remotes) + + @staticmethod + def init(sources_dir: Path) -> None: + """ + create empty git repository at the specified path + :param sources_dir: local path to sources + """ + Sources._check_output("git", "init", "--initial-branch", Sources._branch, + exception=None, cwd=sources_dir, logger=Sources.logger) + + @staticmethod + def load(sources_dir: Path, remote: str, patch_dir: Path) -> None: """ fetch sources from remote and apply patches - :param local_path: local path to fetch + :param sources_dir: local path to fetch :param remote: remote target (from where to fetch) :param patch_dir: path to directory with package patches """ - Sources.fetch(local_path, remote) - Sources.patch_apply(local_path, patch_dir) + Sources.fetch(sources_dir, remote) + Sources.patch_apply(sources_dir, patch_dir) @staticmethod - def patch_apply(local_path: Path, patch_dir: Path) -> None: + def patch_apply(sources_dir: Path, patch_dir: Path) -> None: """ apply patches if any - :param local_path: local path to directory with git sources + :param sources_dir: local path to directory with git sources :param patch_dir: path to directory with package patches """ # check if even there are patches @@ -110,15 +136,15 @@ class Sources: for patch in patches: Sources.logger.info("apply patch %s", patch.name) Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace", str(patch), - exception=None, cwd=local_path, logger=Sources.logger) + exception=None, cwd=sources_dir, logger=Sources.logger) @staticmethod - def patch_create(local_path: Path, patch_path: Path, *pattern: str) -> None: + def patch_create(sources_dir: Path, patch_path: Path, *pattern: str) -> None: """ create patch set for the specified local path - :param local_path: local path to git repository + :param sources_dir: local path to git repository :param patch_path: path to result patch :param pattern: glob patterns """ - Sources.add(local_path, *pattern) - Sources.diff(local_path, patch_path) + Sources.add(sources_dir, *pattern) + Sources.diff(sources_dir, patch_path) diff --git a/src/ahriman/core/repository/executor.py b/src/ahriman/core/repository/executor.py index 7115cffe..c698743d 100644 --- a/src/ahriman/core/repository/executor.py +++ b/src/ahriman/core/repository/executor.py @@ -72,9 +72,16 @@ class Executor(Cleaner): :param packages: list of package names or bases to remove :return: path to repository database """ - def remove_single(package: str, fn: Path) -> None: + def remove_base(package_base: str) -> None: try: - self.repo.remove(package, fn) + self.paths.tree_clear(package_base) # remove all internal files + self.reporter.remove(package_base) # we only update status page in case of base removal + except Exception: + self.logger.exception("could not remove base %s", package_base) + + def remove_package(package: str, fn: Path) -> None: + try: + self.repo.remove(package, fn) # remove the package itself except Exception: self.logger.exception("could not remove %s", package) @@ -86,7 +93,7 @@ class Executor(Cleaner): for package, properties in local.packages.items() if properties.filename is not None } - self.reporter.remove(local.base) # we only update status page in case of base removal + remove_base(local.base) elif requested.intersection(local.packages.keys()): to_remove = { package: Path(properties.filename) @@ -95,8 +102,9 @@ class Executor(Cleaner): } else: to_remove = {} + for package, filename in to_remove.items(): - remove_single(package, filename) + remove_package(package, filename) return self.repo.repo_path diff --git a/src/ahriman/core/repository/properties.py b/src/ahriman/core/repository/properties.py index 88d860e4..bd0ff5ee 100644 --- a/src/ahriman/core/repository/properties.py +++ b/src/ahriman/core/repository/properties.py @@ -58,7 +58,7 @@ class Properties: self.name = configuration.get("repository", "name") self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture) - self.paths.create_tree() + self.paths.tree_create() self.ignore_list = configuration.getlist("build", "ignore_packages", fallback=[]) self.pacman = Pacman(configuration) diff --git a/src/ahriman/models/package_source.py b/src/ahriman/models/package_source.py index b40c9dbb..8bd23c1f 100644 --- a/src/ahriman/models/package_source.py +++ b/src/ahriman/models/package_source.py @@ -30,14 +30,16 @@ class PackageSource(Enum): package source for addition enumeration :cvar Auto: automatically determine type of the source :cvar Archive: source is a package archive - :cvar Directory: source is a directory which contains packages :cvar AUR: source is an AUR package for which it should search + :cvar Directory: source is a directory which contains packages + :cvar Local: source is locally stored PKGBUILD """ Auto = "auto" Archive = "archive" - Directory = "directory" AUR = "aur" + Directory = "directory" + Local = "local" def resolve(self, source: str) -> PackageSource: """ @@ -47,7 +49,10 @@ class PackageSource(Enum): """ if self != PackageSource.Auto: return self + maybe_path = Path(source) + if (maybe_path / "PKGBUILD").is_file(): + return PackageSource.Local if maybe_path.is_dir(): return PackageSource.Directory if maybe_path.is_file() and package_like(maybe_path): diff --git a/src/ahriman/models/repository_paths.py b/src/ahriman/models/repository_paths.py index 1025c111..baeb97cf 100644 --- a/src/ahriman/models/repository_paths.py +++ b/src/ahriman/models/repository_paths.py @@ -19,6 +19,8 @@ # from __future__ import annotations +import shutil + from dataclasses import dataclass from pathlib import Path from typing import Set, Type @@ -101,24 +103,12 @@ class RepositoryPaths: def cache_for(self, package_base: str) -> Path: """ - get cache path for specific package base + get path to cached PKGBUILD and package sources for the package base :param package_base: package base name :return: full path to directory for specified package base cache """ return self.cache / package_base - def create_tree(self) -> None: - """ - create ahriman working tree - """ - self.cache.mkdir(mode=0o755, parents=True, exist_ok=True) - self.chroot.mkdir(mode=0o755, parents=True, exist_ok=True) - self.manual.mkdir(mode=0o755, parents=True, exist_ok=True) - self.packages.mkdir(mode=0o755, parents=True, exist_ok=True) - self.patches.mkdir(mode=0o755, parents=True, exist_ok=True) - self.repository.mkdir(mode=0o755, parents=True, exist_ok=True) - self.sources.mkdir(mode=0o755, parents=True, exist_ok=True) - def manual_for(self, package_base: str) -> Path: """ get manual path for specific package base @@ -137,8 +127,34 @@ class RepositoryPaths: def sources_for(self, package_base: str) -> Path: """ - get sources path for specific package base + get path to directory from where build will start for the package base :param package_base: package base name :return: full path to directory for specified package base sources """ return self.sources / package_base + + def tree_clear(self, package_base: str) -> None: + """ + clear package specific files + :param package_base: package base name + """ + for directory in ( + self.cache_for(package_base), + self.manual_for(package_base), + self.patches_for(package_base), + self.sources_for(package_base)): + shutil.rmtree(directory, ignore_errors=True) + + def tree_create(self) -> None: + """ + create ahriman working tree + """ + for directory in ( + self.cache, + self.chroot, + self.manual, + self.packages, + self.patches, + self.repository, + self.sources): + directory.mkdir(mode=0o755, parents=True, exist_ok=True) diff --git a/tests/ahriman/application/handlers/test_handler_init.py b/tests/ahriman/application/handlers/test_handler_init.py index 3613b2bb..6d66c4de 100644 --- a/tests/ahriman/application/handlers/test_handler_init.py +++ b/tests/ahriman/application/handlers/test_handler_init.py @@ -10,11 +10,11 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc """ must run command """ - create_tree_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.create_tree") + tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init") Init.run(args, "x86_64", configuration, True) - create_tree_mock.assert_called_once() + tree_create_mock.assert_called_once() init_mock.assert_called_once() diff --git a/tests/ahriman/application/test_application.py b/tests/ahriman/application/test_application.py index b8c30341..7a850cc8 100644 --- a/tests/ahriman/application/test_application.py +++ b/tests/ahriman/application/test_application.py @@ -1,5 +1,6 @@ import pytest +from pathlib import Path from pytest_mock import MockerFixture from unittest import mock @@ -104,6 +105,43 @@ def test_get_updates_with_filter(application: Application, mocker: MockerFixture updates_manual_mock.assert_called_once() +def test_add_archive(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must add package from archive + """ + mocker.patch("ahriman.application.application.Application._known_packages", return_value=set()) + move_mock = mocker.patch("shutil.move") + + application.add([package_ahriman.base], PackageSource.Archive, False) + move_mock.assert_called_once() + + +def test_add_remote(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must add package from AUR + """ + mocker.patch("ahriman.application.application.Application._known_packages", return_value=set()) + mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) + load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load") + + application.add([package_ahriman.base], PackageSource.AUR, True) + load_mock.assert_called_once() + + +def test_add_remote_with_dependencies(application: Application, package_ahriman: Package, + mocker: MockerFixture) -> None: + """ + must add package from AUR with dependencies + """ + mocker.patch("ahriman.application.application.Application._known_packages", return_value=set()) + mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) + mocker.patch("ahriman.core.build_tools.sources.Sources.load") + dependencies_mock = mocker.patch("ahriman.models.package.Package.dependencies") + + application.add([package_ahriman.base], PackageSource.AUR, False) + dependencies_mock.assert_called_once() + + def test_add_directory(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: """ must add packages from directory @@ -118,43 +156,38 @@ def test_add_directory(application: Application, package_ahriman: Package, mocke move_mock.assert_called_once() -def test_add_manual(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: +def test_add_local(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: """ - must add package from AUR + must add package from local sources """ mocker.patch("ahriman.application.application.Application._known_packages", return_value=set()) mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) - load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load") + init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init") + copytree_mock = mocker.patch("shutil.copytree") - application.add([package_ahriman.base], PackageSource.AUR, True) - load_mock.assert_called_once() + application.add([package_ahriman.base], PackageSource.Local, True) + init_mock.assert_called_once() + copytree_mock.assert_has_calls([ + mock.call(Path(package_ahriman.base), application.repository.paths.cache_for(package_ahriman.base)), + mock.call(application.repository.paths.cache_for(package_ahriman.base), + application.repository.paths.manual_for(package_ahriman.base)), + ]) -def test_add_manual_with_dependencies(application: Application, package_ahriman: Package, - mocker: MockerFixture) -> None: +def test_add_local_with_dependencies(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: """ - must add package from AUR with dependencies + must add package from local sources with dependencies """ mocker.patch("ahriman.application.application.Application._known_packages", return_value=set()) mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) - mocker.patch("ahriman.core.build_tools.sources.Sources.load") + mocker.patch("ahriman.core.build_tools.sources.Sources.init") + mocker.patch("shutil.copytree") dependencies_mock = mocker.patch("ahriman.models.package.Package.dependencies") - application.add([package_ahriman.base], PackageSource.AUR, False) + application.add([package_ahriman.base], PackageSource.Local, False) dependencies_mock.assert_called_once() -def test_add_package(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: - """ - must add package from archive - """ - mocker.patch("ahriman.application.application.Application._known_packages", return_value=set()) - move_mock = mocker.patch("shutil.move") - - application.add([package_ahriman.base], PackageSource.Archive, False) - move_mock.assert_called_once() - - def test_clean_build(application: Application, mocker: MockerFixture) -> None: """ must clean build directory @@ -282,23 +315,37 @@ def test_sync(application: Application, mocker: MockerFixture) -> None: executor_mock.assert_called_once() -def test_unknown(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: +def test_unknown_no_aur(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: """ - must return list of packages missing in aur + must return empty list in case if there is locally stored PKGBUILD """ mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman]) mocker.patch("ahriman.models.package.Package.from_aur", side_effect=Exception()) + mocker.patch("pathlib.Path.is_dir", return_value=True) + mocker.patch("ahriman.core.build_tools.sources.Sources.has_remotes", return_value=False) + + assert not application.unknown() + + +def test_unknown_no_aur_no_local(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must return list of packages missing in aur and in local storage + """ + mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman]) + mocker.patch("ahriman.models.package.Package.from_aur", side_effect=Exception()) + mocker.patch("pathlib.Path.is_dir", return_value=False) packages = application.unknown() assert packages == [package_ahriman] -def test_unknown_empty(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: +def test_unknown_no_local(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: """ - must return list of packages missing in aur + must return empty list in case if there is package in AUR """ mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman]) mocker.patch("ahriman.models.package.Package.from_aur") + mocker.patch("pathlib.Path.is_dir", return_value=False) assert not application.unknown() diff --git a/tests/ahriman/core/build_tools/test_sources.py b/tests/ahriman/core/build_tools/test_sources.py index 084acbad..a3ef10a1 100644 --- a/tests/ahriman/core/build_tools/test_sources.py +++ b/tests/ahriman/core/build_tools/test_sources.py @@ -35,19 +35,34 @@ def test_diff(mocker: MockerFixture) -> None: check_output_mock.assert_called_with("git", "diff", exception=None, cwd=local, logger=pytest.helpers.anyvar(int)) -def test_fetch_existing(mocker: MockerFixture) -> None: +def test_fetch_empty(mocker: MockerFixture) -> None: """ - must fetch new package via clone command + must do nothing in case if no branches available """ mocker.patch("pathlib.Path.is_dir", return_value=True) + mocker.patch("ahriman.core.build_tools.sources.Sources.has_remotes", return_value=False) + check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") + + Sources.fetch(Path("local"), "remote") + check_output_mock.assert_not_called() + + +def test_fetch_existing(mocker: MockerFixture) -> None: + """ + must fetch new package via fetch command + """ + mocker.patch("pathlib.Path.is_dir", return_value=True) + mocker.patch("ahriman.core.build_tools.sources.Sources.has_remotes", return_value=True) check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") local = Path("local") - Sources.fetch(local, "remote", "master") + Sources.fetch(local, "remote") check_output_mock.assert_has_calls([ - mock.call("git", "fetch", "origin", "master", exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), - mock.call("git", "checkout", "--force", "master", exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), - mock.call("git", "reset", "--hard", "origin/master", + mock.call("git", "fetch", "origin", Sources._branch, + exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), + mock.call("git", "checkout", "--force", Sources._branch, + exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), + mock.call("git", "reset", "--hard", f"origin/{Sources._branch}", exception=None, cwd=local, logger=pytest.helpers.anyvar(int)) ]) @@ -60,15 +75,47 @@ def test_fetch_new(mocker: MockerFixture) -> None: check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") local = Path("local") - Sources.fetch(local, "remote", "master") + Sources.fetch(local, "remote") check_output_mock.assert_has_calls([ mock.call("git", "clone", "remote", str(local), exception=None, logger=pytest.helpers.anyvar(int)), - mock.call("git", "checkout", "--force", "master", exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), - mock.call("git", "reset", "--hard", "origin/master", + mock.call("git", "checkout", "--force", Sources._branch, + exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), + mock.call("git", "reset", "--hard", f"origin/{Sources._branch}", exception=None, cwd=local, logger=pytest.helpers.anyvar(int)) ]) +def test_has_remotes(mocker: MockerFixture) -> None: + """ + must ask for remotes + """ + check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output", return_value="origin") + + local = Path("local") + assert Sources.has_remotes(local) + check_output_mock.assert_called_with("git", "remote", exception=None, cwd=local, logger=pytest.helpers.anyvar(int)) + + +def test_has_remotes_empty(mocker: MockerFixture) -> None: + """ + must ask for remotes and return false in case if no remotes found + """ + mocker.patch("ahriman.core.build_tools.sources.Sources._check_output", return_value="") + assert not Sources.has_remotes(Path("local")) + + +def test_init(mocker: MockerFixture) -> None: + """ + must create empty repository at the specified path + """ + check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") + + local = Path("local") + Sources.init(local) + check_output_mock.assert_called_with("git", "init", "--initial-branch", Sources._branch, + exception=None, cwd=local, logger=pytest.helpers.anyvar(int)) + + def test_load(mocker: MockerFixture) -> None: """ must load packages sources correctly diff --git a/tests/ahriman/core/repository/test_executor.py b/tests/ahriman/core/repository/test_executor.py index 2062e37a..d8ca4e7c 100644 --- a/tests/ahriman/core/repository/test_executor.py +++ b/tests/ahriman/core/repository/test_executor.py @@ -60,13 +60,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]) + tree_clear_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_clear") repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove") status_client_mock = mocker.patch("ahriman.core.status.client.Client.remove") executor.process_remove([package_ahriman.base]) # must remove via alpm wrapper repo_remove_mock.assert_called_once() - # must update status + # must update status and remove package files + tree_clear_mock.assert_called_with(package_ahriman.base) status_client_mock.assert_called_once() @@ -106,6 +108,15 @@ def test_process_remove_base_single(executor: Executor, package_python_schedule: 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.models.repository_paths.RepositoryPaths.tree_clear", 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 """ diff --git a/tests/ahriman/core/repository/test_properties.py b/tests/ahriman/core/repository/test_properties.py index cda9ca4c..262138cf 100644 --- a/tests/ahriman/core/repository/test_properties.py +++ b/tests/ahriman/core/repository/test_properties.py @@ -9,17 +9,17 @@ def test_create_tree_on_load(configuration: Configuration, mocker: MockerFixture """ must create tree on load """ - create_tree_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.create_tree") + tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") Properties("x86_64", configuration, True) - create_tree_mock.assert_called_once() + tree_create_mock.assert_called_once() def test_create_dummy_report_client(configuration: Configuration, mocker: MockerFixture) -> None: """ must create dummy report client if report is disabled """ - mocker.patch("ahriman.models.repository_paths.RepositoryPaths.create_tree") + mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") load_mock = mocker.patch("ahriman.core.status.client.Client.load") properties = Properties("x86_64", configuration, True) @@ -31,7 +31,7 @@ def test_create_full_report_client(configuration: Configuration, mocker: MockerF """ must create load report client if report is enabled """ - mocker.patch("ahriman.models.repository_paths.RepositoryPaths.create_tree") + mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") load_mock = mocker.patch("ahriman.core.status.client.Client.load") Properties("x86_64", configuration, False) diff --git a/tests/ahriman/models/test_package_source.py b/tests/ahriman/models/test_package_source.py index 78068a5d..9bcecb36 100644 --- a/tests/ahriman/models/test_package_source.py +++ b/tests/ahriman/models/test_package_source.py @@ -1,8 +1,21 @@ from pytest_mock import MockerFixture +from pathlib import Path +from typing import Callable from ahriman.models.package_source import PackageSource +def _is_file_mock(is_any_file: bool, is_pkgbuild: bool) -> Callable[[Path], bool]: + """ + helper to mock is_file method + :param is_any_file: value which will be return for any file + :param is_pkgbuild: value which will be return if PKGBUILD like path asked + :return: side effect function for the mocker object + """ + side_effect: Callable[[Path], bool] = lambda source: is_pkgbuild if source.name == "PKGBUILD" else is_any_file + return side_effect + + def test_resolve_non_auto() -> None: """ must resolve non auto type to itself @@ -16,19 +29,10 @@ def test_resolve_archive(mocker: MockerFixture) -> None: must resolve auto type into the archive """ mocker.patch("pathlib.Path.is_dir", return_value=False) - mocker.patch("pathlib.Path.is_file", return_value=True) + mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=_is_file_mock(True, False)) assert PackageSource.Auto.resolve("linux-5.14.2.arch1-2-x86_64.pkg.tar.zst") == PackageSource.Archive -def test_resolve_directory(mocker: MockerFixture) -> None: - """ - must resolve auto type into the directory - """ - mocker.patch("pathlib.Path.is_dir", return_value=True) - mocker.patch("pathlib.Path.is_file", return_value=False) - assert PackageSource.Auto.resolve("path") == PackageSource.Directory - - def test_resolve_aur(mocker: MockerFixture) -> None: """ must resolve auto type into the AUR package @@ -43,5 +47,23 @@ def test_resolve_aur_not_package_like(mocker: MockerFixture) -> None: must resolve auto type into the AUR package if it is file, but does not look like a package archive """ mocker.patch("pathlib.Path.is_dir", return_value=False) - mocker.patch("pathlib.Path.is_file", return_value=True) + mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=_is_file_mock(True, False)) assert PackageSource.Auto.resolve("package") == PackageSource.AUR + + +def test_resolve_directory(mocker: MockerFixture) -> None: + """ + must resolve auto type into the directory + """ + mocker.patch("pathlib.Path.is_dir", return_value=True) + mocker.patch("pathlib.Path.is_file", return_value=False) + assert PackageSource.Auto.resolve("path") == PackageSource.Directory + + +def test_resolve_local(mocker: MockerFixture) -> None: + """ + must resolve auto type into the directory + """ + mocker.patch("pathlib.Path.is_dir", return_value=False) + mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=_is_file_mock(True, True)) + assert PackageSource.Auto.resolve("path") == PackageSource.Local diff --git a/tests/ahriman/models/test_repository_paths.py b/tests/ahriman/models/test_repository_paths.py index c91058fc..43eb8883 100644 --- a/tests/ahriman/models/test_repository_paths.py +++ b/tests/ahriman/models/test_repository_paths.py @@ -23,27 +23,6 @@ def test_cache_for(repository_paths: RepositoryPaths, package_ahriman: Package) assert path.parent == repository_paths.cache -def test_create_tree(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: - """ - must create whole tree - """ - paths = { - prop - for prop in dir(repository_paths) - if not prop.startswith("_") - and not prop.endswith("_for") - and prop not in ("architecture", "create_tree", "known_architectures", "root") - } - mkdir_mock = mocker.patch("pathlib.Path.mkdir") - - repository_paths.create_tree() - mkdir_mock.assert_has_calls( - [ - mock.call(mode=0o755, parents=True, exist_ok=True) - for _ in paths - ]) - - def test_manual_for(repository_paths: RepositoryPaths, package_ahriman: Package) -> None: """ must return correct path for manual directory @@ -69,3 +48,41 @@ def test_sources_for(repository_paths: RepositoryPaths, package_ahriman: Package path = repository_paths.sources_for(package_ahriman.base) assert path.name == package_ahriman.base assert path.parent == repository_paths.sources + + +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") + } + rmtree_mock = mocker.patch("shutil.rmtree") + + repository_paths.tree_clear(package_ahriman.base) + rmtree_mock.assert_has_calls( + [ + mock.call(path, ignore_errors=True) for path in paths + ], any_order=True) + + +def test_tree_create(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must create whole tree + """ + paths = { + prop + for prop in dir(repository_paths) + if not prop.startswith("_") + and not prop.endswith("_for") + and prop not in ("architecture", "known_architectures", "root", "tree_clear", "tree_create") + } + mkdir_mock = mocker.patch("pathlib.Path.mkdir") + + repository_paths.tree_create() + mkdir_mock.assert_has_calls( + [ + mock.call(mode=0o755, parents=True, exist_ok=True) + for _ in paths + ], any_order=True)