mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-06-27 14:22:10 +00:00
Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
9d2a3bcbc1 | |||
a5455b697d | |||
0bfb763b2a | |||
9f3566a150 | |||
16a6c4fdd7 | |||
91f66fdcee | |||
bb45b1d868 | |||
3d10fa472b | |||
a90c93bbc4 | |||
41a3c08d9f | |||
cb328ad797 | |||
810091cde9 | |||
fc0474fa8f | |||
b94179e071 | |||
9c5a9f5837 | |||
83047d8270 | |||
990d5dda81 | |||
48e79ce39c | |||
375d7c55e5 | |||
db52b8e844 | |||
50af309c80 | |||
581401d60f | |||
c2685f4746 |
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']
|
5
.github/workflows/run-tests.yml
vendored
5
.github/workflows/run-tests.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: tests
|
||||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -18,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
|
||||
|
||||
|
10
Makefile
10
Makefile
@ -3,7 +3,7 @@
|
||||
|
||||
PROJECT := ahriman
|
||||
|
||||
FILES := AUTHORS COPYING CONFIGURING.md README.md package src setup.py
|
||||
FILES := AUTHORS COPYING CONFIGURING.md README.md package src setup.cfg setup.py
|
||||
TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES))
|
||||
IGNORE_FILES := package/archlinux src/.mypy_cache
|
||||
|
||||
@ -24,8 +24,10 @@ archlinux: archive
|
||||
sed -i "s/pkgver=[0-9.]*/pkgver=$(VERSION)/" package/archlinux/PKGBUILD
|
||||
|
||||
check: clean mypy
|
||||
find "src/$(PROJECT)" "tests/$(PROJECT)" -name "*.py" -execdir autopep8 --exit-code --max-line-length 120 -aa -i {} +
|
||||
cd src && pylint --rcfile=../.pylintrc "$(PROJECT)"
|
||||
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,7 +37,7 @@ directory: clean
|
||||
mkdir "$(PROJECT)"
|
||||
|
||||
mypy:
|
||||
cd src && echo y | mypy --implicit-reexport --strict -p "$(PROJECT)" --install-types || true
|
||||
cd src && mypy --implicit-reexport --strict -p "$(PROJECT)" --install-types --non-interactive || true
|
||||
cd src && mypy --implicit-reexport --strict -p "$(PROJECT)"
|
||||
|
||||
push: archlinux
|
||||
|
@ -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,30 +1,27 @@
|
||||
# Maintainer: Evgeniy Alekseev
|
||||
|
||||
pkgname='ahriman'
|
||||
pkgver=1.1.0
|
||||
pkgver=1.2.6
|
||||
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'
|
||||
'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')
|
||||
|
||||
@ -42,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
|
@ -40,8 +40,8 @@ target =
|
||||
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,13 +18,13 @@
|
||||
|
||||
<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>
|
||||
@ -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
|
||||
|
23
setup.py
23
setup.py
@ -1,11 +1,14 @@
|
||||
from distutils.util import convert_path
|
||||
from build_manpages import build_manpages
|
||||
from pathlib import Path
|
||||
from setuptools import setup, find_packages
|
||||
from os import path
|
||||
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",
|
||||
@ -74,9 +77,13 @@ setup(
|
||||
extras_require={
|
||||
"check": [
|
||||
"autopep8",
|
||||
"bandit",
|
||||
"mypy",
|
||||
"pylint",
|
||||
],
|
||||
"s3": [
|
||||
"boto3",
|
||||
],
|
||||
"test": [
|
||||
"pytest",
|
||||
"pytest-aiohttp",
|
||||
@ -92,4 +99,8 @@ setup(
|
||||
"aiohttp_jinja2",
|
||||
],
|
||||
},
|
||||
|
||||
cmdclass={
|
||||
"build_manpages": build_manpages.build_manpages,
|
||||
}
|
||||
)
|
||||
|
@ -19,6 +19,7 @@
|
||||
#
|
||||
import argparse
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@ -29,8 +30,7 @@ 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:
|
||||
@ -44,7 +44,12 @@ def _parser() -> argparse.ArgumentParser:
|
||||
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")
|
||||
|
@ -185,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
|
||||
@ -211,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)
|
||||
@ -222,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)
|
||||
|
@ -94,8 +94,10 @@ class Lock:
|
||||
"""
|
||||
status = self.reporter.get_internal()
|
||||
if status.version is not None and status.version != version.__version__:
|
||||
logging.getLogger("root").warning(f"status watcher version mismatch, "
|
||||
f"our {version.__version__}, their {status.version}")
|
||||
logging.getLogger("root").warning(
|
||||
"status watcher version mismatch, our %s, their %s",
|
||||
version.__version__,
|
||||
status.version)
|
||||
|
||||
def check_user(self) -> None:
|
||||
"""
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -104,7 +104,7 @@ class GPG:
|
||||
})
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not download key {key} from {server}: {exception_response_text(e)}")
|
||||
self.logger.exception("could not download key %s from %s: %s", key, server, exception_response_text(e))
|
||||
raise
|
||||
return response.text
|
||||
|
||||
@ -142,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)
|
||||
|
||||
|
@ -53,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
|
||||
@ -63,16 +62,14 @@ class Client:
|
||||
del base
|
||||
return []
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_internal(self) -> InternalStatus:
|
||||
def get_internal(self) -> InternalStatus: # pylint: disable=no-self-use
|
||||
"""
|
||||
get internal service status
|
||||
:return: current internal (web) service status
|
||||
"""
|
||||
return InternalStatus()
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_self(self) -> BuildStatus:
|
||||
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:
|
||||
"""
|
||||
|
@ -84,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}: {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]]:
|
||||
"""
|
||||
@ -104,9 +104,9 @@ class WebClient(Client):
|
||||
for package in status_json
|
||||
]
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not get {base}: {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:
|
||||
@ -121,7 +121,7 @@ class WebClient(Client):
|
||||
status_json = response.json()
|
||||
return InternalStatus.from_json(status_json)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not get web service status: {exception_response_text(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()
|
||||
@ -138,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: {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()
|
||||
@ -152,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}: {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:
|
||||
"""
|
||||
@ -168,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}: {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:
|
||||
"""
|
||||
@ -183,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: {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 = {"ContentType": mime} if mime is not None else None
|
||||
|
||||
self.bucket.upload_file(Filename=str(local_path), Key=str(remote_path), ExtraArgs=extra_args)
|
||||
|
@ -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__ = "1.1.0"
|
||||
__version__ = "1.2.6"
|
||||
|
@ -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
|
||||
|
@ -13,17 +13,32 @@ 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,
|
||||
@ -44,9 +59,19 @@ def aur_package_ahriman(package_ahriman: Package) -> aur.Package:
|
||||
|
||||
@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()
|
||||
|
@ -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
|
||||
|
@ -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.key = "0xE989490C"
|
||||
args.key_server = "keys.gnupg.net"
|
||||
return args
|
||||
|
@ -8,6 +8,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.depends_on = []
|
||||
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.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
|
||||
|
||||
|
@ -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.search = ["ahriman"]
|
||||
return args
|
||||
|
||||
|
@ -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
|
||||
|
@ -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])
|
||||
|
@ -111,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()
|
||||
@ -122,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()
|
||||
|
||||
|
||||
@ -139,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()
|
||||
@ -150,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):
|
||||
@ -172,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)
|
||||
|
@ -14,4 +14,4 @@ def test_all_packages_with_provides(pacman: Pacman) -> None:
|
||||
"""
|
||||
package list must contain provides packages
|
||||
"""
|
||||
assert 'sh' in pacman.all_packages()
|
||||
assert "sh" in pacman.all_packages()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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())}
|
||||
|
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={"ContentType": "text/html"}),
|
||||
mock.call(
|
||||
Filename=str(root / s3.architecture / "d"),
|
||||
Key=f"{s3.architecture}/{s3.architecture}/d",
|
||||
ExtraArgs=None),
|
||||
],
|
||||
any_order=True)
|
||||
|
@ -12,11 +12,19 @@ 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,
|
||||
@ -27,6 +35,11 @@ def counters() -> Counters:
|
||||
|
||||
@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__,
|
||||
@ -35,6 +48,10 @@ def internal_status(counters: Counters) -> InternalStatus:
|
||||
|
||||
@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",
|
||||
@ -44,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
|
||||
@ -51,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)
|
||||
@ -60,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)
|
||||
|
@ -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))
|
||||
|
@ -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
|
@ -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.SysLogFileHandler
|
||||
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
|
||||
|
BIN
tests/testresources/models/big_file_checksum
Normal file
BIN
tests/testresources/models/big_file_checksum
Normal file
Binary file not shown.
0
tests/testresources/models/empty_file_checksum
Normal file
0
tests/testresources/models/empty_file_checksum
Normal file
1
tests/testresources/web/templates
Symbolic link
1
tests/testresources/web/templates
Symbolic link
@ -0,0 +1 @@
|
||||
../../../package/share/ahriman
|
@ -1,54 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ repository|e }}</title>
|
||||
|
||||
{% include "style.jinja2" %}
|
||||
|
||||
{% include "sorttable.jinja2" %}
|
||||
{% include "search.jinja2" %}
|
||||
</head>
|
||||
|
||||
<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 }}">
|
||||
</h1>
|
||||
|
||||
{% include "search-line.jinja2" %}
|
||||
|
||||
<section class="element">
|
||||
<table class="sortable search-table">
|
||||
<tr class="header">
|
||||
<th>package base</th>
|
||||
<th>packages</th>
|
||||
<th>version</th>
|
||||
<th>last update</th>
|
||||
<th>status</th>
|
||||
</tr>
|
||||
|
||||
{% 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">{{ 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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<ul class="navigation">
|
||||
<li><a href="https://github.com/arcan1s/ahriman" title="sources">ahriman</a></li>
|
||||
<li><a href="https://github.com/arcan1s/ahriman/releases" title="releases list">releases</a></li>
|
||||
<li><a href="https://github.com/arcan1s/ahriman/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,62 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ repository|e }}</title>
|
||||
|
||||
{% include "style.jinja2" %}
|
||||
|
||||
{% include "sorttable.jinja2" %}
|
||||
{% include "search.jinja2" %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="root">
|
||||
<h1>Archlinux user repository</h1>
|
||||
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
<code>
|
||||
$ cat /etc/pacman.conf<br>
|
||||
[{{ repository|e }}]<br>
|
||||
Server = {{ link_path|e }}<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" %}
|
||||
|
||||
<section class="element">
|
||||
<table class="sortable search-table">
|
||||
<tr class="header">
|
||||
<th>package</th>
|
||||
<th>version</th>
|
||||
<th>archive size</th>
|
||||
<th>installed size</th>
|
||||
<th>build date</th>
|
||||
</tr>
|
||||
|
||||
{% 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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<ul class="navigation">
|
||||
{% if homepage is not none %}
|
||||
<li><a href="{{ homepage|e }}" title="homepage">Homepage</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,3 +0,0 @@
|
||||
<section class="element">
|
||||
<input type="search" id="search" onkeyup="searchInTable()" placeholder="search for package" title="search for package"/>
|
||||
</section>
|
@ -1,25 +0,0 @@
|
||||
<script type="text/javascript">
|
||||
function searchInTable() {
|
||||
const input = document.getElementById("search");
|
||||
const filter = input.value.toLowerCase();
|
||||
const tables = document.getElementsByClassName("search-table");
|
||||
|
||||
for (let i = 0; i < tables.length; i++) {
|
||||
const tr = tables[i].getElementsByTagName("tr");
|
||||
// from 1 coz of header
|
||||
for (let i = 1; i < tr.length; i++) {
|
||||
let td = tr[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) {
|
||||
display = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
tr[i].style.display = display;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1 +0,0 @@
|
||||
<script src="https://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script>
|
@ -1,136 +0,0 @@
|
||||
<style>
|
||||
:root {
|
||||
--color-building: 255, 255, 146;
|
||||
--color-failed: 255, 94, 94;
|
||||
--color-pending: 255, 255, 146;
|
||||
--color-success: 94, 255, 94;
|
||||
--color-unknown: 225, 225, 225;
|
||||
|
||||
--color-header: 200, 200, 255;
|
||||
--color-hover: 255, 255, 225;
|
||||
--color-line-blue: 235, 235, 255;
|
||||
--color-line-white: 255, 255, 255;
|
||||
}
|
||||
|
||||
@keyframes blink-building {
|
||||
0% { background-color: rgba(var(--color-building), 1.0); }
|
||||
10% { background-color: rgba(var(--color-building), 0.9); }
|
||||
20% { background-color: rgba(var(--color-building), 0.8); }
|
||||
30% { background-color: rgba(var(--color-building), 0.7); }
|
||||
40% { background-color: rgba(var(--color-building), 0.6); }
|
||||
50% { background-color: rgba(var(--color-building), 0.5); }
|
||||
60% { background-color: rgba(var(--color-building), 0.4); }
|
||||
70% { background-color: rgba(var(--color-building), 0.3); }
|
||||
80% { background-color: rgba(var(--color-building), 0.2); }
|
||||
90% { background-color: rgba(var(--color-building), 0.1); }
|
||||
100% { background-color: rgba(var(--color-building), 0.0); }
|
||||
}
|
||||
|
||||
div.root {
|
||||
width: 70%;
|
||||
padding: 15px 15% 0;
|
||||
}
|
||||
|
||||
section.element, footer {
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
code, input, table {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
tr.package:nth-child(odd) {
|
||||
background-color: rgba(var(--color-line-white), 1.0);
|
||||
}
|
||||
|
||||
tr.package:nth-child(even) {
|
||||
background-color: rgba(var(--color-line-blue), 1.0);
|
||||
}
|
||||
|
||||
tr.package:hover {
|
||||
background-color: rgba(var(--color-hover), 1.0);
|
||||
}
|
||||
|
||||
tr.header{
|
||||
background-color: rgba(var(--color-header), 1.0);
|
||||
}
|
||||
|
||||
td.status {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td.package-unknown {
|
||||
background-color: rgba(var(--color-unknown), 1.0);
|
||||
}
|
||||
td.package-pending {
|
||||
background-color: rgba(var(--color-pending), 1.0);
|
||||
}
|
||||
td.package-building {
|
||||
background-color: rgba(var(--color-building), 1.0);
|
||||
animation-name: blink-building;
|
||||
animation-duration: 1s;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
td.package-failed {
|
||||
background-color: rgba(var(--color-failed), 1.0);
|
||||
}
|
||||
td.package-success {
|
||||
background-color: rgba(var(--color-success), 1.0);
|
||||
}
|
||||
|
||||
li.service-unknown {
|
||||
background-color: rgba(var(--color-unknown), 1.0);
|
||||
}
|
||||
li.service-building {
|
||||
background-color: rgba(var(--color-building), 1.0);
|
||||
animation-name: blink-building;
|
||||
animation-duration: 1s;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
li.service-failed {
|
||||
background-color: rgba(var(--color-failed), 1.0);
|
||||
}
|
||||
li.service-success {
|
||||
background-color: rgba(var(--color-success), 1.0);
|
||||
}
|
||||
|
||||
ul.navigation {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: rgba(var(--color-header), 1.0);
|
||||
}
|
||||
|
||||
ul.navigation li {
|
||||
float: left;
|
||||
}
|
||||
|
||||
ul.navigation li.status {
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
ul.navigation li a {
|
||||
display: block;
|
||||
color: black;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
ul.navigation li a:hover {
|
||||
background-color: rgba(var(--color-hover), 1.0);
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user