implement mirrorlist package generator

This commit is contained in:
Evgenii Alekseev 2023-05-02 18:08:25 +03:00
parent 8e0e57e193
commit 21ea9a4dd1
31 changed files with 1132 additions and 34 deletions

View File

@ -30,8 +30,8 @@ Base configuration settings.
* ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference. * ``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. * ``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. 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. * ``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. * ``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`` - 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``. * ``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 ``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. * ``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. * ``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 ``remote-pull`` group
--------------------- ---------------------

View File

@ -25,6 +25,7 @@ ignore_packages =
makechrootpkg_flags = makechrootpkg_flags =
makepkg_flags = --nocolor --ignorearch makepkg_flags = --nocolor --ignorearch
triggers = ahriman.core.gitremote.RemotePullTrigger ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger ahriman.core.gitremote.RemotePushTrigger 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 vcs_allowed_age = 604800
[repository] [repository]
@ -34,6 +35,9 @@ root = /var/lib/ahriman
[sign] [sign]
target = target =
[mirrorlist]
target =
[remote-pull] [remote-pull]
target = target =

View File

@ -100,6 +100,7 @@ def _parser() -> argparse.ArgumentParser:
_set_patch_set_add_parser(subparsers) _set_patch_set_add_parser(subparsers)
_set_repo_backup_parser(subparsers) _set_repo_backup_parser(subparsers)
_set_repo_check_parser(subparsers) _set_repo_check_parser(subparsers)
_set_repo_create_mirrorlist_parser(subparsers)
_set_repo_daemon_parser(subparsers) _set_repo_daemon_parser(subparsers)
_set_repo_rebuild_parser(subparsers) _set_repo_rebuild_parser(subparsers)
_set_repo_remove_unknown_parser(subparsers) _set_repo_remove_unknown_parser(subparsers)
@ -478,6 +479,25 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser 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: def _set_repo_daemon_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for daemon subcommand add parser for daemon subcommand

View File

@ -26,6 +26,7 @@ from typing import Any
from ahriman.application.application.application_properties import ApplicationProperties from ahriman.application.application.application_properties import ApplicationProperties
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.util import package_like from ahriman.core.util import package_like
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
@ -43,8 +44,14 @@ class ApplicationPackages(ApplicationProperties):
Args: Args:
source(str): path to package archive source(str): path to package archive
Raises:
UnknownPackageError: if specified path doesn't exist
""" """
local_path = Path(source) local_path = Path(source)
if not local_path.is_file():
raise UnknownPackageError(source)
dst = self.repository.paths.packages / local_path.name dst = self.repository.paths.packages / local_path.name
shutil.copy(local_path, dst) shutil.copy(local_path, dst)
@ -68,6 +75,9 @@ class ApplicationPackages(ApplicationProperties):
source(str): path to local directory source(str): path to local directory
""" """
local_dir = Path(source) local_dir = Path(source)
if not local_dir.is_dir():
raise UnknownPackageError(source)
for full_path in filter(package_like, local_dir.iterdir()): for full_path in filter(package_like, local_dir.iterdir()):
self._add_archive(str(full_path)) self._add_archive(str(full_path))
@ -77,12 +87,19 @@ class ApplicationPackages(ApplicationProperties):
Args: Args:
source(str): path to directory with local source files source(str): path to directory with local source files
Raises:
UnknownPackageError: if specified package is unknown or doesn't exist
""" """
source_dir = Path(source) if (source_dir := Path(source)).is_dir():
package = Package.from_build(source_dir, self.architecture) package = Package.from_build(source_dir, self.architecture)
cache_dir = self.repository.paths.cache_for(package.base) cache_dir = self.repository.paths.cache_for(package.base)
shutil.copytree(source_dir, cache_dir) # copy package to store in caches 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 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) 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? dst = self.repository.paths.packages / Path(source).name # URL is path, is not it?
# timeout=None to suppress pylint warns. Also suppress bandit warnings # timeout=None to suppress pylint warns. Also suppress bandit warnings
response = requests.get(source, stream=True, timeout=None) # nosec try:
response.raise_for_status() 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: with dst.open("wb") as local_file:
for chunk in response.iter_content(chunk_size=1024): for chunk in response.iter_content(chunk_size=1024):

View File

@ -78,7 +78,9 @@ class Validate(Handler):
# create trigger loader instance # create trigger loader instance
loader = TriggerLoader() loader = TriggerLoader()
for trigger in loader.selected_triggers(configuration): triggers = loader.selected_triggers(configuration) + loader.known_triggers(configuration)
for trigger in triggers:
try: try:
trigger_class = loader.load_trigger_class(trigger) trigger_class = loader.load_trigger_class(trigger)
except ExtensionError: except ExtensionError:

View File

@ -99,6 +99,16 @@ class Configuration(configparser.RawConfigParser):
""" """
return self.getpath("settings", "logging") 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 @property
def repository_paths(self) -> RepositoryPaths: def repository_paths(self) -> RepositoryPaths:
""" """

View File

@ -163,6 +163,11 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"coerce": "list", "coerce": "list",
"schema": {"type": "string"}, "schema": {"type": "string"},
}, },
"triggers_known": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
"vcs_allowed_age": { "vcs_allowed_age": {
"type": "integer", "type": "integer",
"coerce": "integer", "coerce": "integer",

View File

@ -144,6 +144,24 @@ class Validator(RootValidator):
if constraint and url.scheme not in constraint: if constraint and url.scheme not in constraint:
self._error(field, f"Url {value} scheme must be one of {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: def _validate_path_exists(self, constraint: bool, field: str, value: Path) -> None:
""" """
check if paths exists check if paths exists
@ -159,3 +177,5 @@ class Validator(RootValidator):
""" """
if constraint and not value.exists(): if constraint and not value.exists():
self._error(field, f"Path {value} must exist") self._error(field, f"Path {value} must exist")
if not constraint and value.exists():
self._error(field, f"Path {value} must not exist")

View File

@ -75,7 +75,7 @@ class JinjaTemplate:
# base template vars # base template vars
self.homepage = configuration.get(section, "homepage", fallback=None) 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) self.sign_targets, self.default_pgp_key = GPG.sign_options(configuration)

View File

@ -67,7 +67,7 @@ class RepositoryProperties(LazyLogging):
self.configuration = configuration self.configuration = configuration
self.database = database 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.vcs_allowed_age = configuration.getint("build", "vcs_allowed_age", fallback=0)
self.paths: RepositoryPaths = configuration.repository_paths # additional workaround for pycharm typing self.paths: RepositoryPaths = configuration.repository_paths # additional workaround for pycharm typing

View File

@ -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 <http://www.gnu.org/licenses/>.
#
from ahriman.core.support.mirrorlist_trigger import MirrorlistTrigger

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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()

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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())

View File

@ -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 <http://www.gnu.org/licenses/>.
#

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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
}

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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]),
]

View File

@ -84,6 +84,20 @@ class TriggerLoader(LazyLogging):
return instance 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 @staticmethod
def selected_triggers(configuration: Configuration) -> list[str]: def selected_triggers(configuration: Configuration) -> list[str]:
""" """

View File

@ -5,25 +5,36 @@ from pytest_mock import MockerFixture
from unittest.mock import MagicMock from unittest.mock import MagicMock
from ahriman.application.application.application_packages import ApplicationPackages from ahriman.application.application.application_packages import ApplicationPackages
from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
from ahriman.models.result import Result from ahriman.models.result import Result
def test_add_archive( def test_add_archive(application_packages: ApplicationPackages, package_ahriman: Package,
application_packages: ApplicationPackages, mocker: MockerFixture) -> None:
package_ahriman: Package,
mocker: MockerFixture) -> None:
""" """
must add package from archive must add package from archive
""" """
is_file_mock = mocker.patch("pathlib.Path.is_file", return_value=True)
copy_mock = mocker.patch("shutil.copy") copy_mock = mocker.patch("shutil.copy")
application_packages._add_archive(package_ahriman.base) application_packages._add_archive(package_ahriman.base)
is_file_mock.assert_called_once_with()
copy_mock.assert_called_once_with( copy_mock.assert_called_once_with(
Path(package_ahriman.base), application_packages.repository.paths.packages / package_ahriman.base) 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: def test_add_aur(application_packages: ApplicationPackages, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must add package from AUR 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) update_remote_mock.assert_called_once_with(package_ahriman)
def test_add_directory( def test_add_directory(application_packages: ApplicationPackages, package_ahriman: Package,
application_packages: ApplicationPackages, mocker: MockerFixture) -> None:
package_ahriman: Package,
mocker: MockerFixture) -> None:
""" """
must add packages from directory must add packages from directory
""" """
iterdir_mock = mocker.patch("pathlib.Path.iterdir", is_dir_mock = mocker.patch("pathlib.Path.is_dir", return_value=True)
return_value=[package.filepath for package in package_ahriman.packages.values()])
copy_mock = mocker.patch("shutil.copy")
filename = package_ahriman.packages[package_ahriman.base].filepath 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) application_packages._add_directory(package_ahriman.base)
is_dir_mock.assert_called_once_with()
iterdir_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: 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 must add package from local sources
""" """
mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) 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") init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init")
copytree_mock = mocker.patch("shutil.copytree") copytree_mock = mocker.patch("shutil.copytree")
build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert") build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert")
application_packages._add_local(package_ahriman.base) application_packages._add_local(package_ahriman.base)
is_dir_mock.assert_called_once_with()
copytree_mock.assert_called_once_with( copytree_mock.assert_called_once_with(
Path(package_ahriman.base), application_packages.repository.paths.cache_for(package_ahriman.base)) 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)) init_mock.assert_called_once_with(application_packages.repository.paths.cache_for(package_ahriman.base))
build_queue_mock.assert_called_once_with(package_ahriman) 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, def test_add_remote(application_packages: ApplicationPackages, package_description_ahriman: PackageDescription,
mocker: MockerFixture) -> None: 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() 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, def test_add_repository(application_packages: ApplicationPackages, package_ahriman: Package,
mocker: MockerFixture) -> None: 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) add_mock.assert_called_once_with(package_ahriman.base)
def test_add_add_aur( def test_add_add_aur(application_packages: ApplicationPackages, package_ahriman: Package,
application_packages: ApplicationPackages, mocker: MockerFixture) -> None:
package_ahriman: Package,
mocker: MockerFixture) -> None:
""" """
must add package from AUR via add function must add package from AUR via add function
""" """

View File

@ -62,6 +62,8 @@ def test_schema(configuration: Configuration) -> None:
assert schema.pop("email") assert schema.pop("email")
assert schema.pop("github") assert schema.pop("github")
assert schema.pop("html") assert schema.pop("html")
assert schema.pop("mirrorlist")
assert schema.pop("mirrorlist_generator")
assert schema.pop("report") assert schema.pop("report")
assert schema.pop("rsync") assert schema.pop("rsync")
assert schema.pop("s3") 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 must skip trigger if it caused exception on load
""" """
configuration.set_option("build", "triggers", "some.invalid.trigger.path.Trigger") configuration.set_option("build", "triggers", "some.invalid.trigger.path.Trigger")
configuration.remove_option("build", "triggers_known")
assert Validate.schema("x86_64", configuration) == CONFIGURATION_SCHEMA assert Validate.schema("x86_64", configuration) == CONFIGURATION_SCHEMA

View File

@ -373,6 +373,24 @@ def test_subparsers_repo_check_option_refresh(parser: argparse.ArgumentParser) -
assert args.refresh == 2 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: def test_subparsers_repo_daemon(parser: argparse.ArgumentParser) -> None:
""" """
repo-daemon command must imply dry run, exit code and package repo-daemon command must imply dry run, exit code and package

View File

@ -10,6 +10,13 @@ from ahriman.core.exceptions import InitializeError
from ahriman.models.repository_paths import RepositoryPaths 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: def test_repository_paths(configuration: Configuration, repository_paths: RepositoryPaths) -> None:
""" """
must return repository paths must return repository paths

View File

@ -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: def test_validate_is_url(validator: Validator, mocker: MockerFixture) -> None:
""" """
must validate url correctly 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) mocker.patch("pathlib.Path.exists", return_value=False)
validator._validate_path_exists(False, "field", Path("1")) 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) 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")) 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([ error_mock.assert_has_calls([
MockCall("field", "Path 2 must exist"), MockCall("field", "Path 2 must not exist"),
MockCall("field", "Path 3 must exist"),
]) ])

View File

@ -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)

View File

@ -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()

View File

@ -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")

View File

@ -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")

View File

@ -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()

View File

@ -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))

View File

@ -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 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) getpwuid_mock = mocker.patch("ahriman.core.util.getpwuid", return_value=passwd)
user = os.getuid() 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) getpwuid_mock.assert_called_once_with(user)

View File

@ -11,6 +11,17 @@ from ahriman.models.package import Package
from ahriman.models.result import Result 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: def test_selected_triggers(configuration: Configuration) -> None:
""" """
must return used triggers must return used triggers

View File

@ -25,6 +25,7 @@ ignore_packages =
makechrootpkg_flags = makechrootpkg_flags =
makepkg_flags = --skippgpcheck makepkg_flags = --skippgpcheck
triggers = ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger triggers = ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger
triggers_known = ahriman.core.support.MirrorlistTrigger
[repository] [repository]
name = aur-clone name = aur-clone
@ -33,6 +34,10 @@ root = ../../../
[sign] [sign]
target = target =
[mirrorlist]
target = mirrorlist
servers = http://localhost
[remote-push] [remote-push]
target = gitremote target = gitremote