add ability to add manually stored packages (#40)

* add ability to add manually stored packages

* update tests

* handle manual packages in remove-unknown method

* live fixes

also rename branches to has_remotes method and change return type
This commit is contained in:
Evgenii Alekseev 2021-10-12 21:15:35 +03:00 committed by GitHub
parent ab8ca16981
commit 72b26603bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 371 additions and 144 deletions

View File

@ -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", 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 " 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) 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("-n", "--now", help="run update function after", action="store_true")
parser.add_argument("-s", "--source", help="package source", parser.add_argument("-s", "--source", help="package source",
type=PackageSource, choices=PackageSource, default=PackageSource.Auto) type=PackageSource, choices=PackageSource, default=PackageSource.Auto)

View File

@ -105,21 +105,30 @@ class Application:
:param without_dependencies: if set, dependency check will be disabled :param without_dependencies: if set, dependency check will be disabled
""" """
known_packages = self._known_packages() 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: def add_directory(path: Path) -> None:
for full_path in filter(package_like, path.iterdir()): for full_path in filter(package_like, path.iterdir()):
add_archive(full_path) add_archive(full_path)
def add_manual(src: str) -> Path: def add_local(path: Path) -> Path:
package = Package.load(src, self.repository.pacman, self.configuration.get("alpm", "aur_url")) 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, Sources.load(self.repository.paths.manual_for(package.base), package.git_url,
self.repository.paths.patches_for(package.base)) self.repository.paths.patches_for(package.base))
return self.repository.paths.manual_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: def process_dependencies(path: Path) -> None:
if without_dependencies: if without_dependencies:
return return
@ -128,12 +137,15 @@ class Application:
def process_single(src: str) -> None: def process_single(src: str) -> None:
resolved_source = source.resolve(src) resolved_source = source.resolve(src)
if resolved_source == PackageSource.Directory: if resolved_source == PackageSource.Archive:
add_directory(Path(src))
elif resolved_source == PackageSource.Archive:
add_archive(Path(src)) add_archive(Path(src))
else: elif resolved_source == PackageSource.AUR:
path = add_manual(src) 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) process_dependencies(path)
for name in names: for name in names:
@ -213,13 +225,22 @@ class Application:
get packages which were not found in AUR get packages which were not found in AUR
:return: unknown package list :return: unknown package list
""" """
packages = [] def has_aur(package_base: str, aur_url: str) -> bool:
for base in self.repository.packages():
try: try:
_ = Package.from_aur(base.base, base.aur_url) _ = Package.from_aur(package_base, aur_url)
except Exception: except Exception:
packages.append(base) return False
return packages 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: def update(self, updates: Iterable[Package]) -> None:
""" """

View File

@ -33,72 +33,98 @@ class Sources:
logger = logging.getLogger("build_details") logger = logging.getLogger("build_details")
_branch = "master" # in case if BLM would like to change it
_check_output = check_output _check_output = check_output
@staticmethod @staticmethod
def add(local_path: Path, *pattern: str) -> None: def add(sources_dir: Path, *pattern: str) -> None:
""" """
track found files via git 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 :param pattern: glob patterns
""" """
# glob directory to find files which match the specified patterns # glob directory to find files which match the specified patterns
found_files: List[Path] = [] found_files: List[Path] = []
for glob in pattern: 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) Sources.logger.info("found matching files %s", found_files)
# add them to index # add them to index
Sources._check_output("git", "add", "--intent-to-add", *[str(fn.relative_to(local_path)) for fn in found_files], Sources._check_output("git", "add", "--intent-to-add",
exception=None, cwd=local_path, logger=Sources.logger) *[str(fn.relative_to(sources_dir)) for fn in found_files],
exception=None, cwd=sources_dir, logger=Sources.logger)
@staticmethod @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 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 :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) patch_path.write_text(patch)
@staticmethod @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` 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 remote: remote target (from where to fetch)
:param branch: branch name to checkout, master by default
""" """
# local directory exists and there is .git directory # local directory exists and there is .git directory
if (local_path / ".git").is_dir(): is_initialized_git = (sources_dir / ".git").is_dir()
Sources.logger.info("update HEAD to remote to %s", local_path) if is_initialized_git and not Sources.has_remotes(sources_dir):
Sources._check_output("git", "fetch", "origin", branch, # there is git repository, but no remote configured so far
exception=None, cwd=local_path, logger=Sources.logger) 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: else:
Sources.logger.info("clone remote %s to %s", remote, local_path) Sources.logger.info("clone remote %s to %s", remote, sources_dir)
Sources._check_output("git", "clone", remote, str(local_path), exception=None, logger=Sources.logger) Sources._check_output("git", "clone", remote, str(sources_dir), exception=None, logger=Sources.logger)
# and now force reset to our branch # and now force reset to our branch
Sources._check_output("git", "checkout", "--force", branch, Sources._check_output("git", "checkout", "--force", Sources._branch,
exception=None, cwd=local_path, logger=Sources.logger) exception=None, cwd=sources_dir, logger=Sources.logger)
Sources._check_output("git", "reset", "--hard", f"origin/{branch}", Sources._check_output("git", "reset", "--hard", f"origin/{Sources._branch}",
exception=None, cwd=local_path, logger=Sources.logger) exception=None, cwd=sources_dir, logger=Sources.logger)
@staticmethod @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 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 remote: remote target (from where to fetch)
:param patch_dir: path to directory with package patches :param patch_dir: path to directory with package patches
""" """
Sources.fetch(local_path, remote) Sources.fetch(sources_dir, remote)
Sources.patch_apply(local_path, patch_dir) Sources.patch_apply(sources_dir, patch_dir)
@staticmethod @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 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 :param patch_dir: path to directory with package patches
""" """
# check if even there are patches # check if even there are patches
@ -110,15 +136,15 @@ class Sources:
for patch in patches: for patch in patches:
Sources.logger.info("apply patch %s", patch.name) Sources.logger.info("apply patch %s", patch.name)
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace", str(patch), 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 @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 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 patch_path: path to result patch
:param pattern: glob patterns :param pattern: glob patterns
""" """
Sources.add(local_path, *pattern) Sources.add(sources_dir, *pattern)
Sources.diff(local_path, patch_path) Sources.diff(sources_dir, patch_path)

View File

@ -72,9 +72,16 @@ class Executor(Cleaner):
:param packages: list of package names or bases to remove :param packages: list of package names or bases to remove
:return: path to repository database :return: path to repository database
""" """
def remove_single(package: str, fn: Path) -> None: def remove_base(package_base: str) -> None:
try: 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: except Exception:
self.logger.exception("could not remove %s", package) self.logger.exception("could not remove %s", package)
@ -86,7 +93,7 @@ class Executor(Cleaner):
for package, properties in local.packages.items() for package, properties in local.packages.items()
if properties.filename is not None 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()): elif requested.intersection(local.packages.keys()):
to_remove = { to_remove = {
package: Path(properties.filename) package: Path(properties.filename)
@ -95,8 +102,9 @@ class Executor(Cleaner):
} }
else: else:
to_remove = {} to_remove = {}
for package, filename in to_remove.items(): for package, filename in to_remove.items():
remove_single(package, filename) remove_package(package, filename)
return self.repo.repo_path return self.repo.repo_path

View File

@ -58,7 +58,7 @@ class Properties:
self.name = configuration.get("repository", "name") self.name = configuration.get("repository", "name")
self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture) 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.ignore_list = configuration.getlist("build", "ignore_packages", fallback=[])
self.pacman = Pacman(configuration) self.pacman = Pacman(configuration)

View File

@ -30,14 +30,16 @@ class PackageSource(Enum):
package source for addition enumeration package source for addition enumeration
:cvar Auto: automatically determine type of the source :cvar Auto: automatically determine type of the source
:cvar Archive: source is a package archive :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 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" Auto = "auto"
Archive = "archive" Archive = "archive"
Directory = "directory"
AUR = "aur" AUR = "aur"
Directory = "directory"
Local = "local"
def resolve(self, source: str) -> PackageSource: def resolve(self, source: str) -> PackageSource:
""" """
@ -47,7 +49,10 @@ class PackageSource(Enum):
""" """
if self != PackageSource.Auto: if self != PackageSource.Auto:
return self return self
maybe_path = Path(source) maybe_path = Path(source)
if (maybe_path / "PKGBUILD").is_file():
return PackageSource.Local
if maybe_path.is_dir(): if maybe_path.is_dir():
return PackageSource.Directory return PackageSource.Directory
if maybe_path.is_file() and package_like(maybe_path): if maybe_path.is_file() and package_like(maybe_path):

View File

@ -19,6 +19,8 @@
# #
from __future__ import annotations from __future__ import annotations
import shutil
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Set, Type from typing import Set, Type
@ -101,24 +103,12 @@ class RepositoryPaths:
def cache_for(self, package_base: str) -> Path: 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 :param package_base: package base name
:return: full path to directory for specified package base cache :return: full path to directory for specified package base cache
""" """
return self.cache / package_base 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: def manual_for(self, package_base: str) -> Path:
""" """
get manual path for specific package base get manual path for specific package base
@ -137,8 +127,34 @@ class RepositoryPaths:
def sources_for(self, package_base: str) -> Path: 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 :param package_base: package base name
:return: full path to directory for specified package base sources :return: full path to directory for specified package base sources
""" """
return self.sources / package_base 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)

View File

@ -10,11 +10,11 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
""" """
must run command 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_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init")
Init.run(args, "x86_64", configuration, True) Init.run(args, "x86_64", configuration, True)
create_tree_mock.assert_called_once() tree_create_mock.assert_called_once()
init_mock.assert_called_once() init_mock.assert_called_once()

View File

@ -1,5 +1,6 @@
import pytest import pytest
from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest import mock from unittest import mock
@ -104,6 +105,43 @@ def test_get_updates_with_filter(application: Application, mocker: MockerFixture
updates_manual_mock.assert_called_once() 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: def test_add_directory(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must add packages from directory must add packages from directory
@ -118,43 +156,38 @@ def test_add_directory(application: Application, package_ahriman: Package, mocke
move_mock.assert_called_once() 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.application.application.Application._known_packages", return_value=set())
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) 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) application.add([package_ahriman.base], PackageSource.Local, True)
load_mock.assert_called_once() 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, def test_add_local_with_dependencies(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
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.application.application.Application._known_packages", return_value=set())
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) 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") 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() 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: def test_clean_build(application: Application, mocker: MockerFixture) -> None:
""" """
must clean build directory must clean build directory
@ -282,23 +315,37 @@ def test_sync(application: Application, mocker: MockerFixture) -> None:
executor_mock.assert_called_once() 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.core.repository.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=Exception()) 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() packages = application.unknown()
assert packages == [package_ahriman] 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.core.repository.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.models.package.Package.from_aur") mocker.patch("ahriman.models.package.Package.from_aur")
mocker.patch("pathlib.Path.is_dir", return_value=False)
assert not application.unknown() assert not application.unknown()

View File

@ -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)) 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("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") check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
local = Path("local") local = Path("local")
Sources.fetch(local, "remote", "master") Sources.fetch(local, "remote")
check_output_mock.assert_has_calls([ check_output_mock.assert_has_calls([
mock.call("git", "fetch", "origin", "master", exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), mock.call("git", "fetch", "origin", Sources._branch,
mock.call("git", "checkout", "--force", "master", exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), 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)) 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") check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
local = Path("local") local = Path("local")
Sources.fetch(local, "remote", "master") Sources.fetch(local, "remote")
check_output_mock.assert_has_calls([ check_output_mock.assert_has_calls([
mock.call("git", "clone", "remote", str(local), exception=None, logger=pytest.helpers.anyvar(int)), 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", "checkout", "--force", Sources._branch,
mock.call("git", "reset", "--hard", "origin/master", 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)) 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: def test_load(mocker: MockerFixture) -> None:
""" """
must load packages sources correctly must load packages sources correctly

View File

@ -60,13 +60,15 @@ def test_process_remove_base(executor: Executor, package_ahriman: Package, mocke
must run remove process for whole base must run remove process for whole base
""" """
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman]) 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") repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
status_client_mock = mocker.patch("ahriman.core.status.client.Client.remove") status_client_mock = mocker.patch("ahriman.core.status.client.Client.remove")
executor.process_remove([package_ahriman.base]) executor.process_remove([package_ahriman.base])
# must remove via alpm wrapper # must remove via alpm wrapper
repo_remove_mock.assert_called_once() 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() 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: 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 must suppress remove errors
""" """

View File

@ -9,17 +9,17 @@ def test_create_tree_on_load(configuration: Configuration, mocker: MockerFixture
""" """
must create tree on load 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) 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: def test_create_dummy_report_client(configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must create dummy report client if report is disabled 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") load_mock = mocker.patch("ahriman.core.status.client.Client.load")
properties = Properties("x86_64", configuration, True) 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 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") load_mock = mocker.patch("ahriman.core.status.client.Client.load")
Properties("x86_64", configuration, False) Properties("x86_64", configuration, False)

View File

@ -1,8 +1,21 @@
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from pathlib import Path
from typing import Callable
from ahriman.models.package_source import PackageSource 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: def test_resolve_non_auto() -> None:
""" """
must resolve non auto type to itself 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 must resolve auto type into the archive
""" """
mocker.patch("pathlib.Path.is_dir", return_value=False) 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 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: def test_resolve_aur(mocker: MockerFixture) -> None:
""" """
must resolve auto type into the AUR package 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 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_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 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

View File

@ -23,27 +23,6 @@ def test_cache_for(repository_paths: RepositoryPaths, package_ahriman: Package)
assert path.parent == repository_paths.cache 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: def test_manual_for(repository_paths: RepositoryPaths, package_ahriman: Package) -> None:
""" """
must return correct path for manual directory 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) path = repository_paths.sources_for(package_ahriman.base)
assert path.name == package_ahriman.base assert path.name == package_ahriman.base
assert path.parent == repository_paths.sources 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)