diff --git a/docs/configuration.rst b/docs/configuration.rst index 4568ce60..a4fcfc3a 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -30,8 +30,8 @@ Base configuration settings. * ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference. * ``suppress_http_log_errors`` - suppress http log errors, boolean, optional, default ``no``. If set to ``yes``, any http log errors (e.g. if web server is not available, but http logging is enabled) will be suppressed. -``alpm`` group --------------- +``alpm:*`` groups +----------------- libalpm and AUR related configuration. Group name can refer to architecture, e.g. ``alpm:x86_64`` can be used for x86_64 architecture specific settings. @@ -69,6 +69,7 @@ Build related configuration. Group name can refer to architecture, e.g. ``build: * ``makepkg_flags`` - additional flags passed to ``makepkg`` command, space separated list of strings, optional. * ``makechrootpkg_flags`` - additional flags passed to ``makechrootpkg`` command, space separated list of strings, optional. * ``triggers`` - list of ``ahriman.core.triggers.Trigger`` class implementation (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of mention. +* ``triggers_known`` - optional list of ``ahriman.core.triggers.Trigger`` class implementations which are not run automatically and used only for trigger discovery and configuration validation. * ``vcs_allowed_age`` - maximal age in seconds of the VCS packages before their version will be updated with its remote source, int, optional, default ``604800``. ``repository`` group @@ -107,6 +108,23 @@ Web server settings. If any of ``host``/``port`` is not set, web integration wil * ``unix_socket_unsafe`` - set unsafe (o+w) permissions to unix socket, boolean, optional, default ``yes``. This option is enabled by default, because it is supposed that unix socket is created in safe environment (only web service is supposed to be used in unsafe), but it can be disabled by configuration. * ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled. +``mirrorlist`` group +-------------------- + +Mirrorlist package generator plugin. + +* ``target`` - list of generator settings sections, space separated list of strings, required. It must point to valid section name. + +Mirrorlist generator plugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``description`` - mirrorlist package description, string, optional, default is ``repo mirror list for use by pacman``, where ``repo`` is the repository name. +* ``homepage`` - url to homepage location if any, string, optional. +* ``license`` - list of licenses which are applied to this package, space separated list of strings, optional, default is ``Unlicense``. +* ``package`` - mirrorlist package name, string, optional, default is ``repo-mirrorlist``, where ``repo`` is the repository name. +* ``path`` - absolute path to generated mirrorlist file, string, optional, default is ``/etc/pacman.d/repo-mirrorlist``, where ``repo`` is the repository name. +* ``servers`` - list of repository mirrors, space separated list of strings, required. + ``remote-pull`` group --------------------- diff --git a/package/share/ahriman/settings/ahriman.ini b/package/share/ahriman/settings/ahriman.ini index e246a2f9..8ae1f406 100644 --- a/package/share/ahriman/settings/ahriman.ini +++ b/package/share/ahriman/settings/ahriman.ini @@ -25,6 +25,7 @@ ignore_packages = makechrootpkg_flags = makepkg_flags = --nocolor --ignorearch triggers = ahriman.core.gitremote.RemotePullTrigger ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger ahriman.core.gitremote.RemotePushTrigger +triggers_known = ahriman.core.support.MirrorlistTrigger vcs_allowed_age = 604800 [repository] @@ -34,6 +35,9 @@ root = /var/lib/ahriman [sign] target = +[mirrorlist] +target = + [remote-pull] target = diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index 3ace5315..52388f5e 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -100,6 +100,7 @@ def _parser() -> argparse.ArgumentParser: _set_patch_set_add_parser(subparsers) _set_repo_backup_parser(subparsers) _set_repo_check_parser(subparsers) + _set_repo_create_mirrorlist_parser(subparsers) _set_repo_daemon_parser(subparsers) _set_repo_rebuild_parser(subparsers) _set_repo_remove_unknown_parser(subparsers) @@ -478,6 +479,25 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser: return parser +def _set_repo_create_mirrorlist_parser(root: SubParserAction) -> argparse.ArgumentParser: + """ + add parser for create-mirrorlist subcommand + + Args: + root(SubParserAction): subparsers for the commands + + Returns: + argparse.ArgumentParser: created argument parser + """ + parser = root.add_parser("repo-create-mirrorlist", help="create mirrorlist package", + description="create package which contains list of available mirrors as set by " + "configuration. Note, that this action will only create package, the package " + "itself has to be built manually", + formatter_class=_formatter) + parser.set_defaults(handler=handlers.Triggers, trigger=["ahriman.core.support.MirrorlistTrigger"]) + return parser + + def _set_repo_daemon_parser(root: SubParserAction) -> argparse.ArgumentParser: """ add parser for daemon subcommand diff --git a/src/ahriman/application/application/application_packages.py b/src/ahriman/application/application/application_packages.py index 2bb30216..0c78f6e6 100644 --- a/src/ahriman/application/application/application_packages.py +++ b/src/ahriman/application/application/application_packages.py @@ -26,6 +26,7 @@ from typing import Any from ahriman.application.application.application_properties import ApplicationProperties from ahriman.core.build_tools.sources import Sources +from ahriman.core.exceptions import UnknownPackageError from ahriman.core.util import package_like from ahriman.models.package import Package from ahriman.models.package_source import PackageSource @@ -43,8 +44,14 @@ class ApplicationPackages(ApplicationProperties): Args: source(str): path to package archive + + Raises: + UnknownPackageError: if specified path doesn't exist """ local_path = Path(source) + if not local_path.is_file(): + raise UnknownPackageError(source) + dst = self.repository.paths.packages / local_path.name shutil.copy(local_path, dst) @@ -68,6 +75,9 @@ class ApplicationPackages(ApplicationProperties): source(str): path to local directory """ local_dir = Path(source) + if not local_dir.is_dir(): + raise UnknownPackageError(source) + for full_path in filter(package_like, local_dir.iterdir()): self._add_archive(str(full_path)) @@ -77,12 +87,19 @@ class ApplicationPackages(ApplicationProperties): Args: source(str): path to directory with local source files + + Raises: + UnknownPackageError: if specified package is unknown or doesn't exist """ - source_dir = Path(source) - package = Package.from_build(source_dir, self.architecture) - cache_dir = self.repository.paths.cache_for(package.base) - shutil.copytree(source_dir, 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 + if (source_dir := Path(source)).is_dir(): + package = Package.from_build(source_dir, self.architecture) + cache_dir = self.repository.paths.cache_for(package.base) + shutil.copytree(source_dir, 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 + elif (source_dir := self.repository.paths.cache_for(source)).is_dir(): + package = Package.from_build(source_dir, self.architecture) + else: + raise UnknownPackageError(source) self.database.build_queue_insert(package) @@ -95,8 +112,11 @@ class ApplicationPackages(ApplicationProperties): """ dst = self.repository.paths.packages / Path(source).name # URL is path, is not it? # timeout=None to suppress pylint warns. Also suppress bandit warnings - response = requests.get(source, stream=True, timeout=None) # nosec - response.raise_for_status() + try: + response = requests.get(source, stream=True, timeout=None) # nosec + response.raise_for_status() + except Exception: + raise UnknownPackageError(source) with dst.open("wb") as local_file: for chunk in response.iter_content(chunk_size=1024): diff --git a/src/ahriman/application/handlers/validate.py b/src/ahriman/application/handlers/validate.py index f67812ee..857db3a5 100644 --- a/src/ahriman/application/handlers/validate.py +++ b/src/ahriman/application/handlers/validate.py @@ -78,7 +78,9 @@ class Validate(Handler): # create trigger loader instance loader = TriggerLoader() - for trigger in loader.selected_triggers(configuration): + triggers = loader.selected_triggers(configuration) + loader.known_triggers(configuration) + + for trigger in triggers: try: trigger_class = loader.load_trigger_class(trigger) except ExtensionError: diff --git a/src/ahriman/core/configuration/configuration.py b/src/ahriman/core/configuration/configuration.py index 4547a205..c2cd20c5 100644 --- a/src/ahriman/core/configuration/configuration.py +++ b/src/ahriman/core/configuration/configuration.py @@ -99,6 +99,16 @@ class Configuration(configparser.RawConfigParser): """ return self.getpath("settings", "logging") + @property + def repository_name(self) -> str: + """ + repository name as defined by configuration + + Returns: + str: repository name from configuration + """ + return self.get("repository", "name") + @property def repository_paths(self) -> RepositoryPaths: """ diff --git a/src/ahriman/core/configuration/schema.py b/src/ahriman/core/configuration/schema.py index e490d459..30da4764 100644 --- a/src/ahriman/core/configuration/schema.py +++ b/src/ahriman/core/configuration/schema.py @@ -163,6 +163,11 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = { "coerce": "list", "schema": {"type": "string"}, }, + "triggers_known": { + "type": "list", + "coerce": "list", + "schema": {"type": "string"}, + }, "vcs_allowed_age": { "type": "integer", "coerce": "integer", diff --git a/src/ahriman/core/configuration/validator.py b/src/ahriman/core/configuration/validator.py index c7e98915..d8edf399 100644 --- a/src/ahriman/core/configuration/validator.py +++ b/src/ahriman/core/configuration/validator.py @@ -144,6 +144,24 @@ class Validator(RootValidator): if constraint and url.scheme not in constraint: self._error(field, f"Url {value} scheme must be one of {constraint}") + def _validate_path_is_absolute(self, constraint: bool, field: str, value: Path) -> None: + """ + check if path is absolute or not + + Args: + constraint(bool): True in case if path must be absolute and False if it must be relative + field(str): field name to be checked + value(Path): value to be checked + + Examples: + The rule's arguments are validated against this schema: + {"type": "boolean"} + """ + if constraint and not value.is_absolute(): + self._error(field, f"Path {value} must be absolute") + if not constraint and value.is_absolute(): + self._error(field, f"Path {value} must be relative") + def _validate_path_exists(self, constraint: bool, field: str, value: Path) -> None: """ check if paths exists @@ -159,3 +177,5 @@ class Validator(RootValidator): """ if constraint and not value.exists(): self._error(field, f"Path {value} must exist") + if not constraint and value.exists(): + self._error(field, f"Path {value} must not exist") diff --git a/src/ahriman/core/report/jinja_template.py b/src/ahriman/core/report/jinja_template.py index eb232706..a525ecf6 100644 --- a/src/ahriman/core/report/jinja_template.py +++ b/src/ahriman/core/report/jinja_template.py @@ -75,7 +75,7 @@ class JinjaTemplate: # base template vars self.homepage = configuration.get(section, "homepage", fallback=None) - self.name = configuration.get("repository", "name") + self.name = configuration.repository_name self.sign_targets, self.default_pgp_key = GPG.sign_options(configuration) diff --git a/src/ahriman/core/repository/repository_properties.py b/src/ahriman/core/repository/repository_properties.py index 0c961d65..a07849b5 100644 --- a/src/ahriman/core/repository/repository_properties.py +++ b/src/ahriman/core/repository/repository_properties.py @@ -67,7 +67,7 @@ class RepositoryProperties(LazyLogging): self.configuration = configuration self.database = database - self.name = configuration.get("repository", "name") + self.name = configuration.repository_name self.vcs_allowed_age = configuration.getint("build", "vcs_allowed_age", fallback=0) self.paths: RepositoryPaths = configuration.repository_paths # additional workaround for pycharm typing diff --git a/src/ahriman/core/support/__init__.py b/src/ahriman/core/support/__init__.py new file mode 100644 index 00000000..d8f2df96 --- /dev/null +++ b/src/ahriman/core/support/__init__.py @@ -0,0 +1,20 @@ +# +# Copyright (c) 2021-2023 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 ahriman.core.support.mirrorlist_trigger import MirrorlistTrigger diff --git a/src/ahriman/core/support/mirrorlist_trigger.py b/src/ahriman/core/support/mirrorlist_trigger.py new file mode 100644 index 00000000..a1b89373 --- /dev/null +++ b/src/ahriman/core/support/mirrorlist_trigger.py @@ -0,0 +1,105 @@ +# +# Copyright (c) 2021-2023 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 ahriman.core.configuration import Configuration +from ahriman.core.support.package_creator import PackageCreator +from ahriman.core.support.pkgbuild.mirrorlist_generator import MirrorlistGenerator +from ahriman.core.triggers import Trigger + + +class MirrorlistTrigger(Trigger): + """ + mirrorlist generator trigger + + Attributes: + targets(list[str]): git remote target list + """ + + CONFIGURATION_SCHEMA = { + "mirrorlist": { + "type": "dict", + "schema": { + "target": { + "type": "list", + "coerce": "list", + "schema": {"type": "string"}, + }, + }, + }, + "mirrorlist_generator": { + "type": "dict", + "schema": { + "description": { + "type": "string", + }, + "homepage": { + "type": "string", + }, + "license": { + "type": "list", + "coerce": "list", + }, + "package": { + "type": "string", + }, + "path": { + "type": "string", + "path_is_absolute": True, + }, + "servers": { + "type": "list", + "coerce": "list", + "required": True, + }, + }, + }, + } + + def __init__(self, architecture: str, configuration: Configuration) -> None: + """ + default constructor + + Args: + architecture(str): repository architecture + configuration(Configuration): configuration instance + """ + Trigger.__init__(self, architecture, configuration) + self.targets = self.configuration_sections(configuration) + + @classmethod + def configuration_sections(cls, configuration: Configuration) -> list[str]: + """ + extract configuration sections from configuration + + Args: + configuration(Configuration): configuration instance + + Returns: + list[str]: read configuration sections belong to this trigger + """ + return configuration.getlist("mirrorlist", "target", fallback=[]) + + def on_start(self) -> None: + """ + trigger action which will be called at the start of the application + """ + for target in self.targets: + generator = MirrorlistGenerator(self.configuration, target) + runner = PackageCreator(self.configuration, generator) + runner.run() diff --git a/src/ahriman/core/support/package_creator.py b/src/ahriman/core/support/package_creator.py new file mode 100644 index 00000000..61dfad73 --- /dev/null +++ b/src/ahriman/core/support/package_creator.py @@ -0,0 +1,72 @@ +# +# Copyright (c) 2021-2023 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 . +# +import shutil + +from ahriman.core import context +from ahriman.core.build_tools.sources import Sources +from ahriman.core.configuration import Configuration +from ahriman.core.database import SQLite +from ahriman.core.log import LazyLogging +from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator +from ahriman.models.build_status import BuildStatus +from ahriman.models.context_key import ContextKey +from ahriman.models.package import Package + + +class PackageCreator(LazyLogging): + """ + helper which creates packages based on pkgbuild generator + + Attributes: + configuration(Configuration): configuration instance + generator(PkgbuildGenerator): PKGBUILD generator instance + """ + + def __init__(self, configuration: Configuration, generator: PkgbuildGenerator) -> None: + """ + default constructor + + Args: + configuration(Configuration): configuration instance + generator(PkgbuildGenerator): PKGBUILD generator instance + """ + self.configuration = configuration + self.generator = generator + + def run(self) -> None: + """ + create new local package + """ + local_path = self.configuration.repository_paths.cache_for(self.generator.pkgname) + + # clear old tree if any + shutil.rmtree(local_path, ignore_errors=True) + + # create local tree + local_path.mkdir(mode=0o755, parents=True, exist_ok=True) + self.generator.write_pkgbuild(local_path) + Sources.init(local_path) + + # register package + ctx = context.get() + database: SQLite = ctx.get(ContextKey("database", SQLite)) + _, architecture = self.configuration.check_loaded() + package = Package.from_build(local_path, architecture) + database.package_update(package, BuildStatus()) diff --git a/src/ahriman/core/support/pkgbuild/__init__.py b/src/ahriman/core/support/pkgbuild/__init__.py new file mode 100644 index 00000000..8fc622e9 --- /dev/null +++ b/src/ahriman/core/support/pkgbuild/__init__.py @@ -0,0 +1,19 @@ +# +# Copyright (c) 2021-2023 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 . +# diff --git a/src/ahriman/core/support/pkgbuild/mirrorlist_generator.py b/src/ahriman/core/support/pkgbuild/mirrorlist_generator.py new file mode 100644 index 00000000..9f8983b5 --- /dev/null +++ b/src/ahriman/core/support/pkgbuild/mirrorlist_generator.py @@ -0,0 +1,143 @@ +# +# Copyright (c) 2021-2023 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 collections.abc import Callable +from pathlib import Path + +from ahriman.core.configuration import Configuration +from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator +from ahriman.models.pkgbuild_patch import PkgbuildPatch + + +class MirrorlistGenerator(PkgbuildGenerator): + """ + generator for mirrorlist PKGBUILD + + Attributes: + path(Path): path to mirrorlist relative to / + pkgbuild_license(list[str]): mirrorlist package license + pkgbuild_pkgdesc(str): mirrorlist package description + pkgbuild_pkgname(str): mirrorlist package name + pkgbuild_url(str): mirrorlist package home page + servers(list[str]): list of mirror servers + """ + + def __init__(self, configuration: Configuration, section: str) -> None: + """ + default constructor + + Args: + configuration(Configuration): configuration instance + section(str): settings section name + """ + name = configuration.repository_name + + # configuration fields + self.servers = configuration.getlist(section, "servers") + self.path = configuration.getpath(section, "path", fallback=Path("/etc") / "pacman.d" / f"{name}-mirrorlist") + self.path = self.path.relative_to("/") # in pkgbuild we are always operating with relative to / path + # pkgbuild description fields + self.pkgbuild_pkgname = configuration.get(section, "package", fallback=f"{name}-mirrorlist") + self.pkgbuild_pkgdesc = configuration.get( + section, "description", fallback=f"{name} mirror list for use by pacman") + self.pkgbuild_license = configuration.getlist(section, "license", fallback=["Unlicense"]) + self.pkgbuild_url = configuration.get(section, "homepage", fallback="") + + @property + def license(self) -> list[str]: + """ + package licenses list + + Returns: + list[str]: package licenses as PKGBUILD property + """ + return self.pkgbuild_license + + @property + def pkgdesc(self) -> str: + """ + package description + + Returns: + str: package description as PKGBUILD property + """ + return self.pkgbuild_pkgdesc + + @property + def pkgname(self) -> str: + """ + package name + + Returns: + str: package name as PKGBUILD property + """ + return self.pkgbuild_pkgname + + @property + def url(self) -> str: + """ + package upstream url + + Returns: + str: package upstream url as PKGBUILD property + """ + return self.pkgbuild_url + + def _generate_mirrorlist(self, source_path: Path) -> None: + """ + generate mirrorlist file + + Args: + source_path(Path): destination of the mirrorlist content + """ + with source_path.open("w") as source_file: + source_file.writelines([f"Server = {server}\n" for server in self.servers]) + + def package(self) -> str: + """ + package function generator + + Returns: + str: package() function for PKGBUILD + """ + return f"""{{ + install -Dm644 "{Path("$srcdir") / "mirrorlist"}" "{Path("$pkgdir") / self.path}" +}}""" + + def patches(self) -> list[PkgbuildPatch]: + """ + list of additional PKGBUILD properties + + Returns: + list[PkgbuildPatch]: list of patches which generate PKGBUILD content + """ + return [ + PkgbuildPatch("backup", [str(self.path)]), + ] + + def sources(self) -> dict[str, Callable[[Path], None]]: + """ + return list of sources for the package + + Returns: + dict[str, Callable[[Path], None]]: map of source identifier (e.g. filename) to its generator function + """ + return { + "mirrorlist": self._generate_mirrorlist + } diff --git a/src/ahriman/core/support/pkgbuild/pkgbuild_generator.py b/src/ahriman/core/support/pkgbuild/pkgbuild_generator.py new file mode 100644 index 00000000..60cbfdcb --- /dev/null +++ b/src/ahriman/core/support/pkgbuild/pkgbuild_generator.py @@ -0,0 +1,175 @@ +# +# Copyright (c) 2021-2023 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 . +# +import hashlib +import itertools + +from collections.abc import Callable, Generator +from pathlib import Path + +from ahriman.core.log import LazyLogging +from ahriman.core.util import utcnow +from ahriman.models.pkgbuild_patch import PkgbuildPatch + + +class PkgbuildGenerator(LazyLogging): + """ + main class for generating PKGBUILDs + + Attributes: + PKGBUILD_STATIC_PROPERTIES(list[PkgbuildPatch]): (class attribute) list of default pkgbuild static properties + """ + + PKGBUILD_STATIC_PROPERTIES = [ + PkgbuildPatch("pkgrel", "1"), + PkgbuildPatch("arch", ["any"]), + ] + + @property + def license(self) -> list[str]: + """ + package licenses list + + Returns: + list[str]: package licenses as PKGBUILD property + """ + return [] + + @property + def pkgdesc(self) -> str: + """ + package description + + Returns: + str: package description as PKGBUILD property + + Raises: + NotImplementedError: not implemented method + """ + raise NotImplementedError + + @property + def pkgname(self) -> str: + """ + package name + + Returns: + str: package name as PKGBUILD property + + Raises: + NotImplementedError: not implemented method + """ + raise NotImplementedError + + @property + def pkgver(self) -> str: + """ + package version + + Returns: + str: package version as PKGBUILD property + """ + return utcnow().strftime("%Y%m%d") + + @property + def url(self) -> str: + """ + package upstream url + + Returns: + str: package upstream url as PKGBUILD property + """ + return "" + + def package(self) -> str: + """ + package function generator + + Returns: + str: package() function for PKGBUILD + + Raises: + NotImplementedError: not implemented method + """ + raise NotImplementedError + + def patches(self) -> list[PkgbuildPatch]: + """ + list of additional PKGBUILD properties + + Returns: + list[PkgbuildPatch]: list of patches which generate PKGBUILD content + """ + return [] + + def sources(self) -> dict[str, Callable[[Path], None]]: + """ + return list of sources for the package + + Returns: + dict[str, Callable[[Path], None]]: map of source identifier (e.g. filename) to its generator function + """ + return {} + + def write_pkgbuild(self, source_dir: Path) -> None: + """ + generate PKGBUILD content to the specified path + + Args: + source_dir(Path): path to directory in which sources must be generated + """ + patches = self.PKGBUILD_STATIC_PROPERTIES # default static properties... + patches.extend([ + PkgbuildPatch("license", self.license), + PkgbuildPatch("pkgdesc", self.pkgdesc), + PkgbuildPatch("pkgname", self.pkgname), + PkgbuildPatch("pkgver", self.pkgver), + PkgbuildPatch("url", self.url), + ]) # ...main properties as defined by derived class... + patches.extend(self.patches()) # ...optional properties as defined by derived class... + patches.append(PkgbuildPatch("package()", self.package())) # ...package function... + + patches.extend(self.write_sources(source_dir)) # ...and finally source files + + for patch in patches: + patch.write(source_dir / "PKGBUILD") + + def write_sources(self, source_dir: Path) -> list[PkgbuildPatch]: + """ + write sources and returns valid PKGBUILD properties for them + + Args: + source_dir(Path): path to directory in which sources must be generated + + Returns: + list[PkgbuildPatch]: list of patches to be applied to the PKGBUILD + """ + def sources_generator() -> Generator[tuple[str, str], None, None]: + for source, generator in sorted(self.sources().items()): + source_path = source_dir / source + generator(source_path) + with source_path.open("rb") as source_file: + source_hash = hashlib.sha512(source_file.read()) + yield source, source_hash.hexdigest() + + sources_iter, hashes_iter = itertools.tee(sources_generator()) + return [ + PkgbuildPatch("source", [source for source, _ in sources_iter]), + PkgbuildPatch("sha512sums", [sha512 for _, sha512 in hashes_iter]), + ] diff --git a/src/ahriman/core/triggers/trigger_loader.py b/src/ahriman/core/triggers/trigger_loader.py index ace455cf..0b0a7e88 100644 --- a/src/ahriman/core/triggers/trigger_loader.py +++ b/src/ahriman/core/triggers/trigger_loader.py @@ -84,6 +84,20 @@ class TriggerLoader(LazyLogging): return instance + @staticmethod + def known_triggers(configuration: Configuration) -> list[str]: + """ + read configuration and return list of known triggers. Unlike ``selected_triggers`` this option is used mainly + for configuration and validation and mentioned triggers are not being executed automatically + + Args: + configuration(Configuration): configuration instance + + Returns: + list[str]: list of registered, but not enabled, triggers + """ + return configuration.getlist("build", "triggers_known", fallback=[]) + @staticmethod def selected_triggers(configuration: Configuration) -> list[str]: """ diff --git a/tests/ahriman/application/application/test_application_packages.py b/tests/ahriman/application/application/test_application_packages.py index 3ae3f6bb..dd03caa1 100644 --- a/tests/ahriman/application/application/test_application_packages.py +++ b/tests/ahriman/application/application/test_application_packages.py @@ -5,25 +5,36 @@ from pytest_mock import MockerFixture from unittest.mock import MagicMock from ahriman.application.application.application_packages import ApplicationPackages +from ahriman.core.exceptions import UnknownPackageError from ahriman.models.package import Package from ahriman.models.package_description import PackageDescription from ahriman.models.package_source import PackageSource from ahriman.models.result import Result -def test_add_archive( - application_packages: ApplicationPackages, - package_ahriman: Package, - mocker: MockerFixture) -> None: +def test_add_archive(application_packages: ApplicationPackages, package_ahriman: Package, + mocker: MockerFixture) -> None: """ must add package from archive """ + is_file_mock = mocker.patch("pathlib.Path.is_file", return_value=True) copy_mock = mocker.patch("shutil.copy") + application_packages._add_archive(package_ahriman.base) + is_file_mock.assert_called_once_with() copy_mock.assert_called_once_with( Path(package_ahriman.base), application_packages.repository.paths.packages / package_ahriman.base) +def test_add_archive_missing(application_packages: ApplicationPackages, mocker: MockerFixture) -> None: + """ + must raise UnknownPackageError on unknown path + """ + mocker.patch("pathlib.Path.is_file", return_value=False) + with pytest.raises(UnknownPackageError): + application_packages._add_archive("package") + + def test_add_aur(application_packages: ApplicationPackages, package_ahriman: Package, mocker: MockerFixture) -> None: """ must add package from AUR @@ -37,21 +48,29 @@ def test_add_aur(application_packages: ApplicationPackages, package_ahriman: Pac update_remote_mock.assert_called_once_with(package_ahriman) -def test_add_directory( - application_packages: ApplicationPackages, - package_ahriman: Package, - mocker: MockerFixture) -> None: +def test_add_directory(application_packages: ApplicationPackages, package_ahriman: Package, + mocker: MockerFixture) -> None: """ must add packages from directory """ - iterdir_mock = mocker.patch("pathlib.Path.iterdir", - return_value=[package.filepath for package in package_ahriman.packages.values()]) - copy_mock = mocker.patch("shutil.copy") + is_dir_mock = mocker.patch("pathlib.Path.is_dir", return_value=True) filename = package_ahriman.packages[package_ahriman.base].filepath + iterdir_mock = mocker.patch("pathlib.Path.iterdir", return_value=[filename]) + add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_archive") application_packages._add_directory(package_ahriman.base) + is_dir_mock.assert_called_once_with() iterdir_mock.assert_called_once_with() - copy_mock.assert_called_once_with(filename, application_packages.repository.paths.packages / filename.name) + add_mock.assert_called_once_with(str(filename)) + + +def test_add_directory_missing(application_packages: ApplicationPackages, mocker: MockerFixture) -> None: + """ + must raise UnknownPackageError on unknown directory path + """ + mocker.patch("pathlib.Path.is_dir", return_value=False) + with pytest.raises(UnknownPackageError): + application_packages._add_directory("package") def test_add_local(application_packages: ApplicationPackages, package_ahriman: Package, mocker: MockerFixture) -> None: @@ -59,17 +78,46 @@ def test_add_local(application_packages: ApplicationPackages, package_ahriman: P must add package from local sources """ mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) + is_dir_mock = mocker.patch("pathlib.Path.is_dir", return_value=True) init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init") copytree_mock = mocker.patch("shutil.copytree") build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert") application_packages._add_local(package_ahriman.base) + is_dir_mock.assert_called_once_with() copytree_mock.assert_called_once_with( Path(package_ahriman.base), application_packages.repository.paths.cache_for(package_ahriman.base)) init_mock.assert_called_once_with(application_packages.repository.paths.cache_for(package_ahriman.base)) build_queue_mock.assert_called_once_with(package_ahriman) +def test_add_local_cache(application_packages: ApplicationPackages, package_ahriman: Package, + mocker: MockerFixture) -> None: + """ + must add package from local source if there is cache + """ + mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) + mocker.patch("pathlib.Path.is_dir", autospec=True, + side_effect=lambda p: True if p.is_relative_to(application_packages.repository.paths.cache) else False) + init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init") + copytree_mock = mocker.patch("shutil.copytree") + build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert") + + application_packages._add_local(package_ahriman.base) + copytree_mock.assert_not_called() + init_mock.assert_not_called() + build_queue_mock.assert_called_once_with(package_ahriman) + + +def test_add_local_missing(application_packages: ApplicationPackages, mocker: MockerFixture) -> None: + """ + must raise UnknownPackageError if package wasn't found + """ + mocker.patch("pathlib.Path.is_dir", return_value=False) + with pytest.raises(UnknownPackageError): + application_packages._add_local("package") + + def test_add_remote(application_packages: ApplicationPackages, package_description_ahriman: PackageDescription, mocker: MockerFixture) -> None: """ @@ -87,6 +135,15 @@ def test_add_remote(application_packages: ApplicationPackages, package_descripti response_mock.raise_for_status.assert_called_once_with() +def test_add_remote_missing(application_packages: ApplicationPackages, mocker: MockerFixture) -> None: + """ + must add package from remote source + """ + mocker.patch("requests.get", side_effect=Exception()) + with pytest.raises(UnknownPackageError): + application_packages._add_remote("url") + + def test_add_repository(application_packages: ApplicationPackages, package_ahriman: Package, mocker: MockerFixture) -> None: """ @@ -112,10 +169,8 @@ def test_add_add_archive(application_packages: ApplicationPackages, package_ahri add_mock.assert_called_once_with(package_ahriman.base) -def test_add_add_aur( - application_packages: ApplicationPackages, - package_ahriman: Package, - mocker: MockerFixture) -> None: +def test_add_add_aur(application_packages: ApplicationPackages, package_ahriman: Package, + mocker: MockerFixture) -> None: """ must add package from AUR via add function """ diff --git a/tests/ahriman/application/handlers/test_handler_validate.py b/tests/ahriman/application/handlers/test_handler_validate.py index a488bd90..237d0bdd 100644 --- a/tests/ahriman/application/handlers/test_handler_validate.py +++ b/tests/ahriman/application/handlers/test_handler_validate.py @@ -62,6 +62,8 @@ def test_schema(configuration: Configuration) -> None: assert schema.pop("email") assert schema.pop("github") assert schema.pop("html") + assert schema.pop("mirrorlist") + assert schema.pop("mirrorlist_generator") assert schema.pop("report") assert schema.pop("rsync") assert schema.pop("s3") @@ -76,6 +78,7 @@ def test_schema_invalid_trigger(configuration: Configuration) -> None: must skip trigger if it caused exception on load """ configuration.set_option("build", "triggers", "some.invalid.trigger.path.Trigger") + configuration.remove_option("build", "triggers_known") assert Validate.schema("x86_64", configuration) == CONFIGURATION_SCHEMA diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py index 1fe5f9a3..a03caae8 100644 --- a/tests/ahriman/application/test_ahriman.py +++ b/tests/ahriman/application/test_ahriman.py @@ -373,6 +373,24 @@ def test_subparsers_repo_check_option_refresh(parser: argparse.ArgumentParser) - assert args.refresh == 2 +def test_subparsers_repo_create_mirrorlist(parser: argparse.ArgumentParser) -> None: + """ + repo-create-mirrorlist command must imply trigger + """ + args = parser.parse_args(["repo-create-mirrorlist"]) + assert args.trigger == ["ahriman.core.support.MirrorlistTrigger"] + + +def test_subparsers_repo_create_mirrorlist_architecture(parser: argparse.ArgumentParser) -> None: + """ + repo-create-mirrorlist command must correctly parse architecture list + """ + args = parser.parse_args(["repo-create-mirrorlist"]) + assert args.architecture is None + args = parser.parse_args(["-a", "x86_64", "repo-create-mirrorlist"]) + assert args.architecture == ["x86_64"] + + def test_subparsers_repo_daemon(parser: argparse.ArgumentParser) -> None: """ repo-daemon command must imply dry run, exit code and package diff --git a/tests/ahriman/core/configuration/test_configuration.py b/tests/ahriman/core/configuration/test_configuration.py index f56ad7a0..9aa1d249 100644 --- a/tests/ahriman/core/configuration/test_configuration.py +++ b/tests/ahriman/core/configuration/test_configuration.py @@ -10,6 +10,13 @@ from ahriman.core.exceptions import InitializeError from ahriman.models.repository_paths import RepositoryPaths +def test_repository_name(configuration: Configuration) -> None: + """ + must return valid repository name + """ + assert configuration.repository_name == "aur-clone" + + def test_repository_paths(configuration: Configuration, repository_paths: RepositoryPaths) -> None: """ must return repository paths diff --git a/tests/ahriman/core/configuration/test_validator.py b/tests/ahriman/core/configuration/test_validator.py index db4bb34e..54535fa4 100644 --- a/tests/ahriman/core/configuration/test_validator.py +++ b/tests/ahriman/core/configuration/test_validator.py @@ -73,6 +73,30 @@ def test_validate_is_ip_address(validator: Validator, mocker: MockerFixture) -> ]) +def test_validate_path_is_absolute(validator: Validator, mocker: MockerFixture) -> None: + """ + must validate that path is absolute + """ + error_mock = mocker.patch("ahriman.core.configuration.validator.Validator._error") + + mocker.patch("pathlib.Path.is_absolute", return_value=False) + validator._validate_path_is_absolute(False, "field", Path("1")) + + mocker.patch("pathlib.Path.is_absolute", return_value=True) + validator._validate_path_is_absolute(False, "field", Path("2")) + + mocker.patch("pathlib.Path.is_absolute", return_value=False) + validator._validate_path_is_absolute(True, "field", Path("3")) + + mocker.patch("pathlib.Path.is_absolute", return_value=True) + validator._validate_path_is_absolute(True, "field", Path("4")) + + error_mock.assert_has_calls([ + MockCall("field", "Path 2 must be relative"), + MockCall("field", "Path 3 must be absolute"), + ]) + + def test_validate_is_url(validator: Validator, mocker: MockerFixture) -> None: """ must validate url correctly @@ -105,12 +129,16 @@ def test_validate_path_exists(validator: Validator, mocker: MockerFixture) -> No mocker.patch("pathlib.Path.exists", return_value=False) validator._validate_path_exists(False, "field", Path("1")) - mocker.patch("pathlib.Path.exists", return_value=False) - validator._validate_path_exists(True, "field", Path("2")) - mocker.patch("pathlib.Path.exists", return_value=True) + validator._validate_path_exists(False, "field", Path("2")) + + mocker.patch("pathlib.Path.exists", return_value=False) validator._validate_path_exists(True, "field", Path("3")) + mocker.patch("pathlib.Path.exists", return_value=True) + validator._validate_path_exists(True, "field", Path("4")) + error_mock.assert_has_calls([ - MockCall("field", "Path 2 must exist"), + MockCall("field", "Path 2 must not exist"), + MockCall("field", "Path 3 must exist"), ]) diff --git a/tests/ahriman/core/support/conftest.py b/tests/ahriman/core/support/conftest.py new file mode 100644 index 00000000..6e85fca9 --- /dev/null +++ b/tests/ahriman/core/support/conftest.py @@ -0,0 +1,34 @@ +import pytest + +from ahriman.core.configuration import Configuration +from ahriman.core.support.package_creator import PackageCreator +from ahriman.core.support.pkgbuild.mirrorlist_generator import MirrorlistGenerator + + +@pytest.fixture +def mirrorlist_generator(configuration: Configuration) -> MirrorlistGenerator: + """ + fixture for mirrorlist pkgbuild generator + + Args: + configuration(Configuration): configuration fixture + + Returns: + MirrorlistGenerator: mirrorlist pkgbuild generator test instance + """ + return MirrorlistGenerator(configuration, "mirrorlist") + + +@pytest.fixture +def package_creator(configuration: Configuration, mirrorlist_generator: MirrorlistGenerator) -> PackageCreator: + """ + package creator fixture + + Args: + configuration(Configuration): configuration fixture + mirrorlist_generator(MirrorlistGenerator): + + Returns: + PackageCreator: package creator test instance + """ + return PackageCreator(configuration, mirrorlist_generator) diff --git a/tests/ahriman/core/support/pkgbuild/conftest.py b/tests/ahriman/core/support/pkgbuild/conftest.py new file mode 100644 index 00000000..f7e87b07 --- /dev/null +++ b/tests/ahriman/core/support/pkgbuild/conftest.py @@ -0,0 +1,14 @@ +import pytest + +from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator + + +@pytest.fixture +def pkgbuild_generator() -> PkgbuildGenerator: + """ + fixture for dummy pkgbuild generator + + Returns: + PkgbuildGenerator: pkgbuild generator test instance + """ + return PkgbuildGenerator() diff --git a/tests/ahriman/core/support/pkgbuild/test_mirrorlist_generator.py b/tests/ahriman/core/support/pkgbuild/test_mirrorlist_generator.py new file mode 100644 index 00000000..59e764bd --- /dev/null +++ b/tests/ahriman/core/support/pkgbuild/test_mirrorlist_generator.py @@ -0,0 +1,97 @@ +from pathlib import Path +from unittest.mock import MagicMock + +from pytest_mock import MockerFixture + +from ahriman.core.configuration import Configuration +from ahriman.core.support.pkgbuild.mirrorlist_generator import MirrorlistGenerator + + +def test_init_path(configuration: Configuration) -> None: + """ + must set relative path to mirrorlist + """ + assert MirrorlistGenerator(configuration, "mirrorlist").path == Path("etc") / "pacman.d" / "aur-clone-mirrorlist" + + configuration.set_option("mirrorlist", "path", "/etc") + assert MirrorlistGenerator(configuration, "mirrorlist").path == Path("etc") + + +def test_license(configuration: Configuration) -> None: + """ + must generate correct licenses list + """ + assert MirrorlistGenerator(configuration, "mirrorlist").license == ["Unlicense"] + + configuration.set_option("mirrorlist", "license", "GPL MPL") + assert MirrorlistGenerator(configuration, "mirrorlist").license == ["GPL", "MPL"] + + +def test_pkgdesc(configuration: Configuration) -> None: + """ + must generate correct pkgdesc property + """ + assert MirrorlistGenerator(configuration, "mirrorlist").pkgdesc == "aur-clone mirror list for use by pacman" + + configuration.set_option("mirrorlist", "description", "description") + assert MirrorlistGenerator(configuration, "mirrorlist").pkgdesc == "description" + + +def test_pkgname(configuration: Configuration) -> None: + """ + must generate correct pkgname property + """ + assert MirrorlistGenerator(configuration, "mirrorlist").pkgname == "aur-clone-mirrorlist" + + configuration.set_option("mirrorlist", "package", "mirrorlist") + assert MirrorlistGenerator(configuration, "mirrorlist").pkgname == "mirrorlist" + + +def test_url(configuration: Configuration) -> None: + """ + must generate correct url property + """ + assert MirrorlistGenerator(configuration, "mirrorlist").url == "" + + configuration.set_option("mirrorlist", "homepage", "homepage") + assert MirrorlistGenerator(configuration, "mirrorlist").url == "homepage" + + +def test_generate_mirrorlist(mirrorlist_generator: MirrorlistGenerator, mocker: MockerFixture) -> None: + """ + must correctly generate mirrorlist file + """ + path = Path("local") + file_mock = MagicMock() + open_mock = mocker.patch("pathlib.Path.open") + open_mock.return_value.__enter__.return_value = file_mock + + mirrorlist_generator._generate_mirrorlist(path) + open_mock.assert_called_once_with("w") + file_mock.writelines.assert_called_once_with(["Server = http://localhost\n"]) + + +def test_package(mirrorlist_generator: MirrorlistGenerator) -> None: + """ + must generate package function correctly + """ + assert mirrorlist_generator.package() == """{ + install -Dm644 "$srcdir/mirrorlist" "$pkgdir/etc/pacman.d/aur-clone-mirrorlist" +}""" + + +def test_patches(mirrorlist_generator: MirrorlistGenerator) -> None: + """ + must generate additional patch list + """ + patches = {patch.key: patch for patch in mirrorlist_generator.patches()} + + assert "backup" in patches + assert patches["backup"].value == [str(mirrorlist_generator.path)] + + +def test_sources(mirrorlist_generator: MirrorlistGenerator) -> None: + """ + must return valid sources files list + """ + assert mirrorlist_generator.sources().get("mirrorlist") diff --git a/tests/ahriman/core/support/pkgbuild/test_pkgbuild_generator.py b/tests/ahriman/core/support/pkgbuild/test_pkgbuild_generator.py new file mode 100644 index 00000000..77e38a90 --- /dev/null +++ b/tests/ahriman/core/support/pkgbuild/test_pkgbuild_generator.py @@ -0,0 +1,112 @@ +import datetime +import pytest + +from pathlib import Path +from pytest_mock import MockerFixture +from unittest.mock import MagicMock, call as MockCall + +from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator +from ahriman.core.util import utcnow +from ahriman.models.pkgbuild_patch import PkgbuildPatch + + +def test_license(pkgbuild_generator: PkgbuildGenerator) -> None: + """ + must return empty license list + """ + assert pkgbuild_generator.license == [] + + +def test_pkgdesc(pkgbuild_generator: PkgbuildGenerator) -> None: + """ + must raise NotImplementedError on missing pkgdesc property + """ + with pytest.raises(NotImplementedError): + assert pkgbuild_generator.pkgdesc + + +def test_pkgname(pkgbuild_generator: PkgbuildGenerator) -> None: + """ + must raise NotImplementedError on missing pkgname property + """ + with pytest.raises(NotImplementedError): + assert pkgbuild_generator.pkgname + + +def test_pkgver(pkgbuild_generator: PkgbuildGenerator, mocker: MockerFixture) -> None: + """ + must implement default version as current date + """ + mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.utcnow", return_value=datetime.datetime(2002, 3, 11)) + assert pkgbuild_generator.pkgver == utcnow().strftime("20020311") + + +def test_url(pkgbuild_generator: PkgbuildGenerator) -> None: + """ + must return empty url + """ + assert pkgbuild_generator.url == "" + + +def test_package(pkgbuild_generator: PkgbuildGenerator) -> None: + """ + must raise NotImplementedError on missing package function + """ + with pytest.raises(NotImplementedError): + pkgbuild_generator.package() + + +def test_patches(pkgbuild_generator: PkgbuildGenerator) -> None: + """ + must return empty patches list + """ + assert pkgbuild_generator.patches() == [] + + +def test_sources(pkgbuild_generator: PkgbuildGenerator) -> None: + """ + must return empty sources list + """ + assert pkgbuild_generator.sources() == {} + + +def test_write_pkgbuild(pkgbuild_generator: PkgbuildGenerator, mocker: MockerFixture) -> None: + """ + must write PKGBUILD content to file + """ + path = Path("local") + for prop in ("pkgdesc", "pkgname"): + mocker.patch.object(PkgbuildGenerator, prop, "") + mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.PkgbuildGenerator.package", return_value="{}") + patches_mock = mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.PkgbuildGenerator.patches", + return_value=[PkgbuildPatch("property", "value")]) + sources_mock = mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.PkgbuildGenerator.write_sources", + return_value=[PkgbuildPatch("source", []), PkgbuildPatch("sha512sums", [])]) + write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write") + + pkgbuild_generator.write_pkgbuild(path) + patches_mock.assert_called_once_with() + sources_mock.assert_called_once_with(path) + write_mock.assert_has_calls([MockCall(path / "PKGBUILD")] * 11) + + +def test_write_sources(pkgbuild_generator: PkgbuildGenerator, mocker: MockerFixture) -> None: + """ + must write sources files + """ + path = Path("local") + generator_mock = MagicMock() + sources_mock = mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.PkgbuildGenerator.sources", + return_value={"source": generator_mock}) + open_mock = mocker.patch("pathlib.Path.open") + hash_mock = MagicMock() + hash_mock.hexdigest.return_value = "hash" + mocker.patch("hashlib.sha512", return_value=hash_mock) + + assert pkgbuild_generator.write_sources(path) == [ + PkgbuildPatch("source", ["source"]), + PkgbuildPatch("sha512sums", ["hash"]), + ] + generator_mock.assert_called_once_with(path / "source") + sources_mock.assert_called_once_with() + open_mock.assert_called_once_with("rb") diff --git a/tests/ahriman/core/support/test_mirrorlist_trigger.py b/tests/ahriman/core/support/test_mirrorlist_trigger.py new file mode 100644 index 00000000..25abbcb6 --- /dev/null +++ b/tests/ahriman/core/support/test_mirrorlist_trigger.py @@ -0,0 +1,27 @@ +from pytest_mock import MockerFixture + +from ahriman.core.configuration import Configuration +from ahriman.core.support import MirrorlistTrigger + + +def test_configuration_sections(configuration: Configuration) -> None: + """ + must correctly parse target list + """ + configuration.set_option("mirrorlist", "target", "a b c") + assert MirrorlistTrigger.configuration_sections(configuration) == ["a", "b", "c"] + + configuration.remove_option("mirrorlist", "target") + assert MirrorlistTrigger.configuration_sections(configuration) == [] + + +def test_on_start(configuration: Configuration, mocker: MockerFixture) -> None: + """ + must run report for specified targets + """ + configuration.set_option("mirrorlist", "target", "mirrorlist") + run_mock = mocker.patch("ahriman.core.support.package_creator.PackageCreator.run") + + trigger = MirrorlistTrigger("x86_64", configuration) + trigger.on_start() + run_mock.assert_called_once_with() diff --git a/tests/ahriman/core/support/test_package_creator.py b/tests/ahriman/core/support/test_package_creator.py new file mode 100644 index 00000000..616df485 --- /dev/null +++ b/tests/ahriman/core/support/test_package_creator.py @@ -0,0 +1,40 @@ +import pytest + +from pytest_mock import MockerFixture + +from ahriman.core.database import SQLite +from ahriman.core.support.package_creator import PackageCreator +from ahriman.models.context_key import ContextKey +from ahriman.models.package import Package +from ahriman.models.package_description import PackageDescription + + +def test_run(package_creator: PackageCreator, database: SQLite, mocker: MockerFixture) -> None: + """ + must correctly process package creation + """ + package = Package( + base=package_creator.generator.pkgname, + version=package_creator.generator.pkgver, + remote=None, + packages={package_creator.generator.pkgname: PackageDescription()}, + ) + local_path = package_creator.configuration.repository_paths.cache_for(package_creator.generator.pkgname) + + rmtree_mock = mocker.patch("shutil.rmtree") + database_mock = mocker.patch("ahriman.core._Context.get", return_value=database) + init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init") + insert_mock = mocker.patch("ahriman.core.database.SQLite.package_update") + mkdir_mock = mocker.patch("pathlib.Path.mkdir") + package_mock = mocker.patch("ahriman.models.package.Package.from_build", return_value=package) + write_mock = mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.PkgbuildGenerator.write_pkgbuild") + + package_creator.run() + rmtree_mock.assert_called_once_with(local_path, ignore_errors=True) + mkdir_mock.assert_called_once_with(mode=0o755, parents=True, exist_ok=True) + write_mock.assert_called_once_with(local_path) + init_mock.assert_called_once_with(local_path) + + package_mock.assert_called_once_with(local_path, "x86_64") + database_mock.assert_called_once_with(ContextKey("database", SQLite)) + insert_mock.assert_called_once_with(package, pytest.helpers.anyvar(int)) diff --git a/tests/ahriman/core/test_util.py b/tests/ahriman/core/test_util.py index 31c4bb53..3aa6bd43 100644 --- a/tests/ahriman/core/test_util.py +++ b/tests/ahriman/core/test_util.py @@ -81,12 +81,12 @@ def test_check_output_with_user(passwd: Any, mocker: MockerFixture) -> None: """ must run command as specified user and set its homedir """ - assert check_output("python", "-c", "import os; print(os.getenv('HOME'))") != passwd.pw_dir + assert check_output("python", "-c", """import os; print(os.getenv("HOME"))""") != passwd.pw_dir getpwuid_mock = mocker.patch("ahriman.core.util.getpwuid", return_value=passwd) user = os.getuid() - assert check_output("python", "-c", "import os; print(os.getenv('HOME'))", user=user) == passwd.pw_dir + assert check_output("python", "-c", """import os; print(os.getenv("HOME"))""", user=user) == passwd.pw_dir getpwuid_mock.assert_called_once_with(user) diff --git a/tests/ahriman/core/triggers/test_trigger_loader.py b/tests/ahriman/core/triggers/test_trigger_loader.py index a43d1596..187a8451 100644 --- a/tests/ahriman/core/triggers/test_trigger_loader.py +++ b/tests/ahriman/core/triggers/test_trigger_loader.py @@ -11,6 +11,17 @@ from ahriman.models.package import Package from ahriman.models.result import Result +def test_known_triggers(configuration: Configuration) -> None: + """ + must return used triggers + """ + configuration.set_option("build", "triggers_known", "a b c") + assert TriggerLoader.known_triggers(configuration) == ["a", "b", "c"] + + configuration.remove_option("build", "triggers_known") + assert TriggerLoader.known_triggers(configuration) == [] + + def test_selected_triggers(configuration: Configuration) -> None: """ must return used triggers diff --git a/tests/testresources/core/ahriman.ini b/tests/testresources/core/ahriman.ini index 8dfb33fe..ff7da9fb 100644 --- a/tests/testresources/core/ahriman.ini +++ b/tests/testresources/core/ahriman.ini @@ -25,6 +25,7 @@ ignore_packages = makechrootpkg_flags = makepkg_flags = --skippgpcheck triggers = ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger +triggers_known = ahriman.core.support.MirrorlistTrigger [repository] name = aur-clone @@ -33,6 +34,10 @@ root = ../../../ [sign] target = +[mirrorlist] +target = mirrorlist +servers = http://localhost + [remote-push] target = gitremote