Compare commits

..

11 Commits

Author SHA1 Message Date
5003cabeb5 Release 0.18.0 2021-03-29 11:48:54 +03:00
bc6af9256b more properties to be shown in status pages 2021-03-29 11:48:32 +03:00
1ac7c87317 architecture depending pacman.conf 2021-03-29 10:08:11 +03:00
803b7bee1e add status update subcommand
also satisfy pylint with too big method with too much variables
2021-03-29 04:17:10 +03:00
646190121a Release 0.17.0 2021-03-29 03:25:43 +03:00
10e4f3b629 Setup command (#9)
* block issues without templates

* add setup subcommand

* handle devtools config correctly
2021-03-29 03:24:58 +03:00
80a1f37c85 more templates 2021-03-29 00:13:20 +03:00
751676a07e Add issue templates 2021-03-28 23:50:41 +03:00
e1a7071ce5 try to integrate with github workflows 2021-03-28 23:13:42 +03:00
1605d185e2 remove unused import 2021-03-28 16:24:51 +03:00
2fdf910e78 add sign command (#7) (#8) 2021-03-28 16:24:00 +03:00
37 changed files with 1079 additions and 113 deletions

24
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,24 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
## Summary
A clear and concise description of what the bug is.
### Steps to Reproduce
Steps to reproduce the behavior (commands, environment etc)
### Expected behavior
A clear and concise description of what you expected to happen.
### Logs
Add logs to help explain your problem. Logs to stderr can be generated by using `--no-log` command line option.

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

12
.github/ISSUE_TEMPLATE/discussion.md vendored Normal file
View File

@ -0,0 +1,12 @@
---
name: Question
about: Create an issue to get help with project
title: ''
labels: question
assignees: ''
---
## Describe your question below
A clear and concise description of your issue for which you would like to get help.

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
## Feature summary
Brief description of the feature required
### Cause of the feature request
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
### Proposed changes and/or features
A clear and concise description of what you want to happen.

13
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,13 @@
## Summary
Brief description of the pull request. Try to provide clear explanation for major changes.
Please make sure that branch called either `feature/feature-name` for feature-related pull requests or `bug/bug-name` for bug-related ones.
Put `closes #ISSUE` in case if the pull requests solves one of the opened issues.
### Checklist
- [ ] Tests to cover new code
- [ ] `make check` passed
- [ ] `make tests` passed

27
.github/workflows/python-app.yml vendored Normal file
View File

@ -0,0 +1,27 @@
# based on https://github.com/actions/starter-workflows/blob/main/ci/python-app.yml
name: ahriman
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: run check and tests in archlinux container
run: |
docker run \
-v ${{ github.workspace }}:/build -w /build \
archlinux:latest \
/bin/bash -c "pacman --noconfirm -Syu base-devel python python-pip && \
pip install -e .[web] && \
pip install -e .[check] && \
pip install -e .[test] && \
make check tests"

2
AUTHORS Normal file
View File

@ -0,0 +1,2 @@
Current developers:
Evgenii Alekseev aka arcanis <esalexeev (at) gmail (dot) com>

View File

@ -3,7 +3,7 @@
PROJECT := ahriman PROJECT := ahriman
FILES := COPYING CONFIGURING.md README.md package src setup.py FILES := AUTHORS COPYING CONFIGURING.md README.md package src setup.py
TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES)) TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES))
IGNORE_FILES := package/archlinux src/.mypy_cache IGNORE_FILES := package/archlinux src/.mypy_cache

View File

@ -19,14 +19,14 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
* Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`): * Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`):
```shell ```shell
echo 'PACKAGES="John Doe <john@doe.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf echo 'PACKAGER="John Doe <john@doe.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf
``` ```
* Configure build tools (it is required for correct dependency management system): * Configure build tools (it is required for correct dependency management system):
* create build command, e.g. `ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build` (you can choose any name for command, basically it should be `{name}-{arch}-build`); * create build command, e.g. `ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build` (you can choose any name for command, basically it should be `{name}-{arch}-build`);
* create configuration file, e.g. `cp /usr/share/devtools/pacman-{extra,ahriman}.conf` (same as previous `pacman-{name}.conf`); * create configuration file, e.g. `cp /usr/share/devtools/pacman-{extra,ahriman}.conf` (same as previous `pacman-{name}.conf`);
* change configuration file, add your own repository, add multilib repository etc. Hint: you can use `Include` option as well; * change configuration file, add your own repository, add multilib repository etc;
* set `build_command` option to point to your command; * set `build_command` option to point to your command;
* configure `/etc/sudoers.d/ahriman` to allow running command without a password. * configure `/etc/sudoers.d/ahriman` to allow running command without a password.
@ -66,3 +66,5 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
```shell ```shell
sudo -u ahriman ahriman -a x86_64 add yay sudo -u ahriman ahriman -a x86_64 add yay
``` ```
Note that initial service configuration can be done by running `ahriman setup` with specific arguments.

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev # Maintainer: Evgeniy Alekseev
pkgname='ahriman' pkgname='ahriman'
pkgver=0.16.0 pkgver=0.18.0
pkgrel=1 pkgrel=1
pkgdesc="ArcHlinux ReposItory MANager" pkgdesc="ArcHlinux ReposItory MANager"
arch=('any') 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" source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz"
'ahriman.sysusers' 'ahriman.sysusers'
'ahriman.tmpfiles') 'ahriman.tmpfiles')
sha512sums=('b337bd936d8bc768a703eaa519e4a178993454e15696135fc21cd4216fbd03bcf433c9887cc96b4cf96f8738488d338338601b5b04c5c3b099ab69c52305d8f6' sha512sums=('8acc57f937d587ca665c29092cadddbaf3ba0b80e870b80d1551e283aba8f21306f9030a26fec8c71ab5863316f5f5f061b7ddc63cdff9e6d5a885f28ef1893d'
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075' '13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4') '55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
backup=('etc/ahriman.ini' backup=('etc/ahriman.ini'

View File

@ -71,8 +71,25 @@ setup(
], ],
extras_require={ extras_require={
"html-templates": ["Jinja2"], "check": [
"test": ["pytest", "pytest-cov", "pytest-helpers-namespace", "pytest-mock", "pytest-pspec", "pytest-resource-path"], "autopep8",
"web": ["Jinja2", "aiohttp", "aiohttp_jinja2", "requests"], "mypy",
"pylint",
],
"test": [
"pytest",
"pytest-aiohttp",
"pytest-cov",
"pytest-helpers-namespace",
"pytest-mock",
"pytest-pspec",
"pytest-resource-path",
],
"web": [
"Jinja2",
"aiohttp",
"aiohttp_jinja2",
"requests",
],
}, },
) )

View File

@ -23,20 +23,22 @@ import sys
import ahriman.application.handlers as handlers import ahriman.application.handlers as handlers
import ahriman.version as version import ahriman.version as version
from ahriman.models.build_status import BuildStatusEnum
# pylint thinks it is bad idea, but get the fuck off
# pylint: disable=protected-access
SubParserAction = argparse._SubParsersAction
# pylint: disable=too-many-statements
def _parser() -> argparse.ArgumentParser: def _parser() -> argparse.ArgumentParser:
""" """
command line parser generator command line parser generator
:return: command line parser for the application :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")
parser.add_argument( parser.add_argument("-a", "--architecture", help="target architectures (can be used multiple times)",
"-a", action="append", required=True)
"--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", "--config", help="configuration path", default="/etc/ahriman.ini")
parser.add_argument("--force", help="force run, remove file lock", action="store_true") 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("--lock", help="lock file", default="/tmp/ahriman.lock")
@ -44,70 +46,216 @@ def _parser() -> argparse.ArgumentParser:
parser.add_argument("--no-report", help="force disable reporting to web service", 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") parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user", action="store_true")
parser.add_argument("-v", "--version", action="version", version=version.__version__) parser.add_argument("-v", "--version", action="version", version=version.__version__)
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True) subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True)
add_parser = subparsers.add_parser("add", description="add package") _set_add_parser(subparsers)
add_parser.add_argument("package", help="package base/name or archive path", nargs="+") _set_check_parser(subparsers)
add_parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true") _set_clean_parser(subparsers)
add_parser.set_defaults(handler=handlers.Add) _set_config_parser(subparsers)
_set_rebuild_parser(subparsers)
check_parser = subparsers.add_parser("check", description="check for updates. Same as update --dry-run --no-manual") _set_remove_parser(subparsers)
check_parser.add_argument("package", help="filter check by package base", nargs="*") _set_report_parser(subparsers)
check_parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true") _set_setup_parser(subparsers)
check_parser.set_defaults(handler=handlers.Update, no_aur=False, no_manual=True, dry_run=True) _set_sign_parser(subparsers)
_set_status_parser(subparsers)
clean_parser = subparsers.add_parser("clean", description="clear all local caches") _set_status_update_parser(subparsers)
clean_parser.add_argument("--no-build", help="do not clear directory with package sources", action="store_true") _set_sync_parser(subparsers)
clean_parser.add_argument("--no-cache", help="do not clear directory with package caches", action="store_true") _set_update_parser(subparsers)
clean_parser.add_argument("--no-chroot", help="do not clear build chroot", action="store_true") _set_web_parser(subparsers)
clean_parser.add_argument(
"--no-manual",
help="do not clear directory with manually added packages",
action="store_true")
clean_parser.add_argument("--no-packages", help="do not clear directory with built packages", action="store_true")
clean_parser.set_defaults(handler=handlers.Clean)
config_parser = subparsers.add_parser("config", description="dump configuration for specified architecture")
config_parser.set_defaults(handler=handlers.Dump, lock=None, no_report=True, unsafe=True)
rebuild_parser = subparsers.add_parser("rebuild", description="rebuild whole repository")
rebuild_parser.set_defaults(handler=handlers.Rebuild)
remove_parser = subparsers.add_parser("remove", description="remove package")
remove_parser.add_argument("package", help="package name or base", nargs="+")
remove_parser.set_defaults(handler=handlers.Remove)
report_parser = subparsers.add_parser("report", description="generate report")
report_parser.add_argument("target", help="target to generate report", nargs="*")
report_parser.set_defaults(handler=handlers.Report)
status_parser = subparsers.add_parser("status", description="request status of the package")
status_parser.add_argument("--ahriman", help="get service status itself", action="store_true")
status_parser.add_argument("package", help="filter status by package base", nargs="*")
status_parser.set_defaults(handler=handlers.Status, lock=None, no_report=True, unsafe=True)
sync_parser = subparsers.add_parser("sync", description="sync packages to remote server")
sync_parser.add_argument("target", help="target to sync", nargs="*")
sync_parser.set_defaults(handler=handlers.Sync)
update_parser = subparsers.add_parser("update", description="run updates")
update_parser.add_argument("package", help="filter check by package base", nargs="*")
update_parser.add_argument(
"--dry-run", help="just perform check for updates, same as check command", action="store_true")
update_parser.add_argument("--no-aur", help="do not check for AUR updates. Implies --no-vcs", action="store_true")
update_parser.add_argument("--no-manual", help="do not include manual updates", action="store_true")
update_parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
update_parser.set_defaults(handler=handlers.Update)
web_parser = subparsers.add_parser("web", description="start web server")
web_parser.set_defaults(handler=handlers.Web, lock=None, no_report=True)
return parser return parser
def _set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for add subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("add", description="add package")
parser.add_argument("package", help="package base/name or archive path", nargs="+")
parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
parser.set_defaults(handler=handlers.Add)
return parser
def _set_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for check subcommand
: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.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)
return parser
def _set_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for clean subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("clean", description="clear all local caches")
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")
parser.add_argument("--no-manual", help="do not clear directory with manually added packages", action="store_true")
parser.add_argument("--no-packages", help="do not clear directory with built packages", action="store_true")
parser.set_defaults(handler=handlers.Clean, unsafe=True)
return parser
def _set_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for config subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("config", description="dump configuration for specified architecture")
parser.set_defaults(handler=handlers.Dump, lock=None, no_report=True, unsafe=True)
return parser
def _set_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for rebuild subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("rebuild", description="rebuild whole repository")
parser.set_defaults(handler=handlers.Rebuild)
return parser
def _set_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for remove subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("remove", description="remove package")
parser.add_argument("package", help="package name or base", nargs="+")
parser.set_defaults(handler=handlers.Remove)
return parser
def _set_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for report subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("report", description="generate report")
parser.add_argument("target", help="target to generate report", nargs="*")
parser.set_defaults(handler=handlers.Report)
return parser
def _set_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for setup subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("setup", description="create initial service configuration, requires root")
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("--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.set_defaults(handler=handlers.Setup, lock=None, no_report=True, unsafe=True)
return parser
def _set_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for sign subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("sign", description="(re-)sign packages and repository database")
parser.add_argument("package", help="sign only specified packages", nargs="*")
parser.set_defaults(handler=handlers.Sign)
return parser
def _set_status_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for status subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("status", description="request status of the package")
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)
return parser
def _set_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for status update subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("status-update", description="request status of the package")
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.set_defaults(handler=handlers.StatusUpdate, lock=None, no_report=True, unsafe=True)
return parser
def _set_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for sync subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("sync", description="sync packages to remote server")
parser.add_argument("target", help="target to sync", nargs="*")
parser.set_defaults(handler=handlers.Sync)
return parser
def _set_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for update subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("update", description="run updates")
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")
parser.add_argument("--no-manual", help="do not include manual updates", action="store_true")
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
parser.set_defaults(handler=handlers.Update)
return parser
def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for web subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("web", description="start web server")
parser.set_defaults(handler=handlers.Web, lock=None, no_report=True)
return parser
if __name__ == "__main__": if __name__ == "__main__":
arg_parser = _parser() args_parser = _parser()
args = arg_parser.parse_args() args = args_parser.parse_args()
handler: handlers.Handler = args.handler handler: handlers.Handler = args.handler
status = handler.execute(args) status = handler.execute(args)

View File

@ -21,7 +21,7 @@ import logging
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import Callable, Iterable, List, Optional, Set from typing import Callable, Iterable, List, Set
from ahriman.core.build_tools.task import Task from ahriman.core.build_tools.task import Task
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -67,8 +67,8 @@ class Application:
""" """
generate report and sync to remote server generate report and sync to remote server
""" """
self.report() self.report([])
self.sync() self.sync([])
def get_updates(self, filter_packages: List[str], no_aur: bool, no_manual: bool, no_vcs: bool, def get_updates(self, filter_packages: List[str], no_aur: bool, no_manual: bool, no_vcs: bool,
log_fn: Callable[[str], None]) -> List[Package]: log_fn: Callable[[str], None]) -> List[Package]:
@ -162,7 +162,7 @@ class Application:
self.repository.process_remove(names) self.repository.process_remove(names)
self._finalize() self._finalize()
def report(self, target: Optional[Iterable[str]] = None) -> None: def report(self, target: Iterable[str]) -> None:
""" """
generate report generate report
:param target: list of targets to run (e.g. html) :param target: list of targets to run (e.g. html)
@ -170,7 +170,29 @@ class Application:
targets = target or None targets = target or None
self.repository.process_report(targets) self.repository.process_report(targets)
def sync(self, target: Optional[Iterable[str]] = None) -> None: def sign(self, packages: Iterable[str]) -> None:
"""
sign packages and repository
:param packages: only sign specified packages
"""
# copy to prebuilt directory
for package in self.repository.packages():
# no one requested this package
if packages and package.base not in packages:
continue
for archive in package.packages.values():
if archive.filepath is None:
continue # avoid mypy warning
src = self.repository.paths.repository / archive.filepath
dst = self.repository.paths.packages / archive.filepath
shutil.copy(src, dst)
# run generic update function
self.update([])
# sign repository database if set
self.repository.sign.sign_repository(self.repository.repo.repo_path)
self._finalize()
def sync(self, target: Iterable[str]) -> None:
""" """
sync to remote server sync to remote server
:param target: list of targets to run (e.g. s3) :param target: list of targets to run (e.g. s3)

View File

@ -25,7 +25,10 @@ from ahriman.application.handlers.dump import Dump
from ahriman.application.handlers.rebuild import Rebuild from ahriman.application.handlers.rebuild import Rebuild
from ahriman.application.handlers.remove import Remove from ahriman.application.handlers.remove import Remove
from ahriman.application.handlers.report import Report from ahriman.application.handlers.report import Report
from ahriman.application.handlers.setup import Setup
from ahriman.application.handlers.sign import Sign
from ahriman.application.handlers.status import Status from ahriman.application.handlers.status import Status
from ahriman.application.handlers.status_update import StatusUpdate
from ahriman.application.handlers.sync import Sync from ahriman.application.handlers.sync import Sync
from ahriman.application.handlers.update import Update from ahriman.application.handlers.update import Update
from ahriman.application.handlers.web import Web from ahriman.application.handlers.web import Web

View File

@ -0,0 +1,160 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import configparser
from pathlib import Path
from typing import Type
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.repository_paths import RepositoryPaths
class Setup(Handler):
"""
setup handler
:cvar ARCHBUILD_COMMAND_PATH: default devtools command
:cvar BIN_DIR_PATH: directory for custom binaries
:cvar MIRRORLIST_PATH: path to pacman default mirrorlist (used by multilib repository)
:cvar SUDOERS_PATH: path to sudoers.d include configuration
"""
ARCHBUILD_COMMAND_PATH = Path("/usr/bin/archbuild")
BIN_DIR_PATH = Path("/usr/local/bin")
MIRRORLIST_PATH = Path("/etc/pacman.d/mirrorlist")
SUDOERS_PATH = Path("/etc/sudoers.d/ahriman")
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
"""
application = Application(architecture, config)
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_sudo_configuration(args.build_command, architecture)
@staticmethod
def build_command(prefix: str, architecture: str) -> Path:
"""
generate build command name
:param prefix: command prefix in {prefix}-{architecture}-build
:param architecture: repository architecture
:return: valid devtools command name
"""
return Setup.BIN_DIR_PATH / f"{prefix}-{architecture}-build"
@staticmethod
def create_ahriman_configuration(prefix: str, architecture: str, repository: str, include_path: Path) -> None:
"""
create service specific configuration
:param prefix: command prefix in {prefix}-{architecture}-build
:param architecture: repository architecture
:param repository: repository name
:param include_path: path to directory with configuration includes
"""
config = configparser.ConfigParser()
config.add_section("build")
config.set("build", "build_command", str(Setup.build_command(prefix, architecture)))
config.add_section("repository")
config.set("repository", "name", repository)
target = include_path / "build-overrides.ini"
with target.open("w") as ahriman_config:
config.write(ahriman_config)
@staticmethod
def create_devtools_configuration(prefix: str, architecture: str, source: Path,
no_multilib: bool, repository: str, paths: RepositoryPaths) -> None:
"""
create configuration for devtools based on `source` configuration
:param prefix: command prefix in {prefix}-{architecture}-build
:param architecture: repository architecture
:param source: path to source configuration file
:param no_multilib: do not add multilib repository
:param repository: repository name
:param paths: repository paths instance
"""
config = configparser.ConfigParser()
# preserve case
# stupid mypy thinks that it is impossible
config.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)
# set our architecture now
config.set("options", "Architecture", architecture)
# add multilib
if not no_multilib:
config.add_section("multilib")
config.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}")
target = source.parent / f"pacman-{prefix}-{architecture}.conf"
with target.open("w") as devtools_config:
config.write(devtools_config)
@staticmethod
def create_makepkg_configuration(packager: str, paths: RepositoryPaths) -> None:
"""
create configuration for makepkg
:param packager: packager identifier (e.g. name, email)
:param paths: repository paths instance
"""
(paths.root / ".makepkg.conf").write_text(f"PACKAGER='{packager}'\n")
@staticmethod
def create_sudo_configuration(prefix: str, architecture: str) -> None:
"""
create configuration to run build command with sudo without password
:param prefix: command prefix in {prefix}-{architecture}-build
:param architecture: repository architecture
"""
command = Setup.build_command(prefix, architecture)
Setup.SUDOERS_PATH.write_text(f"ahriman ALL=(ALL) NOPASSWD: {command} *\n")
Setup.SUDOERS_PATH.chmod(0o400) # security!
@staticmethod
def create_executable(prefix: str, architecture: str) -> None:
"""
create executable for the service
:param prefix: command prefix in {prefix}-{architecture}-build
:param architecture: repository architecture
"""
command = Setup.build_command(prefix, architecture)
command.unlink(missing_ok=True)
command.symlink_to(Setup.ARCHBUILD_COMMAND_PATH)

View File

@ -0,0 +1,42 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
from typing import Type
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
class Sign(Handler):
"""
(re-)sign handler
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
"""
Application(architecture, config).sign(args.package)

View File

@ -0,0 +1,51 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
from typing import 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):
"""
status update handler
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
"""
client = Application(architecture, config).repository.reporter
status = BuildStatusEnum(args.status)
if args.package:
# update packages statuses
for package in args.package:
client.update(package, status)
else:
# update service status
client.update_self(status)

View File

@ -23,7 +23,7 @@ from typing import Callable, Dict, Iterable
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.report.report import Report from ahriman.core.report.report import Report
from ahriman.core.util import pretty_size, pretty_datetime from ahriman.core.util import pretty_datetime, pretty_size
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.sign_settings import SignSettings from ahriman.models.sign_settings import SignSettings
@ -38,7 +38,18 @@ class HTML(Report):
link_path - prefix fo packages to download, string, required link_path - prefix fo packages to download, string, required
has_package_signed - True in case if package sign enabled, False otherwise, required has_package_signed - True in case if package sign enabled, False otherwise, required
has_repo_signed - True in case if repository database sign enabled, False otherwise, required has_repo_signed - True in case if repository database sign enabled, False otherwise, required
packages - sorted list of packages properties: archive_size, build_date, filename, installed_size, name, version. Required packages - sorted list of packages properties, required
* architecture, string
* archive_size, pretty printed size, string
* build_date, pretty printed datetime, string
* description, string
* filename, string,
* groups, sorted list of strings
* installed_size, pretty printed datetime, string
* licenses, sorted list of strings
* name, string
* url, string
* version, string
pgp_key - default PGP key ID, string, optional pgp_key - default PGP key ID, string, optional
repository - repository name, string, required repository - repository name, string, required
@ -48,7 +59,7 @@ class HTML(Report):
:ivar pgp_key: default PGP key :ivar pgp_key: default PGP key
:ivar report_path: output path to html report :ivar report_path: output path to html report
:ivar sign_targets: targets to sign enabled in configuration :ivar sign_targets: targets to sign enabled in configuration
:ivar tempate_path: path to directory with jinja templates :ivar template_path: path to directory with jinja templates
""" """
def __init__(self, architecture: str, config: Configuration) -> None: def __init__(self, architecture: str, config: Configuration) -> None:
@ -83,11 +94,16 @@ class HTML(Report):
content = [ content = [
{ {
"architecture": properties.architecture or "",
"archive_size": pretty_size(properties.archive_size), "archive_size": pretty_size(properties.archive_size),
"build_date": pretty_datetime(properties.build_date), "build_date": pretty_datetime(properties.build_date),
"description": properties.description or "",
"filename": properties.filename, "filename": properties.filename,
"groups": properties.groups,
"installed_size": pretty_size(properties.installed_size), "installed_size": pretty_size(properties.installed_size),
"licenses": properties.licenses,
"name": package, "name": package,
"url": properties.url or "",
"version": base.version "version": base.version
} for base in packages for package, properties in base.packages.items() } for base in packages for package, properties in base.packages.items()
] ]

View File

@ -59,6 +59,13 @@ class Package:
""" """
return f"{self.aur_url}/{self.base}.git" return f"{self.aur_url}/{self.base}.git"
@property
def groups(self) -> List[str]:
"""
:return: sum of groups per each package
"""
return sorted(set(sum([package.groups for package in self.packages.values()], start=[])))
@property @property
def is_single_package(self) -> bool: def is_single_package(self) -> bool:
""" """
@ -78,6 +85,13 @@ class Package:
or self.base.endswith("-hg")\ or self.base.endswith("-hg")\
or self.base.endswith("-svn") or self.base.endswith("-svn")
@property
def licenses(self) -> List[str]:
"""
:return: sum of licenses per each package
"""
return sorted(set(sum([package.licenses for package in self.packages.values()], start=[])))
@property @property
def web_url(self) -> str: def web_url(self) -> str:
""" """
@ -95,8 +109,8 @@ class Package:
:return: package properties :return: package properties
""" """
package = pacman.handle.load_pkg(str(path)) package = pacman.handle.load_pkg(str(path))
properties = PackageDescription(package.size, package.builddate, path.name, package.isize) return cls(package.base, package.version, aur_url,
return cls(package.base, package.version, aur_url, {package.name: properties}) {package.name: PackageDescription.from_package(package, path)})
@classmethod @classmethod
def from_aur(cls: Type[Package], name: str, aur_url: str) -> Package: def from_aur(cls: Type[Package], name: str, aur_url: str) -> Package:

View File

@ -17,25 +17,38 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from dataclasses import dataclass from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Optional from pyalpm import Package # type: ignore
from typing import List, Optional, Type
@dataclass @dataclass
class PackageDescription: class PackageDescription:
""" """
package specific properties package specific properties
:ivar architecture: package architecture
:ivar archive_size: package archive size :ivar archive_size: package archive size
:ivar build_date: package build date :ivar build_date: package build date
:ivar description: package description
:ivar filename: package archive name :ivar filename: package archive name
:ivar groups: package groups
:ivar installed_size: package installed size :ivar installed_size: package installed size
:ivar licenses: package licenses list
:ivar url: package url
""" """
architecture: Optional[str] = None
archive_size: Optional[int] = None archive_size: Optional[int] = None
build_date: Optional[int] = None build_date: Optional[int] = None
description: Optional[str] = None
filename: Optional[str] = None filename: Optional[str] = None
groups: List[str] = field(default_factory=list)
installed_size: Optional[int] = None installed_size: Optional[int] = None
licenses: List[str] = field(default_factory=list)
url: Optional[str] = None
@property @property
def filepath(self) -> Optional[Path]: def filepath(self) -> Optional[Path]:
@ -43,3 +56,22 @@ class PackageDescription:
:return: path object for current filename :return: path object for current filename
""" """
return Path(self.filename) if self.filename is not None else None return Path(self.filename) if self.filename is not None else None
@classmethod
def from_package(cls: Type[PackageDescription], package: Package, path: Path) -> PackageDescription:
"""
construct class from alpm package class
:param package: alpm generated object
:param path: path to package archive
:return: package properties based on tarball
"""
return PackageDescription(
architecture=package.arch,
archive_size=package.size,
build_date=package.builddate,
description=package.desc,
filename=path.name,
groups=package.groups,
installed_size=package.isize,
licenses=package.licenses,
url=package.url)

View File

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

View File

@ -34,10 +34,20 @@ class IndexView(BaseView):
It uses jinja2 templates for report generation, the following variables are allowed: It uses jinja2 templates for report generation, the following variables are allowed:
architecture - repository architecture, string, required architecture - repository architecture, string, required
packages - sorted list of packages properties: base, packages (sorted list), status, packages - sorted list of packages properties, required
timestamp, version, web_url. Required * base, string
* groups, sorted list of strings
* licenses, sorted list of strings
* packages, sorted list of strings
* status, string based on enum value
* timestamp, pretty printed datetime, string
* version, string
* web_url, string
repository - repository name, string, required repository - repository name, string, required
service - service status properties: status, status_color, timestamp. Required service - service status properties, required
* status, string based on enum value
* status_color, string based on enum value
* timestamp, pretty printed datetime, string
version - ahriman version, string, required version - ahriman version, string, required
""" """
@ -51,6 +61,8 @@ class IndexView(BaseView):
packages = [ packages = [
{ {
"base": package.base, "base": package.base,
"groups": package.groups,
"licenses": package.licenses,
"packages": list(sorted(package.packages)), "packages": list(sorted(package.packages)),
"status": status.status.value, "status": status.status.value,
"timestamp": pretty_datetime(status.timestamp), "timestamp": pretty_datetime(status.timestamp),

View File

@ -6,12 +6,17 @@ from ahriman.application.handlers import Add
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.package = []
args.without_dependencies = False
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run command must run command
""" """
args.package = [] args = _default_args(args)
args.without_dependencies = False
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.add") application_mock = mocker.patch("ahriman.application.application.Application.add")

View File

@ -6,15 +6,20 @@ from ahriman.application.handlers import Clean
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
must run command
"""
args.no_build = False args.no_build = False
args.no_cache = False args.no_cache = False
args.no_chroot = False args.no_chroot = False
args.no_manual = False args.no_manual = False
args.no_packages = False args.no_packages = False
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") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.clean") application_mock = mocker.patch("ahriman.application.application.Application.clean")

View File

@ -6,11 +6,16 @@ from ahriman.application.handlers import Remove
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.package = []
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run command must run command
""" """
args.package = [] args = _default_args(args)
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.remove") application_mock = mocker.patch("ahriman.application.application.Application.remove")

View File

@ -6,11 +6,16 @@ from ahriman.application.handlers import Report
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.target = []
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run command must run command
""" """
args.target = [] args = _default_args(args)
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.report") application_mock = mocker.patch("ahriman.application.application.Application.report")

View File

@ -0,0 +1,145 @@
import argparse
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.application.handlers import Setup
from ahriman.core.configuration import Configuration
from ahriman.models.repository_paths import RepositoryPaths
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.build_command = "ahriman"
args.from_config = "/usr/share/devtools/pacman-extra.conf"
args.no_multilib = False
args.packager = "John Doe <john@doe.com>"
args.repository = "aur-clone"
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")
ahriman_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.create_ahriman_configuration")
devtools_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.create_devtools_configuration")
makepkg_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.create_makepkg_configuration")
sudo_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.create_sudo_configuration")
executable_mock = mocker.patch("ahriman.application.handlers.setup.Setup.create_executable")
Setup.run(args, "x86_64", configuration)
ahriman_configuration_mock.assert_called_once()
devtools_configuration_mock.assert_called_once()
makepkg_configuration_mock.assert_called_once()
sudo_configuration_mock.assert_called_once()
executable_mock.assert_called_once()
def test_build_command(args: argparse.Namespace) -> None:
"""
must generate correct build command name
"""
args = _default_args(args)
assert Setup.build_command(args.build_command, "x86_64").name == f"{args.build_command}-x86_64-build"
def test_create_ahriman_configuration(args: argparse.Namespace, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must create configuration for the service
"""
args = _default_args(args)
mocker.patch("pathlib.Path.open")
add_section_mock = mocker.patch("configparser.RawConfigParser.add_section")
set_mock = mocker.patch("configparser.RawConfigParser.set")
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)
add_section_mock.assert_has_calls([
mock.call("build"),
mock.call("repository"),
])
set_mock.assert_has_calls([
mock.call("build", "build_command", str(command)),
mock.call("repository", "name", args.repository),
])
write_mock.assert_called_once()
def test_create_devtools_configuration(args: argparse.Namespace, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must create configuration for the devtools
"""
args = _default_args(args)
mocker.patch("pathlib.Path.open")
mocker.patch("configparser.RawConfigParser.set")
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)
add_section_mock.assert_has_calls([
mock.call("multilib"),
mock.call(args.repository)
])
write_mock.assert_called_once()
def test_create_devtools_configuration_no_multilib(args: argparse.Namespace, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must create configuration for the devtools without multilib
"""
args = _default_args(args)
mocker.patch("pathlib.Path.open")
mocker.patch("configparser.RawConfigParser.set")
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)
add_section_mock.assert_called_once()
write_mock.assert_called_once()
def test_create_makepkg_configuration(args: argparse.Namespace, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must create makepkg configuration
"""
args = _default_args(args)
write_text_mock = mocker.patch("pathlib.Path.write_text")
Setup.create_makepkg_configuration(args.packager, repository_paths)
write_text_mock.assert_called_once()
def test_create_sudo_configuration(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
must create sudo configuration
"""
args = _default_args(args)
chmod_text_mock = mocker.patch("pathlib.Path.chmod")
write_text_mock = mocker.patch("pathlib.Path.write_text")
Setup.create_sudo_configuration(args.build_command, "x86_64")
chmod_text_mock.assert_called_with(0o400)
write_text_mock.assert_called_once()
def test_create_executable(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
must create executable
"""
args = _default_args(args)
symlink_text_mock = mocker.patch("pathlib.Path.symlink_to")
unlink_text_mock = mocker.patch("pathlib.Path.unlink")
Setup.create_executable(args.build_command, "x86_64")
symlink_text_mock.assert_called_once()
unlink_text_mock.assert_called_once()

View File

@ -0,0 +1,23 @@
import argparse
from pytest_mock import MockerFixture
from ahriman.application.handlers import Sign
from ahriman.core.configuration import Configuration
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.package = []
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_mock = mocker.patch("ahriman.application.application.Application.sign")
Sign.run(args, "x86_64", configuration)
application_mock.assert_called_once()

View File

@ -6,13 +6,17 @@ from ahriman.application.handlers import Status
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.ahriman = True
args.package = []
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run command must run command
""" """
args.ahriman = True args = _default_args(args)
args.package = []
args.without_dependencies = False
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.core.status.client.Client.get_self") 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")

View File

@ -0,0 +1,40 @@
import argparse
from pytest_mock import MockerFixture
from ahriman.application.handlers import StatusUpdate
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.status = BuildStatusEnum.Success.value
args.package = 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")
update_self_mock = mocker.patch("ahriman.core.status.client.Client.update_self")
StatusUpdate.run(args, "x86_64", configuration)
update_self_mock.assert_called_once()
def test_run_packages(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must run command with specified packages
"""
args = _default_args(args)
args.package = [package_ahriman.base]
mocker.patch("pathlib.Path.mkdir")
update_mock = mocker.patch("ahriman.core.status.client.Client.update")
StatusUpdate.run(args, "x86_64", configuration)
update_mock.assert_called_once()

View File

@ -6,11 +6,16 @@ from ahriman.application.handlers import Sync
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.target = []
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run command must run command
""" """
args.target = [] args = _default_args(args)
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.sync") application_mock = mocker.patch("ahriman.application.application.Application.sync")

View File

@ -6,15 +6,20 @@ from ahriman.application.handlers import Update
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
must run command
"""
args.package = [] args.package = []
args.dry_run = False args.dry_run = False
args.no_aur = False args.no_aur = False
args.no_manual = False args.no_manual = False
args.no_vcs = False args.no_vcs = False
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") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.update") application_mock = mocker.patch("ahriman.application.application.Application.update")
updates_mock = mocker.patch("ahriman.application.application.Application.get_updates") updates_mock = mocker.patch("ahriman.application.application.Application.get_updates")
@ -28,11 +33,8 @@ def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, moc
""" """
must run simplified command must run simplified command
""" """
args.package = [] args = _default_args(args)
args.dry_run = True args.dry_run = True
args.no_aur = False
args.no_manual = False
args.no_vcs = False
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
updates_mock = mocker.patch("ahriman.application.application.Application.get_updates") updates_mock = mocker.patch("ahriman.application.application.Application.get_updates")

View File

@ -26,6 +26,14 @@ def test_subparsers_check(parser: argparse.ArgumentParser) -> None:
assert args.dry_run assert args.dry_run
def test_subparsers_clean(parser: argparse.ArgumentParser) -> None:
"""
clean command must imply unsafe
"""
args = parser.parse_args(["-a", "x86_64", "clean"])
assert args.unsafe
def test_subparsers_config(parser: argparse.ArgumentParser) -> None: def test_subparsers_config(parser: argparse.ArgumentParser) -> None:
""" """
config command must imply lock, no_report and unsafe config command must imply lock, no_report and unsafe
@ -36,6 +44,16 @@ def test_subparsers_config(parser: argparse.ArgumentParser) -> None:
assert args.unsafe assert args.unsafe
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>"])
assert args.lock is None
assert args.no_report
assert args.unsafe
def test_subparsers_status(parser: argparse.ArgumentParser) -> None: def test_subparsers_status(parser: argparse.ArgumentParser) -> None:
""" """
status command must imply lock, no_report and unsafe status command must imply lock, no_report and unsafe
@ -46,6 +64,16 @@ def test_subparsers_status(parser: argparse.ArgumentParser) -> None:
assert args.unsafe assert args.unsafe
def test_subparsers_status_update(parser: argparse.ArgumentParser) -> None:
"""
status-update command must imply lock, no_report and unsafe
"""
args = parser.parse_args(["-a", "x86_64", "status-update"])
assert args.lock is None
assert args.no_report
assert args.unsafe
def test_subparsers_web(parser: argparse.ArgumentParser) -> None: def test_subparsers_web(parser: argparse.ArgumentParser) -> None:
""" """
web command must imply lock and no_report web command must imply lock and no_report

View File

@ -1,3 +1,5 @@
import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest import mock from unittest import mock
@ -205,16 +207,57 @@ def test_report(application: Application, mocker: MockerFixture) -> None:
must generate report must generate report
""" """
executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_report") executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_report")
application.report(None) application.report([])
executor_mock.assert_called_once() executor_mock.assert_called_once()
def test_sign(application: Application, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must sign world
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages",
return_value=[package_ahriman, package_python_schedule])
copy_mock = mocker.patch("shutil.copy")
update_mock = mocker.patch("ahriman.application.application.Application.update")
sign_repository_mock = mocker.patch("ahriman.core.sign.gpg.GPG.sign_repository")
finalize_mock = mocker.patch("ahriman.application.application.Application._finalize")
application.sign([])
copy_mock.assert_has_calls([
mock.call(pytest.helpers.anyvar(str), pytest.helpers.anyvar(str)),
mock.call(pytest.helpers.anyvar(str), pytest.helpers.anyvar(str))
])
update_mock.assert_called_with([])
sign_repository_mock.assert_called_once()
finalize_mock.assert_called_once()
def test_sign_specific(application: Application, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must sign only specified packages
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages",
return_value=[package_ahriman, package_python_schedule])
copy_mock = mocker.patch("shutil.copy")
update_mock = mocker.patch("ahriman.application.application.Application.update")
sign_repository_mock = mocker.patch("ahriman.core.sign.gpg.GPG.sign_repository")
finalize_mock = mocker.patch("ahriman.application.application.Application._finalize")
application.sign([package_ahriman.base])
copy_mock.assert_called_once()
update_mock.assert_called_with([])
sign_repository_mock.assert_called_once()
finalize_mock.assert_called_once()
def test_sync(application: Application, mocker: MockerFixture) -> None: def test_sync(application: Application, mocker: MockerFixture) -> None:
""" """
must sync to remote must sync to remote
""" """
executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_sync") executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_sync")
application.sync(None) application.sync([])
executor_mock.assert_called_once() executor_mock.assert_called_once()

View File

@ -58,28 +58,43 @@ def package_python_schedule(
@pytest.fixture @pytest.fixture
def package_description_ahriman() -> PackageDescription: def package_description_ahriman() -> PackageDescription:
return PackageDescription( return PackageDescription(
architecture="x86_64",
archive_size=4200, archive_size=4200,
build_date=42, build_date=42,
description="ArcHlinux ReposItory MANager",
filename="ahriman-0.12.1-1-any.pkg.tar.zst", filename="ahriman-0.12.1-1-any.pkg.tar.zst",
installed_size=4200000) groups=[],
installed_size=4200000,
licenses=["GPL3"],
url="https://github.com/arcan1s/ahriman")
@pytest.fixture @pytest.fixture
def package_description_python_schedule() -> PackageDescription: def package_description_python_schedule() -> PackageDescription:
return PackageDescription( return PackageDescription(
architecture="x86_64",
archive_size=4201, archive_size=4201,
build_date=421, build_date=421,
description="Python job scheduling for humans.",
filename="python-schedule-1.0.0-2-any.pkg.tar.zst", filename="python-schedule-1.0.0-2-any.pkg.tar.zst",
installed_size=4200001) groups=[],
installed_size=4200001,
licenses=["MIT"],
url="https://github.com/dbader/schedule")
@pytest.fixture @pytest.fixture
def package_description_python2_schedule() -> PackageDescription: def package_description_python2_schedule() -> PackageDescription:
return PackageDescription( return PackageDescription(
architecture="x86_64",
archive_size=4202, archive_size=4202,
build_date=422, build_date=422,
description="Python job scheduling for humans.",
filename="python2-schedule-1.0.0-2-any.pkg.tar.zst", filename="python2-schedule-1.0.0-2-any.pkg.tar.zst",
installed_size=4200002) groups=[],
installed_size=4200002,
licenses=["MIT"],
url="https://github.com/dbader/schedule")
@pytest.fixture @pytest.fixture

View File

@ -14,6 +14,17 @@ def test_git_url(package_ahriman: Package) -> None:
assert package_ahriman.base in package_ahriman.git_url assert package_ahriman.base in package_ahriman.git_url
def test_groups(package_ahriman: Package) -> None:
"""
must return list of groups for each package
"""
assert all(
all(group in package_ahriman.groups for group in package.groups)
for package in package_ahriman.packages.values()
)
assert sorted(package_ahriman.groups) == package_ahriman.groups
def test_is_single_package_false(package_python_schedule: Package) -> None: def test_is_single_package_false(package_python_schedule: Package) -> None:
""" """
python-schedule must not be single package python-schedule must not be single package
@ -42,6 +53,17 @@ def test_is_vcs_true(package_tpacpi_bat_git: Package) -> None:
assert package_tpacpi_bat_git.is_vcs assert package_tpacpi_bat_git.is_vcs
def test_licenses(package_ahriman: Package) -> None:
"""
must return list of licenses for each package
"""
assert all(
all(lic in package_ahriman.licenses for lic in package.licenses)
for package in package_ahriman.packages.values()
)
assert sorted(package_ahriman.licenses) == package_ahriman.licenses
def test_web_url(package_ahriman: Package) -> None: def test_web_url(package_ahriman: Package) -> None:
""" """
must generate valid web url must generate valid web url

View File

@ -1,4 +1,5 @@
[settings] [settings]
include = .
logging = logging.ini logging = logging.ini
[alpm] [alpm]