diff --git a/docs/ahriman.core.build_tools.rst b/docs/ahriman.core.build_tools.rst index 4ef0608a..893efaa1 100644 --- a/docs/ahriman.core.build_tools.rst +++ b/docs/ahriman.core.build_tools.rst @@ -12,6 +12,14 @@ ahriman.core.build\_tools.package\_archive module :no-undoc-members: :show-inheritance: +ahriman.core.build\_tools.package\_version module +------------------------------------------------- + +.. automodule:: ahriman.core.build_tools.package_version + :members: + :no-undoc-members: + :show-inheritance: + ahriman.core.build\_tools.sources module ---------------------------------------- diff --git a/src/ahriman/core/build_tools/package_version.py b/src/ahriman/core/build_tools/package_version.py new file mode 100644 index 00000000..4637af7d --- /dev/null +++ b/src/ahriman/core/build_tools/package_version.py @@ -0,0 +1,117 @@ +# +# Copyright (c) 2021-2026 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from pyalpm import vercmp # type: ignore[import-not-found] + +from ahriman.core.build_tools.task import Task +from ahriman.core.configuration import Configuration +from ahriman.core.log import LazyLogging +from ahriman.core.utils import full_version, utcnow +from ahriman.models.package import Package +from ahriman.models.pkgbuild import Pkgbuild + + +class PackageVersion(LazyLogging): + """ + package version extractor and helper + + Attributes: + package(Package): package definitions + """ + + def __init__(self, package: Package) -> None: + """ + Args: + package(Package): package definitions + """ + self.package = package + + def actual_version(self, configuration: Configuration) -> str: + """ + additional method to handle VCS package versions + + Args: + configuration(Configuration): configuration instance + + Returns: + str: package version if package is not VCS and current version according to VCS otherwise + """ + if not self.package.is_vcs: + return self.package.version + + _, repository_id = configuration.check_loaded() + paths = configuration.repository_paths + task = Task(self.package, configuration, repository_id.architecture, paths) + + try: + # create fresh chroot environment, fetch sources and - automagically - update PKGBUILD + task.init(paths.cache_for(self.package.base), [], None) + pkgbuild = Pkgbuild.from_file(paths.cache_for(self.package.base) / "PKGBUILD") + + return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"]) + except Exception: + self.logger.exception("cannot determine version of VCS package") + finally: + # clear log files generated by devtools + for log_file in paths.cache_for(self.package.base).glob("*.log"): + log_file.unlink() + + return self.package.version + + def is_newer_than(self, timestamp: float | int) -> bool: + """ + check if package was built after the specified timestamp + + Args: + timestamp(float | int): timestamp to check build date against + + Returns: + bool: ``True`` in case if package was built after the specified date and ``False`` otherwise. + In case if build date is not set by any of packages, it returns False + """ + return any( + package.build_date > timestamp + for package in self.package.packages.values() + if package.build_date is not None + ) + + def is_outdated(self, remote: Package, configuration: Configuration, *, + calculate_version: bool = True) -> bool: + """ + check if package is out-of-dated + + Args: + remote(Package): package properties from remote source + configuration(Configuration): configuration instance + calculate_version(bool, optional): expand version to actual value (by calculating git versions) + (Default value = True) + + Returns: + bool: ``True`` if the package is out-of-dated and ``False`` otherwise + """ + vcs_allowed_age = configuration.getint("build", "vcs_allowed_age", fallback=0) + min_vcs_build_date = utcnow().timestamp() - vcs_allowed_age + + if calculate_version and not self.is_newer_than(min_vcs_build_date): + remote_version = PackageVersion(remote).actual_version(configuration) + else: + remote_version = remote.version + + result: int = vercmp(self.package.version, remote_version) + return result < 0 diff --git a/src/ahriman/core/build_tools/sources.py b/src/ahriman/core/build_tools/sources.py index 8808497d..82ea5bb8 100644 --- a/src/ahriman/core/build_tools/sources.py +++ b/src/ahriman/core/build_tools/sources.py @@ -27,6 +27,7 @@ from ahriman.core.exceptions import CalledProcessError from ahriman.core.log import LazyLogging from ahriman.core.utils import check_output, utcnow, walk from ahriman.models.package import Package +from ahriman.models.pkgbuild import Pkgbuild from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.remote_source import RemoteSource from ahriman.models.repository_paths import RepositoryPaths @@ -81,7 +82,7 @@ class Sources(LazyLogging): Returns: list[PkgbuildPatch]: generated patch for PKGBUILD architectures if required """ - architectures = Package.supported_architectures(sources_dir) + architectures = Pkgbuild.supported_architectures(sources_dir) if "any" in architectures: # makepkg does not like when there is any other arch except for any return [] architectures.add(architecture) @@ -161,7 +162,7 @@ class Sources(LazyLogging): cwd=sources_dir, logger=instance.logger) # extract local files... - files = ["PKGBUILD", ".SRCINFO"] + [str(path) for path in Package.local_files(sources_dir)] + files = ["PKGBUILD", ".SRCINFO"] + [str(path) for path in Pkgbuild.local_files(sources_dir)] instance.add(sources_dir, *files) # ...and commit them instance.commit(sources_dir) diff --git a/src/ahriman/core/repository/package_info.py b/src/ahriman/core/repository/package_info.py index 097e4ce8..717e31e1 100644 --- a/src/ahriman/core/repository/package_info.py +++ b/src/ahriman/core/repository/package_info.py @@ -17,10 +17,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +import copy + from collections.abc import Iterable from pathlib import Path from tempfile import TemporaryDirectory +from ahriman.core.build_tools.package_version import PackageVersion from ahriman.core.build_tools.sources import Sources from ahriman.core.repository.repository_properties import RepositoryProperties from ahriman.core.utils import package_like @@ -33,6 +36,40 @@ class PackageInfo(RepositoryProperties): handler for the package information """ + def full_depends(self, package: Package, packages: Iterable[Package]) -> list[str]: + """ + generate full dependencies list including transitive dependencies + + Args: + package(Package): package to check dependencies for + packages(Iterable[Package]): repository package list + + Returns: + list[str]: all dependencies of the package + """ + dependencies = {} + # load own package dependencies + for package_base in packages: + for name, repo_package in package_base.packages.items(): + dependencies[name] = repo_package.depends + for provides in repo_package.provides: + dependencies[provides] = repo_package.depends + # load repository dependencies + for database in self.pacman.handle.get_syncdbs(): + for pacman_package in database.pkgcache: + dependencies[pacman_package.name] = pacman_package.depends + for provides in pacman_package.provides: + dependencies[provides] = pacman_package.depends + + result = set(package.depends) + current_depends: set[str] = set() + while result != current_depends: + current_depends = copy.deepcopy(result) + for package_name in current_depends: + result.update(dependencies.get(package_name, [])) + + return sorted(result) + def load_archives(self, packages: Iterable[Path]) -> list[Package]: """ load packages from list of archives @@ -58,7 +95,7 @@ class PackageInfo(RepositoryProperties): # force version to max of them self.logger.warning("version of %s differs, found %s and %s", current.base, current.version, local.version) - if current.is_outdated(local, self.configuration, calculate_version=False): + if PackageVersion(current).is_outdated(local, self.configuration, calculate_version=False): current.version = local.version current.packages.update(local.packages) except Exception: @@ -130,5 +167,5 @@ class PackageInfo(RepositoryProperties): return [ package for package in packages - if depends_on.intersection(package.full_depends(self.pacman, packages)) + if depends_on.intersection(self.full_depends(package, packages)) ] diff --git a/src/ahriman/core/repository/update_handler.py b/src/ahriman/core/repository/update_handler.py index d5ee20b2..1a992489 100644 --- a/src/ahriman/core/repository/update_handler.py +++ b/src/ahriman/core/repository/update_handler.py @@ -19,6 +19,7 @@ # from collections.abc import Iterable +from ahriman.core.build_tools.package_version import PackageVersion from ahriman.core.build_tools.sources import Sources from ahriman.core.exceptions import UnknownPackageError from ahriman.core.repository.cleaner import Cleaner @@ -68,7 +69,7 @@ class UpdateHandler(PackageInfo, Cleaner): try: remote = load_remote(local) - if local.is_outdated(remote, self.configuration, calculate_version=vcs): + if PackageVersion(local).is_outdated(remote, self.configuration, calculate_version=vcs): self.reporter.set_pending(local.base) self.event(local.base, EventType.PackageOutdated, "Remote version is newer than local") result.append(remote) @@ -157,7 +158,7 @@ class UpdateHandler(PackageInfo, Cleaner): if local.remote.is_remote: continue # avoid checking AUR packages - if local.is_outdated(remote, self.configuration, calculate_version=vcs): + if PackageVersion(local).is_outdated(remote, self.configuration, calculate_version=vcs): self.reporter.set_pending(local.base) self.event(local.base, EventType.PackageOutdated, "Locally pulled sources are outdated") result.append(remote) diff --git a/src/ahriman/core/utils.py b/src/ahriman/core/utils.py index 065eebf4..8be3676d 100644 --- a/src/ahriman/core/utils.py +++ b/src/ahriman/core/utils.py @@ -35,6 +35,7 @@ from pwd import getpwuid from typing import Any, IO, TypeVar from ahriman.core.exceptions import CalledProcessError, OptionError, UnsafeRunError +from ahriman.core.types import Comparable __all__ = [ @@ -45,6 +46,7 @@ __all__ = [ "extract_user", "filter_json", "full_version", + "list_flatmap", "minmax", "owner", "package_like", @@ -62,6 +64,7 @@ __all__ = [ ] +R = TypeVar("R", bound=Comparable) T = TypeVar("T") @@ -276,6 +279,24 @@ def full_version(epoch: str | int | None, pkgver: str, pkgrel: str) -> str: return f"{prefix}{pkgver}-{pkgrel}" +def list_flatmap(source: Iterable[T], extractor: Callable[[T], list[R]]) -> list[R]: + """ + extract elements from list of lists, flatten them and apply ``extractor`` + + Args: + source(Iterable[T]): source list + extractor(Callable[[T], list[R]): property extractor + + Returns: + list[T]: combined list of unique entries in properties list + """ + def generator() -> Iterator[R]: + for inner in source: + yield from extractor(inner) + + return sorted(set(generator())) + + def minmax(source: Iterable[T], *, key: Callable[[T], Any] | None = None) -> tuple[T, T]: """ get min and max value from iterable diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py index 3d31eabd..145b42f7 100644 --- a/src/ahriman/models/package.py +++ b/src/ahriman/models/package.py @@ -17,23 +17,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -# pylint: disable=too-many-lines,too-many-public-methods from __future__ import annotations -import copy - -from collections.abc import Callable, Iterable, Iterator +from collections.abc import Iterable from dataclasses import dataclass from pathlib import Path from pyalpm import vercmp # type: ignore[import-not-found] from typing import Any, Self -from urllib.parse import urlparse from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.remote import AUR, Official, OfficialSyncdb -from ahriman.core.configuration import Configuration from ahriman.core.log import LazyLogging -from ahriman.core.utils import dataclass_view, full_version, parse_version, srcinfo_property_list, utcnow +from ahriman.core.utils import dataclass_view, full_version, list_flatmap, parse_version, srcinfo_property_list from ahriman.models.package_description import PackageDescription from ahriman.models.package_source import PackageSource from ahriman.models.pkgbuild import Pkgbuild @@ -89,7 +84,7 @@ class Package(LazyLogging): Returns: list[str]: sum of dependencies per each package """ - return self._package_list_property(lambda package: package.depends) + return list_flatmap(self.packages.values(), lambda package: package.depends) @property def depends_build(self) -> set[str]: @@ -109,7 +104,7 @@ class Package(LazyLogging): Returns: list[str]: sum of test dependencies per each package """ - return self._package_list_property(lambda package: package.check_depends) + return list_flatmap(self.packages.values(), lambda package: package.check_depends) @property def depends_make(self) -> list[str]: @@ -119,7 +114,7 @@ class Package(LazyLogging): Returns: list[str]: sum of make dependencies per each package """ - return self._package_list_property(lambda package: package.make_depends) + return list_flatmap(self.packages.values(), lambda package: package.make_depends) @property def depends_opt(self) -> list[str]: @@ -129,7 +124,7 @@ class Package(LazyLogging): Returns: list[str]: sum of optional dependencies per each package """ - return self._package_list_property(lambda package: package.opt_depends) + return list_flatmap(self.packages.values(), lambda package: package.opt_depends) @property def groups(self) -> list[str]: @@ -139,7 +134,7 @@ class Package(LazyLogging): Returns: list[str]: sum of groups per each package """ - return self._package_list_property(lambda package: package.groups) + return list_flatmap(self.packages.values(), lambda package: package.groups) @property def is_single_package(self) -> bool: @@ -174,7 +169,7 @@ class Package(LazyLogging): Returns: list[str]: sum of licenses per each package """ - return self._package_list_property(lambda package: package.licenses) + return list_flatmap(self.packages.values(), lambda package: package.licenses) @property def packages_full(self) -> list[str]: @@ -345,184 +340,6 @@ class Package(LazyLogging): packager=packager, ) - @staticmethod - def local_files(path: Path) -> Iterator[Path]: - """ - extract list of local files - - Args: - path(Path): path to package sources directory - - Yields: - Path: list of paths of files which belong to the package and distributed together with this tarball. - All paths are relative to the ``path`` - - Raises: - PackageInfoError: if there are parsing errors - """ - pkgbuild = Pkgbuild.from_file(path / "PKGBUILD") - # we could use arch property, but for consistency it is better to call special method - architectures = Package.supported_architectures(path) - - for architecture in architectures: - for source in srcinfo_property_list("source", pkgbuild, {}, architecture=architecture): - if "::" in source: - _, source = source.split("::", maxsplit=1) # in case if filename is specified, remove it - - if urlparse(source).scheme: - # basically file schema should use absolute path which is impossible if we are distributing - # files together with PKGBUILD. In this case we are going to skip it also - continue - - yield Path(source) - - if (install := pkgbuild.get("install")) is not None: - yield Path(install) - - @staticmethod - def supported_architectures(path: Path) -> set[str]: - """ - load supported architectures from package sources - - Args: - path(Path): path to package sources directory - - Returns: - set[str]: list of package supported architectures - """ - pkgbuild = Pkgbuild.from_file(path / "PKGBUILD") - return set(pkgbuild.get("arch", [])) - - def _package_list_property(self, extractor: Callable[[PackageDescription], list[str]]) -> list[str]: - """ - extract list property from single packages and combine them into one list - - Notes: - Basically this method is generic for type of ``list[T]``, but there is no trait ``Comparable`` in default - packages, thus we limit this method only to new types - - Args: - extractor(Callable[[PackageDescription], list[str]): package property extractor - - Returns: - list[str]: combined list of unique entries in properties list - """ - def generator() -> Iterator[str]: - for package in self.packages.values(): - yield from extractor(package) - - return sorted(set(generator())) - - def actual_version(self, configuration: Configuration) -> str: - """ - additional method to handle VCS package versions - - Args: - configuration(Configuration): configuration instance - - Returns: - str: package version if package is not VCS and current version according to VCS otherwise - """ - if not self.is_vcs: - return self.version - - from ahriman.core.build_tools.task import Task - - _, repository_id = configuration.check_loaded() - paths = configuration.repository_paths - task = Task(self, configuration, repository_id.architecture, paths) - - try: - # create fresh chroot environment, fetch sources and - automagically - update PKGBUILD - task.init(paths.cache_for(self.base), [], None) - pkgbuild = Pkgbuild.from_file(paths.cache_for(self.base) / "PKGBUILD") - - return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"]) - except Exception: - self.logger.exception("cannot determine version of VCS package") - finally: - # clear log files generated by devtools - for log_file in paths.cache_for(self.base).glob("*.log"): - log_file.unlink() - - return self.version - - def full_depends(self, pacman: Pacman, packages: Iterable[Package]) -> list[str]: - """ - generate full dependencies list including transitive dependencies - - Args: - pacman(Pacman): alpm wrapper instance - packages(Iterable[Package]): repository package list - - Returns: - list[str]: all dependencies of the package - """ - dependencies = {} - # load own package dependencies - for package_base in packages: - for name, repo_package in package_base.packages.items(): - dependencies[name] = repo_package.depends - for provides in repo_package.provides: - dependencies[provides] = repo_package.depends - # load repository dependencies - for database in pacman.handle.get_syncdbs(): - for pacman_package in database.pkgcache: - dependencies[pacman_package.name] = pacman_package.depends - for provides in pacman_package.provides: - dependencies[provides] = pacman_package.depends - - result = set(self.depends) - current_depends: set[str] = set() - while result != current_depends: - current_depends = copy.deepcopy(result) - for package in current_depends: - result.update(dependencies.get(package, [])) - - return sorted(result) - - def is_newer_than(self, timestamp: float | int) -> bool: - """ - check if package was built after the specified timestamp - - Args: - timestamp(float | int): timestamp to check build date against - - Returns: - bool: ``True`` in case if package was built after the specified date and ``False`` otherwise. - In case if build date is not set by any of packages, it returns False - """ - return any( - package.build_date > timestamp - for package in self.packages.values() - if package.build_date is not None - ) - - def is_outdated(self, remote: Package, configuration: Configuration, *, - calculate_version: bool = True) -> bool: - """ - check if package is out-of-dated - - Args: - remote(Package): package properties from remote source - configuration(Configuration): configuration instance - calculate_version(bool, optional): expand version to actual value (by calculating git versions) - (Default value = True) - - Returns: - bool: ``True`` if the package is out-of-dated and ``False`` otherwise - """ - vcs_allowed_age = configuration.getint("build", "vcs_allowed_age", fallback=0) - min_vcs_build_date = utcnow().timestamp() - vcs_allowed_age - - if calculate_version and not self.is_newer_than(min_vcs_build_date): - remote_version = remote.actual_version(configuration) - else: - remote_version = remote.version - - result: int = vercmp(self.version, remote_version) - return result < 0 - def next_pkgrel(self, local_version: str | None) -> str | None: """ generate next pkgrel variable. The package release will be incremented if ``local_version`` is more or equal to diff --git a/src/ahriman/models/pkgbuild.py b/src/ahriman/models/pkgbuild.py index 67152554..439f91b4 100644 --- a/src/ahriman/models/pkgbuild.py +++ b/src/ahriman/models/pkgbuild.py @@ -22,8 +22,10 @@ from dataclasses import dataclass from io import StringIO from pathlib import Path from typing import Any, ClassVar, IO, Self +from urllib.parse import urlparse from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser, PkgbuildToken +from ahriman.core.utils import srcinfo_property_list from ahriman.models.pkgbuild_patch import PkgbuildPatch @@ -103,6 +105,54 @@ class Pkgbuild(Mapping[str, Any]): return cls({key: value for key, value in fields.items() if key}) + @staticmethod + def local_files(path: Path) -> Iterator[Path]: + """ + extract list of local files + + Args: + path(Path): path to package sources directory + + Yields: + Path: list of paths of files which belong to the package and distributed together with this tarball. + All paths are relative to the ``path`` + + Raises: + PackageInfoError: if there are parsing errors + """ + pkgbuild = Pkgbuild.from_file(path / "PKGBUILD") + # we could use arch property, but for consistency it is better to call special method + architectures = Pkgbuild.supported_architectures(path) + + for architecture in architectures: + for source in srcinfo_property_list("source", pkgbuild, {}, architecture=architecture): + if "::" in source: + _, source = source.split("::", maxsplit=1) # in case if filename is specified, remove it + + if urlparse(source).scheme: + # basically file schema should use absolute path which is impossible if we are distributing + # files together with PKGBUILD. In this case we are going to skip it also + continue + + yield Path(source) + + if (install := pkgbuild.get("install")) is not None: + yield Path(install) + + @staticmethod + def supported_architectures(path: Path) -> set[str]: + """ + load supported architectures from package sources + + Args: + path(Path): path to package sources directory + + Returns: + set[str]: list of package supported architectures + """ + pkgbuild = Pkgbuild.from_file(path / "PKGBUILD") + return set(pkgbuild.get("arch", [])) + def packages(self) -> dict[str, Self]: """ extract properties from internal package functions diff --git a/tests/ahriman/conftest.py b/tests/ahriman/conftest.py index 706c6354..7f780f3b 100644 --- a/tests/ahriman/conftest.py +++ b/tests/ahriman/conftest.py @@ -352,6 +352,27 @@ def package_python_schedule( packages=packages) +@pytest.fixture +def package_tpacpi_bat_git() -> Package: + """ + git package fixture + + Returns: + Package: git package test instance + """ + return Package( + base="tpacpi-bat-git", + version="3.1.r12.g4959b52-1", + remote=RemoteSource( + source=PackageSource.AUR, + git_url=AUR.remote_git_url("tpacpi-bat-git", "aur"), + web_url=AUR.remote_web_url("tpacpi-bat-git"), + path=".", + branch="master", + ), + packages={"tpacpi-bat-git": PackageDescription()}) + + @pytest.fixture def package_description_ahriman() -> PackageDescription: """ diff --git a/tests/ahriman/core/build_tools/test_package_version.py b/tests/ahriman/core/build_tools/test_package_version.py new file mode 100644 index 00000000..d32a09bf --- /dev/null +++ b/tests/ahriman/core/build_tools/test_package_version.py @@ -0,0 +1,111 @@ +from pathlib import Path +from pytest_mock import MockerFixture + +from ahriman.core.build_tools.package_version import PackageVersion +from ahriman.core.configuration import Configuration +from ahriman.core.utils import utcnow +from ahriman.models.package import Package +from ahriman.models.pkgbuild import Pkgbuild + + +def test_actual_version(package_ahriman: Package, configuration: Configuration) -> None: + """ + must return same actual_version as version is + """ + assert PackageVersion(package_ahriman).actual_version(configuration) == package_ahriman.version + + +def test_actual_version_vcs(package_tpacpi_bat_git: Package, configuration: Configuration, + mocker: MockerFixture, resource_path_root: Path) -> None: + """ + must return valid actual_version for VCS package + """ + pkgbuild = resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild" + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild)) + mocker.patch("pathlib.Path.glob", return_value=[Path("local")]) + init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init") + unlink_mock = mocker.patch("pathlib.Path.unlink") + + assert PackageVersion(package_tpacpi_bat_git).actual_version(configuration) == "3.1.r13.g4959b52-1" + init_mock.assert_called_once_with(configuration.repository_paths.cache_for(package_tpacpi_bat_git.base), [], None) + unlink_mock.assert_called_once_with() + + +def test_actual_version_failed(package_tpacpi_bat_git: Package, configuration: Configuration, + mocker: MockerFixture) -> None: + """ + must return same version in case if exception occurred + """ + mocker.patch("ahriman.core.build_tools.task.Task.init", side_effect=Exception) + mocker.patch("pathlib.Path.glob", return_value=[Path("local")]) + unlink_mock = mocker.patch("pathlib.Path.unlink") + + assert PackageVersion(package_tpacpi_bat_git).actual_version(configuration) == package_tpacpi_bat_git.version + unlink_mock.assert_called_once_with() + + +def test_is_newer_than(package_ahriman: Package, package_python_schedule: Package) -> None: + """ + must correctly check if package is newer than specified timestamp + """ + # base checks, true/false + older = package_ahriman.packages[package_ahriman.base].build_date - 1 + assert PackageVersion(package_ahriman).is_newer_than(older) + + newer = package_ahriman.packages[package_ahriman.base].build_date + 1 + assert not PackageVersion(package_ahriman).is_newer_than(newer) + + # list check + min_date = min(package.build_date for package in package_python_schedule.packages.values()) + assert PackageVersion(package_python_schedule).is_newer_than(min_date) + + # null list check + package_python_schedule.packages["python-schedule"].build_date = None + assert PackageVersion(package_python_schedule).is_newer_than(min_date) + + package_python_schedule.packages["python2-schedule"].build_date = None + assert not PackageVersion(package_python_schedule).is_newer_than(min_date) + + +def test_is_outdated_false(package_ahriman: Package, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must be not outdated for the same package + """ + actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version", + return_value=package_ahriman.version) + assert not PackageVersion(package_ahriman).is_outdated(package_ahriman, configuration) + actual_version_mock.assert_called_once_with(configuration) + + +def test_is_outdated_true(package_ahriman: Package, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must be outdated for the new version + """ + other = Package.from_json(package_ahriman.view()) + other.version = other.version.replace("-1", "-2") + actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version", + return_value=other.version) + + assert PackageVersion(package_ahriman).is_outdated(other, configuration) + actual_version_mock.assert_called_once_with(configuration) + + +def test_is_outdated_no_version_calculation(package_ahriman: Package, configuration: Configuration, + mocker: MockerFixture) -> None: + """ + must not call actual version if calculation is disabled + """ + actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version") + assert not PackageVersion(package_ahriman).is_outdated(package_ahriman, configuration, calculate_version=False) + actual_version_mock.assert_not_called() + + +def test_is_outdated_fresh_package(package_ahriman: Package, configuration: Configuration, + mocker: MockerFixture) -> None: + """ + must not call actual version if package is never than specified time + """ + configuration.set_option("build", "vcs_allowed_age", str(int(utcnow().timestamp()))) + actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version") + assert not PackageVersion(package_ahriman).is_outdated(package_ahriman, configuration) + actual_version_mock.assert_not_called() diff --git a/tests/ahriman/core/build_tools/test_sources.py b/tests/ahriman/core/build_tools/test_sources.py index 62484a03..bc6a4579 100644 --- a/tests/ahriman/core/build_tools/test_sources.py +++ b/tests/ahriman/core/build_tools/test_sources.py @@ -55,7 +55,8 @@ def test_extend_architectures(mocker: MockerFixture) -> None: must update available architecture list """ mocker.patch("pathlib.Path.is_file", return_value=True) - architectures_mock = mocker.patch("ahriman.models.package.Package.supported_architectures", return_value={"x86_64"}) + architectures_mock = mocker.patch("ahriman.models.pkgbuild.Pkgbuild.supported_architectures", + return_value={"x86_64"}) assert Sources.extend_architectures(Path("local"), "i686") == [PkgbuildPatch("arch", list({"x86_64", "i686"}))] architectures_mock.assert_called_once_with(Path("local")) @@ -66,7 +67,7 @@ def test_extend_architectures_any(mocker: MockerFixture) -> None: must skip architecture patching in case if there is any architecture """ mocker.patch("pathlib.Path.is_file", return_value=True) - mocker.patch("ahriman.models.package.Package.supported_architectures", return_value={"any"}) + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.supported_architectures", return_value={"any"}) assert Sources.extend_architectures(Path("local"), "i686") == [] @@ -191,7 +192,7 @@ def test_init(sources: Sources, mocker: MockerFixture) -> None: """ must create empty repository at the specified path """ - mocker.patch("ahriman.models.package.Package.local_files", return_value=[Path("local")]) + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.local_files", return_value=[Path("local")]) mocker.patch("pathlib.Path.is_dir", return_value=False) add_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.add") check_output_mock = mocker.patch("ahriman.core.build_tools.sources.check_output") @@ -209,7 +210,7 @@ def test_init_skip(mocker: MockerFixture) -> None: """ must skip git init if it was already """ - mocker.patch("ahriman.models.package.Package.local_files", return_value=[Path("local")]) + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.local_files", return_value=[Path("local")]) mocker.patch("pathlib.Path.is_dir", return_value=True) mocker.patch("ahriman.core.build_tools.sources.Sources.add") mocker.patch("ahriman.core.build_tools.sources.Sources.commit") diff --git a/tests/ahriman/core/repository/test_package_info.py b/tests/ahriman/core/repository/test_package_info.py index e96818c0..64b1f8a8 100644 --- a/tests/ahriman/core/repository/test_package_info.py +++ b/tests/ahriman/core/repository/test_package_info.py @@ -2,12 +2,32 @@ import pytest from pathlib import Path from pytest_mock import MockerFixture +from unittest.mock import MagicMock from ahriman.core.repository.package_info import PackageInfo from ahriman.models.changes import Changes from ahriman.models.package import Package +def test_full_depends(package_info: PackageInfo, package_ahriman: Package, package_python_schedule: Package, + pyalpm_package_ahriman: MagicMock) -> None: + """ + must extract all dependencies from the package + """ + package_python_schedule.packages[package_python_schedule.base].provides = ["python3-schedule"] + + database_mock = MagicMock() + database_mock.pkgcache = [pyalpm_package_ahriman] + package_info.pacman = MagicMock() + package_info.pacman.handle.get_syncdbs.return_value = [database_mock] + + assert package_info.full_depends(package_ahriman, [package_python_schedule]) == package_ahriman.depends + + package_python_schedule.packages[package_python_schedule.base].depends = [package_ahriman.base] + expected = sorted(set(package_python_schedule.depends + package_ahriman.depends)) + assert package_info.full_depends(package_python_schedule, [package_python_schedule]) == expected + + def test_load_archives(package_ahriman: Package, package_python_schedule: Package, package_info: PackageInfo, mocker: MockerFixture) -> None: """ diff --git a/tests/ahriman/core/repository/test_update_handler.py b/tests/ahriman/core/repository/test_update_handler.py index 3367d3e3..bed9ec67 100644 --- a/tests/ahriman/core/repository/test_update_handler.py +++ b/tests/ahriman/core/repository/test_update_handler.py @@ -24,7 +24,8 @@ def test_updates_aur(update_handler: UpdateHandler, package_ahriman: Package, mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman) status_client_mock = mocker.patch("ahriman.core.status.Client.set_pending") event_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.event") - package_is_outdated_mock = mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) + package_is_outdated_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.is_outdated", + return_value=True) assert update_handler.updates_aur([], vcs=True) == [package_ahriman] packages_mock.assert_called_once_with([]) @@ -43,7 +44,7 @@ def test_updates_aur_official(update_handler: UpdateHandler, package_ahriman: Pa """ package_ahriman.remote = RemoteSource(source=PackageSource.Repository) mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) - mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) + mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.is_outdated", return_value=True) mocker.patch("ahriman.models.package.Package.from_official", return_value=package_ahriman) status_client_mock = mocker.patch("ahriman.core.status.Client.set_pending") event_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.event") @@ -74,7 +75,7 @@ def test_updates_aur_up_to_date(update_handler: UpdateHandler, package_ahriman: """ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman) - mocker.patch("ahriman.models.package.Package.is_outdated", return_value=False) + mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.is_outdated", return_value=False) status_client_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_status_update") assert update_handler.updates_aur([], vcs=True) == [] @@ -100,7 +101,7 @@ def test_updates_aur_filter(update_handler: UpdateHandler, package_ahriman: Pack """ packages_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) - mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) + mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.is_outdated", return_value=True) package_load_mock = mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman) assert update_handler.updates_aur([package_ahriman.base], vcs=True) == [package_ahriman] @@ -129,7 +130,8 @@ def test_updates_aur_ignore_vcs(update_handler: UpdateHandler, package_ahriman: mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman) mocker.patch("ahriman.models.package.Package.is_vcs", return_value=True) - package_is_outdated_mock = mocker.patch("ahriman.models.package.Package.is_outdated", return_value=False) + package_is_outdated_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.is_outdated", + return_value=False) assert not update_handler.updates_aur([], vcs=False) package_is_outdated_mock.assert_called_once_with( @@ -150,7 +152,7 @@ def test_updates_aur_load_by_package(update_handler: UpdateHandler, package_pyth mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_python_schedule]) mocker.patch("ahriman.models.package.Package.from_aur", side_effect=package_selector) - mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) + mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.is_outdated", return_value=True) assert update_handler.updates_aur([], vcs=True) == [package_python_schedule] @@ -232,7 +234,8 @@ def test_updates_local(update_handler: UpdateHandler, package_ahriman: Package, package_load_mock = mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) status_client_mock = mocker.patch("ahriman.core.status.Client.set_pending") event_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.event") - package_is_outdated_mock = mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) + package_is_outdated_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.is_outdated", + return_value=True) assert update_handler.updates_local(vcs=True) == [package_ahriman] fetch_mock.assert_called_once_with(Path(package_ahriman.base), pytest.helpers.anyvar(int)) @@ -255,7 +258,8 @@ def test_updates_local_ignore_vcs(update_handler: UpdateHandler, package_ahriman mocker.patch("pathlib.Path.iterdir", return_value=[Path(package_ahriman.base)]) mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) - package_is_outdated_mock = mocker.patch("ahriman.models.package.Package.is_outdated", return_value=False) + package_is_outdated_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.is_outdated", + return_value=False) assert not update_handler.updates_local(vcs=False) package_is_outdated_mock.assert_called_once_with( @@ -269,7 +273,7 @@ def test_updates_local_unknown(update_handler: UpdateHandler, package_ahriman: P """ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[]) mocker.patch("pathlib.Path.iterdir", return_value=[Path(package_ahriman.base)]) - mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) + mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.is_outdated", return_value=True) mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) @@ -282,7 +286,7 @@ def test_updates_local_remote(update_handler: UpdateHandler, package_ahriman: Pa """ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) mocker.patch("pathlib.Path.iterdir", return_value=[Path(package_ahriman.base)]) - mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) + mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.is_outdated", return_value=True) mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) diff --git a/tests/ahriman/core/test_utils.py b/tests/ahriman/core/test_utils.py index b43e38c8..c822db84 100644 --- a/tests/ahriman/core/test_utils.py +++ b/tests/ahriman/core/test_utils.py @@ -265,6 +265,15 @@ def test_full_version() -> None: assert full_version(1, "0.12.1", "1") == "1:0.12.1-1" +def test_list_flatmap() -> None: + """ + must flat map iterable correctly + """ + assert list_flatmap([], lambda e: [e * 2]) == [] + assert list_flatmap([3, 1, 2], lambda e: [e * 2]) == [2, 4, 6] + assert list_flatmap([1, 2, 1], lambda e: [e * 2]) == [2, 4] + + def test_minmax() -> None: """ must correctly define minimal and maximal value diff --git a/tests/ahriman/models/conftest.py b/tests/ahriman/models/conftest.py index be478956..ff68a1cc 100644 --- a/tests/ahriman/models/conftest.py +++ b/tests/ahriman/models/conftest.py @@ -78,27 +78,6 @@ def internal_status(counters: Counters) -> InternalStatus: ) -@pytest.fixture -def package_tpacpi_bat_git() -> Package: - """ - git package fixture - - Returns: - Package: git package test instance - """ - return Package( - base="tpacpi-bat-git", - version="3.1.r12.g4959b52-1", - remote=RemoteSource( - source=PackageSource.AUR, - git_url=AUR.remote_git_url("tpacpi-bat-git", "aur"), - web_url=AUR.remote_web_url("tpacpi-bat-git"), - path=".", - branch="master", - ), - packages={"tpacpi-bat-git": PackageDescription()}) - - @pytest.fixture def pkgbuild_ahriman(resource_path_root: Path) -> Pkgbuild: """ diff --git a/tests/ahriman/models/test_package.py b/tests/ahriman/models/test_package.py index d6eec437..ae35f3d0 100644 --- a/tests/ahriman/models/test_package.py +++ b/tests/ahriman/models/test_package.py @@ -5,13 +5,10 @@ from pytest_mock import MockerFixture from unittest.mock import MagicMock, PropertyMock, call as MockCall from ahriman.core.alpm.pacman import Pacman -from ahriman.core.configuration import Configuration -from ahriman.core.utils import utcnow from ahriman.models.aur_package import AURPackage from ahriman.models.package import Package from ahriman.models.package_description import PackageDescription from ahriman.models.pkgbuild import Pkgbuild -from ahriman.models.pkgbuild_patch import PkgbuildPatch def test_depends(package_python_schedule: Package) -> None: @@ -322,183 +319,6 @@ def test_from_official(package_ahriman: Package, aur_package_ahriman: AURPackage assert package_ahriman.packager == package.packager -def test_local_files(mocker: MockerFixture, resource_path_root: Path) -> None: - """ - must extract local file sources - """ - pkgbuild = resource_path_root / "models" / "package_yay_pkgbuild" - parsed_pkgbuild = Pkgbuild.from_file(pkgbuild) - parsed_pkgbuild.fields["source"] = PkgbuildPatch("source", ["local-file.tar.gz"]) - mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=parsed_pkgbuild) - mocker.patch("ahriman.models.package.Package.supported_architectures", return_value=["any"]) - - assert list(Package.local_files(Path("path"))) == [Path("local-file.tar.gz")] - - -def test_local_files_empty(mocker: MockerFixture, resource_path_root: Path) -> None: - """ - must extract empty local files list when there are no local files - """ - pkgbuild = resource_path_root / "models" / "package_yay_pkgbuild" - mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild)) - mocker.patch("ahriman.models.package.Package.supported_architectures", return_value=["any"]) - - assert not list(Package.local_files(Path("path"))) - - -def test_local_files_schema(mocker: MockerFixture, resource_path_root: Path) -> None: - """ - must skip local file source when file schema is used - """ - pkgbuild = resource_path_root / "models" / "package_yay_pkgbuild" - parsed_pkgbuild = Pkgbuild.from_file(pkgbuild) - parsed_pkgbuild.fields["source"] = PkgbuildPatch("source", ["file:///local-file.tar.gz"]) - mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=parsed_pkgbuild) - mocker.patch("ahriman.models.package.Package.supported_architectures", return_value=["any"]) - - assert not list(Package.local_files(Path("path"))) - - -def test_local_files_with_install(mocker: MockerFixture, resource_path_root: Path) -> None: - """ - must extract local file sources with install file - """ - pkgbuild = resource_path_root / "models" / "package_yay_pkgbuild" - parsed_pkgbuild = Pkgbuild.from_file(pkgbuild) - parsed_pkgbuild.fields["install"] = PkgbuildPatch("install", "install") - mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=parsed_pkgbuild) - mocker.patch("ahriman.models.package.Package.supported_architectures", return_value=["any"]) - - assert list(Package.local_files(Path("path"))) == [Path("install")] - - -def test_supported_architectures(mocker: MockerFixture, resource_path_root: Path) -> None: - """ - must generate list of available architectures - """ - pkgbuild = resource_path_root / "models" / "package_yay_pkgbuild" - mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild)) - assert Package.supported_architectures(Path("path")) == \ - {"i686", "pentium4", "x86_64", "arm", "armv7h", "armv6h", "aarch64", "riscv64"} - - -def test_actual_version(package_ahriman: Package, configuration: Configuration) -> None: - """ - must return same actual_version as version is - """ - assert package_ahriman.actual_version(configuration) == package_ahriman.version - - -def test_actual_version_vcs(package_tpacpi_bat_git: Package, configuration: Configuration, - mocker: MockerFixture, resource_path_root: Path) -> None: - """ - must return valid actual_version for VCS package - """ - pkgbuild = resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild" - mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild)) - mocker.patch("pathlib.Path.glob", return_value=[Path("local")]) - init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init") - unlink_mock = mocker.patch("pathlib.Path.unlink") - - assert package_tpacpi_bat_git.actual_version(configuration) == "3.1.r13.g4959b52-1" - init_mock.assert_called_once_with(configuration.repository_paths.cache_for(package_tpacpi_bat_git.base), [], None) - unlink_mock.assert_called_once_with() - - -def test_actual_version_failed(package_tpacpi_bat_git: Package, configuration: Configuration, - mocker: MockerFixture) -> None: - """ - must return same version in case if exception occurred - """ - mocker.patch("ahriman.core.build_tools.task.Task.init", side_effect=Exception) - mocker.patch("pathlib.Path.glob", return_value=[Path("local")]) - unlink_mock = mocker.patch("pathlib.Path.unlink") - - assert package_tpacpi_bat_git.actual_version(configuration) == package_tpacpi_bat_git.version - unlink_mock.assert_called_once_with() - - -def test_full_depends(package_ahriman: Package, package_python_schedule: Package, pyalpm_package_ahriman: MagicMock, - pyalpm_handle: MagicMock) -> None: - """ - must extract all dependencies from the package - """ - package_python_schedule.packages[package_python_schedule.base].provides = ["python3-schedule"] - - database_mock = MagicMock() - database_mock.pkgcache = [pyalpm_package_ahriman] - pyalpm_handle.handle.get_syncdbs.return_value = [database_mock] - - assert package_ahriman.full_depends(pyalpm_handle, [package_python_schedule]) == package_ahriman.depends - - package_python_schedule.packages[package_python_schedule.base].depends = [package_ahriman.base] - expected = sorted(set(package_python_schedule.depends + package_ahriman.depends)) - assert package_python_schedule.full_depends(pyalpm_handle, [package_python_schedule]) == expected - - -def test_is_newer_than(package_ahriman: Package, package_python_schedule: Package) -> None: - """ - must correctly check if package is newer than specified timestamp - """ - # base checks, true/false - assert package_ahriman.is_newer_than(package_ahriman.packages[package_ahriman.base].build_date - 1) - assert not package_ahriman.is_newer_than(package_ahriman.packages[package_ahriman.base].build_date + 1) - - # list check - min_date = min(package.build_date for package in package_python_schedule.packages.values()) - assert package_python_schedule.is_newer_than(min_date) - - # null list check - package_python_schedule.packages["python-schedule"].build_date = None - assert package_python_schedule.is_newer_than(min_date) - - package_python_schedule.packages["python2-schedule"].build_date = None - assert not package_python_schedule.is_newer_than(min_date) - - -def test_is_outdated_false(package_ahriman: Package, configuration: Configuration, mocker: MockerFixture) -> None: - """ - must be not outdated for the same package - """ - actual_version_mock = mocker.patch("ahriman.models.package.Package.actual_version", - return_value=package_ahriman.version) - assert not package_ahriman.is_outdated(package_ahriman, configuration) - actual_version_mock.assert_called_once_with(configuration) - - -def test_is_outdated_true(package_ahriman: Package, configuration: Configuration, mocker: MockerFixture) -> None: - """ - must be outdated for the new version - """ - other = Package.from_json(package_ahriman.view()) - other.version = other.version.replace("-1", "-2") - actual_version_mock = mocker.patch("ahriman.models.package.Package.actual_version", return_value=other.version) - - assert package_ahriman.is_outdated(other, configuration) - actual_version_mock.assert_called_once_with(configuration) - - -def test_is_outdated_no_version_calculation(package_ahriman: Package, configuration: Configuration, - mocker: MockerFixture) -> None: - """ - must not call actual version if calculation is disabled - """ - actual_version_mock = mocker.patch("ahriman.models.package.Package.actual_version") - assert not package_ahriman.is_outdated(package_ahriman, configuration, calculate_version=False) - actual_version_mock.assert_not_called() - - -def test_is_outdated_fresh_package(package_ahriman: Package, configuration: Configuration, - mocker: MockerFixture) -> None: - """ - must not call actual version if package is never than specified time - """ - configuration.set_option("build", "vcs_allowed_age", str(int(utcnow().timestamp()))) - actual_version_mock = mocker.patch("ahriman.models.package.Package.actual_version") - assert not package_ahriman.is_outdated(package_ahriman, configuration) - actual_version_mock.assert_not_called() - - def test_next_pkgrel(package_ahriman: Package) -> None: """ must correctly bump pkgrel diff --git a/tests/ahriman/models/test_pkgbuild.py b/tests/ahriman/models/test_pkgbuild.py index 4fc91bd4..949f8091 100644 --- a/tests/ahriman/models/test_pkgbuild.py +++ b/tests/ahriman/models/test_pkgbuild.py @@ -78,6 +78,66 @@ def test_from_io_empty(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> Non assert Pkgbuild.from_io(StringIO("mock")) == pkgbuild_ahriman +def test_local_files(mocker: MockerFixture, resource_path_root: Path) -> None: + """ + must extract local file sources + """ + pkgbuild = resource_path_root / "models" / "package_yay_pkgbuild" + parsed_pkgbuild = Pkgbuild.from_file(pkgbuild) + parsed_pkgbuild.fields["source"] = PkgbuildPatch("source", ["local-file.tar.gz"]) + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=parsed_pkgbuild) + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.supported_architectures", return_value=["any"]) + + assert list(Pkgbuild.local_files(Path("path"))) == [Path("local-file.tar.gz")] + + +def test_local_files_empty(mocker: MockerFixture, resource_path_root: Path) -> None: + """ + must extract empty local files list when there are no local files + """ + pkgbuild = resource_path_root / "models" / "package_yay_pkgbuild" + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild)) + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.supported_architectures", return_value=["any"]) + + assert not list(Pkgbuild.local_files(Path("path"))) + + +def test_local_files_schema(mocker: MockerFixture, resource_path_root: Path) -> None: + """ + must skip local file source when file schema is used + """ + pkgbuild = resource_path_root / "models" / "package_yay_pkgbuild" + parsed_pkgbuild = Pkgbuild.from_file(pkgbuild) + parsed_pkgbuild.fields["source"] = PkgbuildPatch("source", ["file:///local-file.tar.gz"]) + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=parsed_pkgbuild) + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.supported_architectures", return_value=["any"]) + + assert not list(Pkgbuild.local_files(Path("path"))) + + +def test_local_files_with_install(mocker: MockerFixture, resource_path_root: Path) -> None: + """ + must extract local file sources with install file + """ + pkgbuild = resource_path_root / "models" / "package_yay_pkgbuild" + parsed_pkgbuild = Pkgbuild.from_file(pkgbuild) + parsed_pkgbuild.fields["install"] = PkgbuildPatch("install", "install") + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=parsed_pkgbuild) + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.supported_architectures", return_value=["any"]) + + assert list(Pkgbuild.local_files(Path("path"))) == [Path("install")] + + +def test_supported_architectures(mocker: MockerFixture, resource_path_root: Path) -> None: + """ + must generate list of available architectures + """ + pkgbuild = resource_path_root / "models" / "package_yay_pkgbuild" + mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild)) + assert Pkgbuild.supported_architectures(Path("path")) == \ + {"i686", "pentium4", "x86_64", "arm", "armv7h", "armv6h", "aarch64", "riscv64"} + + def test_packages(pkgbuild_ahriman: Pkgbuild) -> None: """ must correctly load package function