mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-07-14 14:51:08 +00:00
reorder tests
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
[build]
|
||||
; List of well-known triggers. Used only for configuration purposes.
|
||||
triggers_known[] = ahriman.core.archive.ArchiveTrigger
|
||||
triggers_known[] = ahriman.core.distributed.WorkerLoaderTrigger
|
||||
triggers_known[] = ahriman.core.distributed.WorkerTrigger
|
||||
triggers_known[] = ahriman.core.support.KeyringTrigger
|
||||
triggers_known[] = ahriman.core.support.MirrorlistTrigger
|
||||
; List of worker nodes addresses used for build process, e.g.:
|
||||
; workers = http://10.0.0.1:8080 http://10.0.0.3:8080
|
||||
; Empty list means run on the local instance.
|
||||
;workers =
|
||||
|
||||
[keyring]
|
||||
; List of configuration section names for keyring generator plugin, e.g.:
|
||||
; target = keyring-trigger
|
||||
target =
|
||||
|
||||
; Keyring generator trigger sample.
|
||||
;[keyring-trigger]
|
||||
; Generator type name.
|
||||
;type = keyring-generator
|
||||
; Optional keyring package description.
|
||||
;description=
|
||||
; Optional URL to the repository homepage.
|
||||
;homepage=
|
||||
; Keyring package licenses list.
|
||||
;license = Unlicense
|
||||
; Optional keyring package name.
|
||||
;package =
|
||||
; Optional packager PGP keys list. If none set, it will read from database.
|
||||
;packagers =
|
||||
; List of revoked PGP keys.
|
||||
;revoked =
|
||||
; List of master PGP keys. If none set, the sign.key value will be used.
|
||||
;trusted =
|
||||
|
||||
[mirrorlist]
|
||||
; List of configuration section names for mirrorlist generator plugin, e.g.:
|
||||
; target = mirrorlist-trigger
|
||||
target =
|
||||
|
||||
; Mirror list generator trigger sample.
|
||||
;[mirrorlist-trigger]
|
||||
; Generator type name.
|
||||
;type = mirrorlist-generator
|
||||
; Optional mirrorlist package description.
|
||||
;description=
|
||||
; Optional URL to the repository homepage.
|
||||
;homepage=
|
||||
; Mirrorlist package licenses list.
|
||||
;license = Unlicense
|
||||
; Optional mirrorlist package name.
|
||||
;package =
|
||||
; Absolute path to generated mirrorlist file, usually path inside /etc/pacman.d directory.
|
||||
;path =
|
||||
; List of repository mirrors.
|
||||
;servers =
|
||||
@@ -0,0 +1,32 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "ahriman-triggers"
|
||||
description = "ArcH linux ReposItory MANager, additional extensions"
|
||||
readme = "../README.md"
|
||||
requires-python = ">=3.13"
|
||||
license = {file = "../COPYING"}
|
||||
authors = [
|
||||
{name = "ahriman team"},
|
||||
]
|
||||
dependencies = [
|
||||
"ahriman-core",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
[project.urls]
|
||||
Documentation = "https://ahriman.readthedocs.io/"
|
||||
Repository = "https://github.com/arcan1s/ahriman"
|
||||
Changelog = "https://github.com/arcan1s/ahriman/releases"
|
||||
|
||||
[tool.hatch.version]
|
||||
path = "../ahriman-core/src/ahriman/__init__.py"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
only-include = ["src/ahriman"]
|
||||
sources = ["src"]
|
||||
|
||||
[tool.hatch.build.targets.wheel.shared-data]
|
||||
"package/share" = "share"
|
||||
@@ -0,0 +1,19 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from ahriman.application.handlers.handler import SubParserAction
|
||||
from ahriman.application.handlers.triggers import Triggers
|
||||
|
||||
|
||||
class TriggersSupport(Triggers):
|
||||
"""
|
||||
additional triggers handlers for support commands
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _set_repo_create_keyring_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for create-keyring subcommand
|
||||
|
||||
Args:
|
||||
root(SubParserAction): subparsers for the commands
|
||||
|
||||
Returns:
|
||||
argparse.ArgumentParser: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("repo-create-keyring", help="create keyring package",
|
||||
description="create package which contains list of trusted keys as set by "
|
||||
"configuration. Note, that this action will only create package, "
|
||||
"the package itself has to be built manually")
|
||||
parser.set_defaults(trigger=["ahriman.core.support.KeyringTrigger"])
|
||||
return parser
|
||||
|
||||
@staticmethod
|
||||
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")
|
||||
parser.set_defaults(trigger=["ahriman.core.support.MirrorlistTrigger"])
|
||||
return parser
|
||||
|
||||
arguments = [
|
||||
_set_repo_create_keyring_parser,
|
||||
_set_repo_create_mirrorlist_parser,
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
#
|
||||
# Copyright (c) 2021-2025 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.archive.archive_trigger import ArchiveTrigger
|
||||
@@ -0,0 +1,185 @@
|
||||
#
|
||||
# Copyright (c) 2021-2025 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 datetime
|
||||
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core.alpm.repo import Repo
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.utils import package_like, symlink_relative, utcnow, walk
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class ArchiveTree(LazyLogging):
|
||||
"""
|
||||
wrapper around archive tree
|
||||
|
||||
Attributes:
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
sign_args(list[str]): additional args which have to be used to sign repository archive
|
||||
"""
|
||||
|
||||
def __init__(self, repository_path: RepositoryPaths, sign_args: list[str]) -> None:
|
||||
"""
|
||||
Args:
|
||||
repository_path(RepositoryPaths): repository paths instance
|
||||
sign_args(list[str]): additional args which have to be used to sign repository archive
|
||||
"""
|
||||
self.paths = repository_path
|
||||
self.repository_id = repository_path.repository_id
|
||||
self.sign_args = sign_args
|
||||
|
||||
@staticmethod
|
||||
def _package_symlinks_create(package_description: PackageDescription, root: Path, archive: Path) -> bool:
|
||||
"""
|
||||
process symlinks creation for single package
|
||||
|
||||
Args:
|
||||
package_description(PackageDescription): archive descriptor
|
||||
root(Path): path to the archive repository root
|
||||
archive(Path): path to directory with archives
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if symlinks were created and ``False`` otherwise
|
||||
"""
|
||||
symlinks_created = False
|
||||
# here we glob for archive itself and signature if any
|
||||
for file in archive.glob(f"{package_description.filename}*"):
|
||||
try:
|
||||
symlink_relative(root / file.name, file)
|
||||
symlinks_created = True
|
||||
except FileExistsError:
|
||||
continue # symlink is already created, skip processing
|
||||
|
||||
return symlinks_created
|
||||
|
||||
def _repo(self, root: Path) -> Repo:
|
||||
"""
|
||||
constructs :class:`ahriman.core.alpm.repo.Repo` object for given path
|
||||
|
||||
Args:
|
||||
root(Path): root of the repository
|
||||
|
||||
Returns:
|
||||
Repo: constructed object with correct properties
|
||||
"""
|
||||
return Repo(self.repository_id.name, self.paths, self.sign_args, root)
|
||||
|
||||
def directories_fix(self, paths: set[Path]) -> None:
|
||||
"""
|
||||
remove empty repository directories recursively
|
||||
|
||||
Args:
|
||||
paths(set[Path]): repositories to check
|
||||
"""
|
||||
root = self.paths.archive / "repos"
|
||||
for repository in paths:
|
||||
parents = [repository] + list(repository.parents[:-1])
|
||||
for parent in parents:
|
||||
path = root / parent
|
||||
if list(path.iterdir()):
|
||||
continue # directory is not empty
|
||||
path.rmdir()
|
||||
|
||||
def repository_for(self, date: datetime.date | None = None) -> Path:
|
||||
"""
|
||||
get full path to repository at the specified date
|
||||
|
||||
Args:
|
||||
date(datetime.date | None, optional): date to generate path. If none supplied then today will be used
|
||||
(Default value = None)
|
||||
|
||||
Returns:
|
||||
Path: path to the repository root
|
||||
"""
|
||||
date = date or utcnow().date()
|
||||
return (
|
||||
self.paths.archive
|
||||
/ "repos"
|
||||
/ date.strftime("%Y")
|
||||
/ date.strftime("%m")
|
||||
/ date.strftime("%d")
|
||||
/ self.repository_id.name
|
||||
/ self.repository_id.architecture
|
||||
)
|
||||
|
||||
def symlinks_create(self, packages: list[Package]) -> None:
|
||||
"""
|
||||
create symlinks for the specified packages in today's repository
|
||||
|
||||
Args:
|
||||
packages(list[Package]): list of packages to be updated
|
||||
"""
|
||||
root = self.repository_for()
|
||||
repo = self._repo(root)
|
||||
|
||||
for package in packages:
|
||||
archive = self.paths.archive_for(package.base)
|
||||
|
||||
for package_name, single in package.packages.items():
|
||||
if single.filename is None:
|
||||
self.logger.warning("received empty package filename for %s", package_name)
|
||||
continue
|
||||
|
||||
if self._package_symlinks_create(single, root, archive):
|
||||
repo.add(root / single.filename)
|
||||
|
||||
def symlinks_fix(self) -> Iterator[Path]:
|
||||
"""
|
||||
remove broken symlinks across repositories for all dates
|
||||
|
||||
Yields:
|
||||
Path: path of the sub-repository with removed symlinks
|
||||
"""
|
||||
for path in walk(self.paths.archive / "repos"):
|
||||
root = path.parent
|
||||
*_, name, architecture = root.parts
|
||||
if self.repository_id.name != name or self.repository_id.architecture != architecture:
|
||||
continue # we only process same name repositories
|
||||
|
||||
if not package_like(path):
|
||||
continue
|
||||
if not path.is_symlink():
|
||||
continue # find symlinks only
|
||||
if path.exists():
|
||||
continue # filter out not broken symlinks
|
||||
|
||||
# here we don't have access to original archive, so we have to guess name based on archive name
|
||||
# normally it should be fine to do so
|
||||
package_name = path.name.rsplit("-", maxsplit=3)[0]
|
||||
self._repo(root).remove(package_name, path)
|
||||
yield path.parent.relative_to(self.paths.archive / "repos")
|
||||
|
||||
def tree_create(self) -> None:
|
||||
"""
|
||||
create repository tree for current repository
|
||||
"""
|
||||
root = self.repository_for()
|
||||
if root.exists():
|
||||
return
|
||||
|
||||
with self.paths.preserve_owner():
|
||||
root.mkdir(0o755, parents=True)
|
||||
# init empty repository here
|
||||
self._repo(root).init()
|
||||
@@ -0,0 +1,70 @@
|
||||
#
|
||||
# Copyright (c) 2021-2025 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.archive.archive_tree import ArchiveTree
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.triggers import Trigger
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
class ArchiveTrigger(Trigger):
|
||||
"""
|
||||
archive repository extension
|
||||
|
||||
Attributes:
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
tree(ArchiveTree): archive tree wrapper
|
||||
"""
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
Trigger.__init__(self, repository_id, configuration)
|
||||
|
||||
self.paths = configuration.repository_paths
|
||||
self.tree = ArchiveTree(self.paths, GPG(configuration).repository_sign_args)
|
||||
|
||||
def on_result(self, result: Result, packages: list[Package]) -> None:
|
||||
"""
|
||||
run trigger
|
||||
|
||||
Args:
|
||||
result(Result): build result
|
||||
packages(list[Package]): list of all available packages
|
||||
"""
|
||||
self.tree.symlinks_create(packages)
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
trigger action which will be called at the start of the application
|
||||
"""
|
||||
self.tree.tree_create()
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
trigger action which will be called before the stop of the application
|
||||
"""
|
||||
repositories = set(self.tree.symlinks_fix())
|
||||
self.tree.directories_fix(repositories)
|
||||
@@ -0,0 +1,22 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.distributed.worker_loader_trigger import WorkerLoaderTrigger
|
||||
from ahriman.core.distributed.worker_trigger import WorkerTrigger
|
||||
from ahriman.core.distributed.workers_cache import WorkersCache
|
||||
@@ -0,0 +1,127 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import contextlib
|
||||
|
||||
from functools import cached_property
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.status.web_client import WebClient
|
||||
from ahriman.core.triggers import Trigger
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.worker import Worker
|
||||
|
||||
|
||||
class DistributedSystem(Trigger, WebClient):
|
||||
"""
|
||||
simple class to (un)register itself as a distributed worker
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA = {
|
||||
"worker": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"address": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"empty": False,
|
||||
"is_url": [],
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
"time_to_live": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
Trigger.__init__(self, repository_id, configuration)
|
||||
WebClient.__init__(self, repository_id, configuration)
|
||||
|
||||
@cached_property
|
||||
def worker(self) -> Worker:
|
||||
"""
|
||||
load and set worker. Lazy property loaded because it is not always required
|
||||
|
||||
Returns:
|
||||
Worker: unique self worker identifier
|
||||
"""
|
||||
section = next(iter(self.configuration_sections(self.configuration)))
|
||||
|
||||
address = self.configuration.get(section, "address")
|
||||
identifier = self.configuration.get(section, "identifier", fallback="")
|
||||
return Worker(address, identifier=identifier)
|
||||
|
||||
@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 list(cls.CONFIGURATION_SCHEMA.keys())
|
||||
|
||||
def _workers_url(self) -> str:
|
||||
"""
|
||||
workers url generator
|
||||
|
||||
Returns:
|
||||
str: full url of web service for workers
|
||||
"""
|
||||
return f"{self.address}/api/v1/distributed"
|
||||
|
||||
def register(self) -> None:
|
||||
"""
|
||||
register itself in remote system
|
||||
"""
|
||||
with contextlib.suppress(Exception):
|
||||
self.make_request("POST", self._workers_url(), json=self.worker.view())
|
||||
|
||||
def workers(self) -> list[Worker]:
|
||||
"""
|
||||
retrieve list of available remote workers
|
||||
|
||||
Returns:
|
||||
list[Worker]: currently registered workers
|
||||
"""
|
||||
with contextlib.suppress(Exception):
|
||||
response = self.make_request("GET", self._workers_url())
|
||||
response_json = response.json()
|
||||
|
||||
return [
|
||||
Worker(worker["address"], identifier=worker["identifier"])
|
||||
for worker in response_json
|
||||
]
|
||||
|
||||
return []
|
||||
@@ -0,0 +1,40 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.distributed.distributed_system import DistributedSystem
|
||||
|
||||
|
||||
class WorkerLoaderTrigger(DistributedSystem):
|
||||
"""
|
||||
remote worker processor trigger (server side)
|
||||
"""
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
trigger action which will be called at the start of the application
|
||||
"""
|
||||
if self.configuration.has_option("build", "workers"):
|
||||
return # there is manually set option
|
||||
|
||||
workers = [worker.address for worker in self.workers()]
|
||||
if not workers:
|
||||
return
|
||||
|
||||
self.logger.info("load workers %s", workers)
|
||||
self.configuration.set_option("build", "workers", " ".join(workers))
|
||||
@@ -0,0 +1,85 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from threading import Lock, Timer
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.distributed.distributed_system import DistributedSystem
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class WorkerTrigger(DistributedSystem):
|
||||
"""
|
||||
remote worker processor trigger (client side)
|
||||
|
||||
Attributes:
|
||||
ping_interval(float): interval to call remote service in seconds, defined as ``worker.time_to_live / 4``
|
||||
"""
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
DistributedSystem.__init__(self, repository_id, configuration)
|
||||
|
||||
section = next(iter(self.configuration_sections(configuration)))
|
||||
self.ping_interval = configuration.getint(section, "time_to_live", fallback=60) / 4.0
|
||||
|
||||
self._lock = Lock()
|
||||
self._timer: Timer | None = None
|
||||
|
||||
def create_timer(self) -> None:
|
||||
"""
|
||||
create timer object and put it to queue
|
||||
"""
|
||||
self._timer = Timer(self.ping_interval, self.ping)
|
||||
self._timer.start()
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
trigger action which will be called at the start of the application
|
||||
"""
|
||||
self.logger.info("registering instance %s in %s", self.worker, self.address)
|
||||
with self._lock:
|
||||
self.create_timer()
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
trigger action which will be called before the stop of the application
|
||||
"""
|
||||
self.logger.info("removing instance %s in %s", self.worker, self.address)
|
||||
with self._lock:
|
||||
if self._timer is None:
|
||||
return
|
||||
|
||||
self._timer.cancel() # cancel remaining timers
|
||||
self._timer = None # reset state
|
||||
|
||||
def ping(self) -> None:
|
||||
"""
|
||||
register itself as alive worker and update the timer
|
||||
"""
|
||||
with self._lock:
|
||||
if self._timer is None: # no active timer set, exit loop
|
||||
return
|
||||
|
||||
self.register()
|
||||
self.create_timer()
|
||||
@@ -0,0 +1,77 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import time
|
||||
|
||||
from threading import Lock
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.models.worker import Worker
|
||||
|
||||
|
||||
class WorkersCache(LazyLogging):
|
||||
"""
|
||||
cached storage for healthy workers
|
||||
|
||||
Attributes:
|
||||
time_to_live(int): maximal amount of time in seconds to keep worker alive
|
||||
"""
|
||||
|
||||
def __init__(self, configuration: Configuration) -> None:
|
||||
"""
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
self.time_to_live = configuration.getint("worker", "time_to_live", fallback=60)
|
||||
self._lock = Lock()
|
||||
self._workers: dict[str, tuple[Worker, float]] = {}
|
||||
|
||||
@property
|
||||
def workers(self) -> list[Worker]:
|
||||
"""
|
||||
extract currently healthy workers
|
||||
|
||||
Returns:
|
||||
list[Worker]: list of currently registered workers which have been seen not earlier than :attr:`time_to_live`
|
||||
"""
|
||||
valid_from = time.monotonic() - self.time_to_live
|
||||
with self._lock:
|
||||
return [
|
||||
worker
|
||||
for worker, last_seen in self._workers.values()
|
||||
if last_seen > valid_from
|
||||
]
|
||||
|
||||
def workers_remove(self) -> None:
|
||||
"""
|
||||
remove all workers from the cache
|
||||
"""
|
||||
with self._lock:
|
||||
self._workers = {}
|
||||
|
||||
def workers_update(self, worker: Worker) -> None:
|
||||
"""
|
||||
register or update remote worker
|
||||
|
||||
Args:
|
||||
worker(Worker): worker to register
|
||||
"""
|
||||
with self._lock:
|
||||
self._workers[worker.identifier] = (worker, time.monotonic())
|
||||
@@ -0,0 +1,21 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.support.keyring_trigger import KeyringTrigger
|
||||
from ahriman.core.support.mirrorlist_trigger import MirrorlistTrigger
|
||||
@@ -0,0 +1,140 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core import context
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.support.package_creator import PackageCreator
|
||||
from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator
|
||||
from ahriman.core.triggers import Trigger
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class KeyringTrigger(Trigger):
|
||||
"""
|
||||
keyring generator trigger
|
||||
|
||||
Attributes:
|
||||
targets(list[str]): git remote target list
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA = {
|
||||
"keyring": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"target": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"keyring-generator": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["keyring-generator"],
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
"license": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
},
|
||||
"package": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
"packagers": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
},
|
||||
"revoked": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
},
|
||||
"trusted": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
Trigger.__init__(self, repository_id, 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("keyring", "target", fallback=[])
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
trigger action which will be called at the start of the application
|
||||
"""
|
||||
ctx = context.get()
|
||||
sign = ctx.get(GPG)
|
||||
database = ctx.get(SQLite)
|
||||
|
||||
for target in self.targets:
|
||||
generator = KeyringGenerator(database, sign, self.repository_id, self.configuration, target)
|
||||
runner = PackageCreator(self.configuration, generator)
|
||||
runner.run()
|
||||
@@ -0,0 +1,123 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <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
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
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",
|
||||
"empty": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"mirrorlist-generator": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["mirrorlist-generator"],
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
"license": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
},
|
||||
"package": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
"path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
},
|
||||
"servers": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
"required": True,
|
||||
"empty": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
Trigger.__init__(self, repository_id, 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.repository_id, self.configuration, target)
|
||||
runner = PackageCreator(self.configuration, generator)
|
||||
runner.run()
|
||||
@@ -0,0 +1,85 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import shutil
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core import context
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.status import Client
|
||||
from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class PackageCreator:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
generator(PkgbuildGenerator): PKGBUILD generator instance
|
||||
"""
|
||||
self.configuration = configuration
|
||||
self.generator = generator
|
||||
|
||||
def package_create(self, path: Path) -> None:
|
||||
"""
|
||||
create package files
|
||||
|
||||
Args:
|
||||
path(Path): path to directory with package files
|
||||
"""
|
||||
# clear old tree if any
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
||||
# create local tree
|
||||
path.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
self.generator.write_pkgbuild(path)
|
||||
Sources.init(path)
|
||||
|
||||
def package_register(self, path: Path) -> None:
|
||||
"""
|
||||
register package in build worker
|
||||
|
||||
Args:
|
||||
path(Path): path to directory with package files
|
||||
"""
|
||||
ctx = context.get()
|
||||
reporter = ctx.get(Client)
|
||||
_, repository_id = self.configuration.check_loaded()
|
||||
package = Package.from_build(path, repository_id.architecture, None)
|
||||
|
||||
reporter.set_unknown(package)
|
||||
|
||||
def run(self) -> None:
|
||||
"""
|
||||
create new local package
|
||||
"""
|
||||
local_path = self.configuration.repository_paths.cache_for(self.generator.pkgname)
|
||||
self.package_create(local_path)
|
||||
self.package_register(local_path)
|
||||
@@ -0,0 +1,19 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
@@ -0,0 +1,202 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.exceptions import PkgbuildGeneratorError
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class KeyringGenerator(PkgbuildGenerator):
|
||||
"""
|
||||
generator for keyring PKGBUILD
|
||||
|
||||
Attributes:
|
||||
sign(GPG): GPG wrapper instance
|
||||
name(str): repository name
|
||||
packagers(list[str]): list of packagers PGP keys
|
||||
pkgbuild_license(list[str]): keyring package license
|
||||
pkgbuild_pkgdesc(str): keyring package description
|
||||
pkgbuild_pkgname(str): keyring package name
|
||||
pkgbuild_url(str): keyring package home page
|
||||
revoked(list[str]): list of revoked PGP keys
|
||||
trusted(list[str]): lif of trusted PGP keys
|
||||
"""
|
||||
|
||||
def __init__(self, database: SQLite, sign: GPG, repository_id: RepositoryId,
|
||||
configuration: Configuration, section: str) -> None:
|
||||
"""
|
||||
Args:
|
||||
database(SQLite): database instance
|
||||
sign(GPG): GPG wrapper instance
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
section(str): settings section name
|
||||
"""
|
||||
self.sign = sign
|
||||
self.name = repository_id.name
|
||||
|
||||
# configuration fields
|
||||
packager_keys = [packager.key for packager in database.user_list(None, None) if packager.key is not None]
|
||||
self.packagers = configuration.getlist(section, "packagers", fallback=packager_keys)
|
||||
self.revoked = configuration.getlist(section, "revoked", fallback=[])
|
||||
self.trusted = configuration.getlist(
|
||||
section, "trusted", fallback=[sign.default_key] if sign.default_key is not None else [])
|
||||
# pkgbuild description fields
|
||||
self.pkgbuild_pkgname = configuration.get(section, "package", fallback=f"{self.name}-keyring")
|
||||
self.pkgbuild_pkgdesc = configuration.get(section, "description", fallback=f"{self.name} PGP keyring")
|
||||
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_gpg(self, source_path: Path) -> None:
|
||||
"""
|
||||
generate GPG keychain
|
||||
|
||||
Args:
|
||||
source_path(Path): destination of the file content
|
||||
"""
|
||||
with source_path.open("w", encoding="utf8") as source_file:
|
||||
for key in sorted(set(self.trusted + self.packagers + self.revoked)):
|
||||
public_key = self.sign.key_export(key)
|
||||
source_file.write(public_key)
|
||||
source_file.write("\n")
|
||||
|
||||
def _generate_revoked(self, source_path: Path) -> None:
|
||||
"""
|
||||
generate revoked PGP keys
|
||||
|
||||
Args:
|
||||
source_path(Path): destination of the file content
|
||||
"""
|
||||
with source_path.open("w", encoding="utf8") as source_file:
|
||||
for key in sorted(set(self.revoked)):
|
||||
fingerprint = self.sign.key_fingerprint(key)
|
||||
source_file.write(fingerprint)
|
||||
source_file.write("\n")
|
||||
|
||||
def _generate_trusted(self, source_path: Path) -> None:
|
||||
"""
|
||||
generate trusted PGP keys
|
||||
|
||||
Args:
|
||||
source_path(Path): destination of the file content
|
||||
|
||||
Raises:
|
||||
PkgbuildGeneratorError: no trusted keys available
|
||||
"""
|
||||
if not self.trusted:
|
||||
raise PkgbuildGeneratorError
|
||||
with source_path.open("w", encoding="utf8") as source_file:
|
||||
for key in sorted(set(self.trusted)):
|
||||
fingerprint = self.sign.key_fingerprint(key)
|
||||
source_file.write(fingerprint)
|
||||
source_file.write(":4:\n")
|
||||
|
||||
def install(self) -> str | None:
|
||||
"""
|
||||
content of the .install functions
|
||||
|
||||
Returns:
|
||||
str | None: content of the .install functions if any
|
||||
"""
|
||||
# copy-paste from archlinux-keyring
|
||||
return f"""post_upgrade() {{
|
||||
if usr/bin/pacman-key -l >/dev/null 2>&1; then
|
||||
usr/bin/pacman-key --populate {self.name}
|
||||
usr/bin/pacman-key --updatedb
|
||||
fi
|
||||
}}
|
||||
|
||||
post_install() {{
|
||||
if [ -x usr/bin/pacman-key ]; then
|
||||
post_upgrade
|
||||
fi
|
||||
}}"""
|
||||
|
||||
def package(self) -> str:
|
||||
"""
|
||||
package function generator
|
||||
|
||||
Returns:
|
||||
str: package() function for PKGBUILD
|
||||
"""
|
||||
# somehow autopep thinks that construction inside contains valid python code and reformats it
|
||||
return f"""{{
|
||||
install -Dm644 "{Path("$srcdir") / f"{self.name}.gpg"}" "{Path("$pkgdir") / "usr" / "share" / "pacman" / "keyrings" / f"{self.name}.gpg"}"
|
||||
install -Dm644 "{Path("$srcdir") / f"{self.name}-revoked"}" "{Path("$pkgdir") / "usr" / "share" / "pacman" / "keyrings" / f"{self.name}-revoked"}"
|
||||
install -Dm644 "{Path("$srcdir") / f"{self.name}-trusted"}" "{Path("$pkgdir") / "usr" / "share" / "pacman" / "keyrings" / f"{self.name}-trusted"}"
|
||||
}}""" # nopep8
|
||||
|
||||
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 {
|
||||
f"{self.name}.gpg": self._generate_gpg,
|
||||
f"{self.name}-revoked": self._generate_revoked,
|
||||
f"{self.name}-trusted": self._generate_trusted,
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <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
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
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, repository_id: RepositoryId, configuration: Configuration, section: str) -> None:
|
||||
"""
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
section(str): settings section name
|
||||
"""
|
||||
# configuration fields
|
||||
self.servers = configuration.getlist(section, "servers")
|
||||
self.path = configuration.getpath(
|
||||
section, "path", fallback=Path("/") / "etc" / "pacman.d" / f"{repository_id.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"{repository_id.name}-mirrorlist")
|
||||
self.pkgbuild_pkgdesc = configuration.get(
|
||||
section, "description", fallback=f"{repository_id.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
|
||||
"""
|
||||
content = "".join([f"Server = {server}\n" for server in self.servers])
|
||||
source_path.write_text(content, encoding="utf8")
|
||||
|
||||
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,
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import hashlib
|
||||
import itertools
|
||||
|
||||
from collections.abc import Callable, Iterator
|
||||
from pathlib import Path
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.core.utils import utcnow
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
|
||||
|
||||
class PkgbuildGenerator:
|
||||
"""
|
||||
main class for generating PKGBUILDs
|
||||
|
||||
Attributes:
|
||||
PKGBUILD_STATIC_PROPERTIES(list[PkgbuildPatch]): (class attribute) list of default pkgbuild static properties
|
||||
"""
|
||||
|
||||
PKGBUILD_STATIC_PROPERTIES: ClassVar[list[PkgbuildPatch]] = [
|
||||
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 install(self) -> str | None:
|
||||
"""
|
||||
content of the .install functions
|
||||
|
||||
Returns:
|
||||
str | None: content of the .install functions if any
|
||||
"""
|
||||
|
||||
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_install(self, source_dir: Path) -> list[PkgbuildPatch]:
|
||||
"""
|
||||
generate content of install file
|
||||
|
||||
Args:
|
||||
source_dir(Path): path to directory in which sources must be generated
|
||||
|
||||
Returns:
|
||||
list[PkgbuildPatch]: patch for the pkgbuild if install file exists and empty list otherwise
|
||||
"""
|
||||
content: str | None = self.install()
|
||||
if content is None:
|
||||
return []
|
||||
|
||||
source_path = source_dir / f"{self.pkgname}.install"
|
||||
source_path.write_text(content)
|
||||
return [PkgbuildPatch("install", source_path.name)]
|
||||
|
||||
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.extend(self.write_install(source_dir)) # ...install function...
|
||||
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() -> Iterator[tuple[str, str]]:
|
||||
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]),
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
from ahriman.application.handlers.triggers import Triggers
|
||||
from ahriman.application.handlers.triggers_support.triggers_support import TriggersSupport
|
||||
|
||||
|
||||
def test_arguments() -> None:
|
||||
"""
|
||||
must define own arguments
|
||||
"""
|
||||
assert TriggersSupport.arguments != Triggers.arguments
|
||||
@@ -0,0 +1,34 @@
|
||||
import pytest
|
||||
|
||||
from ahriman.core.archive import ArchiveTrigger
|
||||
from ahriman.core.archive.archive_tree import ArchiveTree
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def archive_tree(configuration: Configuration) -> ArchiveTree:
|
||||
"""
|
||||
archive tree fixture
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration fixture
|
||||
|
||||
Returns:
|
||||
ArchiveTree: archive tree test instance
|
||||
"""
|
||||
return ArchiveTree(configuration.repository_paths, [])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def archive_trigger(configuration: Configuration) -> ArchiveTrigger:
|
||||
"""
|
||||
archive trigger fixture
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration fixture
|
||||
|
||||
Returns:
|
||||
ArchiveTrigger: archive trigger test instance
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
return ArchiveTrigger(repository_id, configuration)
|
||||
@@ -0,0 +1,176 @@
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import call as MockCall
|
||||
|
||||
from ahriman.core.archive.archive_tree import ArchiveTree
|
||||
from ahriman.core.utils import utcnow
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
def test_repo(archive_tree: ArchiveTree) -> None:
|
||||
"""
|
||||
must return correct repository object
|
||||
"""
|
||||
local = Path("local")
|
||||
repo = archive_tree._repo(local)
|
||||
|
||||
assert repo.sign_args == archive_tree.sign_args
|
||||
assert repo.name == archive_tree.repository_id.name
|
||||
assert repo.root == local
|
||||
|
||||
|
||||
def test_repository_for(archive_tree: ArchiveTree) -> None:
|
||||
"""
|
||||
must correctly generate path to repository
|
||||
"""
|
||||
path = archive_tree.repository_for()
|
||||
assert path.is_relative_to(archive_tree.paths.archive / "repos")
|
||||
assert (archive_tree.repository_id.name, archive_tree.repository_id.architecture) == path.parts[-2:]
|
||||
assert set(map("{:02d}".format, utcnow().timetuple()[:3])).issubset(path.parts)
|
||||
|
||||
|
||||
def test_directories_fix(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must remove empty directories recursively
|
||||
"""
|
||||
root = archive_tree.paths.archive / "repos"
|
||||
(root / "a" / "b").mkdir(parents=True, exist_ok=True)
|
||||
(root / "a" / "b" / "file").touch()
|
||||
(root / "a" / "b" / "c" / "d").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
_original_rmdir = Path.rmdir
|
||||
rmdir_mock = mocker.patch("pathlib.Path.rmdir", autospec=True, side_effect=_original_rmdir)
|
||||
|
||||
archive_tree.directories_fix({Path("a") / "b" / "c" / "d"})
|
||||
rmdir_mock.assert_has_calls([
|
||||
MockCall(root / "a" / "b" / "c" / "d"),
|
||||
MockCall(root / "a" / "b" / "c"),
|
||||
])
|
||||
|
||||
|
||||
def test_symlinks_create(archive_tree: ArchiveTree, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must create symlinks
|
||||
"""
|
||||
_original_exists = Path.exists
|
||||
|
||||
symlinks_mock = mocker.patch("pathlib.Path.symlink_to", side_effect=(None, FileExistsError, FileExistsError))
|
||||
add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
|
||||
mocker.patch("pathlib.Path.glob", autospec=True, side_effect=lambda path, name: [path / name[:-1]])
|
||||
|
||||
archive_tree.symlinks_create([package_ahriman, package_python_schedule])
|
||||
symlinks_mock.assert_has_calls([
|
||||
MockCall(Path("..") /
|
||||
".." /
|
||||
".." /
|
||||
".." /
|
||||
".." /
|
||||
".." /
|
||||
archive_tree.paths.archive_for(package.base)
|
||||
.relative_to(archive_tree.paths.root)
|
||||
.relative_to("archive") /
|
||||
single.filename
|
||||
)
|
||||
for package in (package_ahriman, package_python_schedule)
|
||||
for single in package.packages.values()
|
||||
])
|
||||
add_mock.assert_called_once_with(
|
||||
archive_tree.repository_for() / package_ahriman.packages[package_ahriman.base].filename
|
||||
)
|
||||
|
||||
|
||||
def test_symlinks_create_empty_filename(archive_tree: ArchiveTree, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip symlinks creation if filename is not set
|
||||
"""
|
||||
package_ahriman.packages[package_ahriman.base].filename = None
|
||||
symlinks_mock = mocker.patch("pathlib.Path.symlink_to")
|
||||
|
||||
archive_tree.symlinks_create([package_ahriman])
|
||||
symlinks_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_symlinks_fix(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must fix broken symlinks
|
||||
"""
|
||||
_original_exists = Path.exists
|
||||
|
||||
def exists_mock(path: Path) -> bool:
|
||||
if path.name.startswith("symlink"):
|
||||
return True
|
||||
return _original_exists(path)
|
||||
|
||||
mocker.patch("pathlib.Path.is_symlink", side_effect=[True, True, False])
|
||||
mocker.patch("pathlib.Path.exists", autospec=True, side_effect=exists_mock)
|
||||
walk_mock = mocker.patch("ahriman.core.archive.archive_tree.walk", return_value=[
|
||||
archive_tree.repository_for() / filename
|
||||
for filename in (
|
||||
"symlink-1.0.0-1-x86_64.pkg.tar.zst",
|
||||
"symlink-1.0.0-1-x86_64.pkg.tar.zst.sig",
|
||||
"broken_symlink-1.0.0-1-x86_64.pkg.tar.zst",
|
||||
"file-1.0.0-1-x86_64.pkg.tar.zst",
|
||||
)
|
||||
])
|
||||
remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
|
||||
|
||||
assert list(archive_tree.symlinks_fix()) == [
|
||||
archive_tree.repository_for().relative_to(archive_tree.paths.archive / "repos"),
|
||||
]
|
||||
walk_mock.assert_called_once_with(archive_tree.paths.archive / "repos")
|
||||
remove_mock.assert_called_once_with(
|
||||
"broken_symlink", archive_tree.repository_for() / "broken_symlink-1.0.0-1-x86_64.pkg.tar.zst")
|
||||
|
||||
|
||||
def test_symlinks_fix_foreign_repository(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip symlinks check if repository name or architecture doesn't match
|
||||
"""
|
||||
_original_exists = Path.exists
|
||||
|
||||
def exists_mock(path: Path) -> bool:
|
||||
if path.name.startswith("symlink"):
|
||||
return True
|
||||
return _original_exists(path)
|
||||
|
||||
mocker.patch("pathlib.Path.is_symlink", side_effect=[True, True, False])
|
||||
mocker.patch("pathlib.Path.exists", autospec=True, side_effect=exists_mock)
|
||||
mocker.patch("ahriman.core.archive.archive_tree.walk", return_value=[
|
||||
archive_tree.repository_for().with_name("i686") / filename
|
||||
for filename in (
|
||||
"symlink-1.0.0-1-x86_64.pkg.tar.zst",
|
||||
"broken_symlink-1.0.0-1-x86_64.pkg.tar.zst",
|
||||
"file-1.0.0-1-x86_64.pkg.tar.zst",
|
||||
)
|
||||
])
|
||||
remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
|
||||
|
||||
assert list(archive_tree.symlinks_fix()) == []
|
||||
remove_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_tree_create(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must create repository root if not exists
|
||||
"""
|
||||
owner_guard_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner")
|
||||
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
|
||||
init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init")
|
||||
|
||||
archive_tree.tree_create()
|
||||
owner_guard_mock.assert_called_once_with()
|
||||
mkdir_mock.assert_called_once_with(0o755, parents=True)
|
||||
init_mock.assert_called_once_with()
|
||||
|
||||
|
||||
def test_tree_create_exists(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip directory creation if already exists
|
||||
"""
|
||||
mocker.patch("pathlib.Path.exists", return_value=True)
|
||||
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
|
||||
|
||||
archive_tree.tree_create()
|
||||
mkdir_mock.assert_not_called()
|
||||
@@ -0,0 +1,37 @@
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.core.archive import ArchiveTrigger
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
def test_on_result(archive_trigger: ArchiveTrigger, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must create symlinks for actual repository
|
||||
"""
|
||||
symlinks_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.symlinks_create")
|
||||
archive_trigger.on_result(Result(), [package_ahriman])
|
||||
symlinks_mock.assert_called_once_with([package_ahriman])
|
||||
|
||||
|
||||
def test_on_start(archive_trigger: ArchiveTrigger, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must create repository tree on load
|
||||
"""
|
||||
tree_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.tree_create")
|
||||
archive_trigger.on_start()
|
||||
tree_mock.assert_called_once_with()
|
||||
|
||||
|
||||
def test_on_stop(archive_trigger: ArchiveTrigger, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must fix broken symlinks on stop
|
||||
"""
|
||||
local = Path("local")
|
||||
symlinks_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.symlinks_fix", return_value=[local])
|
||||
directories_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.directories_fix")
|
||||
|
||||
archive_trigger.on_stop()
|
||||
symlinks_mock.assert_called_once_with()
|
||||
directories_mock.assert_called_once_with({local})
|
||||
@@ -0,0 +1,51 @@
|
||||
import pytest
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.distributed import WorkerTrigger, WorkersCache
|
||||
from ahriman.core.distributed.distributed_system import DistributedSystem
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def distributed_system(configuration: Configuration) -> DistributedSystem:
|
||||
"""
|
||||
distributed system fixture
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration fixture
|
||||
|
||||
Returns:
|
||||
DistributedSystem: distributed system test instance
|
||||
"""
|
||||
configuration.set_option("status", "address", "http://localhost:8081")
|
||||
_, repository_id = configuration.check_loaded()
|
||||
return DistributedSystem(repository_id, configuration)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def worker_trigger(configuration: Configuration) -> WorkerTrigger:
|
||||
"""
|
||||
worker trigger fixture
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration fixture
|
||||
|
||||
Returns:
|
||||
WorkerTrigger: worker trigger test instance
|
||||
"""
|
||||
configuration.set_option("status", "address", "http://localhost:8081")
|
||||
_, repository_id = configuration.check_loaded()
|
||||
return WorkerTrigger(repository_id, configuration)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workers_cache(configuration: Configuration) -> WorkersCache:
|
||||
"""
|
||||
workers cache fixture
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration fixture
|
||||
|
||||
Returns:
|
||||
WorkersCache: workers cache test instance
|
||||
"""
|
||||
return WorkersCache(configuration)
|
||||
@@ -0,0 +1,82 @@
|
||||
import json
|
||||
import requests
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.distributed.distributed_system import DistributedSystem
|
||||
from ahriman.models.worker import Worker
|
||||
|
||||
|
||||
def test_configuration_sections(configuration: Configuration) -> None:
|
||||
"""
|
||||
must correctly parse target list
|
||||
"""
|
||||
assert DistributedSystem.configuration_sections(configuration) == ["worker"]
|
||||
|
||||
|
||||
def test_workers_url(distributed_system: DistributedSystem) -> None:
|
||||
"""
|
||||
must generate workers url correctly
|
||||
"""
|
||||
assert distributed_system._workers_url().startswith(distributed_system.address)
|
||||
assert distributed_system._workers_url().endswith("/api/v1/distributed")
|
||||
|
||||
|
||||
def test_register(distributed_system: DistributedSystem, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must register service
|
||||
"""
|
||||
run_mock = mocker.patch("ahriman.core.distributed.distributed_system.DistributedSystem.make_request")
|
||||
distributed_system.register()
|
||||
run_mock.assert_called_once_with("POST", f"{distributed_system.address}/api/v1/distributed",
|
||||
json=distributed_system.worker.view())
|
||||
|
||||
|
||||
def test_register_failed(distributed_system: DistributedSystem, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress any exception happened during worker registration
|
||||
"""
|
||||
mocker.patch("requests.Session.request", side_effect=Exception)
|
||||
distributed_system.register()
|
||||
|
||||
|
||||
def test_register_failed_http_error(distributed_system: DistributedSystem, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress HTTP exception happened during worker registration
|
||||
"""
|
||||
mocker.patch("requests.Session.request", side_effect=requests.HTTPError)
|
||||
distributed_system.register()
|
||||
|
||||
|
||||
def test_workers(distributed_system: DistributedSystem, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return available remote workers
|
||||
"""
|
||||
worker = Worker("remote")
|
||||
response_obj = requests.Response()
|
||||
response_obj._content = json.dumps([worker.view()]).encode("utf8")
|
||||
response_obj.status_code = 200
|
||||
|
||||
requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request",
|
||||
return_value=response_obj)
|
||||
|
||||
result = distributed_system.workers()
|
||||
requests_mock.assert_called_once_with("GET", distributed_system._workers_url())
|
||||
assert result == [worker]
|
||||
|
||||
|
||||
def test_workers_failed(distributed_system: DistributedSystem, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress any exception happened during worker extraction
|
||||
"""
|
||||
mocker.patch("requests.Session.request", side_effect=Exception)
|
||||
distributed_system.workers()
|
||||
|
||||
|
||||
def test_workers_failed_http_error(distributed_system: DistributedSystem, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress HTTP exception happened during worker extraction
|
||||
"""
|
||||
mocker.patch("requests.Session.request", side_effect=requests.HTTPError)
|
||||
distributed_system.workers()
|
||||
@@ -0,0 +1,47 @@
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.distributed import WorkerLoaderTrigger
|
||||
from ahriman.models.worker import Worker
|
||||
|
||||
|
||||
def test_on_start(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must load workers from remote
|
||||
"""
|
||||
worker = Worker("address")
|
||||
configuration.set_option("status", "address", "http://localhost:8081")
|
||||
run_mock = mocker.patch("ahriman.core.distributed.WorkerLoaderTrigger.workers", return_value=[worker])
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
trigger = WorkerLoaderTrigger(repository_id, configuration)
|
||||
trigger.on_start()
|
||||
run_mock.assert_called_once_with()
|
||||
assert configuration.getlist("build", "workers") == [worker.address]
|
||||
|
||||
|
||||
def test_on_start_skip(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip loading if option is already set
|
||||
"""
|
||||
configuration.set_option("status", "address", "http://localhost:8081")
|
||||
configuration.set_option("build", "workers", "address")
|
||||
run_mock = mocker.patch("ahriman.core.distributed.WorkerLoaderTrigger.workers")
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
trigger = WorkerLoaderTrigger(repository_id, configuration)
|
||||
trigger.on_start()
|
||||
run_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_on_start_empty_list(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must do not set anything if workers are not available
|
||||
"""
|
||||
configuration.set_option("status", "address", "http://localhost:8081")
|
||||
mocker.patch("ahriman.core.distributed.WorkerLoaderTrigger.workers", return_value=[])
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
trigger = WorkerLoaderTrigger(repository_id, configuration)
|
||||
trigger.on_start()
|
||||
assert not configuration.has_option("build", "workers")
|
||||
@@ -0,0 +1,62 @@
|
||||
from pytest_mock import MockerFixture
|
||||
from threading import Timer
|
||||
|
||||
from ahriman.core.distributed import WorkerTrigger
|
||||
|
||||
|
||||
def test_create_timer(worker_trigger: WorkerTrigger) -> None:
|
||||
"""
|
||||
must create a timer and put it to queue
|
||||
"""
|
||||
worker_trigger.create_timer()
|
||||
assert worker_trigger._timer.function == worker_trigger.ping
|
||||
worker_trigger._timer.cancel()
|
||||
|
||||
|
||||
def test_on_start(worker_trigger: WorkerTrigger, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must register itself as worker
|
||||
"""
|
||||
run_mock = mocker.patch("ahriman.core.distributed.WorkerTrigger.create_timer")
|
||||
worker_trigger.on_start()
|
||||
run_mock.assert_called_once_with()
|
||||
|
||||
|
||||
def test_on_stop(worker_trigger: WorkerTrigger, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must unregister itself as worker
|
||||
"""
|
||||
run_mock = mocker.patch("threading.Timer.cancel")
|
||||
worker_trigger._timer = Timer(1, print) # doesn't matter
|
||||
|
||||
worker_trigger.on_stop()
|
||||
run_mock.assert_called_once_with()
|
||||
|
||||
|
||||
def test_on_stop_empty_timer(worker_trigger: WorkerTrigger) -> None:
|
||||
"""
|
||||
must do not fail if no timer was started
|
||||
"""
|
||||
worker_trigger.on_stop()
|
||||
|
||||
|
||||
def test_ping(worker_trigger: WorkerTrigger, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must correctly process timer action
|
||||
"""
|
||||
run_mock = mocker.patch("ahriman.core.distributed.WorkerTrigger.register")
|
||||
timer_mock = mocker.patch("threading.Timer.start")
|
||||
worker_trigger._timer = Timer(1, print) # doesn't matter
|
||||
|
||||
worker_trigger.ping()
|
||||
run_mock.assert_called_once_with()
|
||||
timer_mock.assert_called_once_with()
|
||||
|
||||
|
||||
def test_ping_empty_queue(worker_trigger: WorkerTrigger, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must do nothing in case of empty queue
|
||||
"""
|
||||
run_mock = mocker.patch("ahriman.core.distributed.WorkerTrigger.register")
|
||||
worker_trigger.ping()
|
||||
run_mock.assert_not_called()
|
||||
@@ -0,0 +1,43 @@
|
||||
import time
|
||||
|
||||
from ahriman.core.distributed import WorkersCache
|
||||
from ahriman.models.worker import Worker
|
||||
|
||||
|
||||
def test_workers(workers_cache: WorkersCache) -> None:
|
||||
"""
|
||||
must return alive workers
|
||||
"""
|
||||
workers_cache._workers = {
|
||||
str(index): (Worker(f"address{index}"), index)
|
||||
for index in range(2)
|
||||
}
|
||||
workers_cache.time_to_live = time.monotonic()
|
||||
|
||||
assert workers_cache.workers == [Worker("address1")]
|
||||
|
||||
|
||||
def test_workers_remove(workers_cache: WorkersCache) -> None:
|
||||
"""
|
||||
must remove all workers
|
||||
"""
|
||||
workers_cache.workers_update(Worker("address"))
|
||||
assert workers_cache.workers
|
||||
|
||||
workers_cache.workers_remove()
|
||||
assert not workers_cache.workers
|
||||
|
||||
|
||||
def test_workers_update(workers_cache: WorkersCache) -> None:
|
||||
"""
|
||||
must update worker
|
||||
"""
|
||||
worker = Worker("address")
|
||||
|
||||
workers_cache.workers_update(worker)
|
||||
assert workers_cache.workers == [worker]
|
||||
_, first_last_seen = workers_cache._workers[worker.identifier]
|
||||
|
||||
workers_cache.workers_update(worker)
|
||||
_, second_last_seen = workers_cache._workers[worker.identifier]
|
||||
assert first_last_seen < second_last_seen
|
||||
@@ -0,0 +1,35 @@
|
||||
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
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
return MirrorlistGenerator(repository_id, 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)
|
||||
@@ -0,0 +1,35 @@
|
||||
import pytest
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator
|
||||
from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def keyring_generator(database: SQLite, gpg: GPG, configuration: Configuration) -> KeyringGenerator:
|
||||
"""
|
||||
fixture for keyring pkgbuild generator
|
||||
|
||||
Args:
|
||||
database(SQLite): database fixture
|
||||
gpg(GPG): empty GPG fixture
|
||||
configuration(Configuration): configuration fixture
|
||||
|
||||
Returns:
|
||||
KeyringGenerator: keyring generator test instance
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
return KeyringGenerator(database, gpg, repository_id, configuration, "keyring")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pkgbuild_generator() -> PkgbuildGenerator:
|
||||
"""
|
||||
fixture for dummy pkgbuild generator
|
||||
|
||||
Returns:
|
||||
PkgbuildGenerator: pkgbuild generator test instance
|
||||
"""
|
||||
return PkgbuildGenerator()
|
||||
@@ -0,0 +1,201 @@
|
||||
import pytest
|
||||
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import MagicMock, call as MockCall
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.exceptions import PkgbuildGeneratorError
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator
|
||||
from ahriman.models.user import User
|
||||
|
||||
|
||||
def test_init_packagers(database: SQLite, gpg: GPG, configuration: Configuration, user: User,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must extract packagers keys
|
||||
"""
|
||||
mocker.patch("ahriman.core.database.SQLite.user_list", return_value=[user])
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").packagers == ["key"]
|
||||
|
||||
configuration.set_option("keyring", "packagers", "key1")
|
||||
assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").packagers == ["key1"]
|
||||
|
||||
|
||||
def test_init_revoked(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
|
||||
"""
|
||||
must extract revoked keys
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").revoked == []
|
||||
|
||||
configuration.set_option("keyring", "revoked", "key1")
|
||||
assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").revoked == ["key1"]
|
||||
|
||||
|
||||
def test_init_trusted(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
|
||||
"""
|
||||
must extract trusted keys
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").trusted == []
|
||||
|
||||
gpg.default_key = "key"
|
||||
assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").trusted == ["key"]
|
||||
|
||||
configuration.set_option("keyring", "trusted", "key1")
|
||||
assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").trusted == ["key1"]
|
||||
|
||||
|
||||
def test_license(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
|
||||
"""
|
||||
must generate correct licenses list
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").license == ["Unlicense"]
|
||||
|
||||
configuration.set_option("keyring", "license", "GPL MPL")
|
||||
assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").license == ["GPL", "MPL"]
|
||||
|
||||
|
||||
def test_pkgdesc(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
|
||||
"""
|
||||
must generate correct pkgdesc property
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").pkgdesc == "aur PGP keyring"
|
||||
|
||||
configuration.set_option("keyring", "description", "description")
|
||||
assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").pkgdesc == "description"
|
||||
|
||||
|
||||
def test_pkgname(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
|
||||
"""
|
||||
must generate correct pkgname property
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").pkgname == "aur-keyring"
|
||||
|
||||
configuration.set_option("keyring", "package", "keyring")
|
||||
assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").pkgname == "keyring"
|
||||
|
||||
|
||||
def test_url(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
|
||||
"""
|
||||
must generate correct url property
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").url == ""
|
||||
|
||||
configuration.set_option("keyring", "homepage", "homepage")
|
||||
assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").url == "homepage"
|
||||
|
||||
|
||||
def test_generate_gpg(keyring_generator: KeyringGenerator, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must correctly generate file with all PGP keys
|
||||
"""
|
||||
file_mock = MagicMock()
|
||||
export_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_export", side_effect=lambda key: key)
|
||||
open_mock = mocker.patch("pathlib.Path.open")
|
||||
open_mock.return_value.__enter__.return_value = file_mock
|
||||
keyring_generator.packagers = ["key"]
|
||||
keyring_generator.revoked = ["revoked"]
|
||||
keyring_generator.trusted = ["trusted", "key"]
|
||||
|
||||
keyring_generator._generate_gpg(Path("local"))
|
||||
open_mock.assert_called_once_with("w", encoding="utf8")
|
||||
export_mock.assert_has_calls([MockCall("key"), MockCall("revoked"), MockCall("trusted")])
|
||||
file_mock.write.assert_has_calls([
|
||||
MockCall("key"), MockCall("\n"),
|
||||
MockCall("revoked"), MockCall("\n"),
|
||||
MockCall("trusted"), MockCall("\n"),
|
||||
])
|
||||
|
||||
|
||||
def test_generate_revoked(keyring_generator: KeyringGenerator, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must correctly generate file with revoked keys
|
||||
"""
|
||||
file_mock = MagicMock()
|
||||
fingerprint_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_fingerprint", side_effect=lambda key: key)
|
||||
open_mock = mocker.patch("pathlib.Path.open")
|
||||
open_mock.return_value.__enter__.return_value = file_mock
|
||||
keyring_generator.revoked = ["revoked"]
|
||||
|
||||
keyring_generator._generate_revoked(Path("local"))
|
||||
open_mock.assert_called_once_with("w", encoding="utf8")
|
||||
fingerprint_mock.assert_called_once_with("revoked")
|
||||
file_mock.write.assert_has_calls([MockCall("revoked"), MockCall("\n")])
|
||||
|
||||
|
||||
def test_generate_trusted(keyring_generator: KeyringGenerator, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must correctly generate file with trusted keys
|
||||
"""
|
||||
file_mock = MagicMock()
|
||||
fingerprint_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_fingerprint", side_effect=lambda key: key)
|
||||
open_mock = mocker.patch("pathlib.Path.open")
|
||||
open_mock.return_value.__enter__.return_value = file_mock
|
||||
keyring_generator.trusted = ["trusted", "trusted"]
|
||||
|
||||
keyring_generator._generate_trusted(Path("local"))
|
||||
open_mock.assert_called_once_with("w", encoding="utf8")
|
||||
fingerprint_mock.assert_called_once_with("trusted")
|
||||
file_mock.write.assert_has_calls([MockCall("trusted"), MockCall(":4:\n")])
|
||||
|
||||
|
||||
def test_generate_trusted_empty(keyring_generator: KeyringGenerator) -> None:
|
||||
"""
|
||||
must raise PkgbuildGeneratorError if no trusted keys set
|
||||
"""
|
||||
with pytest.raises(PkgbuildGeneratorError):
|
||||
keyring_generator._generate_trusted(Path("local"))
|
||||
|
||||
|
||||
def test_install(keyring_generator: KeyringGenerator) -> None:
|
||||
"""
|
||||
must return install functions
|
||||
"""
|
||||
assert keyring_generator.install() == """post_upgrade() {
|
||||
if usr/bin/pacman-key -l >/dev/null 2>&1; then
|
||||
usr/bin/pacman-key --populate aur
|
||||
usr/bin/pacman-key --updatedb
|
||||
fi
|
||||
}
|
||||
|
||||
post_install() {
|
||||
if [ -x usr/bin/pacman-key ]; then
|
||||
post_upgrade
|
||||
fi
|
||||
}"""
|
||||
|
||||
|
||||
def test_package(keyring_generator: KeyringGenerator) -> None:
|
||||
"""
|
||||
must generate package function correctly
|
||||
"""
|
||||
assert keyring_generator.package() == """{
|
||||
install -Dm644 "$srcdir/aur.gpg" "$pkgdir/usr/share/pacman/keyrings/aur.gpg"
|
||||
install -Dm644 "$srcdir/aur-revoked" "$pkgdir/usr/share/pacman/keyrings/aur-revoked"
|
||||
install -Dm644 "$srcdir/aur-trusted" "$pkgdir/usr/share/pacman/keyrings/aur-trusted"
|
||||
}"""
|
||||
|
||||
|
||||
def test_sources(keyring_generator: KeyringGenerator) -> None:
|
||||
"""
|
||||
must return valid sources files list
|
||||
"""
|
||||
assert keyring_generator.sources().get("aur.gpg")
|
||||
assert keyring_generator.sources().get("aur-revoked")
|
||||
assert keyring_generator.sources().get("aur-trusted")
|
||||
@@ -0,0 +1,102 @@
|
||||
from pathlib import Path
|
||||
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
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").path == \
|
||||
Path("etc") / "pacman.d" / "aur-mirrorlist"
|
||||
|
||||
configuration.set_option("mirrorlist", "path", "/etc")
|
||||
assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").path == Path("etc")
|
||||
|
||||
|
||||
def test_license(configuration: Configuration) -> None:
|
||||
"""
|
||||
must generate correct licenses list
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").license == ["Unlicense"]
|
||||
|
||||
configuration.set_option("mirrorlist", "license", "GPL MPL")
|
||||
assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").license == ["GPL", "MPL"]
|
||||
|
||||
|
||||
def test_pkgdesc(configuration: Configuration) -> None:
|
||||
"""
|
||||
must generate correct pkgdesc property
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").pkgdesc == \
|
||||
"aur mirror list for use by pacman"
|
||||
|
||||
configuration.set_option("mirrorlist", "description", "description")
|
||||
assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").pkgdesc == "description"
|
||||
|
||||
|
||||
def test_pkgname(configuration: Configuration) -> None:
|
||||
"""
|
||||
must generate correct pkgname property
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").pkgname == "aur-mirrorlist"
|
||||
|
||||
configuration.set_option("mirrorlist", "package", "mirrorlist")
|
||||
assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").pkgname == "mirrorlist"
|
||||
|
||||
|
||||
def test_url(configuration: Configuration) -> None:
|
||||
"""
|
||||
must generate correct url property
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").url == ""
|
||||
|
||||
configuration.set_option("mirrorlist", "homepage", "homepage")
|
||||
assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").url == "homepage"
|
||||
|
||||
|
||||
def test_generate_mirrorlist(mirrorlist_generator: MirrorlistGenerator, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must correctly generate mirrorlist file
|
||||
"""
|
||||
write_mock = mocker.patch("pathlib.Path.write_text")
|
||||
mirrorlist_generator._generate_mirrorlist(Path("local"))
|
||||
write_mock.assert_called_once_with("Server = http://localhost\n", encoding="utf8")
|
||||
|
||||
|
||||
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-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")
|
||||
@@ -0,0 +1,140 @@
|
||||
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.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 == "20020311"
|
||||
|
||||
|
||||
def test_url(pkgbuild_generator: PkgbuildGenerator) -> None:
|
||||
"""
|
||||
must return empty url
|
||||
"""
|
||||
assert pkgbuild_generator.url == ""
|
||||
|
||||
|
||||
def test_install(pkgbuild_generator: PkgbuildGenerator) -> None:
|
||||
"""
|
||||
must return empty install function
|
||||
"""
|
||||
assert pkgbuild_generator.install() is None
|
||||
|
||||
|
||||
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_install(pkgbuild_generator: PkgbuildGenerator, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must write install file
|
||||
"""
|
||||
mocker.patch.object(PkgbuildGenerator, "pkgname", "package")
|
||||
mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.PkgbuildGenerator.install", return_value="content")
|
||||
write_mock = mocker.patch("pathlib.Path.write_text")
|
||||
|
||||
assert pkgbuild_generator.write_install(Path("local")) == [PkgbuildPatch("install", "package.install")]
|
||||
write_mock.assert_called_once_with("content")
|
||||
|
||||
|
||||
def test_write_install_empty(pkgbuild_generator: PkgbuildGenerator) -> None:
|
||||
"""
|
||||
must return empty patch list for missing install function
|
||||
"""
|
||||
assert pkgbuild_generator.write_install(Path("local")) == []
|
||||
|
||||
|
||||
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")])
|
||||
install_mock = mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.PkgbuildGenerator.write_install",
|
||||
return_value=[PkgbuildPatch("install", "pkgname.install")])
|
||||
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()
|
||||
install_mock.assert_called_once_with(path)
|
||||
sources_mock.assert_called_once_with(path)
|
||||
write_mock.assert_has_calls([MockCall(path / "PKGBUILD")] * 12)
|
||||
|
||||
|
||||
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")
|
||||
@@ -0,0 +1,32 @@
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import call as MockCall
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.support import KeyringTrigger
|
||||
|
||||
|
||||
def test_configuration_sections(configuration: Configuration) -> None:
|
||||
"""
|
||||
must correctly parse target list
|
||||
"""
|
||||
configuration.set_option("keyring", "target", "a b c")
|
||||
assert KeyringTrigger.configuration_sections(configuration) == ["a", "b", "c"]
|
||||
|
||||
configuration.remove_option("keyring", "target")
|
||||
assert KeyringTrigger.configuration_sections(configuration) == []
|
||||
|
||||
|
||||
def test_on_start(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run report for specified targets
|
||||
"""
|
||||
context_mock = mocker.patch("ahriman.core._Context.get")
|
||||
run_mock = mocker.patch("ahriman.core.support.package_creator.PackageCreator.run")
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
trigger = KeyringTrigger(repository_id, configuration)
|
||||
trigger.on_start()
|
||||
context_mock.assert_has_calls([MockCall(GPG), MockCall(SQLite)])
|
||||
run_mock.assert_called_once_with()
|
||||
@@ -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
|
||||
"""
|
||||
run_mock = mocker.patch("ahriman.core.support.package_creator.PackageCreator.run")
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
trigger = MirrorlistTrigger(repository_id, configuration)
|
||||
trigger.on_start()
|
||||
run_mock.assert_called_once_with()
|
||||
@@ -0,0 +1,60 @@
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.core.status import Client
|
||||
from ahriman.core.support.package_creator import PackageCreator
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.package_source import PackageSource
|
||||
from ahriman.models.remote_source import RemoteSource
|
||||
|
||||
|
||||
def test_package_create(package_creator: PackageCreator, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must create package
|
||||
"""
|
||||
path = Path("local")
|
||||
rmtree_mock = mocker.patch("shutil.rmtree")
|
||||
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
|
||||
write_mock = mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.PkgbuildGenerator.write_pkgbuild")
|
||||
init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init")
|
||||
|
||||
package_creator.package_create(path)
|
||||
rmtree_mock.assert_called_once_with(path, ignore_errors=True)
|
||||
mkdir_mock.assert_called_once_with(mode=0o755, parents=True, exist_ok=True)
|
||||
write_mock.assert_called_once_with(path)
|
||||
init_mock.assert_called_once_with(path)
|
||||
|
||||
|
||||
def test_package_register(package_creator: PackageCreator, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must register package
|
||||
"""
|
||||
path = Path("local")
|
||||
package = Package(
|
||||
base=package_creator.generator.pkgname,
|
||||
version=package_creator.generator.pkgver,
|
||||
remote=RemoteSource(source=PackageSource.Local),
|
||||
packages={package_creator.generator.pkgname: PackageDescription()},
|
||||
)
|
||||
client_mock = mocker.patch("ahriman.core._Context.get", return_value=Client())
|
||||
insert_mock = mocker.patch("ahriman.core.status.Client.set_unknown")
|
||||
package_mock = mocker.patch("ahriman.models.package.Package.from_build", return_value=package)
|
||||
|
||||
package_creator.package_register(path)
|
||||
package_mock.assert_called_once_with(path, "x86_64", None)
|
||||
client_mock.assert_called_once_with(Client)
|
||||
insert_mock.assert_called_once_with(package)
|
||||
|
||||
|
||||
def test_run(package_creator: PackageCreator, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must correctly process package creation
|
||||
"""
|
||||
path = package_creator.configuration.repository_paths.cache_for(package_creator.generator.pkgname)
|
||||
create_mock = mocker.patch("ahriman.core.support.package_creator.PackageCreator.package_create")
|
||||
register_mock = mocker.patch("ahriman.core.support.package_creator.PackageCreator.package_register")
|
||||
|
||||
package_creator.run()
|
||||
create_mock.assert_called_once_with(path)
|
||||
register_mock.assert_called_once_with(path)
|
||||
@@ -0,0 +1,13 @@
|
||||
# pylint: disable=wildcard-import,unused-wildcard-import
|
||||
from fixtures import *
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tests"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def resource_path_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2] / "tests" / "testresources"
|
||||
Reference in New Issue
Block a user