mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-06-27 14:22:10 +00:00
Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
91f66fdcee | |||
bb45b1d868 | |||
3d10fa472b | |||
a90c93bbc4 | |||
41a3c08d9f | |||
cb328ad797 | |||
810091cde9 | |||
fc0474fa8f | |||
b94179e071 | |||
9c5a9f5837 | |||
83047d8270 | |||
990d5dda81 | |||
48e79ce39c | |||
375d7c55e5 | |||
db52b8e844 | |||
50af309c80 | |||
581401d60f | |||
c2685f4746 | |||
952b55f707 | |||
b9b012be53 | |||
b8036649ab | |||
c90e20587e | |||
3e020ec141 | |||
783b7d043d | |||
5c297d1c67 | |||
b0d1f3c091 | |||
50e219fda5 | |||
75298d1b8a | |||
8196dcc8a0 | |||
f634f1df58 | |||
32df4fc54f |
1
.bandit-test.yml
Normal file
1
.bandit-test.yml
Normal file
@ -0,0 +1 @@
|
||||
skips: ['B101', 'B404']
|
1
.bandit.yml
Normal file
1
.bandit.yml
Normal file
@ -0,0 +1 @@
|
||||
skips: ['B404', 'B603']
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: create release
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
|
6
.github/workflows/run-tests.yml
vendored
6
.github/workflows/run-tests.yml
vendored
@ -1,5 +1,4 @@
|
||||
# based on https://github.com/actions/starter-workflows/blob/main/ci/python-app.yml
|
||||
name: check commit
|
||||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -19,8 +18,9 @@ jobs:
|
||||
docker run \
|
||||
-v ${{ github.workspace }}:/build -w /build \
|
||||
archlinux:latest \
|
||||
/bin/bash -c "pacman --noconfirm -Syu base-devel python python-pip && \
|
||||
/bin/bash -c "pacman --noconfirm -Syu base-devel python-argparse-manpage python-pip && \
|
||||
pip install -e .[web] && \
|
||||
pip install -e .[check] && \
|
||||
pip install -e .[s3] && \
|
||||
pip install -e .[test] && \
|
||||
make check tests"
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -94,3 +94,5 @@ ENV/
|
||||
.venv/
|
||||
|
||||
*.tar.xz
|
||||
|
||||
man/
|
||||
|
@ -22,7 +22,7 @@ ignore-patterns=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use.
|
||||
jobs=1
|
||||
jobs=0
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
@ -149,7 +149,6 @@ disable=print-statement,
|
||||
too-few-public-methods,
|
||||
too-many-instance-attributes,
|
||||
broad-except,
|
||||
logging-fstring-interpolation,
|
||||
too-many-ancestors,
|
||||
fixme,
|
||||
too-many-arguments,
|
||||
|
@ -89,10 +89,13 @@ Group name must refer to architecture, e.g. it should be `rsync:x86_64` for x86_
|
||||
|
||||
### `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.
|
||||
|
||||
* `command` - s3 command to run, space separated list of string, required.
|
||||
* `bucket` - bucket name (e.g. `s3://bucket/path`), string, required.
|
||||
* `access_key` - AWS access key ID, string, required.
|
||||
* `bucket` - bucket name (e.g. `bucket`), string, required.
|
||||
* `chunk_size` - chunk size for calculating entity tags, int, optional, default 8 * 1024 * 1024.
|
||||
* `region` - bucket region (e.g. `eu-central-1`), string, required.
|
||||
* `secret_key` - AWS secret access key, string, required.
|
||||
|
||||
## `web:*` groups
|
||||
|
||||
|
13
Makefile
13
Makefile
@ -23,10 +23,11 @@ archive_directory: $(TARGET_FILES)
|
||||
archlinux: archive
|
||||
sed -i "s/pkgver=[0-9.]*/pkgver=$(VERSION)/" package/archlinux/PKGBUILD
|
||||
|
||||
check: clean
|
||||
cd src && mypy --implicit-reexport --strict -p "$(PROJECT)"
|
||||
find "src/$(PROJECT)" "tests/$(PROJECT)" -name "*.py" -execdir autopep8 --exit-code --max-line-length 120 -aa -i {} +
|
||||
cd src && pylint --rcfile=../.pylintrc "$(PROJECT)"
|
||||
check: clean mypy
|
||||
autopep8 --exit-code --max-line-length 120 -aa -i -j 0 -r "src/$(PROJECT)" "tests/$(PROJECT)"
|
||||
pylint --rcfile=.pylintrc "src/$(PROJECT)"
|
||||
bandit -c .bandit.yml -r "src/$(PROJECT)"
|
||||
bandit -c .bandit-test.yml -r "tests/$(PROJECT)"
|
||||
|
||||
clean:
|
||||
find . -type f -name "$(PROJECT)-*-src.tar.xz" -delete
|
||||
@ -35,6 +36,10 @@ clean:
|
||||
directory: clean
|
||||
mkdir "$(PROJECT)"
|
||||
|
||||
mypy:
|
||||
cd src && mypy --implicit-reexport --strict -p "$(PROJECT)" --install-types --non-interactive || true
|
||||
cd src && mypy --implicit-reexport --strict -p "$(PROJECT)"
|
||||
|
||||
push: archlinux
|
||||
git add package/archlinux/PKGBUILD src/ahriman/version.py
|
||||
git commit -m "Release $(VERSION)"
|
||||
|
@ -1,6 +1,7 @@
|
||||
# ArcHlinux ReposItory MANager
|
||||
|
||||
[](https://github.com/arcan1s/ahriman/actions/workflows/run-tests.yml)
|
||||
[](https://www.codefactor.io/repository/github/arcan1s/ahriman)
|
||||
|
||||
Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
|
||||
|
||||
|
@ -1,31 +1,27 @@
|
||||
# Maintainer: Evgeniy Alekseev
|
||||
|
||||
pkgname='ahriman'
|
||||
pkgver=0.22.1
|
||||
pkgver=1.2.3
|
||||
pkgrel=1
|
||||
pkgdesc="ArcHlinux ReposItory MANager"
|
||||
arch=('any')
|
||||
url="https://github.com/arcan1s/ahriman"
|
||||
license=('GPL3')
|
||||
depends=('devtools' 'git' 'pyalpm' 'python-aur' 'python-srcinfo')
|
||||
makedepends=('python-pip')
|
||||
optdepends=('aws-cli: sync to s3'
|
||||
'breezy: -bzr packages support'
|
||||
makedepends=('python-argparse-manpage' 'python-pip')
|
||||
optdepends=('breezy: -bzr packages support'
|
||||
'darcs: -darcs packages support'
|
||||
'gnupg: package and repository sign'
|
||||
'mercurial: -hg packages support'
|
||||
'python-aiohttp: web server'
|
||||
'python-aiohttp-jinja2: web server'
|
||||
'python-boto3: sync to s3'
|
||||
'python-jinja: html report generation'
|
||||
'python-requests: web server'
|
||||
'rsync: sync by using rsync'
|
||||
'subversion: -svn packages support')
|
||||
source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz"
|
||||
'ahriman.sysusers'
|
||||
'ahriman.tmpfiles')
|
||||
sha512sums=('6ab741bfb42f92ab00d1b6ecfc44426c00e5c433486e014efbdb585715d9a12dbbafc280e5a9f85b941c8681b13a9dad41327a3e3c44a9683ae30c1d6f017f50'
|
||||
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
|
||||
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
|
||||
backup=('etc/ahriman.ini'
|
||||
'etc/ahriman.ini.d/logging.ini')
|
||||
|
||||
@ -43,3 +39,7 @@ package() {
|
||||
install -Dm644 "$srcdir/$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf"
|
||||
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"
|
||||
}
|
||||
|
||||
sha512sums=('6ab741bfb42f92ab00d1b6ecfc44426c00e5c433486e014efbdb585715d9a12dbbafc280e5a9f85b941c8681b13a9dad41327a3e3c44a9683ae30c1d6f017f50'
|
||||
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
|
||||
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
|
||||
|
@ -1,2 +1 @@
|
||||
d /var/lib/ahriman 0775 ahriman log
|
||||
d /var/log/ahriman 0755 ahriman ahriman
|
@ -13,7 +13,7 @@ archbuild_flags =
|
||||
build_command = extra-x86_64-build
|
||||
ignore_packages =
|
||||
makechrootpkg_flags =
|
||||
makepkg_flags = --skippgpcheck
|
||||
makepkg_flags =
|
||||
|
||||
[repository]
|
||||
name = aur-clone
|
||||
@ -37,11 +37,11 @@ template_path = /usr/share/ahriman/repo-index.jinja2
|
||||
target =
|
||||
|
||||
[rsync]
|
||||
command = rsync --archive --verbose --compress --partial --delete
|
||||
command = rsync --archive --compress --partial --delete
|
||||
|
||||
[s3]
|
||||
command = aws s3 sync --quiet --delete
|
||||
chunk_size = 8388608
|
||||
|
||||
[web]
|
||||
host = 0.0.0.0
|
||||
host = 127.0.0.1
|
||||
templates = /usr/share/ahriman
|
@ -2,10 +2,10 @@
|
||||
keys = root,builder,build_details,http
|
||||
|
||||
[handlers]
|
||||
keys = console_handler,build_file_handler,file_handler,http_handler
|
||||
keys = console_handler,build_file_handler,file_handler,http_handler,syslog_handler
|
||||
|
||||
[formatters]
|
||||
keys = generic_format
|
||||
keys = generic_format,syslog_format
|
||||
|
||||
[handler_console_handler]
|
||||
class = StreamHandler
|
||||
@ -31,29 +31,39 @@ level = DEBUG
|
||||
formatter = generic_format
|
||||
args = ("/var/log/ahriman/http.log", "a", 20971520, 20)
|
||||
|
||||
[handler_syslog_handler]
|
||||
class = logging.handlers.SysLogHandler
|
||||
level = DEBUG
|
||||
formatter = syslog_format
|
||||
args = ("/dev/log",)
|
||||
|
||||
[formatter_generic_format]
|
||||
format = [%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
|
||||
datefmt =
|
||||
|
||||
[formatter_syslog_format]
|
||||
format = [%(levelname)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
|
||||
datefmt =
|
||||
|
||||
[logger_root]
|
||||
level = DEBUG
|
||||
handlers = file_handler
|
||||
handlers = syslog_handler
|
||||
qualname = root
|
||||
|
||||
[logger_builder]
|
||||
level = DEBUG
|
||||
handlers = file_handler
|
||||
handlers = syslog_handler
|
||||
qualname = builder
|
||||
propagate = 0
|
||||
|
||||
[logger_build_details]
|
||||
level = DEBUG
|
||||
handlers = build_file_handler
|
||||
handlers = syslog_handler
|
||||
qualname = build_details
|
||||
propagate = 0
|
||||
|
||||
[logger_http]
|
||||
level = DEBUG
|
||||
handlers = http_handler
|
||||
handlers = syslog_handler
|
||||
qualname = http
|
||||
propagate = 0
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ repository|e }}</title>
|
||||
<title>{{ repository }}</title>
|
||||
|
||||
{% include "style.jinja2" %}
|
||||
|
||||
@ -12,9 +12,9 @@
|
||||
<body>
|
||||
<div class="root">
|
||||
<h1>ahriman
|
||||
<img src="https://img.shields.io/badge/version-{{ version|e }}-informational" alt="{{ version|e }}">
|
||||
<img src="https://img.shields.io/badge/architecture-{{ architecture|e }}-informational" alt="{{ architecture|e }}">
|
||||
<img src="https://img.shields.io/badge/service%20status-{{ service.status|e }}-{{ service.status_color|e }}" alt="{{ service.status|e }}" title="{{ service.timestamp|e }}">
|
||||
<img src="https://img.shields.io/badge/version-{{ version }}-informational" alt="{{ version }}">
|
||||
<img src="https://img.shields.io/badge/architecture-{{ architecture }}-informational" alt="{{ architecture }}">
|
||||
<img src="https://img.shields.io/badge/service%20status-{{ service.status }}-{{ service.status_color }}" alt="{{ service.status }}" title="{{ service.timestamp }}">
|
||||
</h1>
|
||||
|
||||
{% include "search-line.jinja2" %}
|
||||
@ -31,11 +31,11 @@
|
||||
|
||||
{% for package in packages %}
|
||||
<tr class="package">
|
||||
<td class="include-search"><a href="{{ package.web_url|e }}" title="{{ package.base|e }}">{{ package.base|e }}</a></td>
|
||||
<td class="include-search"><a href="{{ package.web_url }}" title="{{ package.base }}">{{ package.base }}</a></td>
|
||||
<td class="include-search">{{ package.packages|join("<br>"|safe) }}</td>
|
||||
<td>{{ package.version|e }}</td>
|
||||
<td>{{ package.timestamp|e }}</td>
|
||||
<td class="status package-{{ package.status|e }}">{{ package.status|e }}</td>
|
||||
<td>{{ package.version }}</td>
|
||||
<td>{{ package.timestamp }}</td>
|
||||
<td class="status package-{{ package.status }}">{{ package.status }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ repository|e }}</title>
|
||||
<title>{{ repository }}</title>
|
||||
|
||||
{% include "style.jinja2" %}
|
||||
|
||||
@ -18,20 +18,20 @@
|
||||
|
||||
<section class="element">
|
||||
{% if pgp_key is not none %}
|
||||
<p>This repository is signed with <a href="http://keys.gnupg.net/pks/lookup?search=0x{{ pgp_key|e }}&fingerprint=on&op=index" title="key search">{{ pgp_key|e }}</a> by default.</p>
|
||||
<p>This repository is signed with <a href="http://keys.gnupg.net/pks/lookup?search=0x{{ pgp_key }}&fingerprint=on&op=index" title="key search">{{ pgp_key }}</a> by default.</p>
|
||||
{% endif %}
|
||||
|
||||
<code>
|
||||
$ cat /etc/pacman.conf<br>
|
||||
[{{ repository|e }}]<br>
|
||||
Server = {{ link_path|e }}<br>
|
||||
[{{ repository }}]<br>
|
||||
Server = {{ link_path }}<br>
|
||||
SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly
|
||||
</code>
|
||||
</section>
|
||||
|
||||
{% include "search-line.jinja2" %}
|
||||
{% endif %}
|
||||
|
||||
{% include "search-line.jinja2" %}
|
||||
|
||||
<section class="element">
|
||||
<table class="sortable search-table">
|
||||
<tr class="header">
|
||||
@ -44,11 +44,11 @@
|
||||
|
||||
{% for package in packages %}
|
||||
<tr class="package">
|
||||
<td class="include-search"><a href="{{ link_path|e }}/{{ package.filename|e }}" title="{{ package.name|e }}">{{ package.name|e }}</a></td>
|
||||
<td>{{ package.version|e }}</td>
|
||||
<td>{{ package.archive_size|e }}</td>
|
||||
<td>{{ package.installed_size|e }}</td>
|
||||
<td>{{ package.build_date|e }}</td>
|
||||
<td class="include-search"><a href="{{ link_path }}/{{ package.filename }}" title="{{ package.name }}">{{ package.name }}</a></td>
|
||||
<td>{{ package.version }}</td>
|
||||
<td>{{ package.archive_size }}</td>
|
||||
<td>{{ package.installed_size }}</td>
|
||||
<td>{{ package.build_date }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
@ -58,7 +58,7 @@
|
||||
<footer>
|
||||
<ul class="navigation">
|
||||
{% if homepage is not none %}
|
||||
<li><a href="{{ homepage|e }}" title="homepage">Homepage</a></li>
|
||||
<li><a href="{{ homepage }}" title="homepage">Homepage</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</footer>
|
||||
|
@ -1,3 +1,3 @@
|
||||
<section class="element">
|
||||
<input type="search" id="search" onkeyup="searchInTable()" placeholder="search for package" title="search for package"/>
|
||||
</section>
|
||||
</section>
|
||||
|
@ -5,21 +5,22 @@
|
||||
const tables = document.getElementsByClassName("search-table");
|
||||
|
||||
for (let i = 0; i < tables.length; i++) {
|
||||
const tr = tables[i].getElementsByTagName("tr");
|
||||
const trs = tables[i].getElementsByTagName("tr");
|
||||
// from 1 coz of header
|
||||
for (let i = 1; i < tr.length; i++) {
|
||||
let td = tr[i].getElementsByClassName("include-search");
|
||||
for (let i = 1; i < trs.length; i++) {
|
||||
let tr = trs[i].getElementsByClassName("include-search");
|
||||
let display = "none";
|
||||
for (let j = 0; j < td.length; j++) {
|
||||
if (td[j].tagName.toLowerCase() === "td") {
|
||||
if (td[j].innerHTML.toLowerCase().indexOf(filter) > -1) {
|
||||
for (let j = 0; j < tr.length; j++) {
|
||||
if (tr[j].tagName.toLowerCase() === "td") {
|
||||
let contains = (element) => tr[j].innerHTML.toLowerCase().indexOf(element) > -1
|
||||
if (filter.some(contains)) {
|
||||
display = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
tr[i].style.display = display;
|
||||
trs[i].style.display = display;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
@ -1 +1 @@
|
||||
<script src="https://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script>
|
||||
<script src="https://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script>
|
||||
|
@ -133,4 +133,4 @@
|
||||
ul.navigation li a:hover {
|
||||
background-color: rgba(var(--color-hover), 1.0);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -3,3 +3,6 @@ test = pytest
|
||||
|
||||
[tool:pytest]
|
||||
addopts = --cov=ahriman --cov-report term-missing:skip-covered --pspec
|
||||
|
||||
[build_manpages]
|
||||
manpages = man/ahriman.1:module=ahriman.application.ahriman:function=_parser
|
||||
|
29
setup.py
29
setup.py
@ -1,11 +1,16 @@
|
||||
from distutils.util import convert_path
|
||||
from build_manpages.build_manpages import build_manpages, get_build_py_cmd, get_install_cmd
|
||||
from pathlib import Path
|
||||
from setuptools import setup, find_packages
|
||||
from os import path
|
||||
from setuptools.command.build_py import build_py
|
||||
from setuptools.command.install import install
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
metadata_path = Path(__file__).resolve().parent / "src/ahriman/version.py"
|
||||
metadata: Dict[str, Any] = dict()
|
||||
with metadata_path.open() as metadata_file:
|
||||
exec(metadata_file.read(), metadata) # pylint: disable=exec-used
|
||||
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
metadata = dict()
|
||||
with open(convert_path("src/ahriman/version.py")) as metadata_file:
|
||||
exec(metadata_file.read(), metadata)
|
||||
|
||||
setup(
|
||||
name="ahriman",
|
||||
@ -29,6 +34,7 @@ setup(
|
||||
install_requires=[
|
||||
"aur",
|
||||
"pyalpm",
|
||||
"requests",
|
||||
"srcinfo",
|
||||
],
|
||||
setup_requires=[
|
||||
@ -73,9 +79,13 @@ setup(
|
||||
extras_require={
|
||||
"check": [
|
||||
"autopep8",
|
||||
"bandit",
|
||||
"mypy",
|
||||
"pylint",
|
||||
],
|
||||
"s3": [
|
||||
"boto3",
|
||||
],
|
||||
"test": [
|
||||
"pytest",
|
||||
"pytest-aiohttp",
|
||||
@ -89,7 +99,12 @@ setup(
|
||||
"Jinja2",
|
||||
"aiohttp",
|
||||
"aiohttp_jinja2",
|
||||
"requests",
|
||||
],
|
||||
},
|
||||
|
||||
cmdclass={
|
||||
"build_manpages": build_manpages,
|
||||
"build_py": get_build_py_cmd(build_py),
|
||||
"install": get_install_cmd(install),
|
||||
}
|
||||
)
|
||||
|
@ -19,19 +19,18 @@
|
||||
#
|
||||
import argparse
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import ahriman.application.handlers as handlers
|
||||
import ahriman.version as version
|
||||
|
||||
from ahriman import version
|
||||
from ahriman.application import handlers
|
||||
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
|
||||
# pylint: disable=protected-access
|
||||
SubParserAction = argparse._SubParsersAction
|
||||
SubParserAction = argparse._SubParsersAction # pylint: disable=protected-access
|
||||
|
||||
|
||||
def _parser() -> argparse.ArgumentParser:
|
||||
@ -42,10 +41,17 @@ def _parser() -> argparse.ArgumentParser:
|
||||
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)
|
||||
action="append")
|
||||
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("-l", "--lock", help="lock file", type=Path, default=Path("/tmp/ahriman.lock"))
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--lock",
|
||||
help="lock file",
|
||||
type=Path,
|
||||
default=Path(
|
||||
tempfile.gettempdir()) /
|
||||
"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")
|
||||
@ -57,9 +63,12 @@ def _parser() -> argparse.ArgumentParser:
|
||||
_set_check_parser(subparsers)
|
||||
_set_clean_parser(subparsers)
|
||||
_set_config_parser(subparsers)
|
||||
_set_init_parser(subparsers)
|
||||
_set_key_import_parser(subparsers)
|
||||
_set_rebuild_parser(subparsers)
|
||||
_set_remove_parser(subparsers)
|
||||
_set_report_parser(subparsers)
|
||||
_set_search_parser(subparsers)
|
||||
_set_setup_parser(subparsers)
|
||||
_set_sign_parser(subparsers)
|
||||
_set_status_parser(subparsers)
|
||||
@ -82,7 +91,7 @@ def _set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
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)
|
||||
parser.set_defaults(handler=handlers.Add, architecture=[])
|
||||
return parser
|
||||
|
||||
|
||||
@ -97,7 +106,7 @@ def _set_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
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)
|
||||
parser.set_defaults(handler=handlers.Update, architecture=[], no_aur=False, no_manual=True, dry_run=True)
|
||||
return parser
|
||||
|
||||
|
||||
@ -114,7 +123,7 @@ def _set_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
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)
|
||||
parser.set_defaults(handler=handlers.Clean, architecture=[], no_log=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
@ -127,7 +136,35 @@ def _set_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
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)
|
||||
parser.set_defaults(handler=handlers.Dump, lock=None, no_log=True, no_report=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_init_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for init subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("init", help="create repository tree",
|
||||
description="create empty repository tree. Optional command for auto architecture support",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.set_defaults(handler=handlers.Init, no_report=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_key_import_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for key import subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("key-import", help="import PGP key",
|
||||
description="import PGP key from public sources to repository user",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("--key-server", help="key server for key import", default="keys.gnupg.net")
|
||||
parser.add_argument("key", help="PGP key to import from public server")
|
||||
parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, no_report=True)
|
||||
return parser
|
||||
|
||||
|
||||
@ -139,8 +176,8 @@ def _set_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
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)
|
||||
parser.add_argument("--depends-on", help="only rebuild packages that depend on specified package", action="append")
|
||||
parser.set_defaults(handler=handlers.Rebuild, architecture=[])
|
||||
return parser
|
||||
|
||||
|
||||
@ -153,7 +190,7 @@ def _set_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
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)
|
||||
parser.set_defaults(handler=handlers.Remove, architecture=[])
|
||||
return parser
|
||||
|
||||
|
||||
@ -166,7 +203,19 @@ def _set_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
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)
|
||||
parser.set_defaults(handler=handlers.Report, architecture=[])
|
||||
return parser
|
||||
|
||||
|
||||
def _set_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for search subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("search", help="search for package", description="search for package in AUR using API")
|
||||
parser.add_argument("search", help="search terms, can be specified multiple times", nargs="+")
|
||||
parser.set_defaults(handler=handlers.Search, architecture=[""], lock=None, no_log=True, no_report=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
@ -187,9 +236,9 @@ def _set_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
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="*")
|
||||
choices=SignSettings, action="append")
|
||||
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)
|
||||
parser.set_defaults(handler=handlers.Setup, lock=None, no_log=True, no_report=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
@ -202,7 +251,7 @@ def _set_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
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)
|
||||
parser.set_defaults(handler=handlers.Sign, architecture=[])
|
||||
return parser
|
||||
|
||||
|
||||
@ -216,7 +265,7 @@ def _set_status_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
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)
|
||||
parser.set_defaults(handler=handlers.Status, lock=None, no_log=True, no_report=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
@ -235,7 +284,7 @@ def _set_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
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)
|
||||
parser.set_defaults(handler=handlers.StatusUpdate, lock=None, no_log=True, no_report=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
@ -248,7 +297,7 @@ def _set_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
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)
|
||||
parser.set_defaults(handler=handlers.Sync, architecture=[])
|
||||
return parser
|
||||
|
||||
|
||||
@ -265,7 +314,7 @@ def _set_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
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)
|
||||
parser.set_defaults(handler=handlers.Update, architecture=[])
|
||||
return parser
|
||||
|
||||
|
||||
|
@ -65,8 +65,10 @@ class Application:
|
||||
"""
|
||||
known_packages: Set[str] = set()
|
||||
# local set
|
||||
for package in self.repository.packages():
|
||||
known_packages.update(package.packages.keys())
|
||||
for base in self.repository.packages():
|
||||
for package, properties in base.packages.items():
|
||||
known_packages.add(package)
|
||||
known_packages.update(properties.provides)
|
||||
known_packages.update(self.repository.pacman.all_packages())
|
||||
return known_packages
|
||||
|
||||
@ -183,7 +185,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}")
|
||||
self.logger.warning("filepath is empty for %s", package.base)
|
||||
continue # avoid mypy warning
|
||||
src = self.repository.paths.repository / archive.filepath
|
||||
dst = self.repository.paths.packages / archive.filepath
|
||||
@ -209,6 +211,8 @@ class Application:
|
||||
:param updates: list of packages to update
|
||||
"""
|
||||
def process_update(paths: Iterable[Path]) -> None:
|
||||
if not paths:
|
||||
return # don't need to process if no update supplied
|
||||
updated = [Package.load(path, self.repository.pacman, self.repository.aur_url) for path in paths]
|
||||
self.repository.process_update(paths)
|
||||
self._finalize(updated)
|
||||
@ -220,6 +224,6 @@ class Application:
|
||||
# process manual packages
|
||||
tree = Tree.load(updates)
|
||||
for num, level in enumerate(tree.levels()):
|
||||
self.logger.info(f"processing level #{num} {[package.base for package in level]}")
|
||||
self.logger.info("processing level #%i %s", num, [package.base for package in level])
|
||||
packages = self.repository.process_build(level)
|
||||
process_update(packages)
|
||||
|
@ -22,9 +22,12 @@ from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.application.handlers.add import Add
|
||||
from ahriman.application.handlers.clean import Clean
|
||||
from ahriman.application.handlers.dump import Dump
|
||||
from ahriman.application.handlers.init import Init
|
||||
from ahriman.application.handlers.key_import import KeyImport
|
||||
from ahriman.application.handlers.rebuild import Rebuild
|
||||
from ahriman.application.handlers.remove import Remove
|
||||
from ahriman.application.handlers.report import Report
|
||||
from ahriman.application.handlers.search import Search
|
||||
from ahriman.application.handlers.setup import Setup
|
||||
from ahriman.application.handlers.sign import Sign
|
||||
from ahriman.application.handlers.status import Status
|
||||
|
@ -23,10 +23,12 @@ import argparse
|
||||
import logging
|
||||
|
||||
from multiprocessing import Pool
|
||||
from typing import Type
|
||||
from typing import Set, Type
|
||||
|
||||
from ahriman.application.lock import Lock
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import MissingArchitecture
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Handler:
|
||||
@ -58,11 +60,33 @@ class Handler:
|
||||
:param args: command line args
|
||||
:return: 0 on success, 1 otherwise
|
||||
"""
|
||||
with Pool(len(args.architecture)) as pool:
|
||||
architectures = cls.extract_architectures(args)
|
||||
with Pool(len(architectures)) as pool:
|
||||
result = pool.starmap(
|
||||
cls._call, [(args, architecture) for architecture in set(args.architecture)])
|
||||
cls._call, [(args, architecture) for architecture in architectures])
|
||||
return 0 if all(result) else 1
|
||||
|
||||
@classmethod
|
||||
def extract_architectures(cls: Type[Handler], args: argparse.Namespace) -> Set[str]:
|
||||
"""
|
||||
get known architectures
|
||||
:param args: command line args
|
||||
:return: list of architectures for which tree is created
|
||||
"""
|
||||
if args.architecture is None:
|
||||
raise MissingArchitecture(args.command)
|
||||
if args.architecture:
|
||||
return set(args.architecture)
|
||||
|
||||
config = Configuration()
|
||||
config.load(args.configuration)
|
||||
root = config.getpath("repository", "root")
|
||||
architectures = RepositoryPaths.known_architectures(root)
|
||||
|
||||
if not architectures:
|
||||
raise MissingArchitecture(args.command)
|
||||
return architectures
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||
"""
|
||||
|
42
src/ahriman/application/handlers/init.py
Normal file
42
src/ahriman/application/handlers/init.py
Normal 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 Init(Handler):
|
||||
"""
|
||||
repository init handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
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 configuration: configuration instance
|
||||
"""
|
||||
Application(architecture, configuration).repository.repo.init()
|
42
src/ahriman/application/handlers/key_import.py
Normal file
42
src/ahriman/application/handlers/key_import.py
Normal 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 KeyImport(Handler):
|
||||
"""
|
||||
key import packages handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
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 configuration: configuration instance
|
||||
"""
|
||||
Application(architecture, configuration).repository.sign.import_key(args.key_server, args.key)
|
@ -39,10 +39,12 @@ class Rebuild(Handler):
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
"""
|
||||
depends_on = set(args.depends_on) if args.depends_on else None
|
||||
|
||||
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
|
||||
if depends_on is None or depends_on.intersection(package.depends)
|
||||
] # we have to use explicit list here for testing purpose
|
||||
application.update(packages)
|
||||
|
58
src/ahriman/application/handlers/search.py
Normal file
58
src/ahriman/application/handlers/search.py
Normal file
@ -0,0 +1,58 @@
|
||||
#
|
||||
# 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 aur # type: ignore
|
||||
|
||||
from typing import Callable, Type
|
||||
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class Search(Handler):
|
||||
"""
|
||||
packages search handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
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 configuration: configuration instance
|
||||
"""
|
||||
search = " ".join(args.search)
|
||||
packages = aur.search(search)
|
||||
|
||||
# it actually always should return string
|
||||
# explicit cast to string just to avoid mypy warning for untyped library
|
||||
comparator: Callable[[aur.Package], str] = lambda item: str(item.package_base)
|
||||
for package in sorted(packages, key=comparator):
|
||||
Search.log_fn(package)
|
||||
|
||||
@staticmethod
|
||||
def log_fn(package: aur.Package) -> None:
|
||||
"""
|
||||
log package information
|
||||
:param package: package object as from AUR
|
||||
"""
|
||||
print(f"=> {package.package_base} {package.version}")
|
||||
print(f" {package.description}")
|
@ -20,12 +20,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
from types import TracebackType
|
||||
from typing import Literal, Optional, Type
|
||||
|
||||
from ahriman import version
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import DuplicateRun, UnsafeRun
|
||||
from ahriman.core.status.client import Client
|
||||
@ -61,12 +63,13 @@ class Lock:
|
||||
default workflow is the following:
|
||||
|
||||
check user UID
|
||||
remove lock file if force flag is set
|
||||
check if there is lock file
|
||||
check web status watcher status
|
||||
create lock file
|
||||
report to web if enabled
|
||||
"""
|
||||
self.check_user()
|
||||
self.check_version()
|
||||
self.create()
|
||||
self.reporter.update_self(BuildStatusEnum.Building)
|
||||
return self
|
||||
@ -85,6 +88,17 @@ class Lock:
|
||||
self.reporter.update_self(status)
|
||||
return False
|
||||
|
||||
def check_version(self) -> None:
|
||||
"""
|
||||
check web server version
|
||||
"""
|
||||
status = self.reporter.get_internal()
|
||||
if status.version is not None and status.version != version.__version__:
|
||||
logging.getLogger("root").warning(
|
||||
"status watcher version mismatch, our %s, their %s",
|
||||
version.__version__,
|
||||
status.version)
|
||||
|
||||
def check_user(self) -> None:
|
||||
"""
|
||||
check if current user is actually owner of ahriman root
|
||||
|
@ -18,7 +18,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from pyalpm import Handle # type: ignore
|
||||
from typing import List, Set
|
||||
from typing import Set
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
@ -40,13 +40,15 @@ class Pacman:
|
||||
for repository in configuration.getlist("alpm", "repositories"):
|
||||
self.handle.register_syncdb(repository, 0) # 0 is pgp_level
|
||||
|
||||
def all_packages(self) -> List[str]:
|
||||
def all_packages(self) -> Set[str]:
|
||||
"""
|
||||
get list of packages known for alpm
|
||||
:return: list of package names
|
||||
"""
|
||||
result: Set[str] = set()
|
||||
for database in self.handle.get_syncdbs():
|
||||
result.update({package.name for package in database.pkgcache})
|
||||
for package in database.pkgcache:
|
||||
result.add(package.name) # package itself
|
||||
result.update(package.provides) # provides list for meta-packages
|
||||
|
||||
return list(result)
|
||||
return result
|
||||
|
@ -68,6 +68,16 @@ class Repo:
|
||||
cwd=self.paths.repository,
|
||||
logger=self.logger)
|
||||
|
||||
def init(self) -> None:
|
||||
"""
|
||||
create empty repository database
|
||||
"""
|
||||
Repo._check_output(
|
||||
"repo-add", *self.sign_args, str(self.repo_path),
|
||||
exception=None,
|
||||
cwd=self.paths.repository,
|
||||
logger=self.logger)
|
||||
|
||||
def remove(self, package: str, filename: Path) -> None:
|
||||
"""
|
||||
remove package from repository
|
||||
|
@ -99,7 +99,7 @@ class Task:
|
||||
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}")
|
||||
self.logger.info("using %s for %s", command, self.package.base)
|
||||
|
||||
Task._check_output(
|
||||
*command,
|
||||
|
@ -72,7 +72,8 @@ class Configuration(configparser.RawConfigParser):
|
||||
:return: configuration instance
|
||||
"""
|
||||
config = cls()
|
||||
config.load(path, architecture)
|
||||
config.load(path)
|
||||
config.merge_sections(architecture)
|
||||
config.load_logging(logfile)
|
||||
return config
|
||||
|
||||
@ -120,16 +121,14 @@ class Configuration(configparser.RawConfigParser):
|
||||
return value
|
||||
return self.path.parent / value
|
||||
|
||||
def load(self, path: Path, architecture: str) -> None:
|
||||
def load(self, path: Path) -> 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:
|
||||
"""
|
||||
@ -140,7 +139,7 @@ class Configuration(configparser.RawConfigParser):
|
||||
if path == self.logging_path:
|
||||
continue # we don't want to load logging explicitly
|
||||
self.read(path)
|
||||
except (FileNotFoundError, configparser.NoOptionError):
|
||||
except (FileNotFoundError, configparser.NoOptionError, configparser.NoSectionError):
|
||||
pass
|
||||
|
||||
def load_logging(self, logfile: bool) -> None:
|
||||
|
@ -83,6 +83,19 @@ class InvalidPackageInfo(Exception):
|
||||
Exception.__init__(self, f"There are errors during reading package information: `{details}`")
|
||||
|
||||
|
||||
class MissingArchitecture(Exception):
|
||||
"""
|
||||
exception which will be raised if architecture is required, but missing
|
||||
"""
|
||||
|
||||
def __init__(self, command: str) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param command: command name which throws exception
|
||||
"""
|
||||
Exception.__init__(self, f"Architecture required for subcommand {command}, but missing")
|
||||
|
||||
|
||||
class ReportFailed(Exception):
|
||||
"""
|
||||
report generation exception
|
||||
|
@ -85,7 +85,7 @@ class JinjaTemplate:
|
||||
"""
|
||||
# idea comes from https://stackoverflow.com/a/38642558
|
||||
loader = jinja2.FileSystemLoader(searchpath=self.template_path.parent)
|
||||
environment = jinja2.Environment(loader=loader)
|
||||
environment = jinja2.Environment(loader=loader, autoescape=True)
|
||||
template = environment.get_template(self.template_path.name)
|
||||
|
||||
content = [
|
||||
|
@ -61,7 +61,7 @@ class Executor(Cleaner):
|
||||
build_single(single)
|
||||
except Exception:
|
||||
self.reporter.set_failed(single.base)
|
||||
self.logger.exception(f"{single.base} ({self.architecture}) build exception")
|
||||
self.logger.exception("%s (%s) build exception", single.base, self.architecture)
|
||||
self.clear_build()
|
||||
|
||||
return self.packages_built()
|
||||
@ -76,7 +76,7 @@ class Executor(Cleaner):
|
||||
try:
|
||||
self.repo.remove(package, fn)
|
||||
except Exception:
|
||||
self.logger.exception(f"could not remove {package}")
|
||||
self.logger.exception("could not remove %s", package)
|
||||
|
||||
requested = set(packages)
|
||||
for local in self.packages():
|
||||
@ -132,7 +132,7 @@ class Executor(Cleaner):
|
||||
"""
|
||||
def update_single(fn: Optional[str], base: str) -> None:
|
||||
if fn is None:
|
||||
self.logger.warning(f"received empty package name for base {base}")
|
||||
self.logger.warning("received empty package name for base %s", base)
|
||||
return # suppress type checking, it never can be none actually
|
||||
# in theory it might be NOT packages directory, but we suppose it is
|
||||
full_path = self.paths.packages / fn
|
||||
@ -150,7 +150,7 @@ class Executor(Cleaner):
|
||||
local = Package.load(filename, self.pacman, self.aur_url)
|
||||
updates.setdefault(local.base, local).packages.update(local.packages)
|
||||
except Exception:
|
||||
self.logger.exception(f"could not load package from {filename}")
|
||||
self.logger.exception("could not load package from %s", filename)
|
||||
|
||||
for local in updates.values():
|
||||
try:
|
||||
@ -159,7 +159,7 @@ class Executor(Cleaner):
|
||||
self.reporter.set_success(local)
|
||||
except Exception:
|
||||
self.reporter.set_failed(local.base)
|
||||
self.logger.exception(f"could not process {local.base}")
|
||||
self.logger.exception("could not process %s", local.base)
|
||||
self.clear_packages()
|
||||
|
||||
return self.repo.repo_path
|
||||
|
@ -42,7 +42,7 @@ class Repository(Executor, UpdateHandler):
|
||||
local = Package.load(full_path, self.pacman, self.aur_url)
|
||||
result.setdefault(local.base, local).packages.update(local.packages)
|
||||
except Exception:
|
||||
self.logger.exception(f"could not load package from {full_path}")
|
||||
self.logger.exception("could not load package from %s", full_path)
|
||||
continue
|
||||
return list(result.values())
|
||||
|
||||
|
@ -59,7 +59,7 @@ class UpdateHandler(Cleaner):
|
||||
result.append(remote)
|
||||
except Exception:
|
||||
self.reporter.set_failed(local.base)
|
||||
self.logger.exception(f"could not load remote package {local.base}")
|
||||
self.logger.exception("could not load remote package %s", local.base)
|
||||
continue
|
||||
|
||||
return result
|
||||
@ -81,7 +81,7 @@ class UpdateHandler(Cleaner):
|
||||
else:
|
||||
self.reporter.set_pending(local.base)
|
||||
except Exception:
|
||||
self.logger.exception(f"could not add package from {fn}")
|
||||
self.logger.exception("could not add package from %s", fn)
|
||||
self.clear_manual()
|
||||
|
||||
return result
|
||||
|
@ -18,13 +18,14 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Set, Tuple
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import BuildFailed
|
||||
from ahriman.core.util import check_output
|
||||
from ahriman.core.util import check_output, exception_response_text
|
||||
from ahriman.models.sign_settings import SignSettings
|
||||
|
||||
|
||||
@ -87,6 +88,36 @@ class GPG:
|
||||
default_key = configuration.get("sign", "key") if targets else None
|
||||
return targets, default_key
|
||||
|
||||
def download_key(self, server: str, key: str) -> str:
|
||||
"""
|
||||
download key from public PGP server
|
||||
:param server: public PGP server which will be used to download the key
|
||||
:param key: key ID to download
|
||||
:return: key as plain text
|
||||
"""
|
||||
key = key if key.startswith("0x") else f"0x{key}"
|
||||
try:
|
||||
response = requests.get(f"http://{server}/pks/lookup", params={
|
||||
"op": "get",
|
||||
"options": "mr",
|
||||
"search": key
|
||||
})
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception("could not download key %s from %s: %s", key, server, exception_response_text(e))
|
||||
raise
|
||||
return response.text
|
||||
|
||||
def import_key(self, server: str, key: str) -> None:
|
||||
"""
|
||||
import key to current user and sign it locally
|
||||
:param server: public PGP server which will be used to download the key
|
||||
:param key: key ID to import
|
||||
"""
|
||||
key_body = self.download_key(server, key)
|
||||
GPG._check_output("gpg", "--import", input_data=key_body, exception=None, logger=self.logger)
|
||||
GPG._check_output("gpg", "--quick-lsign-key", key, exception=None, logger=self.logger)
|
||||
|
||||
def process(self, path: Path, key: str) -> List[Path]:
|
||||
"""
|
||||
gpg command wrapper
|
||||
@ -111,7 +142,7 @@ class GPG:
|
||||
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")
|
||||
self.logger.error("no default key set, skip package %s sign", path)
|
||||
return [path]
|
||||
return self.process(path, key)
|
||||
|
||||
|
@ -23,6 +23,7 @@ from typing import List, Optional, Tuple, Type
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@ -52,8 +53,7 @@ class Client:
|
||||
:param status: current package build status
|
||||
"""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
|
||||
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]: # pylint: disable=no-self-use
|
||||
"""
|
||||
get package status
|
||||
:param base: package base to get
|
||||
@ -62,8 +62,14 @@ class Client:
|
||||
del base
|
||||
return []
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_self(self) -> BuildStatus:
|
||||
def get_internal(self) -> InternalStatus: # pylint: disable=no-self-use
|
||||
"""
|
||||
get internal service status
|
||||
:return: current internal (web) service status
|
||||
"""
|
||||
return InternalStatus()
|
||||
|
||||
def get_self(self) -> BuildStatus: # pylint: disable=no-self-use
|
||||
"""
|
||||
get ahriman status itself
|
||||
:return: current ahriman status
|
||||
|
@ -90,7 +90,7 @@ class Watcher:
|
||||
try:
|
||||
parse_single(item)
|
||||
except Exception:
|
||||
self.logger.exception(f"cannot parse item f{item} to package")
|
||||
self.logger.exception("cannot parse item %s to package", item)
|
||||
|
||||
def _cache_save(self) -> None:
|
||||
"""
|
||||
|
@ -23,7 +23,9 @@ import requests
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.core.util import exception_response_text
|
||||
from ahriman.models.build_status import BuildStatusEnum, BuildStatus
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@ -45,16 +47,6 @@ 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
|
||||
@ -70,6 +62,13 @@ class WebClient(Client):
|
||||
"""
|
||||
return f"http://{self.host}:{self.port}/api/v1/packages/{base}"
|
||||
|
||||
def _status_url(self) -> str:
|
||||
"""
|
||||
url generator
|
||||
:return: full url for web service for status
|
||||
"""
|
||||
return f"http://{self.host}:{self.port}/api/v1/status"
|
||||
|
||||
def add(self, package: Package, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
add new package with status
|
||||
@ -85,9 +84,9 @@ 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}: {WebClient._exception_response_text(e)}")
|
||||
self.logger.exception("could not add %s: %s", package.base, exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception(f"could not add {package.base}")
|
||||
self.logger.exception("could not add %s", package.base)
|
||||
|
||||
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
|
||||
"""
|
||||
@ -105,11 +104,28 @@ class WebClient(Client):
|
||||
for package in status_json
|
||||
]
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not get {base}: {WebClient._exception_response_text(e)}")
|
||||
self.logger.exception("could not get %s: %s", base, exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception(f"could not get {base}")
|
||||
self.logger.exception("could not get %s", base)
|
||||
return []
|
||||
|
||||
def get_internal(self) -> InternalStatus:
|
||||
"""
|
||||
get internal service status
|
||||
:return: current internal (web) service status
|
||||
"""
|
||||
try:
|
||||
response = requests.get(self._status_url())
|
||||
response.raise_for_status()
|
||||
|
||||
status_json = response.json()
|
||||
return InternalStatus.from_json(status_json)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception("could not get web service status: %s", exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception("could not get web service status")
|
||||
return InternalStatus()
|
||||
|
||||
def get_self(self) -> BuildStatus:
|
||||
"""
|
||||
get ahriman status itself
|
||||
@ -122,7 +138,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: {WebClient._exception_response_text(e)}")
|
||||
self.logger.exception("could not get service status: %s", exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception("could not get service status")
|
||||
return BuildStatus()
|
||||
@ -136,9 +152,9 @@ 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}: {WebClient._exception_response_text(e)}")
|
||||
self.logger.exception("could not delete %s: %s", base, exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception(f"could not delete {base}")
|
||||
self.logger.exception("could not delete %s", base)
|
||||
|
||||
def update(self, base: str, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
@ -152,9 +168,9 @@ 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}: {WebClient._exception_response_text(e)}")
|
||||
self.logger.exception("could not update %s: %s", base, exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception(f"could not update {base}")
|
||||
self.logger.exception("could not update %s", base)
|
||||
|
||||
def update_self(self, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
@ -167,6 +183,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: {WebClient._exception_response_text(e)}")
|
||||
self.logger.exception("could not update service status: %s", exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception("could not update service status")
|
||||
|
@ -17,24 +17,25 @@
|
||||
# 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 boto3 # type: ignore
|
||||
import hashlib
|
||||
import mimetypes
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
from typing import Any, Dict, Generator, Iterable
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.upload.upload import Upload
|
||||
from ahriman.core.util import check_output
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class S3(Upload):
|
||||
"""
|
||||
aws-cli wrapper
|
||||
:ivar bucket: full bucket name
|
||||
:ivar command: command arguments for sync
|
||||
:ivar bucket: boto3 S3 bucket object
|
||||
:ivar chunk_size: chunk size for calculating checksums
|
||||
"""
|
||||
|
||||
_check_output = check_output
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||
"""
|
||||
default constructor
|
||||
@ -42,8 +43,81 @@ class S3(Upload):
|
||||
:param configuration: configuration instance
|
||||
"""
|
||||
Upload.__init__(self, architecture, configuration)
|
||||
self.bucket = configuration.get("s3", "bucket")
|
||||
self.command = configuration.getlist("s3", "command")
|
||||
self.bucket = self.get_bucket(configuration)
|
||||
self.chunk_size = configuration.getint("s3", "chunk_size", fallback=8 * 1024 * 1024)
|
||||
|
||||
@staticmethod
|
||||
def calculate_etag(path: Path, chunk_size: int) -> str:
|
||||
"""
|
||||
calculate amazon s3 etag
|
||||
credits to https://teppen.io/2018/10/23/aws_s3_verify_etags/
|
||||
For this method we have to define nosec because it is out of any security context and provided by AWS
|
||||
:param path: path to local file
|
||||
:param chunk_size: read chunk size, which depends on client settings
|
||||
:return: calculated entity tag for local file
|
||||
"""
|
||||
md5s = []
|
||||
with path.open("rb") as local_file:
|
||||
for chunk in iter(lambda: local_file.read(chunk_size), b""):
|
||||
md5s.append(hashlib.md5(chunk)) # nosec
|
||||
|
||||
# in case if there is only one chunk it must be just this checksum
|
||||
# and checksum of joined digest otherwise (including empty list)
|
||||
checksum = md5s[0] if len(md5s) == 1 else hashlib.md5(b"".join(md5.digest() for md5 in md5s)) # nosec
|
||||
# in case if there are more than one chunk it should be appended with amount of chunks
|
||||
suffix = f"-{len(md5s)}" if len(md5s) > 1 else ""
|
||||
return f"{checksum.hexdigest()}{suffix}"
|
||||
|
||||
@staticmethod
|
||||
def get_bucket(configuration: Configuration) -> Any:
|
||||
"""
|
||||
create resource client from configuration
|
||||
:param configuration: configuration instance
|
||||
:return: amazon client
|
||||
"""
|
||||
client = boto3.resource(service_name="s3",
|
||||
region_name=configuration.get("s3", "region"),
|
||||
aws_access_key_id=configuration.get("s3", "access_key"),
|
||||
aws_secret_access_key=configuration.get("s3", "secret_key"))
|
||||
return client.Bucket(configuration.get("s3", "bucket"))
|
||||
|
||||
@staticmethod
|
||||
def remove_files(local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None:
|
||||
"""
|
||||
remove files which have been removed locally
|
||||
:param local_files: map of local path object to its checksum
|
||||
:param remote_objects: map of remote path object to the remote s3 object
|
||||
"""
|
||||
for local_file, remote_object in remote_objects.items():
|
||||
if local_file in local_files:
|
||||
continue
|
||||
remote_object.delete()
|
||||
|
||||
def get_local_files(self, path: Path) -> Dict[Path, str]:
|
||||
"""
|
||||
get all local files and their calculated checksums
|
||||
:param path: local path to sync
|
||||
:return: map of path object to its checksum
|
||||
"""
|
||||
# credits to https://stackoverflow.com/a/64915960
|
||||
def walk(directory_path: Path) -> Generator[Path, None, None]:
|
||||
for element in directory_path.iterdir():
|
||||
if element.is_dir():
|
||||
yield from walk(element)
|
||||
continue
|
||||
yield element
|
||||
return {
|
||||
local_file.relative_to(path): self.calculate_etag(local_file, self.chunk_size)
|
||||
for local_file in walk(path)
|
||||
}
|
||||
|
||||
def get_remote_objects(self) -> Dict[Path, Any]:
|
||||
"""
|
||||
get all remote objects and their checksums
|
||||
:return: map of path object to the remote s3 object
|
||||
"""
|
||||
objects = self.bucket.objects.filter(Prefix=self.architecture)
|
||||
return {Path(item.key).relative_to(self.architecture): item for item in objects}
|
||||
|
||||
def sync(self, path: Path, built_packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
@ -51,5 +125,29 @@ class S3(Upload):
|
||||
:param path: local path to sync
|
||||
:param built_packages: list of packages which has just been built
|
||||
"""
|
||||
# TODO rewrite to boto, but it is bullshit
|
||||
S3._check_output(*self.command, str(path), self.bucket, exception=None, logger=self.logger)
|
||||
remote_objects = self.get_remote_objects()
|
||||
local_files = self.get_local_files(path)
|
||||
|
||||
self.upload_files(path, local_files, remote_objects)
|
||||
self.remove_files(local_files, remote_objects)
|
||||
|
||||
def upload_files(self, path: Path, local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None:
|
||||
"""
|
||||
upload changed files to s3
|
||||
:param path: local path to sync
|
||||
:param local_files: map of local path object to its checksum
|
||||
:param remote_objects: map of remote path object to the remote s3 object
|
||||
"""
|
||||
for local_file, checksum in local_files.items():
|
||||
remote_object = remote_objects.get(local_file)
|
||||
# 0 and -1 elements are " (double quote)
|
||||
remote_checksum = remote_object.e_tag[1:-1] if remote_object is not None else None
|
||||
if remote_checksum == checksum:
|
||||
continue
|
||||
|
||||
local_path = path / local_file
|
||||
remote_path = Path(self.architecture) / local_file
|
||||
(mime, _) = mimetypes.guess_type(local_path)
|
||||
extra_args = {"Content-Type": mime} if mime is not None else None
|
||||
|
||||
self.bucket.upload_file(Filename=str(local_path), Key=str(remote_path), ExtraArgs=extra_args)
|
||||
|
@ -19,6 +19,7 @@
|
||||
#
|
||||
import datetime
|
||||
import subprocess
|
||||
import requests
|
||||
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
@ -27,29 +28,42 @@ from typing import Optional, Union
|
||||
from ahriman.core.exceptions import InvalidOption
|
||||
|
||||
|
||||
def check_output(*args: str, exception: Optional[Exception],
|
||||
cwd: Optional[Path] = None, logger: Optional[Logger] = None) -> str:
|
||||
def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] = None,
|
||||
input_data: Optional[str] = None, logger: Optional[Logger] = None) -> str:
|
||||
"""
|
||||
subprocess wrapper
|
||||
:param args: command line arguments
|
||||
:param exception: exception which has to be reraised instead of default subprocess exception
|
||||
:param cwd: current working directory
|
||||
:param input_data: data which will be written to command stdin
|
||||
:param logger: logger to log command result if required
|
||||
:return: command output
|
||||
"""
|
||||
try:
|
||||
result = subprocess.check_output(args, cwd=cwd, stderr=subprocess.STDOUT).decode("utf8").strip()
|
||||
# universal_newlines is required to read input from string
|
||||
result: str = subprocess.check_output(args, cwd=cwd, input=input_data, stderr=subprocess.STDOUT,
|
||||
universal_newlines=True).strip()
|
||||
if logger is not None:
|
||||
for line in result.splitlines():
|
||||
logger.debug(line)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.output is not None and logger is not None:
|
||||
for line in e.output.decode("utf8").splitlines():
|
||||
for line in e.output.splitlines():
|
||||
logger.debug(line)
|
||||
raise exception or e
|
||||
return result
|
||||
|
||||
|
||||
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 package_like(filename: Path) -> bool:
|
||||
"""
|
||||
check if file looks like package
|
||||
|
71
src/ahriman/models/counters.py
Normal file
71
src/ahriman/models/counters.py
Normal file
@ -0,0 +1,71 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, fields
|
||||
from typing import Any, Dict, List, Tuple, Type
|
||||
|
||||
from ahriman.models.build_status import BuildStatus
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@dataclass
|
||||
class Counters:
|
||||
"""
|
||||
package counters
|
||||
:ivar total: total packages count
|
||||
:ivar unknown: packages in unknown status count
|
||||
:ivar pending: packages in pending status count
|
||||
:ivar building: packages in building status count
|
||||
:ivar failed: packages in failed status count
|
||||
:ivar success: packages in success status count
|
||||
"""
|
||||
total: int
|
||||
unknown: int = 0
|
||||
pending: int = 0
|
||||
building: int = 0
|
||||
failed: int = 0
|
||||
success: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Counters], dump: Dict[str, Any]) -> Counters:
|
||||
"""
|
||||
construct counters from json dump
|
||||
:param dump: json dump body
|
||||
:return: status counters
|
||||
"""
|
||||
# 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_packages(cls: Type[Counters], packages: List[Tuple[Package, BuildStatus]]) -> Counters:
|
||||
"""
|
||||
construct counters from packages statuses
|
||||
:param packages: list of package and their status as per watcher property
|
||||
:return: status counters
|
||||
"""
|
||||
per_status = {"total": len(packages)}
|
||||
for _, status in packages:
|
||||
key = status.status.name.lower()
|
||||
per_status.setdefault(key, 0)
|
||||
per_status[key] += 1
|
||||
return cls(**per_status)
|
60
src/ahriman/models/internal_status.py
Normal file
60
src/ahriman/models/internal_status.py
Normal file
@ -0,0 +1,60 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from typing import Any, Dict, Optional, Type
|
||||
|
||||
from ahriman.models.counters import Counters
|
||||
|
||||
|
||||
@dataclass
|
||||
class InternalStatus:
|
||||
"""
|
||||
internal server status
|
||||
:ivar architecture: repository architecture
|
||||
:ivar packages: packages statuses counter object
|
||||
:ivar repository: repository name
|
||||
:ivar version: service version
|
||||
"""
|
||||
architecture: Optional[str] = None
|
||||
packages: Counters = field(default=Counters(total=0))
|
||||
repository: Optional[str] = None
|
||||
version: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[InternalStatus], dump: Dict[str, Any]) -> InternalStatus:
|
||||
"""
|
||||
construct internal status from json dump
|
||||
:param dump: json dump body
|
||||
:return: internal status
|
||||
"""
|
||||
counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0)
|
||||
return cls(architecture=dump.get("architecture"),
|
||||
packages=counters,
|
||||
repository=dump.get("repository"),
|
||||
version=dump.get("version"))
|
||||
|
||||
def view(self) -> Dict[str, Any]:
|
||||
"""
|
||||
generate json status view
|
||||
:return: json-friendly dictionary
|
||||
"""
|
||||
return asdict(self)
|
@ -38,6 +38,7 @@ class PackageDescription:
|
||||
:ivar groups: package groups
|
||||
:ivar installed_size: package installed size
|
||||
:ivar licenses: package licenses list
|
||||
:ivar provides: list of provided packages
|
||||
:ivar url: package url
|
||||
"""
|
||||
|
||||
@ -50,6 +51,7 @@ class PackageDescription:
|
||||
groups: List[str] = field(default_factory=list)
|
||||
installed_size: Optional[int] = None
|
||||
licenses: List[str] = field(default_factory=list)
|
||||
provides: List[str] = field(default_factory=list)
|
||||
url: Optional[str] = None
|
||||
|
||||
@property
|
||||
@ -89,4 +91,5 @@ class PackageDescription:
|
||||
groups=package.groups,
|
||||
installed_size=package.isize,
|
||||
licenses=package.licenses,
|
||||
provides=package.provides,
|
||||
url=package.url)
|
||||
|
@ -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 pathlib import Path
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Set, Type
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -76,6 +78,20 @@ class RepositoryPaths:
|
||||
"""
|
||||
return self.root / "sources" / self.architecture
|
||||
|
||||
@classmethod
|
||||
def known_architectures(cls: Type[RepositoryPaths], root: Path) -> Set[str]:
|
||||
"""
|
||||
get known architectures
|
||||
:param root: repository root
|
||||
:return: list of architectures for which tree is created
|
||||
"""
|
||||
paths = cls(root, "")
|
||||
return {
|
||||
path.name
|
||||
for path in paths.repository.iterdir()
|
||||
if path.is_dir()
|
||||
}
|
||||
|
||||
def create_tree(self) -> None:
|
||||
"""
|
||||
create ahriman working tree
|
||||
|
@ -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.22.1"
|
||||
__version__ = "1.2.3"
|
||||
|
@ -40,7 +40,7 @@ def exception_handler(logger: Logger) -> Callable[[Request, HandlerType], Awaita
|
||||
except HTTPClientError:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception(f"exception during performing request to {request.path}")
|
||||
logger.exception("exception during performing request to %s", request.path)
|
||||
raise
|
||||
|
||||
return handle
|
||||
|
@ -23,6 +23,7 @@ from ahriman.web.views.ahriman import AhrimanView
|
||||
from ahriman.web.views.index import IndexView
|
||||
from ahriman.web.views.package import PackageView
|
||||
from ahriman.web.views.packages import PackagesView
|
||||
from ahriman.web.views.status import StatusView
|
||||
|
||||
|
||||
def setup_routes(application: Application) -> None:
|
||||
@ -44,6 +45,8 @@ def setup_routes(application: Application) -> None:
|
||||
GET /api/v1/package/:base get package base status
|
||||
POST /api/v1/package/:base update package base status
|
||||
|
||||
GET /api/v1/status get web service status itself
|
||||
|
||||
:param application: web application instance
|
||||
"""
|
||||
application.router.add_get("/", IndexView)
|
||||
@ -58,3 +61,5 @@ def setup_routes(application: Application) -> None:
|
||||
application.router.add_delete("/api/v1/packages/{package}", PackageView)
|
||||
application.router.add_get("/api/v1/packages/{package}", PackageView)
|
||||
application.router.add_post("/api/v1/packages/{package}", PackageView)
|
||||
|
||||
application.router.add_get("/api/v1/status", StatusView)
|
||||
|
@ -21,8 +21,7 @@ import aiohttp_jinja2
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
import ahriman.version as version
|
||||
|
||||
from ahriman import version
|
||||
from ahriman.core.util import pretty_datetime
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
45
src/ahriman/web/views/status.py
Normal file
45
src/ahriman/web/views/status.py
Normal file
@ -0,0 +1,45 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
from aiohttp.web import Response, json_response
|
||||
|
||||
from ahriman import version
|
||||
from ahriman.models.counters import Counters
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
class StatusView(BaseView):
|
||||
"""
|
||||
web service status web view
|
||||
"""
|
||||
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
get current service status
|
||||
:return: 200 with service status object
|
||||
"""
|
||||
counters = Counters.from_packages(self.service.packages)
|
||||
status = InternalStatus(
|
||||
architecture=self.service.architecture,
|
||||
packages=counters,
|
||||
repository=self.service.repository.name,
|
||||
version=version.__version__)
|
||||
|
||||
return json_response(status.view())
|
@ -1,4 +1,5 @@
|
||||
import argparse
|
||||
import aur
|
||||
import pytest
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
@ -7,24 +8,70 @@ from ahriman.application.ahriman import _parser
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.lock import Lock
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def application(configuration: Configuration, mocker: MockerFixture) -> Application:
|
||||
"""
|
||||
fixture for application
|
||||
:param configuration: configuration fixture
|
||||
:param mocker: mocker object
|
||||
:return: application test instance
|
||||
"""
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
return Application("x86_64", configuration)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def args() -> argparse.Namespace:
|
||||
"""
|
||||
fixture for command line arguments
|
||||
:return: command line arguments test instance
|
||||
"""
|
||||
return argparse.Namespace(lock=None, force=False, unsafe=False, no_report=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def aur_package_ahriman(package_ahriman: Package) -> aur.Package:
|
||||
"""
|
||||
fixture for AUR package
|
||||
:param package_ahriman: package fixture
|
||||
:return: AUR package test instance
|
||||
"""
|
||||
return aur.Package(
|
||||
num_votes=None,
|
||||
description=package_ahriman.packages[package_ahriman.base].description,
|
||||
url_path=package_ahriman.web_url,
|
||||
last_modified=None,
|
||||
name=package_ahriman.base,
|
||||
out_of_date=None,
|
||||
id=None,
|
||||
first_submitted=None,
|
||||
maintainer=None,
|
||||
version=package_ahriman.version,
|
||||
license=package_ahriman.packages[package_ahriman.base].licenses,
|
||||
url=None,
|
||||
package_base=package_ahriman.base,
|
||||
package_base_id=None,
|
||||
category_id=None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lock(args: argparse.Namespace, configuration: Configuration) -> Lock:
|
||||
"""
|
||||
fixture for file lock
|
||||
:param args: command line arguments fixture
|
||||
:param configuration: configuration fixture
|
||||
:return: file lock test instance
|
||||
"""
|
||||
return Lock(args, "x86_64", configuration)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
"""
|
||||
fixture for command line arguments parser
|
||||
:return: command line arguments parser test instance
|
||||
"""
|
||||
return _parser()
|
||||
|
@ -6,6 +6,7 @@ from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import MissingArchitecture
|
||||
|
||||
|
||||
def test_call(args: argparse.Namespace, mocker: MockerFixture) -> None:
|
||||
@ -43,7 +44,52 @@ def test_execute(args: argparse.Namespace, mocker: MockerFixture) -> None:
|
||||
starmap_mock.assert_called_once()
|
||||
|
||||
|
||||
def test_packages(args: argparse.Namespace, configuration: Configuration) -> None:
|
||||
def test_extract_architectures(args: argparse.Namespace, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must generate list of available architectures
|
||||
"""
|
||||
args.architecture = []
|
||||
args.configuration = Path("")
|
||||
mocker.patch("ahriman.core.configuration.Configuration.getpath")
|
||||
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
|
||||
|
||||
Handler.extract_architectures(args)
|
||||
known_architectures_mock.assert_called_once()
|
||||
|
||||
|
||||
def test_extract_architectures_empty(args: argparse.Namespace, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must raise exception if no available architectures found
|
||||
"""
|
||||
args.architecture = []
|
||||
args.command = "config"
|
||||
args.configuration = Path("")
|
||||
mocker.patch("ahriman.core.configuration.Configuration.getpath")
|
||||
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures", return_value=set())
|
||||
|
||||
with pytest.raises(MissingArchitecture):
|
||||
Handler.extract_architectures(args)
|
||||
|
||||
|
||||
def test_extract_architectures_exception(args: argparse.Namespace) -> None:
|
||||
"""
|
||||
must raise exception on missing architectures
|
||||
"""
|
||||
args.command = "config"
|
||||
args.architecture = None
|
||||
with pytest.raises(MissingArchitecture):
|
||||
Handler.extract_architectures(args)
|
||||
|
||||
|
||||
def test_extract_architectures_specified(args: argparse.Namespace) -> None:
|
||||
"""
|
||||
must return architecture list if it has been specified
|
||||
"""
|
||||
architectures = args.architecture = ["i686", "x86_64"]
|
||||
assert Handler.extract_architectures(args) == set(architectures)
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration) -> None:
|
||||
"""
|
||||
must raise NotImplemented for missing method
|
||||
"""
|
||||
|
@ -7,6 +7,11 @@ from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
:param args: command line arguments fixture
|
||||
:return: generated arguments for these test cases
|
||||
"""
|
||||
args.package = []
|
||||
args.now = False
|
||||
args.without_dependencies = False
|
||||
|
@ -7,6 +7,11 @@ from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
:param args: command line arguments fixture
|
||||
:return: generated arguments for these test cases
|
||||
"""
|
||||
args.no_build = False
|
||||
args.no_cache = False
|
||||
args.no_chroot = False
|
||||
|
18
tests/ahriman/application/handlers/test_handler_init.py
Normal file
18
tests/ahriman/application/handlers/test_handler_init.py
Normal file
@ -0,0 +1,18 @@
|
||||
import argparse
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers import Init
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
create_tree_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.create_tree")
|
||||
init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init")
|
||||
|
||||
Init.run(args, "x86_64", configuration)
|
||||
create_tree_mock.assert_called_once()
|
||||
init_mock.assert_called_once()
|
@ -0,0 +1,29 @@
|
||||
import argparse
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers import KeyImport
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
:param args: command line arguments fixture
|
||||
:return: generated arguments for these test cases
|
||||
"""
|
||||
args.key = "0xE989490C"
|
||||
args.key_server = "keys.gnupg.net"
|
||||
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.core.sign.gpg.GPG.import_key")
|
||||
|
||||
KeyImport.run(args, "x86_64", configuration)
|
||||
application_mock.assert_called_once()
|
@ -8,7 +8,12 @@ from ahriman.models.package import Package
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
args.depends_on = None
|
||||
"""
|
||||
default arguments for these test cases
|
||||
:param args: command line arguments fixture
|
||||
:return: generated arguments for these test cases
|
||||
"""
|
||||
args.depends_on = []
|
||||
return args
|
||||
|
||||
|
||||
@ -33,7 +38,7 @@ def test_run_filter(args: argparse.Namespace, configuration: Configuration,
|
||||
must run command with depends filter
|
||||
"""
|
||||
args = _default_args(args)
|
||||
args.depends_on = "python-aur"
|
||||
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])
|
||||
|
@ -7,6 +7,11 @@ from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
:param args: command line arguments fixture
|
||||
:return: generated arguments for these test cases
|
||||
"""
|
||||
args.package = []
|
||||
return args
|
||||
|
||||
|
@ -7,6 +7,11 @@ from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
:param args: command line arguments fixture
|
||||
:return: generated arguments for these test cases
|
||||
"""
|
||||
args.target = []
|
||||
return args
|
||||
|
||||
|
55
tests/ahriman/application/handlers/test_handler_search.py
Normal file
55
tests/ahriman/application/handlers/test_handler_search.py
Normal file
@ -0,0 +1,55 @@
|
||||
import argparse
|
||||
import aur
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers import Search
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
:param args: command line arguments fixture
|
||||
:return: generated arguments for these test cases
|
||||
"""
|
||||
args.search = ["ahriman"]
|
||||
return args
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: aur.Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
args = _default_args(args)
|
||||
mocker.patch("aur.search", return_value=[aur_package_ahriman])
|
||||
log_mock = mocker.patch("ahriman.application.handlers.search.Search.log_fn")
|
||||
|
||||
Search.run(args, "x86_64", configuration)
|
||||
log_mock.assert_called_once()
|
||||
|
||||
|
||||
def test_run_multiple_search(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command with multiple search arguments
|
||||
"""
|
||||
args = _default_args(args)
|
||||
args.search = ["ahriman", "is", "cool"]
|
||||
search_mock = mocker.patch("aur.search")
|
||||
|
||||
Search.run(args, "x86_64", configuration)
|
||||
search_mock.assert_called_with(" ".join(args.search))
|
||||
|
||||
|
||||
def test_log_fn(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: aur.Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
log function must call print built-in
|
||||
"""
|
||||
args = _default_args(args)
|
||||
mocker.patch("aur.search", return_value=[aur_package_ahriman])
|
||||
print_mock = mocker.patch("builtins.print")
|
||||
|
||||
Search.run(args, "x86_64", configuration)
|
||||
print_mock.assert_called() # we don't really care about call details tbh
|
@ -11,6 +11,11 @@ from ahriman.models.sign_settings import SignSettings
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
:param args: command line arguments fixture
|
||||
:return: generated arguments for these test cases
|
||||
"""
|
||||
args.build_command = "ahriman"
|
||||
args.from_configuration = Path("/usr/share/devtools/pacman-extra.conf")
|
||||
args.no_multilib = False
|
||||
|
@ -7,6 +7,11 @@ from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
:param args: command line arguments fixture
|
||||
:return: generated arguments for these test cases
|
||||
"""
|
||||
args.package = []
|
||||
return args
|
||||
|
||||
|
@ -9,6 +9,11 @@ from ahriman.models.package import Package
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
:param args: command line arguments fixture
|
||||
:return: generated arguments for these test cases
|
||||
"""
|
||||
args.ahriman = True
|
||||
args.package = []
|
||||
return args
|
||||
|
@ -9,6 +9,11 @@ from ahriman.models.package import Package
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
:param args: command line arguments fixture
|
||||
:return: generated arguments for these test cases
|
||||
"""
|
||||
args.status = BuildStatusEnum.Success
|
||||
args.package = None
|
||||
args.remove = False
|
||||
|
@ -7,6 +7,11 @@ from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
:param args: command line arguments fixture
|
||||
:return: generated arguments for these test cases
|
||||
"""
|
||||
args.target = []
|
||||
return args
|
||||
|
||||
|
@ -8,6 +8,11 @@ from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
:param args: command line arguments fixture
|
||||
:return: generated arguments for these test cases
|
||||
"""
|
||||
args.package = []
|
||||
args.dry_run = False
|
||||
args.no_aur = False
|
||||
|
@ -29,9 +29,9 @@ def test_parser_option_lock(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
must convert lock option to Path instance
|
||||
"""
|
||||
args = parser.parse_args(["-a", "x86_64", "update"])
|
||||
args = parser.parse_args(["update"])
|
||||
assert isinstance(args.lock, Path)
|
||||
args = parser.parse_args(["-a", "x86_64", "-l", "ahriman.lock", "update"])
|
||||
args = parser.parse_args(["-l", "ahriman.lock", "update"])
|
||||
assert isinstance(args.lock, Path)
|
||||
|
||||
|
||||
@ -43,11 +43,20 @@ def test_multiple_architectures(parser: argparse.ArgumentParser) -> None:
|
||||
assert len(args.architecture) == 2
|
||||
|
||||
|
||||
def test_subparsers_add(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
add command must imply empty architectures list
|
||||
"""
|
||||
args = parser.parse_args(["add", "ahriman"])
|
||||
assert args.architecture == []
|
||||
|
||||
|
||||
def test_subparsers_check(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
check command must imply no_aur, no_manual and dry_run
|
||||
check command must imply empty architecture list, no-aur, no-manual and dry-run
|
||||
"""
|
||||
args = parser.parse_args(["-a", "x86_64", "check"])
|
||||
args = parser.parse_args(["check"])
|
||||
assert args.architecture == []
|
||||
assert not args.no_aur
|
||||
assert args.no_manual
|
||||
assert args.dry_run
|
||||
@ -55,29 +64,87 @@ def test_subparsers_check(parser: argparse.ArgumentParser) -> None:
|
||||
|
||||
def test_subparsers_clean(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
clean command must imply unsafe
|
||||
clean command must imply empty architectures list, unsafe and no-log
|
||||
"""
|
||||
args = parser.parse_args(["-a", "x86_64", "clean"])
|
||||
args = parser.parse_args(["clean"])
|
||||
assert args.architecture == []
|
||||
assert args.no_log
|
||||
assert args.unsafe
|
||||
|
||||
|
||||
def test_subparsers_config(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
config command must imply lock, no_report and unsafe
|
||||
config command must imply lock, no-log, no-report and unsafe
|
||||
"""
|
||||
args = parser.parse_args(["-a", "x86_64", "config"])
|
||||
args = parser.parse_args(["config"])
|
||||
assert args.lock is None
|
||||
assert args.no_log
|
||||
assert args.no_report
|
||||
assert args.unsafe
|
||||
|
||||
|
||||
def test_subparsers_init(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
init command must imply no_report
|
||||
"""
|
||||
args = parser.parse_args(["init"])
|
||||
assert args.no_report
|
||||
|
||||
|
||||
def test_subparsers_key_import(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
key-import command must imply architecture list, lock and no-report
|
||||
"""
|
||||
args = parser.parse_args(["key-import", "key"])
|
||||
assert args.architecture == [""]
|
||||
assert args.lock is None
|
||||
assert args.no_report
|
||||
|
||||
|
||||
def test_subparsers_rebuild(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
rebuild command must imply empty architectures list
|
||||
"""
|
||||
args = parser.parse_args(["rebuild"])
|
||||
assert args.architecture == []
|
||||
|
||||
|
||||
def test_subparsers_remove(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
remove command must imply empty architectures list
|
||||
"""
|
||||
args = parser.parse_args(["remove", "ahriman"])
|
||||
assert args.architecture == []
|
||||
|
||||
|
||||
def test_subparsers_report(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
report command must imply empty architectures list
|
||||
"""
|
||||
args = parser.parse_args(["report"])
|
||||
assert args.architecture == []
|
||||
|
||||
|
||||
def test_subparsers_search(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
search command must imply architecture list, lock, no-log, no-report and unsafe
|
||||
"""
|
||||
args = parser.parse_args(["search", "ahriman"])
|
||||
assert args.architecture == [""]
|
||||
assert args.lock is None
|
||||
assert args.no_log
|
||||
assert args.no_report
|
||||
assert args.unsafe
|
||||
|
||||
|
||||
def test_subparsers_setup(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
setup command must imply lock, no_report and unsafe
|
||||
setup command must imply lock, no-log, no-report and unsafe
|
||||
"""
|
||||
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_log
|
||||
assert args.no_report
|
||||
assert args.unsafe
|
||||
|
||||
@ -104,22 +171,32 @@ def test_subparsers_setup_option_sign_target(parser: argparse.ArgumentParser) ->
|
||||
assert all(isinstance(target, SignSettings) for target in args.sign_target)
|
||||
|
||||
|
||||
def test_subparsers_sign(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
sign command must imply empty architectures list
|
||||
"""
|
||||
args = parser.parse_args(["sign"])
|
||||
assert args.architecture == []
|
||||
|
||||
|
||||
def test_subparsers_status(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
status command must imply lock, no_report and unsafe
|
||||
status command must imply lock, no-log, no-report and unsafe
|
||||
"""
|
||||
args = parser.parse_args(["-a", "x86_64", "status"])
|
||||
assert args.lock is None
|
||||
assert args.no_log
|
||||
assert args.no_report
|
||||
assert args.unsafe
|
||||
|
||||
|
||||
def test_subparsers_status_update(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
status-update command must imply lock, no_report and unsafe
|
||||
status-update command must imply lock, no-log, no-report and unsafe
|
||||
"""
|
||||
args = parser.parse_args(["-a", "x86_64", "status-update"])
|
||||
assert args.lock is None
|
||||
assert args.no_log
|
||||
assert args.no_report
|
||||
assert args.unsafe
|
||||
|
||||
@ -134,6 +211,22 @@ def test_subparsers_status_update_option_status(parser: argparse.ArgumentParser)
|
||||
assert isinstance(args.status, BuildStatusEnum)
|
||||
|
||||
|
||||
def test_subparsers_sync(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
sync command must imply empty architectures list
|
||||
"""
|
||||
args = parser.parse_args(["sync"])
|
||||
assert args.architecture == []
|
||||
|
||||
|
||||
def test_subparsers_update(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
update command must imply empty architectures list
|
||||
"""
|
||||
args = parser.parse_args(["update"])
|
||||
assert args.architecture == []
|
||||
|
||||
|
||||
def test_subparsers_web(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
web command must imply lock and no_report
|
||||
|
@ -299,5 +299,5 @@ def test_update(application: Application, package_ahriman: Package, mocker: Mock
|
||||
|
||||
application.update([package_ahriman])
|
||||
build_mock.assert_called_once()
|
||||
update_mock.assert_has_calls([mock.call([]), mock.call(paths)])
|
||||
finalize_mock.assert_has_calls([mock.call([]), mock.call([package_ahriman])])
|
||||
update_mock.assert_called_with(paths)
|
||||
finalize_mock.assert_called_with([package_ahriman])
|
||||
|
@ -5,9 +5,11 @@ from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest import mock
|
||||
|
||||
from ahriman import version
|
||||
from ahriman.application.lock import Lock
|
||||
from ahriman.core.exceptions import DuplicateRun, UnsafeRun
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
|
||||
|
||||
def test_enter(lock: Lock, mocker: MockerFixture) -> None:
|
||||
@ -15,6 +17,7 @@ def test_enter(lock: Lock, mocker: MockerFixture) -> None:
|
||||
must process with context manager
|
||||
"""
|
||||
check_user_mock = mocker.patch("ahriman.application.lock.Lock.check_user")
|
||||
check_version_mock = mocker.patch("ahriman.application.lock.Lock.check_version")
|
||||
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")
|
||||
@ -24,6 +27,7 @@ def test_enter(lock: Lock, mocker: MockerFixture) -> None:
|
||||
check_user_mock.assert_called_once()
|
||||
clear_mock.assert_called_once()
|
||||
create_mock.assert_called_once()
|
||||
check_version_mock.assert_called_once()
|
||||
update_status_mock.assert_has_calls([
|
||||
mock.call(BuildStatusEnum.Building),
|
||||
mock.call(BuildStatusEnum.Success)
|
||||
@ -48,6 +52,30 @@ def test_exit_with_exception(lock: Lock, mocker: MockerFixture) -> None:
|
||||
])
|
||||
|
||||
|
||||
def test_check_version(lock: Lock, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must check version correctly
|
||||
"""
|
||||
mocker.patch("ahriman.core.status.client.Client.get_internal",
|
||||
return_value=InternalStatus(version=version.__version__))
|
||||
logging_mock = mocker.patch("logging.Logger.warning")
|
||||
|
||||
lock.check_version()
|
||||
logging_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_check_version_mismatch(lock: Lock, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must check version correctly
|
||||
"""
|
||||
mocker.patch("ahriman.core.status.client.Client.get_internal",
|
||||
return_value=InternalStatus(version="version"))
|
||||
logging_mock = mocker.patch("logging.Logger.warning")
|
||||
|
||||
lock.check_version()
|
||||
logging_mock.assert_called_once()
|
||||
|
||||
|
||||
def test_check_user(lock: Lock, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must check user correctly
|
||||
@ -83,7 +111,7 @@ def test_clear(lock: Lock) -> None:
|
||||
"""
|
||||
must remove lock file
|
||||
"""
|
||||
lock.path = Path(tempfile.mktemp())
|
||||
lock.path = Path(tempfile.mktemp()) # nosec
|
||||
lock.path.touch()
|
||||
|
||||
lock.clear()
|
||||
@ -94,7 +122,7 @@ def test_clear_missing(lock: Lock) -> None:
|
||||
"""
|
||||
must not fail on lock removal if file is missing
|
||||
"""
|
||||
lock.path = Path(tempfile.mktemp())
|
||||
lock.path = Path(tempfile.mktemp()) # nosec
|
||||
lock.clear()
|
||||
|
||||
|
||||
@ -111,7 +139,7 @@ def test_create(lock: Lock) -> None:
|
||||
"""
|
||||
must create lock
|
||||
"""
|
||||
lock.path = Path(tempfile.mktemp())
|
||||
lock.path = Path(tempfile.mktemp()) # nosec
|
||||
|
||||
lock.create()
|
||||
assert lock.path.is_file()
|
||||
@ -122,7 +150,7 @@ def test_create_exception(lock: Lock) -> None:
|
||||
"""
|
||||
must raise exception if file already exists
|
||||
"""
|
||||
lock.path = Path(tempfile.mktemp())
|
||||
lock.path = Path(tempfile.mktemp()) # nosec
|
||||
lock.path.touch()
|
||||
|
||||
with pytest.raises(DuplicateRun):
|
||||
@ -144,7 +172,7 @@ def test_create_unsafe(lock: Lock) -> None:
|
||||
must not raise exception if force flag set
|
||||
"""
|
||||
lock.force = True
|
||||
lock.path = Path(tempfile.mktemp())
|
||||
lock.path = Path(tempfile.mktemp()) # nosec
|
||||
lock.path.touch()
|
||||
|
||||
lock.create()
|
||||
|
@ -1,3 +1,5 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from pathlib import Path
|
||||
@ -10,6 +12,7 @@ from ahriman.models.package import Package
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@ -17,21 +20,47 @@ T = TypeVar("T")
|
||||
# https://stackoverflow.com/a/21611963
|
||||
@pytest.helpers.register
|
||||
def anyvar(cls: Type[T], strict: bool = False) -> T:
|
||||
"""
|
||||
any value helper for mocker calls check
|
||||
:param cls: type class
|
||||
:param strict: if True then check type of supplied argument
|
||||
:return: any wrapper
|
||||
"""
|
||||
class AnyVar(cls):
|
||||
"""
|
||||
any value wrapper
|
||||
"""
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""
|
||||
compare object to other
|
||||
:param other: other object to compare
|
||||
:return: True in case if objects are equal
|
||||
"""
|
||||
return not strict or isinstance(other, cls)
|
||||
|
||||
return AnyVar()
|
||||
|
||||
|
||||
# generic fixtures
|
||||
@pytest.fixture
|
||||
def configuration(resource_path_root: Path) -> Configuration:
|
||||
"""
|
||||
configuration fixture
|
||||
:param resource_path_root: resource path root directory
|
||||
:return: configuration test instance
|
||||
"""
|
||||
path = resource_path_root / "core" / "ahriman.ini"
|
||||
return Configuration.from_path(path=path, architecture="x86_64", logfile=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def package_ahriman(package_description_ahriman: PackageDescription) -> Package:
|
||||
"""
|
||||
package fixture
|
||||
:param package_description_ahriman: description fixture
|
||||
:return: package test instance
|
||||
"""
|
||||
packages = {"ahriman": package_description_ahriman}
|
||||
return Package(
|
||||
base="ahriman",
|
||||
@ -44,6 +73,12 @@ def package_ahriman(package_description_ahriman: PackageDescription) -> Package:
|
||||
def package_python_schedule(
|
||||
package_description_python_schedule: PackageDescription,
|
||||
package_description_python2_schedule: PackageDescription) -> Package:
|
||||
"""
|
||||
multi package fixture
|
||||
:param package_description_python_schedule: description fixture
|
||||
:param package_description_python2_schedule: description fixture
|
||||
:return: multi package test instance
|
||||
"""
|
||||
packages = {
|
||||
"python-schedule": package_description_python_schedule,
|
||||
"python2-schedule": package_description_python2_schedule
|
||||
@ -57,6 +92,10 @@ def package_python_schedule(
|
||||
|
||||
@pytest.fixture
|
||||
def package_description_ahriman() -> PackageDescription:
|
||||
"""
|
||||
package description fixture
|
||||
:return: package description test instance
|
||||
"""
|
||||
return PackageDescription(
|
||||
architecture="x86_64",
|
||||
archive_size=4200,
|
||||
@ -72,6 +111,10 @@ def package_description_ahriman() -> PackageDescription:
|
||||
|
||||
@pytest.fixture
|
||||
def package_description_python_schedule() -> PackageDescription:
|
||||
"""
|
||||
package description fixture
|
||||
:return: package description test instance
|
||||
"""
|
||||
return PackageDescription(
|
||||
architecture="x86_64",
|
||||
archive_size=4201,
|
||||
@ -87,6 +130,10 @@ def package_description_python_schedule() -> PackageDescription:
|
||||
|
||||
@pytest.fixture
|
||||
def package_description_python2_schedule() -> PackageDescription:
|
||||
"""
|
||||
package description fixture
|
||||
:return: package description test instance
|
||||
"""
|
||||
return PackageDescription(
|
||||
architecture="x86_64",
|
||||
archive_size=4202,
|
||||
@ -102,6 +149,10 @@ def package_description_python2_schedule() -> PackageDescription:
|
||||
|
||||
@pytest.fixture
|
||||
def repository_paths(configuration: Configuration) -> RepositoryPaths:
|
||||
"""
|
||||
repository paths fixture
|
||||
:return: repository paths test instance
|
||||
"""
|
||||
return RepositoryPaths(
|
||||
architecture="x86_64",
|
||||
root=configuration.getpath("repository", "root"))
|
||||
@ -109,5 +160,11 @@ def repository_paths(configuration: Configuration) -> RepositoryPaths:
|
||||
|
||||
@pytest.fixture
|
||||
def watcher(configuration: Configuration, mocker: MockerFixture) -> Watcher:
|
||||
"""
|
||||
package status watcher fixture
|
||||
:param configuration: configuration fixture
|
||||
:param mocker: mocker object
|
||||
:return: package status watcher test instance
|
||||
"""
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
return Watcher("x86_64", configuration)
|
||||
|
@ -8,3 +8,10 @@ def test_all_packages(pacman: Pacman) -> None:
|
||||
packages = pacman.all_packages()
|
||||
assert packages
|
||||
assert "pacman" in packages
|
||||
|
||||
|
||||
def test_all_packages_with_provides(pacman: Pacman) -> None:
|
||||
"""
|
||||
package list must contain provides packages
|
||||
"""
|
||||
assert "sh" in pacman.all_packages()
|
||||
|
@ -24,6 +24,17 @@ def test_repo_add(repo: Repo, mocker: MockerFixture) -> None:
|
||||
assert check_output_mock.call_args[0][0] == "repo-add"
|
||||
|
||||
|
||||
def test_repo_init(repo: Repo, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must call repo-add with empty package list on repo initializing
|
||||
"""
|
||||
check_output_mock = mocker.patch("ahriman.core.alpm.repo.Repo._check_output")
|
||||
|
||||
repo.init()
|
||||
check_output_mock.assert_called_once()
|
||||
assert check_output_mock.call_args[0][0] == "repo-add"
|
||||
|
||||
|
||||
def test_repo_remove(repo: Repo, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must call repo-remove on package addition
|
||||
|
@ -11,24 +11,52 @@ from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
@pytest.fixture
|
||||
def leaf_ahriman(package_ahriman: Package) -> Leaf:
|
||||
"""
|
||||
fixture for tree leaf with package
|
||||
:param package_ahriman: package fixture
|
||||
:return: tree leaf test instance
|
||||
"""
|
||||
return Leaf(package_ahriman, set())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def leaf_python_schedule(package_python_schedule: Package) -> Leaf:
|
||||
"""
|
||||
fixture for tree leaf with package
|
||||
:param package_python_schedule: package fixture
|
||||
:return: tree leaf test instance
|
||||
"""
|
||||
return Leaf(package_python_schedule, set())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pacman(configuration: Configuration) -> Pacman:
|
||||
"""
|
||||
fixture for pacman wrapper
|
||||
:param configuration: configuration fixture
|
||||
:return: pacman wrapper test instance
|
||||
"""
|
||||
return Pacman(configuration)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repo(configuration: Configuration, repository_paths: RepositoryPaths) -> Repo:
|
||||
"""
|
||||
fixture for repository wrapper
|
||||
:param configuration: configuration fixture
|
||||
:param repository_paths: repository paths fixture
|
||||
:return: repository wrapper test instance
|
||||
"""
|
||||
return Repo(configuration.get("repository", "name"), repository_paths, [])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def task_ahriman(package_ahriman: Package, configuration: Configuration, repository_paths: RepositoryPaths) -> Task:
|
||||
"""
|
||||
fixture for built task
|
||||
:param package_ahriman: package fixture
|
||||
:param configuration: configuration fixture
|
||||
:param repository_paths: repository paths fixture
|
||||
:return: built task test instance
|
||||
"""
|
||||
return Task(package_ahriman, configuration, repository_paths)
|
||||
|
@ -12,12 +12,24 @@ from ahriman.core.repository.update_handler import UpdateHandler
|
||||
|
||||
@pytest.fixture
|
||||
def cleaner(configuration: Configuration, mocker: MockerFixture) -> Cleaner:
|
||||
"""
|
||||
fixture for cleaner
|
||||
:param configuration: configuration fixture
|
||||
:param mocker: mocker object
|
||||
:return: cleaner test instance
|
||||
"""
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
return Cleaner("x86_64", configuration)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def executor(configuration: Configuration, mocker: MockerFixture) -> Executor:
|
||||
"""
|
||||
fixture for executor
|
||||
:param configuration: configuration fixture
|
||||
:param mocker: mocker object
|
||||
:return: executor test instance
|
||||
"""
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_build")
|
||||
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache")
|
||||
@ -29,17 +41,34 @@ def executor(configuration: Configuration, mocker: MockerFixture) -> Executor:
|
||||
|
||||
@pytest.fixture
|
||||
def repository(configuration: Configuration, mocker: MockerFixture) -> Repository:
|
||||
"""
|
||||
fixture for repository
|
||||
:param configuration: configuration fixture
|
||||
:param mocker: mocker object
|
||||
:return: repository test instance
|
||||
"""
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
return Repository("x86_64", configuration)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def properties(configuration: Configuration) -> Properties:
|
||||
"""
|
||||
fixture for properties
|
||||
:param configuration: configuration fixture
|
||||
:return: properties test instance
|
||||
"""
|
||||
return Properties("x86_64", configuration)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def update_handler(configuration: Configuration, mocker: MockerFixture) -> UpdateHandler:
|
||||
"""
|
||||
fixture for update handler
|
||||
:param configuration: configuration fixture
|
||||
:param mocker: mocker object
|
||||
:return: update handler test instance
|
||||
"""
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_build")
|
||||
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache")
|
||||
|
@ -9,11 +9,18 @@ from ahriman.core.repository.cleaner import Cleaner
|
||||
|
||||
|
||||
def _mock_clear(mocker: MockerFixture) -> None:
|
||||
"""
|
||||
mocker helper for clear function
|
||||
:param mocker: mocker object
|
||||
"""
|
||||
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a"), Path("b"), Path("c")])
|
||||
mocker.patch("shutil.rmtree")
|
||||
|
||||
|
||||
def _mock_clear_check() -> None:
|
||||
"""
|
||||
mocker helper for clear tests
|
||||
"""
|
||||
shutil.rmtree.assert_has_calls([
|
||||
mock.call(Path("a")),
|
||||
mock.call(Path("b")),
|
||||
|
@ -6,10 +6,20 @@ from ahriman.core.sign.gpg import GPG
|
||||
|
||||
@pytest.fixture
|
||||
def gpg(configuration: Configuration) -> GPG:
|
||||
"""
|
||||
fixture for empty GPG
|
||||
:param configuration: configuration fixture
|
||||
:return: GPG test instance
|
||||
"""
|
||||
return GPG("x86_64", configuration)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def gpg_with_key(gpg: GPG) -> GPG:
|
||||
"""
|
||||
fixture for correct GPG
|
||||
:param gpg: empty GPG fixture
|
||||
:return: GPG test instance
|
||||
"""
|
||||
gpg.default_key = "key"
|
||||
return gpg
|
||||
|
@ -1,5 +1,9 @@
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest import mock
|
||||
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.models.sign_settings import SignSettings
|
||||
@ -60,6 +64,38 @@ def test_sign_command(gpg_with_key: GPG) -> None:
|
||||
assert gpg_with_key.sign_command(Path("a"), gpg_with_key.default_key)
|
||||
|
||||
|
||||
def test_download_key(gpg: GPG, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must download the key from public server
|
||||
"""
|
||||
requests_mock = mocker.patch("requests.get")
|
||||
gpg.download_key("keys.gnupg.net", "0xE989490C")
|
||||
requests_mock.assert_called_once()
|
||||
|
||||
|
||||
def test_download_key_failure(gpg: GPG, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must download the key from public server and log error if any (and raise it again)
|
||||
"""
|
||||
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError())
|
||||
with pytest.raises(requests.exceptions.HTTPError):
|
||||
gpg.download_key("keys.gnupg.net", "0xE989490C")
|
||||
|
||||
|
||||
def test_import_key(gpg: GPG, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must import PGP key from the server
|
||||
"""
|
||||
mocker.patch("ahriman.core.sign.gpg.GPG.download_key", return_value="key")
|
||||
check_output_mock = mocker.patch("ahriman.core.sign.gpg.GPG._check_output")
|
||||
|
||||
gpg.import_key("keys.gnupg.net", "0xE989490C")
|
||||
check_output_mock.assert_has_calls([
|
||||
mock.call("gpg", "--import", input_data="key", exception=None, logger=pytest.helpers.anyvar(int)),
|
||||
mock.call("gpg", "--quick-lsign-key", "0xE989490C", exception=None, logger=pytest.helpers.anyvar(int))
|
||||
])
|
||||
|
||||
|
||||
def test_process(gpg_with_key: GPG, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must call process method correctly
|
||||
|
@ -11,20 +11,38 @@ from ahriman.models.package import Package
|
||||
# helpers
|
||||
@pytest.helpers.register
|
||||
def get_package_status(package: Package) -> Dict[str, Any]:
|
||||
"""
|
||||
helper to extract package status from package
|
||||
:param package: package object
|
||||
:return: simplified package status map (with only status and view)
|
||||
"""
|
||||
return {"status": BuildStatusEnum.Unknown.value, "package": package.view()}
|
||||
|
||||
|
||||
@pytest.helpers.register
|
||||
def get_package_status_extended(package: Package) -> Dict[str, Any]:
|
||||
"""
|
||||
helper to extract package status from package
|
||||
:param package: package object
|
||||
:return: full package status map (with timestamped build status and view)
|
||||
"""
|
||||
return {"status": BuildStatus().view(), "package": package.view()}
|
||||
|
||||
|
||||
# fixtures
|
||||
@pytest.fixture
|
||||
def client() -> Client:
|
||||
"""
|
||||
fixture for dummy client
|
||||
:return: dummy client test instance
|
||||
"""
|
||||
return Client()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def web_client() -> WebClient:
|
||||
"""
|
||||
fixture for web client
|
||||
:return: web client test instance
|
||||
"""
|
||||
return WebClient("localhost", 8080)
|
||||
|
@ -4,6 +4,7 @@ from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.core.status.web_client import WebClient
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@ -38,6 +39,13 @@ def test_get(client: Client, package_ahriman: Package) -> None:
|
||||
assert client.get(None) == []
|
||||
|
||||
|
||||
def test_get_internal(client: Client) -> None:
|
||||
"""
|
||||
must return dummy status for web service
|
||||
"""
|
||||
assert client.get_internal() == InternalStatus()
|
||||
|
||||
|
||||
def test_get_self(client: Client) -> None:
|
||||
"""
|
||||
must return unknown status for service
|
||||
|
@ -106,7 +106,7 @@ def test_cache_save_load(watcher: Watcher, package_ahriman: Package, mocker: Moc
|
||||
"""
|
||||
must save state to cache which can be loaded later
|
||||
"""
|
||||
dump_file = Path(tempfile.mktemp())
|
||||
dump_file = Path(tempfile.mktemp()) # nosec
|
||||
mocker.patch("ahriman.core.status.watcher.Watcher.cache_path",
|
||||
new_callable=PropertyMock, return_value=dump_file)
|
||||
known_current = {package_ahriman.base: (package_ahriman, BuildStatus())}
|
||||
|
@ -7,6 +7,7 @@ from requests import Response
|
||||
|
||||
from ahriman.core.status.web_client import WebClient
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@ -26,6 +27,14 @@ def test_package_url(web_client: WebClient, package_ahriman: Package) -> None:
|
||||
assert web_client._package_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}")
|
||||
|
||||
|
||||
def test_status_url(web_client: WebClient) -> None:
|
||||
"""
|
||||
must generate service status url correctly
|
||||
"""
|
||||
assert web_client._status_url().startswith(f"http://{web_client.host}:{web_client.port}")
|
||||
assert web_client._status_url().endswith("/api/v1/status")
|
||||
|
||||
|
||||
def test_add(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must process package addition
|
||||
@ -103,6 +112,37 @@ def test_get_single(web_client: WebClient, package_ahriman: Package, mocker: Moc
|
||||
assert (package_ahriman, BuildStatusEnum.Unknown) in [(package, status.status) for package, status in result]
|
||||
|
||||
|
||||
def test_get_internal(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return web service status
|
||||
"""
|
||||
response_obj = Response()
|
||||
response_obj._content = json.dumps(InternalStatus(architecture="x86_64").view()).encode("utf8")
|
||||
response_obj.status_code = 200
|
||||
|
||||
requests_mock = mocker.patch("requests.get", return_value=response_obj)
|
||||
|
||||
result = web_client.get_internal()
|
||||
requests_mock.assert_called_once()
|
||||
assert result.architecture == "x86_64"
|
||||
|
||||
|
||||
def test_get_internal_failed(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress any exception happened during web service status getting
|
||||
"""
|
||||
mocker.patch("requests.get", side_effect=Exception())
|
||||
assert web_client.get_internal() == InternalStatus()
|
||||
|
||||
|
||||
def test_get_internal_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress any exception happened during web service status getting
|
||||
"""
|
||||
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError())
|
||||
assert web_client.get_internal() == InternalStatus()
|
||||
|
||||
|
||||
def test_get_self(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return service status
|
||||
|
@ -113,6 +113,14 @@ def test_load_includes_no_option(configuration: Configuration) -> None:
|
||||
configuration.load_includes()
|
||||
|
||||
|
||||
def test_load_includes_no_section(configuration: Configuration) -> None:
|
||||
"""
|
||||
must not fail if no option set
|
||||
"""
|
||||
configuration.remove_section("settings")
|
||||
configuration.load_includes()
|
||||
|
||||
|
||||
def test_load_logging_fallback(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must fallback to stderr without errors
|
||||
|
31
tests/ahriman/core/upload/conftest.py
Normal file
31
tests/ahriman/core/upload/conftest.py
Normal file
@ -0,0 +1,31 @@
|
||||
import pytest
|
||||
|
||||
from collections import namedtuple
|
||||
from typing import List
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.upload.s3 import S3
|
||||
|
||||
|
||||
_s3_object = namedtuple("s3_object", ["key", "e_tag", "delete"])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def s3(configuration: Configuration) -> S3:
|
||||
"""
|
||||
fixture for S3 synchronization
|
||||
:param configuration: configuration fixture
|
||||
:return: S3 test instance
|
||||
"""
|
||||
return S3("x86_64", configuration)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def s3_remote_objects() -> List[_s3_object]:
|
||||
"""
|
||||
fixture for boto3 like S3 objects
|
||||
:return: boto3 like S3 objects test instance
|
||||
"""
|
||||
delete_mock = MagicMock()
|
||||
return list(map(lambda item: _s3_object(f"x86_64/{item}", f"\"{item}\"", delete_mock), ["a", "b", "c"]))
|
@ -1,16 +1,131 @@
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from typing import Any, List, Optional, Tuple
|
||||
from unittest import mock
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.upload.s3 import S3
|
||||
|
||||
|
||||
def test_sync(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
_chunk_size = 8 * 1024 * 1024
|
||||
|
||||
|
||||
def test_calculate_etag_big(resource_path_root: Path) -> None:
|
||||
"""
|
||||
must calculate checksum for path which is more than one chunk
|
||||
"""
|
||||
path = resource_path_root / "models" / "big_file_checksum"
|
||||
assert S3.calculate_etag(path, _chunk_size) == "3b15154eaeed22ae19ae4667d4b98d28-2"
|
||||
|
||||
|
||||
def test_calculate_etag_empty(resource_path_root: Path) -> None:
|
||||
"""
|
||||
must calculate checksum for empty file correctly
|
||||
"""
|
||||
path = resource_path_root / "models" / "empty_file_checksum"
|
||||
assert S3.calculate_etag(path, _chunk_size) == "d41d8cd98f00b204e9800998ecf8427e"
|
||||
|
||||
|
||||
def test_calculate_etag_small(resource_path_root: Path) -> None:
|
||||
"""
|
||||
must calculate checksum for path which is single chunk
|
||||
"""
|
||||
path = resource_path_root / "models" / "package_ahriman_srcinfo"
|
||||
assert S3.calculate_etag(path, _chunk_size) == "04e75b4aa0fe6033e711e8ea98e059b2"
|
||||
|
||||
|
||||
def test_remove_files(s3_remote_objects: List[Any]) -> None:
|
||||
"""
|
||||
must remove remote objects
|
||||
"""
|
||||
local_files = {
|
||||
Path(item.key): item.e_tag for item in s3_remote_objects if item.key != "x86_64/a"
|
||||
}
|
||||
remote_objects = {Path(item.key): item for item in s3_remote_objects}
|
||||
|
||||
S3.remove_files(local_files, remote_objects)
|
||||
remote_objects[Path("x86_64/a")].delete.assert_called_once()
|
||||
|
||||
|
||||
def test_get_local_files(s3: S3, resource_path_root: Path) -> None:
|
||||
"""
|
||||
must get all local files recursively
|
||||
"""
|
||||
expected = sorted([
|
||||
Path("core/ahriman.ini"),
|
||||
Path("core/logging.ini"),
|
||||
Path("models/big_file_checksum"),
|
||||
Path("models/empty_file_checksum"),
|
||||
Path("models/package_ahriman_srcinfo"),
|
||||
Path("models/package_tpacpi-bat-git_srcinfo"),
|
||||
Path("models/package_yay_srcinfo"),
|
||||
Path("web/templates/search-line.jinja2"),
|
||||
Path("web/templates/build-status.jinja2"),
|
||||
Path("web/templates/repo-index.jinja2"),
|
||||
Path("web/templates/sorttable.jinja2"),
|
||||
Path("web/templates/style.jinja2"),
|
||||
Path("web/templates/search.jinja2"),
|
||||
])
|
||||
|
||||
local_files = list(sorted(s3.get_local_files(resource_path_root).keys()))
|
||||
assert local_files == expected
|
||||
|
||||
|
||||
def test_get_remote_objects(s3: S3, s3_remote_objects: List[Any]) -> None:
|
||||
"""
|
||||
must generate list of remote objects by calling boto3 function
|
||||
"""
|
||||
expected = {Path(item.key).relative_to(s3.architecture): item for item in s3_remote_objects}
|
||||
|
||||
s3.bucket = MagicMock()
|
||||
s3.bucket.objects.filter.return_value = s3_remote_objects
|
||||
|
||||
assert s3.get_remote_objects() == expected
|
||||
|
||||
|
||||
def test_sync(s3: S3, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run sync command
|
||||
"""
|
||||
check_output_mock = mocker.patch("ahriman.core.upload.s3.S3._check_output")
|
||||
local_files_mock = mocker.patch("ahriman.core.upload.s3.S3.get_local_files")
|
||||
remote_objects_mock = mocker.patch("ahriman.core.upload.s3.S3.get_remote_objects")
|
||||
remove_files_mock = mocker.patch("ahriman.core.upload.s3.S3.remove_files")
|
||||
upload_files_mock = mocker.patch("ahriman.core.upload.s3.S3.upload_files")
|
||||
|
||||
upload = S3("x86_64", configuration)
|
||||
upload.sync(Path("path"), [])
|
||||
check_output_mock.assert_called_once()
|
||||
s3.sync(Path("root"), [])
|
||||
local_files_mock.assert_called_once()
|
||||
remote_objects_mock.assert_called_once()
|
||||
remove_files_mock.assert_called_once()
|
||||
upload_files_mock.assert_called_once()
|
||||
|
||||
|
||||
def test_upload_files(s3: S3, s3_remote_objects: List[Any], mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must upload changed files
|
||||
"""
|
||||
def mimetype(path: Path) -> Tuple[Optional[str], None]:
|
||||
return ("text/html", None) if path.name == "b" else (None, None)
|
||||
|
||||
root = Path("path")
|
||||
local_files = {
|
||||
Path(item.key.replace("a", "d")): item.e_tag.replace("b", "d").replace("\"", "")
|
||||
for item in s3_remote_objects
|
||||
}
|
||||
remote_objects = {Path(item.key): item for item in s3_remote_objects}
|
||||
|
||||
mocker.patch("mimetypes.guess_type", side_effect=mimetype)
|
||||
upload_mock = s3.bucket = MagicMock()
|
||||
|
||||
s3.upload_files(root, local_files, remote_objects)
|
||||
upload_mock.upload_file.assert_has_calls(
|
||||
[
|
||||
mock.call(
|
||||
Filename=str(root / s3.architecture / "b"),
|
||||
Key=f"{s3.architecture}/{s3.architecture}/b",
|
||||
ExtraArgs={"Content-Type": "text/html"}),
|
||||
mock.call(
|
||||
Filename=str(root / s3.architecture / "d"),
|
||||
Key=f"{s3.architecture}/{s3.architecture}/d",
|
||||
ExtraArgs=None),
|
||||
],
|
||||
any_order=True)
|
||||
|
@ -2,18 +2,56 @@ import pytest
|
||||
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
from ahriman import version
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.counters import Counters
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def build_status_failed() -> BuildStatus:
|
||||
"""
|
||||
build result fixture with failed status
|
||||
:return: failed build status test instance
|
||||
"""
|
||||
return BuildStatus(BuildStatusEnum.Failed, 42)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def counters() -> Counters:
|
||||
"""
|
||||
counters fixture
|
||||
:return: counters test instance
|
||||
"""
|
||||
return Counters(total=10,
|
||||
unknown=1,
|
||||
pending=2,
|
||||
building=3,
|
||||
failed=4,
|
||||
success=0)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def internal_status(counters: Counters) -> InternalStatus:
|
||||
"""
|
||||
internal status fixture
|
||||
:param counters: counters fixture
|
||||
:return: internal status test instance
|
||||
"""
|
||||
return InternalStatus(architecture="x86_64",
|
||||
packages=counters,
|
||||
version=version.__version__,
|
||||
repository="aur-clone")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def package_tpacpi_bat_git() -> Package:
|
||||
"""
|
||||
git package fixture
|
||||
:return: git package test instance
|
||||
"""
|
||||
return Package(
|
||||
base="tpacpi-bat-git",
|
||||
version="3.1.r12.g4959b52-1",
|
||||
@ -23,6 +61,11 @@ def package_tpacpi_bat_git() -> Package:
|
||||
|
||||
@pytest.fixture
|
||||
def pyalpm_handle(pyalpm_package_ahriman: MagicMock) -> MagicMock:
|
||||
"""
|
||||
mock object for pyalpm
|
||||
:param pyalpm_package_ahriman: mock object for pyalpm package
|
||||
:return: pyalpm mock
|
||||
"""
|
||||
mock = MagicMock()
|
||||
mock.handle.load_pkg.return_value = pyalpm_package_ahriman
|
||||
return mock
|
||||
@ -30,6 +73,11 @@ def pyalpm_handle(pyalpm_package_ahriman: MagicMock) -> MagicMock:
|
||||
|
||||
@pytest.fixture
|
||||
def pyalpm_package_ahriman(package_ahriman: Package) -> MagicMock:
|
||||
"""
|
||||
mock object for pyalpm package
|
||||
:param package_ahriman: package fixture
|
||||
:return: pyalpm package mock
|
||||
"""
|
||||
mock = MagicMock()
|
||||
type(mock).base = PropertyMock(return_value=package_ahriman.base)
|
||||
type(mock).name = PropertyMock(return_value=package_ahriman.base)
|
||||
@ -39,6 +87,11 @@ def pyalpm_package_ahriman(package_ahriman: Package) -> MagicMock:
|
||||
|
||||
@pytest.fixture
|
||||
def pyalpm_package_description_ahriman(package_description_ahriman: PackageDescription) -> MagicMock:
|
||||
"""
|
||||
mock object for pyalpm package description
|
||||
:param package_description_ahriman: package description fixture
|
||||
:return: pyalpm package description mock
|
||||
"""
|
||||
mock = MagicMock()
|
||||
type(mock).arch = PropertyMock(return_value=package_description_ahriman.architecture)
|
||||
type(mock).builddate = PropertyMock(return_value=package_description_ahriman.build_date)
|
||||
@ -48,5 +101,6 @@ def pyalpm_package_description_ahriman(package_description_ahriman: PackageDescr
|
||||
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).provides = PropertyMock(return_value=package_description_ahriman.provides)
|
||||
type(mock).url = PropertyMock(return_value=package_description_ahriman.url)
|
||||
return mock
|
||||
|
31
tests/ahriman/models/test_counters.py
Normal file
31
tests/ahriman/models/test_counters.py
Normal file
@ -0,0 +1,31 @@
|
||||
from dataclasses import asdict
|
||||
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.counters import Counters
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
def test_counters_from_json_view(counters: Counters) -> None:
|
||||
"""
|
||||
must construct same object from json
|
||||
"""
|
||||
assert Counters.from_json(asdict(counters)) == counters
|
||||
|
||||
|
||||
def test_counters_from_packages(package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||
"""
|
||||
must construct object from list of packages with their statuses
|
||||
"""
|
||||
payload = [
|
||||
(package_ahriman, BuildStatus(status=BuildStatusEnum.Success)),
|
||||
(package_python_schedule, BuildStatus(status=BuildStatusEnum.Failed)),
|
||||
]
|
||||
|
||||
counters = Counters.from_packages(payload)
|
||||
assert counters.total == 2
|
||||
assert counters.success == 1
|
||||
assert counters.failed == 1
|
||||
|
||||
json = asdict(counters)
|
||||
total = json.pop("total")
|
||||
assert total == sum(i for i in json.values())
|
8
tests/ahriman/models/test_internal_status.py
Normal file
8
tests/ahriman/models/test_internal_status.py
Normal file
@ -0,0 +1,8 @@
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
|
||||
|
||||
def test_internal_status_from_json_view(internal_status: InternalStatus) -> None:
|
||||
"""
|
||||
must construct same object from json
|
||||
"""
|
||||
assert InternalStatus.from_json(internal_status.view()) == internal_status
|
@ -4,6 +4,15 @@ from unittest import mock
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
def test_known_architectures(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must list available directory paths
|
||||
"""
|
||||
iterdir_mock = mocker.patch("pathlib.Path.iterdir")
|
||||
repository_paths.known_architectures(repository_paths.root)
|
||||
iterdir_mock.assert_called_once()
|
||||
|
||||
|
||||
def test_create_tree(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must create whole tree
|
||||
@ -11,7 +20,7 @@ def test_create_tree(repository_paths: RepositoryPaths, mocker: MockerFixture) -
|
||||
paths = {
|
||||
prop
|
||||
for prop in dir(repository_paths)
|
||||
if not prop.startswith("_") and prop not in ("architecture", "create_tree", "root")
|
||||
if not prop.startswith("_") and prop not in ("architecture", "create_tree", "known_architectures", "root")
|
||||
}
|
||||
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
|
||||
|
||||
|
@ -9,5 +9,11 @@ from ahriman.web.web import setup_service
|
||||
|
||||
@pytest.fixture
|
||||
def application(configuration: Configuration, mocker: MockerFixture) -> web.Application:
|
||||
"""
|
||||
application fixture
|
||||
:param configuration: configuration fixture
|
||||
:param mocker: mocker object
|
||||
:return: application test instance
|
||||
"""
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
return setup_service("x86_64", configuration)
|
||||
|
15
tests/ahriman/web/middlewares/conftest.py
Normal file
15
tests/ahriman/web/middlewares/conftest.py
Normal file
@ -0,0 +1,15 @@
|
||||
import pytest
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
_request = namedtuple("_request", ["path"])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def aiohttp_request() -> _request:
|
||||
"""
|
||||
fixture for aiohttp like object
|
||||
:return: aiohttp like request test instance
|
||||
"""
|
||||
return _request("path")
|
@ -0,0 +1,47 @@
|
||||
import logging
|
||||
import pytest
|
||||
|
||||
from aiohttp.web_exceptions import HTTPBadRequest
|
||||
from pytest_mock import MockerFixture
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from ahriman.web.middlewares.exception_handler import exception_handler
|
||||
|
||||
|
||||
async def test_exception_handler(aiohttp_request: Any, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must pass success response
|
||||
"""
|
||||
request_handler = AsyncMock()
|
||||
logging_mock = mocker.patch("logging.Logger.exception")
|
||||
|
||||
handler = exception_handler(logging.getLogger())
|
||||
await handler(aiohttp_request, request_handler)
|
||||
logging_mock.assert_not_called()
|
||||
|
||||
|
||||
async def test_exception_handler_client_error(aiohttp_request: Any, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must pass client exception
|
||||
"""
|
||||
request_handler = AsyncMock(side_effect=HTTPBadRequest())
|
||||
logging_mock = mocker.patch("logging.Logger.exception")
|
||||
|
||||
handler = exception_handler(logging.getLogger())
|
||||
with pytest.raises(HTTPBadRequest):
|
||||
await handler(aiohttp_request, request_handler)
|
||||
logging_mock.assert_not_called()
|
||||
|
||||
|
||||
async def test_exception_handler_server_error(aiohttp_request: Any, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must log server exception and re-raise it
|
||||
"""
|
||||
request_handler = AsyncMock(side_effect=Exception())
|
||||
logging_mock = mocker.patch("logging.Logger.exception")
|
||||
|
||||
handler = exception_handler(logging.getLogger())
|
||||
with pytest.raises(Exception):
|
||||
await handler(aiohttp_request, request_handler)
|
||||
logging_mock.assert_called_once()
|
||||
|
@ -0,0 +1,11 @@
|
||||
from aiohttp import web
|
||||
|
||||
from ahriman.web.routes import setup_routes
|
||||
|
||||
|
||||
def test_setup_routes(application: web.Application) -> None:
|
||||
"""
|
||||
must generate non empty list of routes
|
||||
"""
|
||||
setup_routes(application)
|
||||
assert application.router.routes()
|
||||
|
@ -39,5 +39,5 @@ def test_run(application: web.Application, mocker: MockerFixture) -> None:
|
||||
run_application_mock = mocker.patch("aiohttp.web.run_app")
|
||||
|
||||
run_server(application)
|
||||
run_application_mock.assert_called_with(application, host="0.0.0.0", port=port,
|
||||
run_application_mock.assert_called_with(application, host="127.0.0.1", port=port,
|
||||
handle_signals=False, access_log=pytest.helpers.anyvar(int))
|
||||
|
@ -10,5 +10,13 @@ from typing import Any
|
||||
@pytest.fixture
|
||||
def client(application: web.Application, loop: BaseEventLoop,
|
||||
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
|
||||
"""
|
||||
web client fixture
|
||||
:param application: application fixture
|
||||
:param loop: context event loop
|
||||
:param aiohttp_client: aiohttp client fixture
|
||||
:param mocker: mocker object
|
||||
:return: web client test instance
|
||||
"""
|
||||
mocker.patch("pathlib.Path.iterdir", return_value=[])
|
||||
return loop.run_until_complete(aiohttp_client(application))
|
||||
|
22
tests/ahriman/web/views/test_view_status.py
Normal file
22
tests/ahriman/web/views/test_view_status.py
Normal file
@ -0,0 +1,22 @@
|
||||
from pytest_aiohttp import TestClient
|
||||
|
||||
import ahriman.version as version
|
||||
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
async def test_get(client: TestClient, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must generate web service status correctly
|
||||
"""
|
||||
await client.post(f"/api/v1/packages/{package_ahriman.base}",
|
||||
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
||||
|
||||
response = await client.get("/api/v1/status")
|
||||
assert response.status == 200
|
||||
|
||||
json = await response.json()
|
||||
assert json["version"] == version.__version__
|
||||
assert json["packages"]
|
||||
assert json["packages"]["total"] == 1
|
@ -26,7 +26,7 @@ target =
|
||||
target =
|
||||
|
||||
[email]
|
||||
host = 0.0.0.0
|
||||
host = 127.0.0.1
|
||||
link_path =
|
||||
no_empty_report = no
|
||||
port = 587
|
||||
@ -48,9 +48,11 @@ command = rsync --archive --verbose --compress --partial --delete
|
||||
remote =
|
||||
|
||||
[s3]
|
||||
bucket =
|
||||
command = aws s3 sync --quiet --delete
|
||||
access_key =
|
||||
bucket = bucket
|
||||
region = eu-central-1
|
||||
secret_key =
|
||||
|
||||
[web]
|
||||
host = 0.0.0.0
|
||||
host = 127.0.0.1
|
||||
templates = ../web/templates
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user