Compare commits

...

31 Commits

Author SHA1 Message Date
ce0c07cbd9 Release 0.21.4 2021-04-05 02:28:38 +03:00
912a76d5cb drop changelog
the main reason is that it uses github to generate changelog. Thus it
will be updated AFTER release is created
2021-04-05 02:27:12 +03:00
76d0b0bc6d Release 0.21.3 2021-04-05 02:22:44 +03:00
27d018e721 update changelog at correct step
also fix commit filter and do not update sha anymore
2021-04-05 02:22:11 +03:00
a0e20ffb77 Release 0.21.2 2021-04-05 02:01:28 +03:00
96e4abc3c0 add changelog generator to both gh-actions and repository 2021-04-05 02:00:05 +03:00
6df60498aa Release 0.21.1 2021-04-05 00:45:12 +03:00
eb0a4b6b4a use globing instead 2021-04-05 00:44:39 +03:00
8f469e7eac Release 0.21.0 2021-04-05 00:38:23 +03:00
535e955814 try to make auto archive upload 2021-04-05 00:37:03 +03:00
0bd3ba626a implicit type conversion from command line 2021-04-04 23:53:30 +03:00
ffe6aec190 more options in setup command 2021-04-04 15:42:06 +03:00
56c600e5ac fix check errors 2021-04-04 14:00:42 +03:00
461883217d 100% coverage 2021-04-03 21:30:57 +03:00
62d55eff19 add ability to fitler by dependency list 2021-04-02 04:20:39 +03:00
534b5600b4 add ability to remove package from status page 2021-04-02 01:26:46 +03:00
32cbafd12b Release 0.20.0 2021-04-01 02:38:59 +03:00
880c70bd58 constistent classmethod and staticmethod usage
General idea is to use classmethod for every constructor and
statismethod otherwise.
Also use self and cls whenever it's possible to call static and class
methods
2021-03-31 04:29:08 +03:00
d449eb3c2e change arch specific section naming from section_arch to section:arch
Some archs can have _ in their name. Also in future we can use sections
with similar names
2021-03-31 02:31:14 +03:00
17b5cd0751 Release 0.19.0 2021-03-31 02:19:44 +03:00
2aef906fc8 add now argument to add command 2021-03-31 02:19:13 +03:00
e034327501 filter out every foreign arch in config 2021-03-31 01:55:39 +03:00
5d79fcca22 read sign targets from correct path 2021-03-31 00:11:05 +03:00
6e9dcca254 make configuration object arch-specific 2021-03-31 00:04:13 +03:00
fbf6748d4a more verbose variables 2021-03-30 05:29:13 +03:00
2260e52d5c merge settings groups instead of using whole group 2021-03-30 04:58:15 +03:00
bd2b61494f move rsync and s3 options to configuration 2021-03-30 02:38:18 +03:00
7280d30748 verbose help message 2021-03-30 02:25:23 +03:00
710274065d add status badge 2021-03-30 02:01:59 +03:00
e0b09cefad rename gpg test to correct naming 2021-03-30 01:53:23 +03:00
3b93510aad add more tests 2021-03-30 01:42:01 +03:00
94 changed files with 1817 additions and 610 deletions

37
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,37 @@
name: create release
on:
push:
tags:
- '*.*.*'
jobs:
make-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: extract version
id: version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: create changelog
id: changelog
uses: jaywcjlove/changelog-generator@main
with:
token: ${{ secrets.GITHUB_TOKEN }}
filter: 'Release \d+\.\d+\.\d+'
- name: create archive
run: make archive
env:
VERSION: ${{ steps.version.outputs.VERSION }}
- name: Release
uses: softprops/action-gh-release@v1
with:
body: |
${{ steps.changelog.outputs.compareurl }}
${{ steps.changelog.outputs.changelog }}
files: ahriman-*-src.tar.xz
fail_on_unmatched_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,6 +1,5 @@
# based on https://github.com/actions/starter-workflows/blob/main/ci/python-app.yml
name: ahriman
name: check commit
on:
push:
@ -9,7 +8,7 @@ on:
branches: [ master ]
jobs:
build:
run-tests:
runs-on: ubuntu-latest

View File

@ -1,6 +1,6 @@
# ahriman configuration
Some groups can be specified for each architecture separately. E.g. if there are `build` and `build_x86_64` groups it will use the `build_x86_64` for the `x86_64` architecture and `build` for any other (architecture specific group has higher priority).
Some groups can be specified for each architecture separately. E.g. if there are `build` and `build:x86_64` groups it will use the option from `build:x86_64` for the `x86_64` architecture and `build` for any other (architecture specific group has higher priority). In case if both groups are presented, architecture specific options will be merged into global ones overriding them.
## `settings` group
@ -18,9 +18,9 @@ libalpm and AUR related configuration.
* `repositories` - list of pacman repositories, space separated list of strings, required.
* `root` - root for alpm library, string, required.
## `build_*` groups
## `build:*` groups
Build related configuration. Group name must refer to architecture, e.g. it should be `build_x86_64` for x86_64 architecture.
Build related configuration. Group name must refer to architecture, e.g. it should be `build:x86_64` for x86_64 architecture.
* `archbuild_flags` - additional flags passed to `archbuild` command, space separated list of strings, optional.
* `build_command` - default build command, string, required.
@ -35,9 +35,9 @@ Base repository settings.
* `name` - repository name, string, required.
* `root` - root path for application, string, required.
## `sign_*` groups
## `sign:*` groups
Settings for signing packages or repository. Group name must refer to architecture, e.g. it should be `sign_x86_64` for x86_64 architecture.
Settings for signing packages or repository. Group name must refer to architecture, e.g. it should be `sign:x86_64` for x86_64 architecture.
* `target` - configuration flag to enable signing, space separated list of strings, required. Allowed values are `package` (sign each package separately), `repository` (sign repository database file).
* `key` - default PGP key, string, required. This key will also be used for database signing if enabled.
@ -49,9 +49,9 @@ Report generation settings.
* `target` - list of reports to be generated, space separated list of strings, optional. Allowed values are `html`.
### `html_*` groups
### `html:*` groups
Group name must refer to architecture, e.g. it should be `html_x86_64` for x86_64 architecture.
Group name must refer to architecture, e.g. it should be `html:x86_64` for x86_64 architecture.
* `path` - path to html report file, string, required.
* `homepage` - link to homepage, string, optional.
@ -64,21 +64,23 @@ Remote synchronization settings.
* `target` - list of synchronizations to be used, space separated list of strings, optional. Allowed values are `rsync`, `s3`.
### `rsync_*` groups
### `rsync:*` groups
Group name must refer to architecture, e.g. it should be `rsync_x86_64` for x86_64 architecture. Requires `rsync` package to be installed. Do not forget to configure ssh for user `ahriman`.
Group name must refer to architecture, e.g. it should be `rsync:x86_64` for x86_64 architecture. Requires `rsync` package to be installed. Do not forget to configure ssh for user `ahriman`.
* `command` - rsync command to run, space separated list of string, required.
* `remote` - remote server to rsync (e.g. `1.2.3.4:5678:path/to/sync`), string, required.
### `s3_*` groups
### `s3:*` groups
Group name must refer to architecture, e.g. it should be `s3_x86_64` for x86_64 architecture. Requires `aws-cli` package to be installed. Do not forget to configure it for user `ahriman`.
Group name must refer to architecture, e.g. it should be `s3:x86_64` for x86_64 architecture. Requires `aws-cli` package to be installed. Do not forget to configure it for user `ahriman`.
* `command` - s3 command to run, space separated list of string, required.
* `bucket` - bucket name (e.g. `s3://bucket/path`), string, required.
## `web_*` groups
## `web:*` groups
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name must refer to architecture, e.g. it should be `web_x86_64` for x86_64 architecture.
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name must refer to architecture, e.g. it should be `web:x86_64` for x86_64 architecture.
* `host` - host to bind, string, optional.
* `port` - port to bind, int, optional.

View File

@ -21,10 +21,9 @@ archive_directory: $(TARGET_FILES)
find "$(PROJECT)" -depth -type d -name "*.egg-info" -execdir rm -rf {} +
archlinux: archive
sed -i "/sha512sums=('[0-9A-Fa-f]*/s/[^'][^)]*/sha512sums=('$$(sha512sum $(PROJECT)-$(VERSION)-src.tar.xz | awk '{print $$1}')'/" package/archlinux/PKGBUILD
sed -i "s/pkgver=[0-9.]*/pkgver=$(VERSION)/" package/archlinux/PKGBUILD
check:
check: clean
cd src && mypy --implicit-reexport --strict -p "$(PROJECT)"
find "src/$(PROJECT)" tests -name "*.py" -execdir autopep8 --exit-code --max-line-length 120 -aa -i {} +
cd src && pylint --rcfile=../.pylintrc "$(PROJECT)"
@ -39,11 +38,11 @@ directory: clean
push: archlinux
git add package/archlinux/PKGBUILD src/ahriman/version.py
git commit -m "Release $(VERSION)"
git push
git tag "$(VERSION)"
git push
git push --tags
tests:
tests: clean
python setup.py test
version:

View File

@ -1,5 +1,7 @@
# ArcHlinux ReposItory MANager
[![build status](https://github.com/arcan1s/ahriman/actions/workflows/run-tests.yml/badge.svg)](https://github.com/arcan1s/ahriman/actions/workflows/run-tests.yml)
Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
## Features
@ -64,7 +66,7 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
* Add packages by using `ahriman add {package}` command:
```shell
sudo -u ahriman ahriman -a x86_64 add yay
sudo -u ahriman ahriman -a x86_64 add yay --now
```
Note that initial service configuration can be done by running `ahriman setup` with specific arguments.

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=0.18.0
pkgver=0.21.4
pkgrel=1
pkgdesc="ArcHlinux ReposItory MANager"
arch=('any')
@ -23,7 +23,7 @@ optdepends=('aws-cli: sync to s3'
source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz"
'ahriman.sysusers'
'ahriman.tmpfiles')
sha512sums=('8acc57f937d587ca665c29092cadddbaf3ba0b80e870b80d1551e283aba8f21306f9030a26fec8c71ab5863316f5f5f061b7ddc63cdff9e6d5a885f28ef1893d'
sha512sums=('6ab741bfb42f92ab00d1b6ecfc44426c00e5c433486e014efbdb585715d9a12dbbafc280e5a9f85b941c8681b13a9dad41327a3e3c44a9683ae30c1d6f017f50'
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
backup=('etc/ahriman.ini'

View File

@ -21,7 +21,6 @@ root = /var/lib/ahriman
[sign]
target =
key =
[report]
target =
@ -36,10 +35,11 @@ template_path = /usr/share/ahriman/repo-index.jinja2
target =
[rsync]
remote =
command = rsync --archive --verbose --compress --partial --delete
[s3]
bucket =
command = aws s3 sync --quiet --delete
[web]
host = 0.0.0.0
templates = /usr/share/ahriman

View File

@ -2,4 +2,4 @@
test = pytest
[tool:pytest]
addopts = --cov=ahriman --pspec
addopts = --cov=ahriman --cov-report term-missing:skip-covered --pspec

View File

@ -20,10 +20,13 @@
import argparse
import sys
from pathlib import Path
import ahriman.application.handlers as handlers
import ahriman.version as version
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.sign_settings import SignSettings
# pylint thinks it is bad idea, but get the fuck off
@ -36,12 +39,13 @@ def _parser() -> argparse.ArgumentParser:
command line parser generator
:return: command line parser for the application
"""
parser = argparse.ArgumentParser(prog="ahriman", description="ArcHlinux ReposItory MANager")
parser = argparse.ArgumentParser(prog="ahriman", description="ArcHlinux ReposItory MANager",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-a", "--architecture", help="target architectures (can be used multiple times)",
action="append", required=True)
parser.add_argument("-c", "--config", help="configuration path", default="/etc/ahriman.ini")
parser.add_argument("-c", "--configuration", help="configuration path", type=Path, default=Path("/etc/ahriman.ini"))
parser.add_argument("--force", help="force run, remove file lock", action="store_true")
parser.add_argument("--lock", help="lock file", default="/tmp/ahriman.lock")
parser.add_argument("-l", "--lock", help="lock file", type=Path, default=Path("/tmp/ahriman.lock"))
parser.add_argument("--no-log", help="redirect all log messages to stderr", action="store_true")
parser.add_argument("--no-report", help="force disable reporting to web service", action="store_true")
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user", action="store_true")
@ -73,8 +77,10 @@ def _set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("add", description="add package")
parser = root.add_parser("add", help="add package", description="add package",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="package base/name or archive path", nargs="+")
parser.add_argument("--now", help="run update function after", action="store_true")
parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
parser.set_defaults(handler=handlers.Add)
return parser
@ -86,7 +92,9 @@ def _set_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("check", description="check for updates. Same as update --dry-run --no-manual")
parser = root.add_parser("check", help="check for updates",
description="check for updates. Same as update --dry-run --no-manual",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="filter check by package base", nargs="*")
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
parser.set_defaults(handler=handlers.Update, no_aur=False, no_manual=True, dry_run=True)
@ -99,7 +107,8 @@ def _set_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("clean", description="clear all local caches")
parser = root.add_parser("clean", help="clean local caches", description="clear local caches",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--no-build", help="do not clear directory with package sources", action="store_true")
parser.add_argument("--no-cache", help="do not clear directory with package caches", action="store_true")
parser.add_argument("--no-chroot", help="do not clear build chroot", action="store_true")
@ -115,7 +124,9 @@ def _set_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("config", description="dump configuration for specified architecture")
parser = root.add_parser("config", help="dump configuration",
description="dump configuration for specified architecture",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.set_defaults(handler=handlers.Dump, lock=None, no_report=True, unsafe=True)
return parser
@ -126,7 +137,9 @@ def _set_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("rebuild", description="rebuild whole repository")
parser = root.add_parser("rebuild", help="rebuild repository", description="rebuild whole repository",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--depends-on", help="only rebuild packages that depend on specified package")
parser.set_defaults(handler=handlers.Rebuild)
return parser
@ -137,7 +150,8 @@ def _set_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("remove", description="remove package")
parser = root.add_parser("remove", help="remove package", description="remove package",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="package name or base", nargs="+")
parser.set_defaults(handler=handlers.Remove)
return parser
@ -149,7 +163,8 @@ def _set_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("report", description="generate report")
parser = root.add_parser("report", help="generate report", description="generate report",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("target", help="target to generate report", nargs="*")
parser.set_defaults(handler=handlers.Report)
return parser
@ -161,13 +176,19 @@ def _set_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("setup", description="create initial service configuration, requires root")
parser = root.add_parser("setup", help="initial service configuration",
description="create initial service configuration, requires root",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--build-command", help="build command prefix", default="ahriman")
parser.add_argument("--from-config", help="path to default devtools pacman configuration",
default="/usr/share/devtools/pacman-extra.conf")
parser.add_argument("--from-configuration", help="path to default devtools pacman configuration",
type=Path, default=Path("/usr/share/devtools/pacman-extra.conf"))
parser.add_argument("--no-multilib", help="do not add multilib repository", action="store_true")
parser.add_argument("--packager", help="packager name and email", required=True)
parser.add_argument("--repository", help="repository name", default="aur-clone")
parser.add_argument("--repository", help="repository name", required=True)
parser.add_argument("--sign-key", help="sign key id")
parser.add_argument("--sign-target", help="sign options", type=SignSettings.from_option,
choices=SignSettings, nargs="*")
parser.add_argument("--web-port", help="port of the web service", type=int)
parser.set_defaults(handler=handlers.Setup, lock=None, no_report=True, unsafe=True)
return parser
@ -178,7 +199,8 @@ def _set_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("sign", description="(re-)sign packages and repository database")
parser = root.add_parser("sign", help="sign packages", description="(re-)sign packages and repository database",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="sign only specified packages", nargs="*")
parser.set_defaults(handler=handlers.Sign)
return parser
@ -190,7 +212,8 @@ def _set_status_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("status", description="request status of the package")
parser = root.add_parser("status", help="get package status", description="request status of the package",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--ahriman", help="get service status itself", action="store_true")
parser.add_argument("package", help="filter status by package base", nargs="*")
parser.set_defaults(handler=handlers.Status, lock=None, no_report=True, unsafe=True)
@ -203,13 +226,15 @@ def _set_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("status-update", description="request status of the package")
parser = root.add_parser("status-update", help="update package status", description="request status of the package",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(
"package",
help="set status for specified packages. If no packages supplied, service status will be updated",
nargs="*")
parser.add_argument("--status", help="new status", choices=[value.value for value in BuildStatusEnum],
default="success")
parser.add_argument("--status", help="new status", choices=BuildStatusEnum,
type=BuildStatusEnum, default=BuildStatusEnum.Success)
parser.add_argument("--remove", help="remove package status page", action="store_true")
parser.set_defaults(handler=handlers.StatusUpdate, lock=None, no_report=True, unsafe=True)
return parser
@ -220,7 +245,8 @@ def _set_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("sync", description="sync packages to remote server")
parser = root.add_parser("sync", help="sync repository", description="sync packages to remote server",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("target", help="target to sync", nargs="*")
parser.set_defaults(handler=handlers.Sync)
return parser
@ -232,7 +258,8 @@ def _set_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("update", description="run updates")
parser = root.add_parser("update", help="update packages", description="run updates",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="filter check by package base", nargs="*")
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_true")
parser.add_argument("--no-aur", help="do not check for AUR updates. Implies --no-vcs", action="store_true")
@ -248,16 +275,24 @@ def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("web", description="start web server")
parser = root.add_parser("web", help="start web server", description="start web server",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.set_defaults(handler=handlers.Web, lock=None, no_report=True)
return parser
if __name__ == "__main__":
args_parser = _parser()
args = args_parser.parse_args()
def run() -> None:
"""
run application instance
"""
if __name__ == "__main__":
args_parser = _parser()
args = args_parser.parse_args()
handler: handlers.Handler = args.handler
status = handler.execute(args)
handler: handlers.Handler = args.handler
status = handler.execute(args)
sys.exit(status)
sys.exit(status)
run()

View File

@ -35,21 +35,28 @@ class Application:
"""
base application class
:ivar architecture: repository architecture
:ivar config: configuration instance
:ivar configuration: configuration instance
:ivar logger: application logger
:ivar repository: repository instance
"""
def __init__(self, architecture: str, config: Configuration) -> None:
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
self.logger = logging.getLogger("root")
self.config = config
self.configuration = configuration
self.architecture = architecture
self.repository = Repository(architecture, config)
self.repository = Repository(architecture, configuration)
def _finalize(self) -> None:
"""
generate report and sync to remote server
"""
self.report([])
self.sync([])
def _known_packages(self) -> Set[str]:
"""
@ -63,13 +70,6 @@ class Application:
known_packages.update(self.repository.pacman.all_packages())
return known_packages
def _finalize(self) -> None:
"""
generate report and sync to remote server
"""
self.report([])
self.sync([])
def get_updates(self, filter_packages: List[str], no_aur: bool, no_manual: bool, no_vcs: bool,
log_fn: Callable[[str], None]) -> List[Package]:
"""
@ -106,7 +106,7 @@ class Application:
add_archive(full_path)
def add_manual(src: str) -> Path:
package = Package.load(src, self.repository.pacman, self.config.get("alpm", "aur_url"))
package = Package.load(src, self.repository.pacman, self.configuration.get("alpm", "aur_url"))
path = self.repository.paths.manual / package.base
Task.fetch(path, package.git_url)
return path
@ -182,6 +182,7 @@ class Application:
continue
for archive in package.packages.values():
if archive.filepath is None:
self.logger.warning(f"filepath is empty for {package.base}")
continue # avoid mypy warning
src = self.repository.paths.repository / archive.filepath
dst = self.repository.paths.packages / archive.filepath

View File

@ -32,11 +32,17 @@ class Add(Handler):
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
Application(architecture, config).add(args.package, args.without_dependencies)
application = Application(architecture, configuration)
application.add(args.package, args.without_dependencies)
if not args.now:
return
packages = application.get_updates(args.package, True, False, True, application.logger.info)
application.update(packages)

View File

@ -32,12 +32,12 @@ class Clean(Handler):
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
Application(architecture, config).clean(args.no_build, args.no_cache, args.no_chroot,
args.no_manual, args.no_packages)
Application(architecture, configuration).clean(args.no_build, args.no_cache, args.no_chroot,
args.no_manual, args.no_packages)

View File

@ -27,20 +27,22 @@ from ahriman.core.configuration import Configuration
class Dump(Handler):
"""
dump config handler
dump configuration handler
"""
_print = print
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
config_dump = config.dump(architecture)
for section, values in sorted(config_dump.items()):
print(f"[{section}]")
dump = configuration.dump()
for section, values in sorted(dump.items()):
Dump._print(f"[{section}]")
for key, value in sorted(values.items()):
print(f"{key} = {value}")
print()
Dump._print(f"{key} = {value}")
Dump._print()

View File

@ -35,17 +35,17 @@ class Handler:
"""
@classmethod
def _call(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> bool:
def _call(cls: Type[Handler], args: argparse.Namespace, architecture: str) -> bool:
"""
additional function to wrap all calls for multiprocessing library
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:return: True on success, False otherwise
"""
try:
with Lock(args, architecture, config):
cls.run(args, architecture, config)
configuration = Configuration.from_path(args.configuration, architecture, not args.no_log)
with Lock(args, architecture, configuration):
cls.run(args, architecture, configuration)
return True
except Exception:
logging.getLogger("root").exception("process exception")
@ -58,18 +58,17 @@ class Handler:
:param args: command line args
:return: 0 on success, 1 otherwise
"""
configuration = Configuration.from_path(args.config, not args.no_log)
with Pool(len(args.architecture)) as pool:
result = pool.starmap(
cls._call, [(args, architecture, configuration) for architecture in args.architecture])
cls._call, [(args, architecture) for architecture in set(args.architecture)])
return 0 if all(result) else 1
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
raise NotImplementedError

View File

@ -32,13 +32,17 @@ class Rebuild(Handler):
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
application = Application(architecture, config)
packages = application.repository.packages()
application = Application(architecture, configuration)
packages = [
package
for package in application.repository.packages()
if args.depends_on is None or args.depends_on in package.depends
] # we have to use explicit list here for testing purpose
application.update(packages)

View File

@ -32,11 +32,11 @@ class Remove(Handler):
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
Application(architecture, config).remove(args.package)
Application(architecture, configuration).remove(args.package)

View File

@ -32,11 +32,11 @@ class Report(Handler):
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
Application(architecture, config).report(args.target)
Application(architecture, configuration).report(args.target)

View File

@ -44,19 +44,19 @@ class Setup(Handler):
SUDOERS_PATH = Path("/etc/sudoers.d/ahriman")
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
application = Application(architecture, config)
application = Application(architecture, configuration)
Setup.create_makepkg_configuration(args.packager, application.repository.paths)
Setup.create_executable(args.build_command, architecture)
Setup.create_devtools_configuration(args.build_command, architecture, Path(args.from_config), args.no_multilib,
args.repository, application.repository.paths)
Setup.create_ahriman_configuration(args.build_command, architecture, args.repository, config.include)
Setup.create_devtools_configuration(args.build_command, architecture, args.from_configuration,
args.no_multilib, args.repository, application.repository.paths)
Setup.create_ahriman_configuration(args, architecture, args.repository, configuration.include)
Setup.create_sudo_configuration(args.build_command, architecture)
@staticmethod
@ -70,25 +70,38 @@ class Setup(Handler):
return Setup.BIN_DIR_PATH / f"{prefix}-{architecture}-build"
@staticmethod
def create_ahriman_configuration(prefix: str, architecture: str, repository: str, include_path: Path) -> None:
def create_ahriman_configuration(args: argparse.Namespace, architecture: str, repository: str,
include_path: Path) -> None:
"""
create service specific configuration
:param prefix: command prefix in {prefix}-{architecture}-build
:param args: command line args
:param architecture: repository architecture
:param repository: repository name
:param include_path: path to directory with configuration includes
"""
config = configparser.ConfigParser()
configuration = configparser.ConfigParser()
config.add_section("build")
config.set("build", "build_command", str(Setup.build_command(prefix, architecture)))
section = Configuration.section_name("build", architecture)
configuration.add_section(section)
configuration.set(section, "build_command", str(Setup.build_command(args.build_command, architecture)))
config.add_section("repository")
config.set("repository", "name", repository)
configuration.add_section("repository")
configuration.set("repository", "name", repository)
target = include_path / "build-overrides.ini"
with target.open("w") as ahriman_config:
config.write(ahriman_config)
if args.sign_key is not None:
section = Configuration.section_name("sign", architecture)
configuration.add_section(section)
configuration.set(section, "target", " ".join([target.name.lower() for target in args.sign_target]))
configuration.set(section, "key", args.sign_key)
if args.web_port is not None:
section = Configuration.section_name("web", architecture)
configuration.add_section(section)
configuration.set(section, "port", str(args.web_port))
target = include_path / "setup-overrides.ini"
with target.open("w") as ahriman_configuration:
configuration.write(ahriman_configuration)
@staticmethod
def create_devtools_configuration(prefix: str, architecture: str, source: Path,
@ -102,31 +115,31 @@ class Setup(Handler):
:param repository: repository name
:param paths: repository paths instance
"""
config = configparser.ConfigParser()
configuration = configparser.ConfigParser()
# preserve case
# stupid mypy thinks that it is impossible
config.optionxform = lambda key: key # type: ignore
configuration.optionxform = lambda key: key # type: ignore
# load default configuration first
# we cannot use Include here because it will be copied to new chroot, thus no includes there
config.read(source)
configuration.read(source)
# set our architecture now
config.set("options", "Architecture", architecture)
configuration.set("options", "Architecture", architecture)
# add multilib
if not no_multilib:
config.add_section("multilib")
config.set("multilib", "Include", str(Setup.MIRRORLIST_PATH))
configuration.add_section("multilib")
configuration.set("multilib", "Include", str(Setup.MIRRORLIST_PATH))
# add repository itself
config.add_section(repository)
config.set(repository, "SigLevel", "Optional TrustAll") # we don't care
config.set(repository, "Server", f"file://{paths.repository}")
configuration.add_section(repository)
configuration.set(repository, "SigLevel", "Optional TrustAll") # we don't care
configuration.set(repository, "Server", f"file://{paths.repository}")
target = source.parent / f"pacman-{prefix}-{architecture}.conf"
with target.open("w") as devtools_config:
config.write(devtools_config)
with target.open("w") as devtools_configuration:
configuration.write(devtools_configuration)
@staticmethod
def create_makepkg_configuration(packager: str, paths: RepositoryPaths) -> None:

View File

@ -32,11 +32,11 @@ class Sign(Handler):
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
Application(architecture, config).sign(args.package)
Application(architecture, configuration).sign(args.package)

View File

@ -34,14 +34,14 @@ class Status(Handler):
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
application = Application(architecture, config)
application = Application(architecture, configuration)
if args.ahriman:
ahriman = application.repository.reporter.get_self()
print(ahriman.pretty_print())

View File

@ -19,12 +19,11 @@
#
import argparse
from typing import Type
from typing import Callable, Type
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatusEnum
class StatusUpdate(Handler):
@ -33,19 +32,19 @@ class StatusUpdate(Handler):
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
client = Application(architecture, config).repository.reporter
status = BuildStatusEnum(args.status)
client = Application(architecture, configuration).repository.reporter
callback: Callable[[str], None] = lambda p: client.remove(p) if args.remove else client.update(p, args.status)
if args.package:
# update packages statuses
for package in args.package:
client.update(package, status)
callback(package)
else:
# update service status
client.update_self(status)
client.update_self(args.status)

View File

@ -32,11 +32,11 @@ class Sync(Handler):
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
Application(architecture, config).sync(args.target)
Application(architecture, configuration).sync(args.target)

View File

@ -19,7 +19,7 @@
#
import argparse
from typing import Type
from typing import Callable, Type
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler
@ -32,20 +32,29 @@ class Update(Handler):
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
# typing workaround
def log_fn(line: str) -> None:
return print(line) if args.dry_run else application.logger.info(line)
application = Application(architecture, config)
packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn)
application = Application(architecture, configuration)
packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs,
Update.log_fn(application, args.dry_run))
if args.dry_run:
return
application.update(packages)
@staticmethod
def log_fn(application: Application, dry_run: bool) -> Callable[[str], None]:
"""
package updates log function
:param application: application instance
:param dry_run: do not perform update itself
:return: in case if dry_run is set it will return print, logger otherwise
"""
def inner(line: str) -> None:
return print(line) if dry_run else application.logger.info(line)
return inner

View File

@ -31,13 +31,13 @@ class Web(Handler):
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
from ahriman.web.web import run_server, setup_service
application = setup_service(architecture, config)
application = setup_service(architecture, configuration)
run_server(application)

View File

@ -42,19 +42,19 @@ class Lock:
:ivar unsafe: skip user check
"""
def __init__(self, args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def __init__(self, args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
self.path = Path(f"{args.lock}_{architecture}") if args.lock is not None else None
self.force = args.force
self.unsafe = args.unsafe
self.root = Path(config.get("repository", "root"))
self.reporter = Client() if args.no_report else Client.load(architecture, config)
self.root = Path(configuration.get("repository", "root"))
self.reporter = Client() if args.no_report else Client.load(configuration)
def __enter__(self) -> Lock:
"""
@ -80,7 +80,7 @@ class Lock:
:param exc_tb: exception traceback if any
:return: always False (do not suppress any exception)
"""
self.remove()
self.clear()
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
self.reporter.update_self(status)
return False
@ -96,6 +96,14 @@ class Lock:
if current_uid != root_uid:
raise UnsafeRun(current_uid, root_uid)
def clear(self) -> None:
"""
remove lock file
"""
if self.path is None:
return
self.path.unlink(missing_ok=True)
def create(self) -> None:
"""
create lock file
@ -106,11 +114,3 @@ class Lock:
self.path.touch(exist_ok=self.force)
except FileExistsError:
raise DuplicateRun()
def remove(self) -> None:
"""
remove lock file
"""
if self.path is None:
return
self.path.unlink(missing_ok=True)

View File

@ -29,15 +29,15 @@ class Pacman:
:ivar handle: pyalpm root `Handle`
"""
def __init__(self, config: Configuration) -> None:
def __init__(self, configuration: Configuration) -> None:
"""
default constructor
:param config: configuration instance
:param configuration: configuration instance
"""
root = config.get("alpm", "root")
pacman_root = config.getpath("alpm", "database")
root = configuration.get("alpm", "root")
pacman_root = configuration.getpath("alpm", "database")
self.handle = Handle(root, str(pacman_root))
for repository in config.getlist("alpm", "repositories"):
for repository in configuration.getlist("alpm", "repositories"):
self.handle.register_syncdb(repository, 0) # 0 is pgp_level
def all_packages(self) -> List[str]:

View File

@ -41,12 +41,11 @@ class Task:
_check_output = check_output
def __init__(self, package: Package, architecture: str, config: Configuration, paths: RepositoryPaths) -> None:
def __init__(self, package: Package, configuration: Configuration, paths: RepositoryPaths) -> None:
"""
default constructor
:param package: package definitions
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
:param paths: repository paths instance
"""
self.logger = logging.getLogger("builder")
@ -54,11 +53,10 @@ class Task:
self.package = package
self.paths = paths
section = config.get_section_name("build", architecture)
self.archbuild_flags = config.getlist(section, "archbuild_flags")
self.build_command = config.get(section, "build_command")
self.makepkg_flags = config.getlist(section, "makepkg_flags")
self.makechrootpkg_flags = config.getlist(section, "makechrootpkg_flags")
self.archbuild_flags = configuration.getlist("build", "archbuild_flags")
self.build_command = configuration.get("build", "build_command")
self.makepkg_flags = configuration.getlist("build", "makepkg_flags")
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags")
@property
def cache_path(self) -> Path:
@ -97,14 +95,14 @@ class Task:
run package build
:return: paths of produced packages
"""
cmd = [self.build_command, "-r", str(self.paths.chroot)]
cmd.extend(self.archbuild_flags)
cmd.extend(["--"] + self.makechrootpkg_flags)
cmd.extend(["--"] + self.makepkg_flags)
self.logger.info(f"using {cmd} for {self.package.base}")
command = [self.build_command, "-r", str(self.paths.chroot)]
command.extend(self.archbuild_flags)
command.extend(["--"] + self.makechrootpkg_flags)
command.extend(["--"] + self.makepkg_flags)
self.logger.info(f"using {command} for {self.package.base}")
Task._check_output(
*cmd,
*command,
exception=BuildFailed(self.package.base),
cwd=self.git_path,
logger=self.build_logger)
@ -125,4 +123,4 @@ class Task:
if self.cache_path.is_dir():
# no need to clone whole repository, just copy from cache first
shutil.copytree(self.cache_path, git_path)
return Task.fetch(git_path, self.package.git_url)
return self.fetch(git_path, self.package.git_url)

View File

@ -34,13 +34,11 @@ class Configuration(configparser.RawConfigParser):
:cvar ARCHITECTURE_SPECIFIC_SECTIONS: known sections which can be architecture specific (required by dump)
:cvar DEFAULT_LOG_FORMAT: default log format (in case of fallback)
:cvar DEFAULT_LOG_LEVEL: default log level (in case of fallback)
:cvar STATIC_SECTIONS: known sections which are not architecture specific (required by dump)
"""
DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s"
DEFAULT_LOG_LEVEL = logging.DEBUG
STATIC_SECTIONS = ["alpm", "report", "repository", "settings", "upload"]
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "html", "rsync", "s3", "sign", "web"]
def __init__(self) -> None:
@ -57,37 +55,46 @@ class Configuration(configparser.RawConfigParser):
"""
return self.getpath("settings", "include")
@property
def logging_path(self) -> Path:
"""
:return: path to logging configuration
"""
return self.getpath("settings", "logging")
@classmethod
def from_path(cls: Type[Configuration], path: Path, logfile: bool) -> Configuration:
def from_path(cls: Type[Configuration], path: Path, architecture: str, logfile: bool) -> Configuration:
"""
constructor with full object initialization
:param path: path to root configuration file
:param architecture: repository architecture
:param logfile: use log file to output messages
:return: configuration instance
"""
config = cls()
config.load(path)
config.load(path, architecture)
config.load_logging(logfile)
return config
def dump(self, architecture: str) -> Dict[str, Dict[str, str]]:
@staticmethod
def section_name(section: str, architecture: str) -> str:
"""
generate section name for architecture specific sections
:param section: section name
:param architecture: repository architecture
:return: correct section name for repository specific section
"""
return f"{section}:{architecture}"
def dump(self) -> Dict[str, Dict[str, str]]:
"""
dump configuration to dictionary
:param architecture: repository architecture
:return: configuration dump for specific architecture
"""
result: Dict[str, Dict[str, str]] = {}
for section in Configuration.STATIC_SECTIONS:
if not self.has_section(section):
continue
result[section] = dict(self[section])
for group in Configuration.ARCHITECTURE_SPECIFIC_SECTIONS:
section = self.get_section_name(group, architecture)
if not self.has_section(section):
continue
result[section] = dict(self[section])
return result
return {
section: dict(self[section])
for section in self.sections()
}
def getlist(self, section: str, key: str) -> List[str]:
"""
@ -113,24 +120,16 @@ class Configuration(configparser.RawConfigParser):
return value
return self.path.parent / value
def get_section_name(self, prefix: str, suffix: str) -> str:
"""
check if there is `prefix`_`suffix` section and return it on success. Return `prefix` otherwise
:param prefix: section name prefix
:param suffix: section name suffix (e.g. architecture name)
:return: found section name
"""
probe = f"{prefix}_{suffix}"
return probe if self.has_section(probe) else prefix
def load(self, path: Path) -> None:
def load(self, path: Path, architecture: str) -> None:
"""
fully load configuration
:param path: path to root configuration file
:param architecture: repository architecture
"""
self.path = path
self.read(self.path)
self.load_includes()
self.merge_sections(architecture)
def load_includes(self) -> None:
"""
@ -138,6 +137,8 @@ class Configuration(configparser.RawConfigParser):
"""
try:
for path in sorted(self.include.glob("*.ini")):
if path == self.logging_path:
continue # we don't want to load logging explicitly
self.read(path)
except (FileNotFoundError, configparser.NoOptionError):
pass
@ -149,17 +150,39 @@ class Configuration(configparser.RawConfigParser):
"""
def file_logger() -> None:
try:
config_path = self.getpath("settings", "logging")
fileConfig(config_path)
path = self.logging_path
fileConfig(path)
except (FileNotFoundError, PermissionError):
console_logger()
logging.exception("could not create logfile, fallback to stderr")
def console_logger() -> None:
logging.basicConfig(filename=None, format=Configuration.DEFAULT_LOG_FORMAT,
level=Configuration.DEFAULT_LOG_LEVEL)
logging.basicConfig(filename=None, format=self.DEFAULT_LOG_FORMAT,
level=self.DEFAULT_LOG_LEVEL)
if logfile:
file_logger()
else:
console_logger()
def merge_sections(self, architecture: str) -> None:
"""
merge architecture specific sections into main configuration
:param architecture: repository architecture
"""
for section in self.ARCHITECTURE_SPECIFIC_SECTIONS:
if not self.has_section(section):
self.add_section(section) # add section if not exists
# get overrides
specific = self.section_name(section, architecture)
if self.has_section(specific):
# if there is no such section it means that there is no overrides for this arch
# but we anyway will have to delete sections for others archs
for key, value in self[specific].items():
self.set(section, key, value)
# remove any arch specific section
for foreign in self.sections():
# we would like to use lambda filter here, but pylint is too dumb
if not foreign.startswith(f"{section}:"):
continue
self.remove_section(foreign)

View File

@ -23,6 +23,7 @@ from typing import Callable, Dict, Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.report.report import Report
from ahriman.core.sign.gpg import GPG
from ahriman.core.util import pretty_datetime, pretty_size
from ahriman.models.package import Package
from ahriman.models.sign_settings import SignSettings
@ -42,6 +43,7 @@ class HTML(Report):
* architecture, string
* archive_size, pretty printed size, string
* build_date, pretty printed datetime, string
* depends, sorted list of strings
* description, string
* filename, string,
* groups, sorted list of strings
@ -56,31 +58,28 @@ class HTML(Report):
:ivar homepage: homepage link if any (for footer)
:ivar link_path: prefix fo packages to download
:ivar name: repository name
:ivar pgp_key: default PGP key
:ivar default_pgp_key: default PGP key
:ivar report_path: output path to html report
:ivar sign_targets: targets to sign enabled in configuration
:ivar template_path: path to directory with jinja templates
"""
def __init__(self, architecture: str, config: Configuration) -> None:
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
Report.__init__(self, architecture, config)
section = config.get_section_name("html", architecture)
self.report_path = config.getpath(section, "path")
self.link_path = config.get(section, "link_path")
self.template_path = config.getpath(section, "template_path")
Report.__init__(self, architecture, configuration)
self.report_path = configuration.getpath("html", "path")
self.link_path = configuration.get("html", "link_path")
self.template_path = configuration.getpath("html", "template_path")
# base template vars
self.homepage = config.get(section, "homepage", fallback=None)
self.name = config.get("repository", "name")
self.homepage = configuration.get("html", "homepage", fallback=None)
self.name = configuration.get("repository", "name")
sign_section = config.get_section_name("sign", architecture)
self.sign_targets = [SignSettings.from_option(opt) for opt in config.getlist(sign_section, "target")]
self.pgp_key = config.get(sign_section, "key") if self.sign_targets else None
self.sign_targets, self.default_pgp_key = GPG.sign_options(configuration)
def generate(self, packages: Iterable[Package]) -> None:
"""
@ -97,6 +96,7 @@ class HTML(Report):
"architecture": properties.architecture or "",
"archive_size": pretty_size(properties.archive_size),
"build_date": pretty_datetime(properties.build_date),
"depends": properties.depends,
"description": properties.description or "",
"filename": properties.filename,
"groups": properties.groups,
@ -112,10 +112,10 @@ class HTML(Report):
html = template.render(
homepage=self.homepage,
link_path=self.link_path,
has_package_signed=SignSettings.SignPackages in self.sign_targets,
has_repo_signed=SignSettings.SignRepository in self.sign_targets,
has_package_signed=SignSettings.Packages in self.sign_targets,
has_repo_signed=SignSettings.Repository in self.sign_targets,
packages=sorted(content, key=comparator),
pgp_key=self.pgp_key,
pgp_key=self.default_pgp_key,
repository=self.name)
self.report_path.write_text(html)

View File

@ -17,9 +17,11 @@
# 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 __future__ import annotations
import logging
from typing import Iterable
from typing import Iterable, Type
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ReportFailed
@ -31,44 +33,48 @@ class Report:
"""
base report generator
:ivar architecture: repository architecture
:ivar config: configuration instance
:ivar configuration: configuration instance
:ivar logger: class logger
"""
def __init__(self, architecture: str, config: Configuration) -> None:
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
self.logger = logging.getLogger("builder")
self.architecture = architecture
self.config = config
self.configuration = configuration
@staticmethod
def run(architecture: str, config: Configuration, target: str, packages: Iterable[Package]) -> None:
@classmethod
def load(cls: Type[Report], architecture: str, configuration: Configuration, target: str) -> Report:
"""
run report generation
load client from settings
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
:param target: target to generate report (e.g. html)
:param packages: list of packages to generate report
:return: client according to current settings
"""
provider = ReportSettings.from_option(target)
if provider == ReportSettings.HTML:
from ahriman.core.report.html import HTML
report: Report = HTML(architecture, config)
else:
report = Report(architecture, config)
try:
report.generate(packages)
except Exception:
report.logger.exception(f"report generation failed for target {provider.name}")
raise ReportFailed()
return HTML(architecture, configuration)
return cls(architecture, configuration) # should never happen
def generate(self, packages: Iterable[Package]) -> None:
"""
generate report for the specified packages
:param packages: list of packages to generate report
"""
def run(self, packages: Iterable[Package]) -> None:
"""
run report generation
:param packages: list of packages to generate report
"""
try:
self.generate(packages)
except Exception:
self.logger.exception("report generation failed")
raise ReportFailed()

View File

@ -25,7 +25,7 @@ from typing import Dict, Iterable, List, Optional
from ahriman.core.build_tools.task import Task
from ahriman.core.report.report import Report
from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.upload.uploader import Uploader
from ahriman.core.upload.upload import Upload
from ahriman.models.package import Package
@ -49,7 +49,7 @@ class Executor(Cleaner):
"""
def build_single(package: Package) -> None:
self.reporter.set_building(package.base)
task = Task(package, self.architecture, self.config, self.paths)
task = Task(package, self.configuration, self.paths)
task.init()
built = task.build()
for src in built:
@ -106,9 +106,10 @@ class Executor(Cleaner):
:param targets: list of targets to generate reports. Configuration option will be used if it is not set
"""
if targets is None:
targets = self.config.getlist("report", "target")
targets = self.configuration.getlist("report", "target")
for target in targets:
Report.run(self.architecture, self.config, target, self.packages())
runner = Report.load(self.architecture, self.configuration, target)
runner.run(self.packages())
def process_sync(self, targets: Optional[Iterable[str]]) -> None:
"""
@ -116,9 +117,10 @@ class Executor(Cleaner):
:param targets: list of targets to sync. Configuration option will be used if it is not set
"""
if targets is None:
targets = self.config.getlist("upload", "target")
targets = self.configuration.getlist("upload", "target")
for target in targets:
Uploader.run(self.architecture, self.config, target, self.paths.repository)
runner = Upload.load(self.architecture, self.configuration, target)
runner.run(self.paths.repository)
def process_update(self, packages: Iterable[Path]) -> Path:
"""

View File

@ -32,7 +32,8 @@ class Properties:
repository internal objects holder
:ivar architecture: repository architecture
:ivar aur_url: base AUR url
:ivar config: configuration instance
:ivar configuration: configuration instance
:ivar ignore_list: package bases which will be ignored during auto updates
:ivar logger: class logger
:ivar name: repository name
:ivar pacman: alpm wrapper instance
@ -42,18 +43,19 @@ class Properties:
:ivar sign: GPG wrapper instance
"""
def __init__(self, architecture: str, config: Configuration) -> None:
def __init__(self, architecture: str, configuration: Configuration) -> None:
self.logger = logging.getLogger("builder")
self.architecture = architecture
self.config = config
self.configuration = configuration
self.aur_url = config.get("alpm", "aur_url")
self.name = config.get("repository", "name")
self.aur_url = configuration.get("alpm", "aur_url")
self.name = configuration.get("repository", "name")
self.paths = RepositoryPaths(config.getpath("repository", "root"), architecture)
self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture)
self.paths.create_tree()
self.pacman = Pacman(config)
self.sign = GPG(architecture, config)
self.ignore_list = configuration.getlist("build", "ignore_packages")
self.pacman = Pacman(configuration)
self.sign = GPG(architecture, configuration)
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
self.reporter = Client.load(architecture, config)
self.reporter = Client.load(configuration)

View File

@ -53,4 +53,4 @@ class Repository(Executor, UpdateHandler):
get list of files in built packages directory
:return: list of filenames from the directory
"""
return list(self.paths.packages.iterdir())
return list(filter(package_like, self.paths.packages.iterdir()))

View File

@ -44,11 +44,8 @@ class UpdateHandler(Cleaner):
"""
result: List[Package] = []
build_section = self.config.get_section_name("build", self.architecture)
ignore_list = self.config.getlist(build_section, "ignore_packages")
for local in self.packages():
if local.base in ignore_list:
if local.base in self.ignore_list:
continue
if local.is_vcs and no_vcs:
continue

View File

@ -20,7 +20,7 @@
import logging
from pathlib import Path
from typing import List
from typing import List, Optional, Set, Tuple
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import BuildFailed
@ -32,37 +32,39 @@ class GPG:
"""
gnupg wrapper
:ivar architecture: repository architecture
:ivar config: configuration instance
:ivar configuration: configuration instance
:ivar default_key: default PGP key ID to use
:ivar logger: class logger
:ivar target: list of targets to sign (repository, package etc)
:ivar targets: list of targets to sign (repository, package etc)
"""
_check_output = check_output
def __init__(self, architecture: str, config: Configuration) -> None:
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
self.logger = logging.getLogger("build_details")
self.config = config
self.section = config.get_section_name("sign", architecture)
self.target = {SignSettings.from_option(opt) for opt in config.getlist(self.section, "target")}
self.default_key = config.get(self.section, "key") if self.target else ""
self.architecture = architecture
self.configuration = configuration
self.targets, self.default_key = self.sign_options(configuration)
@property
def repository_sign_args(self) -> List[str]:
"""
:return: command line arguments for repo-add command to sign database
"""
if SignSettings.SignRepository not in self.target:
if SignSettings.Repository not in self.targets:
return []
if self.default_key is None:
self.logger.error("no default key set, skip repository sign")
return []
return ["--sign", "--key", self.default_key]
@staticmethod
def sign_cmd(path: Path, key: str) -> List[str]:
def sign_command(path: Path, key: str) -> List[str]:
"""
gpg command to run
:param path: path to file to sign
@ -71,6 +73,20 @@ class GPG:
"""
return ["gpg", "-u", key, "-b", str(path)]
@staticmethod
def sign_options(configuration: Configuration) -> Tuple[Set[SignSettings], Optional[str]]:
"""
extract default sign options from configuration
:param configuration: configuration instance
:return: tuple of sign targets and default PGP key
"""
targets = {
SignSettings.from_option(option)
for option in configuration.getlist("sign", "target")
}
default_key = configuration.get("sign", "key") if targets else None
return targets, default_key
def process(self, path: Path, key: str) -> List[Path]:
"""
gpg command wrapper
@ -79,7 +95,7 @@ class GPG:
:return: list of generated files including original file
"""
GPG._check_output(
*GPG.sign_cmd(path, key),
*GPG.sign_command(path, key),
exception=BuildFailed(path.name),
logger=self.logger)
return [path, path.parent / f"{path.name}.sig"]
@ -91,9 +107,12 @@ class GPG:
:param base: package base required to check for key overrides
:return: list of generated files including original file
"""
if SignSettings.SignPackages not in self.target:
if SignSettings.Packages not in self.targets:
return [path]
key = self.configuration.get("sign", f"key_{base}", fallback=self.default_key)
if key is None:
self.logger.error(f"no default key set, skip package {path} sign")
return [path]
key = self.config.get(self.section, f"key_{base}", fallback=self.default_key)
return self.process(path, key)
def sign_repository(self, path: Path) -> List[Path]:
@ -103,6 +122,9 @@ class GPG:
:param path: path to repository database
:return: list of generated files including original file
"""
if SignSettings.SignRepository not in self.target:
if SignSettings.Repository not in self.targets:
return [path]
if self.default_key is None:
self.logger.error("no default key set, skip repository sign")
return [path]
return self.process(path, self.default_key)

View File

@ -19,7 +19,7 @@
#
from __future__ import annotations
from typing import List, Optional, Tuple
from typing import List, Optional, Tuple, Type
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
@ -31,6 +31,20 @@ class Client:
base build status reporter client
"""
@classmethod
def load(cls: Type[Client], configuration: Configuration) -> Client:
"""
load client from settings
:param configuration: configuration instance
:return: client according to current settings
"""
host = configuration.get("web", "host", fallback=None)
port = configuration.getint("web", "port", fallback=None)
if host is not None and port is not None:
from ahriman.core.status.web_client import WebClient
return WebClient(host, port)
return cls()
def add(self, package: Package, status: BuildStatusEnum) -> None:
"""
add new package with status
@ -109,20 +123,3 @@ class Client:
:param package: current package properties
"""
return self.add(package, BuildStatusEnum.Unknown)
@staticmethod
def load(architecture: str, config: Configuration) -> Client:
"""
load client from settings
:param architecture: repository architecture
:param config: configuration instance
:return: client according to current settings
"""
section = config.get_section_name("web", architecture)
host = config.get(section, "host", fallback=None)
port = config.getint(section, "port", fallback=None)
if host is None or port is None:
return Client()
from ahriman.core.status.web_client import WebClient
return WebClient(host, port)

View File

@ -40,16 +40,16 @@ class Watcher:
:ivar status: daemon status
"""
def __init__(self, architecture: str, config: Configuration) -> None:
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
self.logger = logging.getLogger("http")
self.architecture = architecture
self.repository = Repository(architecture, config)
self.repository = Repository(architecture, configuration)
self.known: Dict[str, Tuple[Package, BuildStatus]] = {}
self.status = BuildStatus()

View File

@ -45,6 +45,16 @@ class WebClient(Client):
self.host = host
self.port = port
@staticmethod
def _exception_response_text(exception: requests.exceptions.HTTPError) -> str:
"""
safe response exception text generation
:param exception: exception raised
:return: text of the response if it is not None and empty string otherwise
"""
result: str = exception.response.text if exception.response is not None else ""
return result
def _ahriman_url(self) -> str:
"""
url generator
@ -75,7 +85,7 @@ class WebClient(Client):
response = requests.post(self._package_url(package.base), json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
self.logger.exception(f"could not add {package.base}: {e.response.text}")
self.logger.exception(f"could not add {package.base}: {WebClient._exception_response_text(e)}")
except Exception:
self.logger.exception(f"could not add {package.base}")
@ -95,7 +105,7 @@ class WebClient(Client):
for package in status_json
]
except requests.exceptions.HTTPError as e:
self.logger.exception(f"could not get {base}: {e.response.text}")
self.logger.exception(f"could not get {base}: {WebClient._exception_response_text(e)}")
except Exception:
self.logger.exception(f"could not get {base}")
return []
@ -112,7 +122,7 @@ class WebClient(Client):
status_json = response.json()
return BuildStatus.from_json(status_json)
except requests.exceptions.HTTPError as e:
self.logger.exception(f"could not get service status: {e.response.text}")
self.logger.exception(f"could not get service status: {WebClient._exception_response_text(e)}")
except Exception:
self.logger.exception("could not get service status")
return BuildStatus()
@ -126,7 +136,7 @@ class WebClient(Client):
response = requests.delete(self._package_url(base))
response.raise_for_status()
except requests.exceptions.HTTPError as e:
self.logger.exception(f"could not delete {base}: {e.response.text}")
self.logger.exception(f"could not delete {base}: {WebClient._exception_response_text(e)}")
except Exception:
self.logger.exception(f"could not delete {base}")
@ -142,7 +152,7 @@ class WebClient(Client):
response = requests.post(self._package_url(base), json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
self.logger.exception(f"could not update {base}: {e.response.text}")
self.logger.exception(f"could not update {base}: {WebClient._exception_response_text(e)}")
except Exception:
self.logger.exception(f"could not update {base}")
@ -157,6 +167,6 @@ class WebClient(Client):
response = requests.post(self._ahriman_url(), json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
self.logger.exception(f"could not update service status: {e.response.text}")
self.logger.exception(f"could not update service status: {WebClient._exception_response_text(e)}")
except Exception:
self.logger.exception("could not update service status")

View File

@ -20,41 +20,32 @@
from pathlib import Path
from ahriman.core.configuration import Configuration
from ahriman.core.upload.uploader import Uploader
from ahriman.core.upload.upload import Upload
from ahriman.core.util import check_output
class Rsync(Uploader):
class Rsync(Upload):
"""
rsync wrapper
:ivar command: command arguments for sync
:ivar remote: remote address to sync
"""
_check_output = check_output
def __init__(self, architecture: str, config: Configuration) -> None:
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
Uploader.__init__(self, architecture, config)
section = config.get_section_name("rsync", architecture)
self.remote = config.get(section, "remote")
Upload.__init__(self, architecture, configuration)
self.command = configuration.getlist("rsync", "command")
self.remote = configuration.get("rsync", "remote")
def sync(self, path: Path) -> None:
"""
sync data to remote server
:param path: local path to sync
"""
Rsync._check_output(
"rsync",
"--archive",
"--verbose",
"--compress",
"--partial",
"--delete",
str(path),
self.remote,
exception=None,
logger=self.logger)
Rsync._check_output(*self.command, str(path), self.remote, exception=None, logger=self.logger)

View File

@ -20,27 +20,28 @@
from pathlib import Path
from ahriman.core.configuration import Configuration
from ahriman.core.upload.uploader import Uploader
from ahriman.core.upload.upload import Upload
from ahriman.core.util import check_output
class S3(Uploader):
class S3(Upload):
"""
aws-cli wrapper
:ivar bucket: full bucket name
:ivar command: command arguments for sync
"""
_check_output = check_output
def __init__(self, architecture: str, config: Configuration) -> None:
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
Uploader.__init__(self, architecture, config)
section = config.get_section_name("s3", architecture)
self.bucket = config.get(section, "bucket")
Upload.__init__(self, architecture, configuration)
self.bucket = configuration.get("s3", "bucket")
self.command = configuration.getlist("s3", "command")
def sync(self, path: Path) -> None:
"""
@ -48,6 +49,4 @@ class S3(Uploader):
:param path: local path to sync
"""
# TODO rewrite to boto, but it is bullshit
S3._check_output("aws", "s3", "sync", "--quiet", "--delete", str(path), self.bucket,
exception=None,
logger=self.logger)
S3._check_output(*self.command, str(path), self.bucket, exception=None, logger=self.logger)

View File

@ -17,56 +17,63 @@
# 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 __future__ import annotations
import logging
from pathlib import Path
from typing import Type
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import SyncFailed
from ahriman.models.upload_settings import UploadSettings
class Uploader:
class Upload:
"""
base remote sync class
:ivar architecture: repository architecture
:ivar config: configuration instance
:ivar configuration: configuration instance
:ivar logger: application logger
"""
def __init__(self, architecture: str, config: Configuration) -> None:
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
"""
self.logger = logging.getLogger("builder")
self.architecture = architecture
self.config = config
self.config = configuration
@staticmethod
def run(architecture: str, config: Configuration, target: str, path: Path) -> None:
@classmethod
def load(cls: Type[Upload], architecture: str, configuration: Configuration, target: str) -> Upload:
"""
run remote sync
load client from settings
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
:param target: target to run sync (e.g. s3)
:param path: local path to sync
:return: client according to current settings
"""
provider = UploadSettings.from_option(target)
if provider == UploadSettings.Rsync:
from ahriman.core.upload.rsync import Rsync
uploader: Uploader = Rsync(architecture, config)
elif provider == UploadSettings.S3:
return Rsync(architecture, configuration)
if provider == UploadSettings.S3:
from ahriman.core.upload.s3 import S3
uploader = S3(architecture, config)
else:
uploader = Uploader(architecture, config)
return S3(architecture, configuration)
return cls(architecture, configuration) # should never happen
def run(self, path: Path) -> None:
"""
run remote sync
:param path: local path to sync
"""
try:
uploader.sync(path)
self.sync(path)
except Exception:
uploader.logger.exception(f"remote sync failed for {provider.name}")
self.logger.exception("remote sync failed")
raise SyncFailed()
def sync(self, path: Path) -> None:

View File

@ -89,6 +89,6 @@ def pretty_size(size: Optional[float], level: int = 0) -> str:
if size is None:
return ""
if size < 1024 or level == 3:
if size < 1024 or level >= 3:
return f"{size:.1f} {str_level()}"
return pretty_size(size / 1024, level + 1)

View File

@ -63,7 +63,7 @@ class BuildStatus:
"""
build status holder
:ivar status: build status
:ivar _timestamp: build status update time
:ivar timestamp: build status update time
"""
def __init__(self, status: Union[BuildStatusEnum, str, None] = None,

View File

@ -31,7 +31,7 @@ from typing import Any, Dict, List, Optional, Set, Type, Union
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.core.util import check_output
from ahriman.models.package_desciption import PackageDescription
from ahriman.models.package_description import PackageDescription
from ahriman.models.repository_paths import RepositoryPaths
@ -52,6 +52,13 @@ class Package:
_check_output = check_output
@property
def depends(self) -> List[str]:
"""
:return: sum of dependencies per arch package
"""
return sorted(set(sum([package.depends for package in self.packages.values()], start=[])))
@property
def git_url(self) -> str:
"""
@ -147,7 +154,7 @@ class Package:
:return: package properties
"""
packages = {
key: PackageDescription(**value)
key: PackageDescription.from_json(value)
for key, value in dump.get("packages", {}).items()
}
return Package(
@ -156,6 +163,27 @@ class Package:
aur_url=dump["aur_url"],
packages=packages)
@classmethod
def load(cls: Type[Package], path: Union[Path, str], pacman: Pacman, aur_url: str) -> Package:
"""
package constructor from available sources
:param path: one of path to sources directory, path to archive or package name/base
:param pacman: alpm wrapper instance (required to load from archive)
:param aur_url: AUR root url
:return: package properties
"""
try:
maybe_path = Path(path)
if maybe_path.is_dir():
return cls.from_build(maybe_path, aur_url)
if maybe_path.is_file():
return cls.from_archive(maybe_path, pacman, aur_url)
return cls.from_aur(str(path), aur_url)
except InvalidPackageInfo:
raise
except Exception as e:
raise InvalidPackageInfo(str(e))
@staticmethod
def dependencies(path: Path) -> Set[str]:
"""
@ -194,29 +222,6 @@ class Package:
prefix = f"{epoch}:" if epoch else ""
return f"{prefix}{pkgver}-{pkgrel}"
@staticmethod
def load(path: Union[Path, str], pacman: Pacman, aur_url: str) -> Package:
"""
package constructor from available sources
:param path: one of path to sources directory, path to archive or package name/base
:param pacman: alpm wrapper instance (required to load from archive)
:param aur_url: AUR root url
:return: package properties
"""
try:
maybe_path = Path(path)
if maybe_path.is_dir():
package: Package = Package.from_build(maybe_path, aur_url)
elif maybe_path.is_file():
package = Package.from_archive(maybe_path, pacman, aur_url)
else:
package = Package.from_aur(str(path), aur_url)
return package
except InvalidPackageInfo:
raise
except Exception as e:
raise InvalidPackageInfo(str(e))
def actual_version(self, paths: RepositoryPaths) -> str:
"""
additional method to handle VCS package versions

View File

@ -19,10 +19,10 @@
#
from __future__ import annotations
from dataclasses import dataclass, field
from dataclasses import dataclass, field, fields
from pathlib import Path
from pyalpm import Package # type: ignore
from typing import List, Optional, Type
from typing import Any, Dict, List, Optional, Type
@dataclass
@ -32,6 +32,7 @@ class PackageDescription:
:ivar architecture: package architecture
:ivar archive_size: package archive size
:ivar build_date: package build date
:ivar depends: package dependencies list
:ivar description: package description
:ivar filename: package archive name
:ivar groups: package groups
@ -43,6 +44,7 @@ class PackageDescription:
architecture: Optional[str] = None
archive_size: Optional[int] = None
build_date: Optional[int] = None
depends: List[str] = field(default_factory=list)
description: Optional[str] = None
filename: Optional[str] = None
groups: List[str] = field(default_factory=list)
@ -57,6 +59,18 @@ class PackageDescription:
"""
return Path(self.filename) if self.filename is not None else None
@classmethod
def from_json(cls: Type[PackageDescription], dump: Dict[str, Any]) -> PackageDescription:
"""
construct package properties from json dump
:param dump: json dump body
:return: package properties
"""
# filter to only known fields
known_fields = [pair.name for pair in fields(cls)]
dump = {key: value for key, value in dump.items() if key in known_fields}
return cls(**dump)
@classmethod
def from_package(cls: Type[PackageDescription], package: Package, path: Path) -> PackageDescription:
"""
@ -65,10 +79,11 @@ class PackageDescription:
:param path: path to package archive
:return: package properties based on tarball
"""
return PackageDescription(
return cls(
architecture=package.arch,
archive_size=package.size,
build_date=package.builddate,
depends=package.depends,
description=package.desc,
filename=path.name,
groups=package.groups,

View File

@ -20,6 +20,7 @@
from __future__ import annotations
from enum import Enum, auto
from typing import Type
from ahriman.core.exceptions import InvalidOption
@ -30,15 +31,16 @@ class ReportSettings(Enum):
:cvar HTML: html report generation
"""
Disabled = auto() # for testing purpose
HTML = auto()
@staticmethod
def from_option(value: str) -> ReportSettings:
@classmethod
def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings:
"""
construct value from configuration
:param value: configuration value
:return: parsed value
"""
if value.lower() in ("html",):
return ReportSettings.HTML
return cls.HTML
raise InvalidOption(value)

View File

@ -20,6 +20,7 @@
from __future__ import annotations
from enum import Enum, auto
from typing import Type
from ahriman.core.exceptions import InvalidOption
@ -27,22 +28,22 @@ from ahriman.core.exceptions import InvalidOption
class SignSettings(Enum):
"""
sign targets enumeration
:cvar SignPackages: sign each package
:cvar SignRepository: sign repository database file
:cvar Packages: sign each package
:cvar Repository: sign repository database file
"""
SignPackages = auto()
SignRepository = auto()
Packages = auto()
Repository = auto()
@staticmethod
def from_option(value: str) -> SignSettings:
@classmethod
def from_option(cls: Type[SignSettings], value: str) -> SignSettings:
"""
construct value from configuration
:param value: configuration value
:return: parsed value
"""
if value.lower() in ("package", "packages", "sign-package"):
return SignSettings.SignPackages
return cls.Packages
if value.lower() in ("repository", "sign-repository"):
return SignSettings.SignRepository
return cls.Repository
raise InvalidOption(value)

View File

@ -20,6 +20,7 @@
from __future__ import annotations
from enum import Enum, auto
from typing import Type
from ahriman.core.exceptions import InvalidOption
@ -31,18 +32,19 @@ class UploadSettings(Enum):
:cvar S3: sync to Amazon S3
"""
Disabled = auto() # for testing purpose
Rsync = auto()
S3 = auto()
@staticmethod
def from_option(value: str) -> UploadSettings:
@classmethod
def from_option(cls: Type[UploadSettings], value: str) -> UploadSettings:
"""
construct value from configuration
:param value: configuration value
:return: parsed value
"""
if value.lower() in ("rsync",):
return UploadSettings.Rsync
return cls.Rsync
if value.lower() in ("s3",):
return UploadSettings.S3
return cls.S3
raise InvalidOption(value)

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__version__ = "0.18.0"
__version__ = "0.21.4"

View File

@ -36,6 +36,7 @@ class IndexView(BaseView):
architecture - repository architecture, string, required
packages - sorted list of packages properties, required
* base, string
* depends, sorted list of strings
* groups, sorted list of strings
* licenses, sorted list of strings
* packages, sorted list of strings
@ -61,6 +62,7 @@ class IndexView(BaseView):
packages = [
{
"base": package.base,
"depends": package.depends,
"groups": package.groups,
"licenses": package.licenses,
"packages": list(sorted(package.packages)),

View File

@ -58,19 +58,19 @@ def run_server(application: web.Application) -> None:
"""
application.logger.info("start server")
section = application["config"].get_section_name("web", application["architecture"])
host = application["config"].get(section, "host")
port = application["config"].getint(section, "port")
configuration: Configuration = application["configuration"]
host = configuration.get("web", "host")
port = configuration.getint("web", "port")
web.run_app(application, host=host, port=port, handle_signals=False,
access_log=logging.getLogger("http"))
def setup_service(architecture: str, config: Configuration) -> web.Application:
def setup_service(architecture: str, configuration: Configuration) -> web.Application:
"""
create web application
:param architecture: repository architecture
:param config: configuration instance
:param configuration: configuration instance
:return: web application instance
"""
application = web.Application(logger=logging.getLogger("http"))
@ -84,13 +84,12 @@ def setup_service(architecture: str, config: Configuration) -> web.Application:
setup_routes(application)
application.logger.info("setup templates")
aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader(config.getpath("web", "templates")))
aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader(configuration.getpath("web", "templates")))
application.logger.info("setup configuration")
application["config"] = config
application["architecture"] = architecture
application["configuration"] = configuration
application.logger.info("setup watcher")
application["watcher"] = Watcher(architecture, config)
application["watcher"] = Watcher(architecture, configuration)
return application

View File

@ -1,27 +1,51 @@
import argparse
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
def test_call(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
def test_call(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
must call inside lock
"""
args.configuration = Path("")
args.no_log = False
mocker.patch("ahriman.application.handlers.Handler.run")
mocker.patch("ahriman.core.configuration.Configuration.from_path")
enter_mock = mocker.patch("ahriman.application.lock.Lock.__enter__")
exit_mock = mocker.patch("ahriman.application.lock.Lock.__exit__")
assert Handler._call(args, "x86_64", configuration)
assert Handler._call(args, "x86_64")
enter_mock.assert_called_once()
exit_mock.assert_called_once()
def test_call_exception(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
def test_call_exception(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
must process exception
"""
mocker.patch("ahriman.application.lock.Lock.__enter__", side_effect=Exception())
assert not Handler._call(args, "x86_64", configuration)
assert not Handler._call(args, "x86_64")
def test_execute(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
must run execution in multiple processes
"""
args.architecture = ["i686", "x86_64"]
starmap_mock = mocker.patch("multiprocessing.pool.Pool.starmap")
Handler.execute(args)
starmap_mock.assert_called_once()
def test_packages(args: argparse.Namespace, configuration: Configuration) -> None:
"""
must raise NotImplemented for missing method
"""
with pytest.raises(NotImplementedError):
Handler.run(args, "x86_64", configuration)

View File

@ -8,6 +8,7 @@ from ahriman.core.configuration import Configuration
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.package = []
args.now = False
args.without_dependencies = False
return args
@ -22,3 +23,19 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
Add.run(args, "x86_64", configuration)
application_mock.assert_called_once()
def test_run_with_updates(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command with updates after
"""
args = _default_args(args)
args.now = True
mocker.patch("pathlib.Path.mkdir")
mocker.patch("ahriman.application.application.Application.add")
application_mock = mocker.patch("ahriman.application.application.Application.update")
updates_mock = mocker.patch("ahriman.application.application.Application.get_updates")
Add.run(args, "x86_64", configuration)
application_mock.assert_called_once()
updates_mock.assert_called_once()

View File

@ -11,7 +11,10 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
must run command
"""
mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.core.configuration.Configuration.dump")
print_mock = mocker.patch("ahriman.application.handlers.dump.Dump._print")
application_mock = mocker.patch("ahriman.core.configuration.Configuration.dump",
return_value=configuration.dump())
Dump.run(args, "x86_64", configuration)
application_mock.assert_called_once()
print_mock.assert_called()

View File

@ -4,12 +4,19 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Rebuild
from ahriman.core.configuration import Configuration
from ahriman.models.package import Package
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.depends_on = None
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("pathlib.Path.mkdir")
application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages")
application_mock = mocker.patch("ahriman.application.application.Application.update")
@ -17,3 +24,36 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
Rebuild.run(args, "x86_64", configuration)
application_packages_mock.assert_called_once()
application_mock.assert_called_once()
def test_run_filter(args: argparse.Namespace, configuration: Configuration,
package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must run command with depends filter
"""
args = _default_args(args)
args.depends_on = "python-aur"
mocker.patch("pathlib.Path.mkdir")
mocker.patch("ahriman.core.repository.repository.Repository.packages",
return_value=[package_ahriman, package_python_schedule])
application_mock = mocker.patch("ahriman.application.application.Application.update")
Rebuild.run(args, "x86_64", configuration)
application_mock.assert_called_with([package_ahriman])
def test_run_without_filter(args: argparse.Namespace, configuration: Configuration,
package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must run command for all packages if no filter supplied
"""
args = _default_args(args)
mocker.patch("pathlib.Path.mkdir")
mocker.patch("ahriman.core.repository.repository.Repository.packages",
return_value=[package_ahriman, package_python_schedule])
application_mock = mocker.patch("ahriman.application.application.Application.update")
Rebuild.run(args, "x86_64", configuration)
application_mock.assert_called_with([package_ahriman, package_python_schedule])

View File

@ -7,14 +7,18 @@ from unittest import mock
from ahriman.application.handlers import Setup
from ahriman.core.configuration import Configuration
from ahriman.models.repository_paths import RepositoryPaths
from ahriman.models.sign_settings import SignSettings
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.build_command = "ahriman"
args.from_config = "/usr/share/devtools/pacman-extra.conf"
args.from_configuration = Path("/usr/share/devtools/pacman-extra.conf")
args.no_multilib = False
args.packager = "John Doe <john@doe.com>"
args.repository = "aur-clone"
args.sign_key = "key"
args.sign_target = [SignSettings.Packages]
args.web_port = 8080
return args
@ -58,14 +62,20 @@ def test_create_ahriman_configuration(args: argparse.Namespace, configuration: C
write_mock = mocker.patch("configparser.RawConfigParser.write")
command = Setup.build_command(args.build_command, "x86_64")
Setup.create_ahriman_configuration(args.build_command, "x86_64", args.repository, configuration.include)
Setup.create_ahriman_configuration(args, "x86_64", args.repository, configuration.include)
add_section_mock.assert_has_calls([
mock.call("build"),
mock.call(Configuration.section_name("build", "x86_64")),
mock.call("repository"),
mock.call(Configuration.section_name("sign", "x86_64")),
mock.call(Configuration.section_name("web", "x86_64")),
])
set_mock.assert_has_calls([
mock.call("build", "build_command", str(command)),
mock.call(Configuration.section_name("build", "x86_64"), "build_command", str(command)),
mock.call("repository", "name", args.repository),
mock.call(Configuration.section_name("sign", "x86_64"), "target",
" ".join([target.name.lower() for target in args.sign_target])),
mock.call(Configuration.section_name("sign", "x86_64"), "key", args.sign_key),
mock.call(Configuration.section_name("web", "x86_64"), "port", str(args.web_port)),
])
write_mock.assert_called_once()
@ -81,8 +91,8 @@ def test_create_devtools_configuration(args: argparse.Namespace, repository_path
add_section_mock = mocker.patch("configparser.RawConfigParser.add_section")
write_mock = mocker.patch("configparser.RawConfigParser.write")
Setup.create_devtools_configuration(args.build_command, "x86_64", Path(args.from_config), args.no_multilib,
args.repository, repository_paths)
Setup.create_devtools_configuration(args.build_command, "x86_64", args.from_configuration,
args.no_multilib, args.repository, repository_paths)
add_section_mock.assert_has_calls([
mock.call("multilib"),
mock.call(args.repository)
@ -101,8 +111,8 @@ def test_create_devtools_configuration_no_multilib(args: argparse.Namespace, rep
add_section_mock = mocker.patch("configparser.RawConfigParser.add_section")
write_mock = mocker.patch("configparser.RawConfigParser.write")
Setup.create_devtools_configuration(args.build_command, "x86_64", Path(args.from_config), True,
args.repository, repository_paths)
Setup.create_devtools_configuration(args.build_command, "x86_64", args.from_configuration,
True, args.repository, repository_paths)
add_section_mock.assert_called_once()
write_mock.assert_called_once()

View File

@ -4,6 +4,8 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Status
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
@ -12,15 +14,32 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
def test_run(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.core.status.client.Client.get_self")
packages_mock = mocker.patch("ahriman.core.status.client.Client.get")
packages_mock = mocker.patch("ahriman.core.status.client.Client.get",
return_value=[(package_ahriman, BuildStatus())])
Status.run(args, "x86_64", configuration)
application_mock.assert_called_once()
packages_mock.assert_called_once()
def test_run_with_package_filter(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
args.package = [package_ahriman.base]
mocker.patch("pathlib.Path.mkdir")
packages_mock = mocker.patch("ahriman.core.status.client.Client.get",
return_value=[(package_ahriman, BuildStatus())])
Status.run(args, "x86_64", configuration)
packages_mock.assert_called_once()

View File

@ -9,8 +9,9 @@ from ahriman.models.package import Package
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.status = BuildStatusEnum.Success.value
args.status = BuildStatusEnum.Success
args.package = None
args.remove = False
return args
@ -38,3 +39,18 @@ def test_run_packages(args: argparse.Namespace, configuration: Configuration, pa
StatusUpdate.run(args, "x86_64", configuration)
update_mock.assert_called_once()
def test_run_remove(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must remove package from status page
"""
args = _default_args(args)
args.package = [package_ahriman.base]
args.remove = True
mocker.patch("pathlib.Path.mkdir")
update_mock = mocker.patch("ahriman.core.status.client.Client.remove")
StatusUpdate.run(args, "x86_64", configuration)
update_mock.assert_called_once()

View File

@ -2,6 +2,7 @@ import argparse
from pytest_mock import MockerFixture
from ahriman.application.application import Application
from ahriman.application.handlers import Update
from ahriman.core.configuration import Configuration
@ -40,3 +41,12 @@ def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, moc
Update.run(args, "x86_64", configuration)
updates_mock.assert_called_once()
def test_log_fn(application: Application, mocker: MockerFixture) -> None:
"""
must print package updates
"""
logger_mock = mocker.patch("logging.Logger.info")
Update.log_fn(application, False)("hello")
logger_mock.assert_called_once()

View File

@ -1,5 +1,12 @@
import argparse
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.application.handlers import Handler
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.sign_settings import SignSettings
def test_parser(parser: argparse.ArgumentParser) -> None:
"""
@ -8,6 +15,26 @@ def test_parser(parser: argparse.ArgumentParser) -> None:
parser.parse_args(["-a", "x86_64", "config"])
def test_parser_option_configuration(parser: argparse.ArgumentParser) -> None:
"""
must convert configuration option to Path instance
"""
args = parser.parse_args(["-a", "x86_64", "config"])
assert isinstance(args.configuration, Path)
args = parser.parse_args(["-a", "x86_64", "-c", "ahriman.ini", "config"])
assert isinstance(args.configuration, Path)
def test_parser_option_lock(parser: argparse.ArgumentParser) -> None:
"""
must convert lock option to Path instance
"""
args = parser.parse_args(["-a", "x86_64", "update"])
assert isinstance(args.lock, Path)
args = parser.parse_args(["-a", "x86_64", "-l", "ahriman.lock", "update"])
assert isinstance(args.lock, Path)
def test_multiple_architectures(parser: argparse.ArgumentParser) -> None:
"""
must accept multiple architectures
@ -48,12 +75,35 @@ def test_subparsers_setup(parser: argparse.ArgumentParser) -> None:
"""
setup command must imply lock, no_report and unsafe
"""
args = parser.parse_args(["-a", "x86_64", "setup", "--packager", "John Doe <john@doe.com>"])
args = parser.parse_args(["-a", "x86_64", "setup", "--packager", "John Doe <john@doe.com>",
"--repository", "aur-clone"])
assert args.lock is None
assert args.no_report
assert args.unsafe
def test_subparsers_setup_option_from_configuration(parser: argparse.ArgumentParser) -> None:
"""
setup command must convert from-configuration option to path instance
"""
args = parser.parse_args(["-a", "x86_64", "setup", "--packager", "John Doe <john@doe.com>",
"--repository", "aur-clone"])
assert isinstance(args.from_configuration, Path)
args = parser.parse_args(["-a", "x86_64", "setup", "--packager", "John Doe <john@doe.com>",
"--repository", "aur-clone", "--from-configuration", "path"])
assert isinstance(args.from_configuration, Path)
def test_subparsers_setup_option_sign_target(parser: argparse.ArgumentParser) -> None:
"""
setup command must convert sign-target option to signsettings instance
"""
args = parser.parse_args(["-a", "x86_64", "setup", "--packager", "John Doe <john@doe.com>",
"--repository", "aur-clone", "--sign-target", "packages"])
assert args.sign_target
assert all(isinstance(target, SignSettings) for target in args.sign_target)
def test_subparsers_status(parser: argparse.ArgumentParser) -> None:
"""
status command must imply lock, no_report and unsafe
@ -74,6 +124,16 @@ def test_subparsers_status_update(parser: argparse.ArgumentParser) -> None:
assert args.unsafe
def test_subparsers_status_update_option_status(parser: argparse.ArgumentParser) -> None:
"""
status-update command must convert status option to buildstatusenum instance
"""
args = parser.parse_args(["-a", "x86_64", "status-update"])
assert isinstance(args.status, BuildStatusEnum)
args = parser.parse_args(["-a", "x86_64", "status-update", "--status", "failed"])
assert isinstance(args.status, BuildStatusEnum)
def test_subparsers_web(parser: argparse.ArgumentParser) -> None:
"""
web command must imply lock and no_report
@ -81,3 +141,19 @@ def test_subparsers_web(parser: argparse.ArgumentParser) -> None:
args = parser.parse_args(["-a", "x86_64", "web"])
assert args.lock is None
assert args.no_report
def test_run(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
application must be run
"""
args.architecture = "x86_64"
args.handler = Handler
from ahriman.application import ahriman
mocker.patch.object(ahriman, "__name__", "__main__")
mocker.patch("argparse.ArgumentParser.parse_args", return_value=args)
exit_mock = mocker.patch("sys.exit")
ahriman.run()
exit_mock.assert_called_once()

View File

@ -20,11 +20,22 @@ def test_finalize(application: Application, mocker: MockerFixture) -> None:
sync_mock.assert_called_once()
def test_get_updates_all(application: Application, mocker: MockerFixture) -> None:
def test_known_packages(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return not empty list of known packages
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
packages = application._known_packages()
assert len(packages) > 1
assert package_ahriman.base in packages
def test_get_updates_all(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must get updates for all
"""
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur",
return_value=[package_ahriman])
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
application.get_updates([], no_aur=False, no_manual=False, no_vcs=False, log_fn=print)
@ -233,6 +244,17 @@ def test_sign(application: Application, package_ahriman: Package, package_python
finalize_mock.assert_called_once()
def test_sign_skip(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must skip sign packages with empty filename
"""
package_ahriman.packages[package_ahriman.base].filename = None
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.application.application.Application.update")
application.sign([])
def test_sign_specific(application: Application, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""

View File

@ -15,14 +15,14 @@ def test_enter(lock: Lock, mocker: MockerFixture) -> None:
must process with context manager
"""
check_user_mock = mocker.patch("ahriman.application.lock.Lock.check_user")
remove_mock = mocker.patch("ahriman.application.lock.Lock.remove")
clear_mock = mocker.patch("ahriman.application.lock.Lock.clear")
create_mock = mocker.patch("ahriman.application.lock.Lock.create")
update_status_mock = mocker.patch("ahriman.core.status.client.Client.update_self")
with lock:
pass
check_user_mock.assert_called_once()
remove_mock.assert_called_once()
clear_mock.assert_called_once()
create_mock.assert_called_once()
update_status_mock.assert_has_calls([
mock.call(BuildStatusEnum.Building),
@ -35,7 +35,7 @@ def test_exit_with_exception(lock: Lock, mocker: MockerFixture) -> None:
must process with context manager in case if exception raised
"""
mocker.patch("ahriman.application.lock.Lock.check_user")
mocker.patch("ahriman.application.lock.Lock.remove")
mocker.patch("ahriman.application.lock.Lock.clear")
mocker.patch("ahriman.application.lock.Lock.create")
update_status_mock = mocker.patch("ahriman.core.status.client.Client.update_self")
@ -79,6 +79,34 @@ def test_check_user_unsafe(lock: Lock) -> None:
lock.check_user()
def test_clear(lock: Lock) -> None:
"""
must remove lock file
"""
lock.path = Path(tempfile.mktemp())
lock.path.touch()
lock.clear()
assert not lock.path.is_file()
def test_clear_missing(lock: Lock) -> None:
"""
must not fail on lock removal if file is missing
"""
lock.path = Path(tempfile.mktemp())
lock.clear()
def test_clear_skip(lock: Lock, mocker: MockerFixture) -> None:
"""
must skip removal if no file set
"""
unlink_mock = mocker.patch("pathlib.Path.unlink")
lock.clear()
unlink_mock.assert_not_called()
def test_create(lock: Lock) -> None:
"""
must create lock
@ -121,31 +149,3 @@ def test_create_unsafe(lock: Lock) -> None:
lock.create()
lock.path.unlink()
def test_remove(lock: Lock) -> None:
"""
must remove lock file
"""
lock.path = Path(tempfile.mktemp())
lock.path.touch()
lock.remove()
assert not lock.path.is_file()
def test_remove_missing(lock: Lock) -> None:
"""
must not fail on lock removal if file is missing
"""
lock.path = Path(tempfile.mktemp())
lock.remove()
def test_remove_skip(lock: Lock, mocker: MockerFixture) -> None:
"""
must skip removal if no file set
"""
unlink_mock = mocker.patch("pathlib.Path.unlink")
lock.remove()
unlink_mock.assert_not_called()

View File

@ -7,7 +7,7 @@ from typing import Any, Type, TypeVar
from ahriman.core.configuration import Configuration
from ahriman.core.status.watcher import Watcher
from ahriman.models.package import Package
from ahriman.models.package_desciption import PackageDescription
from ahriman.models.package_description import PackageDescription
from ahriman.models.repository_paths import RepositoryPaths
T = TypeVar("T")
@ -27,7 +27,7 @@ def anyvar(cls: Type[T], strict: bool = False) -> T:
@pytest.fixture
def configuration(resource_path_root: Path) -> Configuration:
path = resource_path_root / "core" / "ahriman.ini"
return Configuration.from_path(path=path, logfile=False)
return Configuration.from_path(path=path, architecture="x86_64", logfile=False)
@pytest.fixture
@ -61,6 +61,7 @@ def package_description_ahriman() -> PackageDescription:
architecture="x86_64",
archive_size=4200,
build_date=42,
depends=["devtools", "git", "pyalpm", "python-aur", "python-srcinfo"],
description="ArcHlinux ReposItory MANager",
filename="ahriman-0.12.1-1-any.pkg.tar.zst",
groups=[],
@ -75,6 +76,7 @@ def package_description_python_schedule() -> PackageDescription:
architecture="x86_64",
archive_size=4201,
build_date=421,
depends=["python"],
description="Python job scheduling for humans.",
filename="python-schedule-1.0.0-2-any.pkg.tar.zst",
groups=[],
@ -89,6 +91,7 @@ def package_description_python2_schedule() -> PackageDescription:
architecture="x86_64",
archive_size=4202,
build_date=422,
depends=["python2"],
description="Python job scheduling for humans.",
filename="python2-schedule-1.0.0-2-any.pkg.tar.zst",
groups=[],

View File

@ -1,5 +1,4 @@
import pytest
import shutil
from pathlib import Path
from pytest_mock import MockerFixture
@ -52,6 +51,15 @@ def test_fetch_new(mocker: MockerFixture) -> None:
])
def test_build(task_ahriman: Task, mocker: MockerFixture) -> None:
"""
must build package
"""
check_output_mock = mocker.patch("ahriman.core.build_tools.task.Task._check_output")
task_ahriman.build()
check_output_mock.assert_called()
def test_init_with_cache(task_ahriman: Task, mocker: MockerFixture) -> None:
"""
must copy tree instead of fetch

View File

@ -31,4 +31,4 @@ def repo(configuration: Configuration, repository_paths: RepositoryPaths) -> Rep
@pytest.fixture
def task_ahriman(package_ahriman: Package, configuration: Configuration, repository_paths: RepositoryPaths) -> Task:
return Task(package_ahriman, "x86_64", configuration, repository_paths)
return Task(package_ahriman, configuration, repository_paths)

View File

@ -0,0 +1,16 @@
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.report.html import HTML
from ahriman.models.package import Package
def test_generate(configuration: Configuration, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must generate report
"""
write_mock = mocker.patch("pathlib.Path.write_text")
report = HTML("x86_64", configuration)
report.generate([package_ahriman])
write_mock.assert_called_once()

View File

@ -0,0 +1,37 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ReportFailed
from ahriman.core.report.report import Report
from ahriman.models.report_settings import ReportSettings
def test_report_failure(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must raise ReportFailed on errors
"""
mocker.patch("ahriman.core.report.html.HTML.generate", side_effect=Exception())
with pytest.raises(ReportFailed):
Report.load("x86_64", configuration, ReportSettings.HTML.name).run(Path("path"))
def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must construct dummy report class
"""
mocker.patch("ahriman.models.report_settings.ReportSettings.from_option", return_value=ReportSettings.Disabled)
report_mock = mocker.patch("ahriman.core.report.report.Report.generate")
Report.load("x86_64", configuration, ReportSettings.Disabled.name).run(Path("path"))
report_mock.assert_called_once()
def test_report_html(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must generate html report
"""
report_mock = mocker.patch("ahriman.core.report.html.HTML.generate")
Report.load("x86_64", configuration, ReportSettings.HTML.name).run(Path("path"))
report_mock.assert_called_once()

View File

@ -1,3 +1,4 @@
import pytest
import shutil
from pathlib import Path
@ -20,6 +21,14 @@ def _mock_clear_check() -> None:
])
def test_packages_built(cleaner: Cleaner) -> None:
"""
must raise NotImplemented for missing method
"""
with pytest.raises(NotImplementedError):
cleaner.packages_built()
def test_clear_build(cleaner: Cleaner, mocker: MockerFixture) -> None:
"""
must remove directories with sources

View File

@ -1,23 +1,34 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.core.report.report import Report
from ahriman.core.repository.executor import Executor
from ahriman.core.upload.upload import Upload
from ahriman.models.package import Package
def test_packages(executor: Executor) -> None:
"""
must raise NotImplemented for missing method
"""
with pytest.raises(NotImplementedError):
executor.packages()
def test_process_build(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run build process
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages_built", return_value=[package_ahriman])
mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)])
mocker.patch("ahriman.core.build_tools.task.Task.init")
move_mock = mocker.patch("shutil.move")
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building")
built_packages_mock = mocker.patch("ahriman.core.repository.executor.Executor.packages_built")
# must return list of built packages
assert executor.process_build([package_ahriman]) == [package_ahriman]
executor.process_build([package_ahriman])
# must move files (once)
move_mock.assert_called_once()
# must update status
@ -25,6 +36,8 @@ def test_process_build(executor: Executor, package_ahriman: Package, mocker: Moc
# must clear directory
from ahriman.core.repository.cleaner import Cleaner
Cleaner.clear_build.assert_called_once()
# must return build packages after all
built_packages_mock.assert_called_once()
def test_process_build_failure(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -68,7 +81,7 @@ def test_process_remove_base_multiple(executor: Executor, package_python_schedul
executor.process_remove([package_python_schedule.base])
# must remove via alpm wrapper
repo_remove_mock.assert_has_calls([
mock.call(package, Path(props.filename))
mock.call(package, props.filepath)
for package, props in package_python_schedule.packages.items()
], any_order=True)
# must update status
@ -91,6 +104,15 @@ def test_process_remove_base_single(executor: Executor, package_python_schedule:
status_client_mock.assert_not_called()
def test_process_remove_failed(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress remove errors
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
mocker.patch("ahriman.core.alpm.repo.Repo.remove", side_effect=Exception())
executor.process_remove([package_ahriman.base])
def test_process_remove_nothing(executor: Executor, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
@ -103,24 +125,47 @@ def test_process_remove_nothing(executor: Executor, package_ahriman: Package, pa
repo_remove_mock.assert_not_called()
def test_process_report(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must process report
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
mocker.patch("ahriman.core.report.report.Report.load", return_value=Report("x86_64", executor.configuration))
report_mock = mocker.patch("ahriman.core.report.report.Report.run")
executor.process_report(["dummy"])
report_mock.assert_called_once()
def test_process_report_auto(executor: Executor, mocker: MockerFixture) -> None:
"""
must process report in auto mode if no targets supplied
"""
config_getlist_mock = mocker.patch("ahriman.core.configuration.Configuration.getlist")
configuration_getlist_mock = mocker.patch("ahriman.core.configuration.Configuration.getlist")
executor.process_report(None)
config_getlist_mock.assert_called_once()
configuration_getlist_mock.assert_called_once()
def test_process_sync_auto(executor: Executor, mocker: MockerFixture) -> None:
def test_process_upload(executor: Executor, mocker: MockerFixture) -> None:
"""
must process sync in auto mode if no targets supplied
"""
config_getlist_mock = mocker.patch("ahriman.core.configuration.Configuration.getlist")
mocker.patch("ahriman.core.upload.upload.Upload.load", return_value=Upload("x86_64", executor.configuration))
upload_mock = mocker.patch("ahriman.core.upload.upload.Upload.run")
executor.process_sync(["dummy"])
upload_mock.assert_called_once()
def test_process_upload_auto(executor: Executor, mocker: MockerFixture) -> None:
"""
must process sync in auto mode if no targets supplied
"""
configuration_getlist_mock = mocker.patch("ahriman.core.configuration.Configuration.getlist")
executor.process_sync(None)
config_getlist_mock.assert_called_once()
configuration_getlist_mock.assert_called_once()
def test_process_update(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -134,7 +179,7 @@ def test_process_update(executor: Executor, package_ahriman: Package, mocker: Mo
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success")
# must return complete
assert executor.process_update([Path(package.filename) for package in package_ahriman.packages.values()])
assert executor.process_update([package.filepath for package in package_ahriman.packages.values()])
# must move files (once)
move_mock.assert_called_once()
# must sign package
@ -158,14 +203,23 @@ def test_process_update_group(executor: Executor, package_python_schedule: Packa
repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success")
executor.process_update([Path(package.filename) for package in package_python_schedule.packages.values()])
executor.process_update([package.filepath for package in package_python_schedule.packages.values()])
repo_add_mock.assert_has_calls([
mock.call(executor.paths.repository / package.filename)
mock.call(executor.paths.repository / package.filepath)
for package in package_python_schedule.packages.values()
], any_order=True)
status_client_mock.assert_called_with(package_python_schedule)
def test_process_empty_filename(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must skip update for package which does not have path
"""
package_ahriman.packages[package_ahriman.base].filename = None
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
executor.process_update([package.filepath for package in package_ahriman.packages.values()])
def test_process_update_failed(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must process update for failed package
@ -174,7 +228,7 @@ def test_process_update_failed(executor: Executor, package_ahriman: Package, moc
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_failed")
executor.process_update([Path(package.filename) for package in package_ahriman.packages.values()])
executor.process_update([package.filepath for package in package_ahriman.packages.values()])
status_client_mock.assert_called_once()
@ -185,4 +239,4 @@ def test_process_update_failed_on_load(executor: Executor, package_ahriman: Pack
mocker.patch("shutil.move")
mocker.patch("ahriman.models.package.Package.load", side_effect=Exception())
assert executor.process_update([Path(package.filename) for package in package_ahriman.packages.values()])
assert executor.process_update([package.filepath for package in package_ahriman.packages.values()])

View File

@ -31,3 +31,28 @@ def test_packages(package_ahriman: Package, package_python_schedule: Package,
expected = set(package_ahriman.packages.keys())
expected.update(package_python_schedule.packages.keys())
assert set(archives) == expected
def test_packages_failed(repository: Repository, mocker: MockerFixture) -> None:
"""
must skip packages which cannot be loaded
"""
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.pkg.tar.xz")])
mocker.patch("ahriman.models.package.Package.load", side_effect=Exception())
assert not repository.packages()
def test_packages_not_package(repository: Repository, mocker: MockerFixture) -> None:
"""
must skip not packages from iteration
"""
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.tar.xz")])
assert not repository.packages()
def test_packages_built(repository: Repository, mocker: MockerFixture) -> None:
"""
must return build packages
"""
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.tar.xz"), Path("b.pkg.tar.xz")])
assert repository.packages_built() == [Path("b.pkg.tar.xz")]

View File

@ -1,9 +1,19 @@
import pytest
from pytest_mock import MockerFixture
from ahriman.core.repository.update_handler import UpdateHandler
from ahriman.models.package import Package
def test_packages(update_handler: UpdateHandler) -> None:
"""
must raise NotImplemented for missing method
"""
with pytest.raises(NotImplementedError):
update_handler.packages()
def test_updates_aur(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
@ -50,7 +60,7 @@ def test_updates_aur_ignore(update_handler: UpdateHandler, package_ahriman: Pack
"""
must skip ignore packages
"""
mocker.patch("ahriman.core.configuration.Configuration.getlist", return_value=[package_ahriman.base])
update_handler.ignore_list = [package_ahriman.base]
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
package_load_mock = mocker.patch("ahriman.models.package.Package.load")

View File

@ -7,3 +7,9 @@ from ahriman.core.sign.gpg import GPG
@pytest.fixture
def gpg(configuration: Configuration) -> GPG:
return GPG("x86_64", configuration)
@pytest.fixture
def gpg_with_key(gpg: GPG) -> GPG:
gpg.default_key = "key"
return gpg

View File

@ -1,61 +0,0 @@
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.sign.gpg import GPG
from ahriman.models.sign_settings import SignSettings
def test_repository_sign_args(gpg: GPG) -> None:
"""
must generate correct sign args
"""
gpg.target = {SignSettings.SignRepository}
assert gpg.repository_sign_args
def test_sign_package(gpg: GPG, mocker: MockerFixture) -> None:
"""
must sign package
"""
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.process", return_value=result)
for target in ({SignSettings.SignPackages}, {SignSettings.SignPackages, SignSettings.SignRepository}):
gpg.target = target
assert gpg.sign_package(Path("a"), "a") == result
process_mock.assert_called_once()
def test_sign_package_skip(gpg: GPG, mocker: MockerFixture) -> None:
"""
must not sign package if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.process")
for target in ({}, {SignSettings.SignRepository}):
gpg.target = target
process_mock.assert_not_called()
def test_sign_repository(gpg: GPG, mocker: MockerFixture) -> None:
"""
must sign repository
"""
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.process", return_value=result)
for target in ({SignSettings.SignRepository}, {SignSettings.SignPackages, SignSettings.SignRepository}):
gpg.target = target
assert gpg.sign_repository(Path("a")) == result
process_mock.assert_called_once()
def test_sign_repository_skip(gpg: GPG, mocker: MockerFixture) -> None:
"""
must not sign repository if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.process")
for target in ({}, {SignSettings.SignPackages}):
gpg.target = target
process_mock.assert_not_called()

View File

@ -0,0 +1,199 @@
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.sign.gpg import GPG
from ahriman.models.sign_settings import SignSettings
def test_repository_sign_args_1(gpg_with_key: GPG) -> None:
"""
must generate correct sign args
"""
gpg_with_key.targets = {SignSettings.Repository}
assert gpg_with_key.repository_sign_args
def test_repository_sign_args_2(gpg_with_key: GPG) -> None:
"""
must generate correct sign args
"""
gpg_with_key.targets = {SignSettings.Packages, SignSettings.Repository}
assert gpg_with_key.repository_sign_args
def test_repository_sign_args_skip_1(gpg_with_key: GPG) -> None:
"""
must return empty args if it is not set
"""
gpg_with_key.targets = {}
assert not gpg_with_key.repository_sign_args
def test_repository_sign_args_skip_2(gpg_with_key: GPG) -> None:
"""
must return empty args if it is not set
"""
gpg_with_key.targets = {SignSettings.Packages}
assert not gpg_with_key.repository_sign_args
def test_repository_sign_args_skip_3(gpg: GPG) -> None:
"""
must return empty args if it is not set
"""
gpg.targets = {SignSettings.Repository}
assert not gpg.repository_sign_args
def test_repository_sign_args_skip_4(gpg: GPG) -> None:
"""
must return empty args if it is not set
"""
gpg.targets = {SignSettings.Packages, SignSettings.Repository}
assert not gpg.repository_sign_args
def test_sign_command(gpg_with_key: GPG) -> None:
"""
must generate sign command
"""
assert gpg_with_key.sign_command(Path("a"), gpg_with_key.default_key)
def test_process(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must call process method correctly
"""
result = [Path("a"), Path("a.sig")]
check_output_mock = mocker.patch("ahriman.core.sign.gpg.GPG._check_output")
assert gpg_with_key.process(Path("a"), gpg_with_key.default_key) == result
check_output_mock.assert_called()
def test_sign_package_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must sign package
"""
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process", return_value=result)
gpg_with_key.targets = {SignSettings.Packages}
assert gpg_with_key.sign_package(Path("a"), "a") == result
process_mock.assert_called_once()
def test_sign_package_2(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must sign package
"""
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process", return_value=result)
gpg_with_key.targets = {SignSettings.Packages, SignSettings.Repository}
assert gpg_with_key.sign_package(Path("a"), "a") == result
process_mock.assert_called_once()
def test_sign_package_skip_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must not sign package if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg_with_key.targets = {}
gpg_with_key.sign_package(Path("a"), "a")
process_mock.assert_not_called()
def test_sign_package_skip_2(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must not sign package if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg_with_key.targets = {SignSettings.Repository}
gpg_with_key.sign_package(Path("a"), "a")
process_mock.assert_not_called()
def test_sign_package_skip_3(gpg: GPG, mocker: MockerFixture) -> None:
"""
must not sign package if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.Packages}
gpg.sign_package(Path("a"), "a")
process_mock.assert_not_called()
def test_sign_package_skip_4(gpg: GPG, mocker: MockerFixture) -> None:
"""
must not sign package if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.Packages, SignSettings.Repository}
gpg.sign_package(Path("a"), "a")
process_mock.assert_not_called()
def test_sign_repository_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must sign repository
"""
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process", return_value=result)
gpg_with_key.targets = {SignSettings.Repository}
assert gpg_with_key.sign_repository(Path("a")) == result
process_mock.assert_called_once()
def test_sign_repository_2(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must sign repository
"""
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process", return_value=result)
gpg_with_key.targets = {SignSettings.Packages, SignSettings.Repository}
assert gpg_with_key.sign_repository(Path("a")) == result
process_mock.assert_called_once()
def test_sign_repository_skip_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must not sign repository if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg_with_key.targets = {}
gpg_with_key.sign_repository(Path("a"))
process_mock.assert_not_called()
def test_sign_repository_skip_2(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must not sign repository if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg_with_key.targets = {SignSettings.Packages}
gpg_with_key.sign_repository(Path("a"))
process_mock.assert_not_called()
def test_sign_repository_skip_3(gpg: GPG, mocker: MockerFixture) -> None:
"""
must not sign repository if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.Repository}
gpg.sign_repository(Path("a"))
process_mock.assert_not_called()
def test_sign_repository_skip_4(gpg: GPG, mocker: MockerFixture) -> None:
"""
must not sign repository if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.Packages, SignSettings.Repository}
gpg.sign_repository(Path("a"))
process_mock.assert_not_called()

View File

@ -7,6 +7,22 @@ from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
def test_load_dummy_client(configuration: Configuration) -> None:
"""
must load dummy client if no settings set
"""
assert isinstance(Client.load(configuration), Client)
def test_load_full_client(configuration: Configuration) -> None:
"""
must load full client if no settings set
"""
configuration.set("web", "host", "localhost")
configuration.set("web", "port", "8080")
assert isinstance(Client.load(configuration), WebClient)
def test_add(client: Client, package_ahriman: Package) -> None:
"""
must process package addition without errors
@ -98,19 +114,3 @@ def test_set_unknown(client: Client, package_ahriman: Package, mocker: MockerFix
client.set_unknown(package_ahriman)
add_mock.assert_called_with(package_ahriman, BuildStatusEnum.Unknown)
def test_load_dummy_client(configuration: Configuration) -> None:
"""
must load dummy client if no settings set
"""
assert isinstance(Client.load("x86_64", configuration), Client)
def test_load_full_client(configuration: Configuration) -> None:
"""
must load full client if no settings set
"""
configuration.set("web", "host", "localhost")
configuration.set("web", "port", "8080")
assert isinstance(Client.load("x86_64", configuration), WebClient)

View File

@ -51,6 +51,21 @@ def test_cache_load_no_file(watcher: Watcher, mocker: MockerFixture) -> None:
assert not watcher.known
def test_cache_load_package_load_error(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must not fail on json errors
"""
response = {"packages": [pytest.helpers.get_package_status_extended(package_ahriman)]}
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("pathlib.Path.open")
mocker.patch("ahriman.models.package.Package.from_json", side_effect=Exception())
mocker.patch("json.load", return_value=response)
watcher._cache_load()
assert not watcher.known
def test_cache_load_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must not load unknown package

View File

@ -1,5 +1,6 @@
import json
import pytest
import requests
from pytest_mock import MockerFixture
from requests import Response
@ -44,6 +45,14 @@ def test_add_failed(web_client: WebClient, package_ahriman: Package, mocker: Moc
web_client.add(package_ahriman, BuildStatusEnum.Unknown)
def test_add_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during addition
"""
mocker.patch("requests.post", side_effect=requests.exceptions.HTTPError())
web_client.add(package_ahriman, BuildStatusEnum.Unknown)
def test_get_all(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return all packages status
@ -69,6 +78,14 @@ def test_get_failed(web_client: WebClient, mocker: MockerFixture) -> None:
assert web_client.get(None) == []
def test_get_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during status getting
"""
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError())
assert web_client.get(None) == []
def test_get_single(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return single package status
@ -109,6 +126,14 @@ def test_get_self_failed(web_client: WebClient, mocker: MockerFixture) -> None:
assert web_client.get_self().status == BuildStatusEnum.Unknown
def test_get_self_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during service status getting
"""
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError())
assert web_client.get_self().status == BuildStatusEnum.Unknown
def test_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must process package removal
@ -127,6 +152,14 @@ def test_remove_failed(web_client: WebClient, package_ahriman: Package, mocker:
web_client.remove(package_ahriman.base)
def test_remove_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during removal
"""
mocker.patch("requests.delete", side_effect=requests.exceptions.HTTPError())
web_client.remove(package_ahriman.base)
def test_update(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must process package update
@ -145,6 +178,14 @@ def test_update_failed(web_client: WebClient, package_ahriman: Package, mocker:
web_client.update(package_ahriman.base, BuildStatusEnum.Unknown)
def test_update_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during update
"""
mocker.patch("requests.post", side_effect=requests.exceptions.HTTPError())
web_client.update(package_ahriman.base, BuildStatusEnum.Unknown)
def test_update_self(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must process service update
@ -161,3 +202,11 @@ def test_update_self_failed(web_client: WebClient, mocker: MockerFixture) -> Non
"""
mocker.patch("requests.post", side_effect=Exception())
web_client.update_self(BuildStatusEnum.Unknown)
def test_update_self_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during service update
"""
mocker.patch("requests.post", side_effect=requests.exceptions.HTTPError())
web_client.update_self(BuildStatusEnum.Unknown)

View File

@ -14,13 +14,20 @@ def test_from_path(mocker: MockerFixture) -> None:
load_logging_mock = mocker.patch("ahriman.core.configuration.Configuration.load_logging")
path = Path("path")
config = Configuration.from_path(path, True)
assert config.path == path
configuration = Configuration.from_path(path, "x86_64", True)
assert configuration.path == path
read_mock.assert_called_with(path)
load_includes_mock.assert_called_once()
load_logging_mock.assert_called_once()
def test_section_name(configuration: Configuration) -> None:
"""
must return architecture specific group
"""
assert configuration.section_name("build", "x86_64") == "build:x86_64"
def test_absolute_path_for_absolute(configuration: Configuration) -> None:
"""
must not change path for absolute path in settings
@ -46,20 +53,23 @@ def test_dump(configuration: Configuration) -> None:
"""
dump must not be empty
"""
assert configuration.dump("x86_64")
assert configuration.dump()
def test_dump_architecture_specific(configuration: Configuration) -> None:
"""
dump must contain architecture specific settings
"""
configuration.add_section("build_x86_64")
configuration.set("build_x86_64", "archbuild_flags", "")
section = configuration.section_name("build", "x86_64")
configuration.add_section(section)
configuration.set(section, "archbuild_flags", "hello flag")
configuration.merge_sections("x86_64")
dump = configuration.dump("x86_64")
dump = configuration.dump()
assert dump
assert "build" not in dump
assert "build_x86_64" in dump
assert "build" in dump
assert section not in dump
assert dump["build"]["archbuild_flags"] == "hello flag"
def test_getlist(configuration: Configuration) -> None:
@ -87,23 +97,6 @@ def test_getlist_single(configuration: Configuration) -> None:
assert configuration.getlist("build", "test_list") == ["a"]
def test_get_section_name(configuration: Configuration) -> None:
"""
must return architecture specific group
"""
configuration.add_section("build_x86_64")
configuration.set("build_x86_64", "archbuild_flags", "")
assert configuration.get_section_name("build", "x86_64") == "build_x86_64"
def test_get_section_name_missing(configuration: Configuration) -> None:
"""
must return default group if architecture depending group does not exist
"""
assert configuration.get_section_name("prefix", "suffix") == "prefix"
assert configuration.get_section_name("build", "x86_64") == "build"
def test_load_includes_missing(configuration: Configuration) -> None:
"""
must not fail if not include directory found
@ -112,6 +105,14 @@ def test_load_includes_missing(configuration: Configuration) -> None:
configuration.load_includes()
def test_load_includes_no_option(configuration: Configuration) -> None:
"""
must not fail if no option set
"""
configuration.remove_option("settings", "include")
configuration.load_includes()
def test_load_logging_fallback(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must fallback to stderr without errors
@ -127,3 +128,16 @@ def test_load_logging_stderr(configuration: Configuration, mocker: MockerFixture
logging_mock = mocker.patch("logging.config.fileConfig")
configuration.load_logging(False)
logging_mock.assert_not_called()
def test_merge_sections_missing(configuration: Configuration) -> None:
"""
must merge create section if not exists
"""
section = configuration.section_name("build", "x86_64")
configuration.remove_section("build")
configuration.add_section(section)
configuration.set(section, "key", "value")
configuration.merge_sections("x86_64")
assert configuration.get("build", "key") == "value"

View File

@ -4,6 +4,7 @@ import subprocess
from pytest_mock import MockerFixture
from ahriman.core.exceptions import InvalidOption
from ahriman.core.util import check_output, package_like, pretty_datetime, pretty_size
from ahriman.models.package import Package
@ -124,6 +125,14 @@ def test_pretty_size_pbytes() -> None:
assert abbrev == "GiB"
def test_pretty_size_pbytes_failure() -> None:
"""
must raise exception if level >= 4 supplied
"""
with pytest.raises(InvalidOption):
pretty_size(42 * 1024 * 1024 * 1024 * 1024, 4).split()
def test_pretty_size_empty() -> None:
"""
must generate empty string for None value

View File

@ -0,0 +1,16 @@
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.upload.rsync import Rsync
def test_sync(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run sync command
"""
check_output_mock = mocker.patch("ahriman.core.upload.rsync.Rsync._check_output")
upload = Rsync("x86_64", configuration)
upload.sync(Path("path"))
check_output_mock.assert_called_once()

View File

@ -0,0 +1,16 @@
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.upload.s3 import S3
def test_sync(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run sync command
"""
check_output_mock = mocker.patch("ahriman.core.upload.s3.S3._check_output")
upload = S3("x86_64", configuration)
upload.sync(Path("path"))
check_output_mock.assert_called_once()

View File

@ -0,0 +1,46 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import SyncFailed
from ahriman.core.upload.upload import Upload
from ahriman.models.upload_settings import UploadSettings
def test_upload_failure(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must raise SyncFailed on errors
"""
mocker.patch("ahriman.core.upload.rsync.Rsync.sync", side_effect=Exception())
with pytest.raises(SyncFailed):
Upload.load("x86_64", configuration, UploadSettings.Rsync.name).run(Path("path"))
def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must construct dummy upload class
"""
mocker.patch("ahriman.models.upload_settings.UploadSettings.from_option", return_value=UploadSettings.Disabled)
upload_mock = mocker.patch("ahriman.core.upload.upload.Upload.sync")
Upload.load("x86_64", configuration, UploadSettings.Disabled.name).run(Path("path"))
upload_mock.assert_called_once()
def test_upload_rsync(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must upload via rsync
"""
upload_mock = mocker.patch("ahriman.core.upload.rsync.Rsync.sync")
Upload.load("x86_64", configuration, UploadSettings.Rsync.name).run(Path("path"))
upload_mock.assert_called_once()
def test_upload_s3(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must upload via s3
"""
upload_mock = mocker.patch("ahriman.core.upload.s3.S3.sync")
Upload.load("x86_64", configuration, UploadSettings.S3.name).run(Path("path"))
upload_mock.assert_called_once()

View File

@ -1,8 +1,10 @@
import pytest
from unittest.mock import MagicMock, PropertyMock
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.package_desciption import PackageDescription
from ahriman.models.package_description import PackageDescription
@pytest.fixture
@ -17,3 +19,34 @@ def package_tpacpi_bat_git() -> Package:
version="3.1.r12.g4959b52-1",
aur_url="https://aur.archlinux.org",
packages={"tpacpi-bat-git": PackageDescription()})
@pytest.fixture
def pyalpm_handle(pyalpm_package_ahriman: MagicMock) -> MagicMock:
mock = MagicMock()
mock.handle.load_pkg.return_value = pyalpm_package_ahriman
return mock
@pytest.fixture
def pyalpm_package_ahriman(package_ahriman: Package) -> MagicMock:
mock = MagicMock()
type(mock).base = PropertyMock(return_value=package_ahriman.base)
type(mock).name = PropertyMock(return_value=package_ahriman.base)
type(mock).version = PropertyMock(return_value=package_ahriman.version)
return mock
@pytest.fixture
def pyalpm_package_description_ahriman(package_description_ahriman: PackageDescription) -> MagicMock:
mock = MagicMock()
type(mock).arch = PropertyMock(return_value=package_description_ahriman.architecture)
type(mock).builddate = PropertyMock(return_value=package_description_ahriman.build_date)
type(mock).depends = PropertyMock(return_value=package_description_ahriman.depends)
type(mock).desc = PropertyMock(return_value=package_description_ahriman.description)
type(mock).groups = PropertyMock(return_value=package_description_ahriman.groups)
type(mock).isize = PropertyMock(return_value=package_description_ahriman.installed_size)
type(mock).licenses = PropertyMock(return_value=package_description_ahriman.licenses)
type(mock).size = PropertyMock(return_value=package_description_ahriman.archive_size)
type(mock).url = PropertyMock(return_value=package_description_ahriman.url)
return mock

View File

@ -1,3 +1,5 @@
import datetime
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
@ -36,3 +38,59 @@ def test_build_status_from_json_view(build_status_failed: BuildStatus) -> None:
must construct same object from json
"""
assert BuildStatus.from_json(build_status_failed.view()) == build_status_failed
def test_build_status_pretty_print(build_status_failed: BuildStatus) -> None:
"""
must return string in pretty print function
"""
assert build_status_failed.pretty_print()
assert isinstance(build_status_failed.pretty_print(), str)
def test_build_status_eq(build_status_failed: BuildStatus) -> None:
"""
must be equal
"""
other = BuildStatus.from_json(build_status_failed.view())
assert other == build_status_failed
def test_build_status_eq_self(build_status_failed: BuildStatus) -> None:
"""
must be equal itself
"""
assert build_status_failed == build_status_failed
def test_build_status_ne_by_status(build_status_failed: BuildStatus) -> None:
"""
must be not equal by status
"""
other = BuildStatus.from_json(build_status_failed.view())
other.status = BuildStatusEnum.Success
assert build_status_failed != other
def test_build_status_ne_by_timestamp(build_status_failed: BuildStatus) -> None:
"""
must be not equal by timestamp
"""
other = BuildStatus.from_json(build_status_failed.view())
other.timestamp = datetime.datetime.utcnow().timestamp()
assert build_status_failed != other
def test_build_status_ne_other(build_status_failed: BuildStatus) -> None:
"""
must be not equal to random object
"""
assert build_status_failed != object()
def test_build_status_repr(build_status_failed: BuildStatus) -> None:
"""
must return string in __repr__ function
"""
assert build_status_failed.__repr__()
assert isinstance(build_status_failed.__repr__(), str)

View File

@ -1,10 +1,24 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest.mock import MagicMock, PropertyMock
from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
def test_depends(package_python_schedule: Package) -> None:
"""
must return combined list of dependencies
"""
assert all(
set(package_python_schedule.depends).intersection(package.depends)
for package in package_python_schedule.packages.values()
)
def test_git_url(package_ahriman: Package) -> None:
"""
must generate valid git url
@ -72,6 +86,55 @@ def test_web_url(package_ahriman: Package) -> None:
assert package_ahriman.base in package_ahriman.web_url
def test_from_archive(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must construct package from alpm library
"""
mocker.patch("ahriman.models.package_description.PackageDescription.from_package",
return_value=package_ahriman.packages[package_ahriman.base])
assert Package.from_archive(Path("path"), pyalpm_handle, package_ahriman.aur_url) == package_ahriman
def test_from_aur(package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must construct package from aur
"""
mock = MagicMock()
type(mock).name = PropertyMock(return_value=package_ahriman.base)
type(mock).package_base = PropertyMock(return_value=package_ahriman.base)
type(mock).version = PropertyMock(return_value=package_ahriman.version)
mocker.patch("aur.info", return_value=mock)
package = Package.from_aur(package_ahriman.base, package_ahriman.aur_url)
assert package_ahriman.base == package.base
assert package_ahriman.version == package.version
assert package_ahriman.packages.keys() == package.packages.keys()
def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_path_root: Path) -> None:
"""
must construct package from srcinfo
"""
srcinfo = (resource_path_root / "models" / "package_ahriman_srcinfo").read_text()
mocker.patch("pathlib.Path.read_text", return_value=srcinfo)
package = Package.from_build(Path("path"), package_ahriman.aur_url)
assert package_ahriman.packages.keys() == package.packages.keys()
package_ahriman.packages = package.packages # we are not going to test PackageDescription here
assert package_ahriman == package
def test_from_build_failed(package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must raise exception if there are errors during srcinfo load
"""
mocker.patch("pathlib.Path.read_text", return_value="")
mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"]))
with pytest.raises(InvalidPackageInfo):
Package.from_build(Path("path"), package_ahriman.aur_url)
def test_from_json_view_1(package_ahriman: Package) -> None:
"""
must construct same object from json
@ -93,17 +156,80 @@ def test_from_json_view_3(package_tpacpi_bat_git: Package) -> None:
assert Package.from_json(package_tpacpi_bat_git.view()) == package_tpacpi_bat_git
def test_load_from_archive(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must load package from package archive
"""
mocker.patch("pathlib.Path.is_file", return_value=True)
load_mock = mocker.patch("ahriman.models.package.Package.from_archive")
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
load_mock.assert_called_once()
def test_load_from_aur(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must load package from AUR
"""
load_mock = mocker.patch("ahriman.models.package.Package.from_aur")
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
load_mock.assert_called_once()
def test_load_from_build(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must load package from build directory
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
load_mock = mocker.patch("ahriman.models.package.Package.from_build")
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
load_mock.assert_called_once()
def test_load_failure(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must raise InvalidPackageInfo on exception
"""
mocker.patch("pathlib.Path.is_dir", side_effect=InvalidPackageInfo("exception!"))
with pytest.raises(InvalidPackageInfo):
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
mocker.patch("pathlib.Path.is_dir", side_effect=Exception())
with pytest.raises(InvalidPackageInfo):
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
def test_dependencies_failed(mocker: MockerFixture) -> None:
"""
must raise exception if there are errors during srcinfo load
"""
mocker.patch("pathlib.Path.read_text", return_value="")
mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"]))
with pytest.raises(InvalidPackageInfo):
Package.dependencies(Path("path"))
def test_dependencies_with_version(mocker: MockerFixture, resource_path_root: Path) -> None:
"""
must load correct list of dependencies with version
"""
srcinfo = (resource_path_root / "models" / "package_yay_srcinfo").read_text()
mocker.patch("pathlib.Path.read_text", return_value=srcinfo)
assert Package.dependencies(Path("path")) == {"git", "go", "pacman"}
def test_full_version() -> None:
"""
must construct full version
"""
assert Package.full_version("1", "r2388.d30e3201", "1") == "1:r2388.d30e3201-1"
assert Package.full_version(None, "0.12.1", "1") == "0.12.1-1"
def test_actual_version(package_ahriman: Package, repository_paths: RepositoryPaths) -> None:
"""
must return same actual_version as version is
@ -117,19 +243,31 @@ def test_actual_version_vcs(package_tpacpi_bat_git: Package, repository_paths: R
must return valid actual_version for VCS package
"""
srcinfo = (resource_path_root / "models" / "package_tpacpi-bat-git_srcinfo").read_text()
mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo)
mocker.patch("ahriman.core.build_tools.task.Task.fetch")
assert package_tpacpi_bat_git.actual_version(repository_paths) == "3.1.r13.g4959b52-1"
def test_actual_version_srcinfo_failed(package_tpacpi_bat_git: Package, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must return same version in case if exception occurred
"""
mocker.patch("ahriman.models.package.Package._check_output", side_effect=Exception())
mocker.patch("ahriman.core.build_tools.task.Task.fetch")
assert package_tpacpi_bat_git.actual_version(repository_paths) == package_tpacpi_bat_git.version
def test_actual_version_vcs_failed(package_tpacpi_bat_git: Package, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must return same version in case if exception occurred
"""
mocker.patch("ahriman.models.package.Package._check_output", side_effect=Exception())
mocker.patch("pathlib.Path.read_text", return_value="")
mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"]))
mocker.patch("ahriman.models.package.Package._check_output")
mocker.patch("ahriman.core.build_tools.task.Task.fetch")
assert package_tpacpi_bat_git.actual_version(repository_paths) == package_tpacpi_bat_git.version
@ -150,3 +288,11 @@ def test_is_outdated_true(package_ahriman: Package, repository_paths: Repository
other.version = other.version.replace("-1", "-2")
assert package_ahriman.is_outdated(other, repository_paths)
def test_build_status_pretty_print(package_ahriman: Package) -> None:
"""
must return string in pretty print function
"""
assert package_ahriman.pretty_print()
assert isinstance(package_ahriman.pretty_print(), str)

View File

@ -1,4 +1,7 @@
from ahriman.models.package_desciption import PackageDescription
from dataclasses import asdict
from unittest.mock import MagicMock
from ahriman.models.package_description import PackageDescription
def test_filepath(package_description_ahriman: PackageDescription) -> None:
@ -15,3 +18,29 @@ def test_filepath_empty(package_description_ahriman: PackageDescription) -> None
"""
package_description_ahriman.filename = None
assert package_description_ahriman.filepath is None
def test_from_json(package_description_ahriman: PackageDescription) -> None:
"""
must construct description from json object
"""
assert PackageDescription.from_json(asdict(package_description_ahriman)) == package_description_ahriman
def test_from_json_with_unknown_fields(package_description_ahriman: PackageDescription) -> None:
"""
must construct description from json object containing unknown fields
"""
dump = asdict(package_description_ahriman)
dump.update(unknown_field="value")
assert PackageDescription.from_json(dump) == package_description_ahriman
def test_from_package(package_description_ahriman: PackageDescription,
pyalpm_package_description_ahriman: MagicMock) -> None:
"""
must construct description from package object
"""
package_description = PackageDescription.from_package(pyalpm_package_description_ahriman,
package_description_ahriman.filepath)
assert package_description_ahriman == package_description

View File

@ -16,11 +16,11 @@ def test_from_option_valid() -> None:
"""
must return value from valid options
"""
assert SignSettings.from_option("package") == SignSettings.SignPackages
assert SignSettings.from_option("PACKAGE") == SignSettings.SignPackages
assert SignSettings.from_option("packages") == SignSettings.SignPackages
assert SignSettings.from_option("sign-package") == SignSettings.SignPackages
assert SignSettings.from_option("package") == SignSettings.Packages
assert SignSettings.from_option("PACKAGE") == SignSettings.Packages
assert SignSettings.from_option("packages") == SignSettings.Packages
assert SignSettings.from_option("sign-package") == SignSettings.Packages
assert SignSettings.from_option("repository") == SignSettings.SignRepository
assert SignSettings.from_option("REPOSITORY") == SignSettings.SignRepository
assert SignSettings.from_option("sign-repository") == SignSettings.SignRepository
assert SignSettings.from_option("repository") == SignSettings.Repository
assert SignSettings.from_option("REPOSITORY") == SignSettings.Repository
assert SignSettings.from_option("sign-repository") == SignSettings.Repository

View File

@ -34,12 +34,10 @@ def test_run(application: web.Application, mocker: MockerFixture) -> None:
"""
must run application
"""
host = "localhost"
port = 8080
application["config"].set("web", "host", host)
application["config"].set("web", "port", str(port))
run_app_mock = mocker.patch("aiohttp.web.run_app")
application["configuration"].set("web", "port", str(port))
run_application_mock = mocker.patch("aiohttp.web.run_app")
run_server(application)
run_app_mock.assert_called_with(application, host=host, port=port,
handle_signals=False, access_log=pytest.helpers.anyvar(int))
run_application_mock.assert_called_with(application, host="0.0.0.0", port=port,
handle_signals=False, access_log=pytest.helpers.anyvar(int))

View File

@ -1,4 +1,5 @@
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
@ -35,3 +36,14 @@ async def test_post_exception(client: TestClient) -> None:
"""
post_response = await client.post("/api/v1/ahriman", json={})
assert post_response.status == 400
async def test_post_exception_inside(client: TestClient, mocker: MockerFixture) -> None:
"""
exception handler must handle 500 errors
"""
payload = {"status": BuildStatusEnum.Success.value}
mocker.patch("ahriman.core.status.watcher.Watcher.update_self", side_effect=Exception())
post_response = await client.post("/api/v1/ahriman", json=payload)
assert post_response.status == 500

View File

@ -21,7 +21,6 @@ root = /var/lib/ahriman
[sign]
target =
key =
[report]
target =
@ -36,10 +35,13 @@ template_path = ../web/templates/repo-index.jinja2
target =
[rsync]
command = rsync --archive --verbose --compress --partial --delete
remote =
[s3]
bucket =
command = aws s3 sync --quiet --delete
[web]
host = 0.0.0.0
templates = ../web/templates

View File

@ -0,0 +1,34 @@
pkgbase = ahriman
pkgdesc = ArcHlinux ReposItory MANager
pkgver = 0.12.1
pkgrel = 1
url = https://github.com/arcan1s/ahriman
arch = any
license = GPL3
makedepends = python-pip
depends = devtools
depends = git
depends = pyalpm
depends = python-aur
depends = python-srcinfo
optdepends = aws-cli: sync to s3
optdepends = breezy: -bzr packages support
optdepends = darcs: -darcs packages support
optdepends = gnupg: package and repository sign
optdepends = mercurial: -hg packages support
optdepends = python-aiohttp: web server
optdepends = python-aiohttp-jinja2: web server
optdepends = python-jinja: html report generation
optdepends = python-requests: web server
optdepends = rsync: sync by using rsync
optdepends = subversion: -svn packages support
backup = etc/ahriman.ini
backup = etc/ahriman.ini.d/logging.ini
source = https://github.com/arcan1s/ahriman/releases/download/0.12.1/ahriman-0.12.1-src.tar.xz
source = ahriman.sysusers
source = ahriman.tmpfiles
sha512sums = 8acc57f937d587ca665c29092cadddbaf3ba0b80e870b80d1551e283aba8f21306f9030a26fec8c71ab5863316f5f5f061b7ddc63cdff9e6d5a885f28ef1893d
sha512sums = 13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075
sha512sums = 55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4
pkgname = ahriman