mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-06-29 07:05:50 +00:00
Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
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']
|
3
.github/workflows/run-tests.yml
vendored
3
.github/workflows/run-tests.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: tests
|
name: tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -21,5 +21,6 @@ jobs:
|
|||||||
/bin/bash -c "pacman --noconfirm -Syu base-devel python python-pip && \
|
/bin/bash -c "pacman --noconfirm -Syu base-devel python python-pip && \
|
||||||
pip install -e .[web] && \
|
pip install -e .[web] && \
|
||||||
pip install -e .[check] && \
|
pip install -e .[check] && \
|
||||||
|
pip install -e .[s3] && \
|
||||||
pip install -e .[test] && \
|
pip install -e .[test] && \
|
||||||
make check tests"
|
make check tests"
|
||||||
|
@ -22,7 +22,7 @@ ignore-patterns=
|
|||||||
|
|
||||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||||
# number of processors available to use.
|
# number of processors available to use.
|
||||||
jobs=1
|
jobs=0
|
||||||
|
|
||||||
# Control the amount of potential inferred values when inferring a single
|
# Control the amount of potential inferred values when inferring a single
|
||||||
# object. This can help the performance when dealing with large functions or
|
# object. This can help the performance when dealing with large functions or
|
||||||
@ -149,7 +149,6 @@ disable=print-statement,
|
|||||||
too-few-public-methods,
|
too-few-public-methods,
|
||||||
too-many-instance-attributes,
|
too-many-instance-attributes,
|
||||||
broad-except,
|
broad-except,
|
||||||
logging-fstring-interpolation,
|
|
||||||
too-many-ancestors,
|
too-many-ancestors,
|
||||||
fixme,
|
fixme,
|
||||||
too-many-arguments,
|
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
|
### `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.
|
* `access_key` - AWS access key ID, string, required.
|
||||||
* `bucket` - bucket name (e.g. `s3://bucket/path`), 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
|
## `web:*` groups
|
||||||
|
|
||||||
|
8
Makefile
8
Makefile
@ -24,8 +24,10 @@ archlinux: archive
|
|||||||
sed -i "s/pkgver=[0-9.]*/pkgver=$(VERSION)/" package/archlinux/PKGBUILD
|
sed -i "s/pkgver=[0-9.]*/pkgver=$(VERSION)/" package/archlinux/PKGBUILD
|
||||||
|
|
||||||
check: clean mypy
|
check: clean mypy
|
||||||
find "src/$(PROJECT)" "tests/$(PROJECT)" -name "*.py" -execdir autopep8 --exit-code --max-line-length 120 -aa -i {} +
|
autopep8 --exit-code --max-line-length 120 -aa -i -j 0 -r "src/$(PROJECT)" "tests/$(PROJECT)"
|
||||||
cd src && pylint --rcfile=../.pylintrc "$(PROJECT)"
|
pylint --rcfile=.pylintrc "src/$(PROJECT)"
|
||||||
|
bandit -c .bandit.yml -r "src/$(PROJECT)"
|
||||||
|
bandit -c .bandit-test.yml -r "tests/$(PROJECT)"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
find . -type f -name "$(PROJECT)-*-src.tar.xz" -delete
|
find . -type f -name "$(PROJECT)-*-src.tar.xz" -delete
|
||||||
@ -35,7 +37,7 @@ directory: clean
|
|||||||
mkdir "$(PROJECT)"
|
mkdir "$(PROJECT)"
|
||||||
|
|
||||||
mypy:
|
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)"
|
cd src && mypy --implicit-reexport --strict -p "$(PROJECT)"
|
||||||
|
|
||||||
push: archlinux
|
push: archlinux
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Evgeniy Alekseev
|
# Maintainer: Evgeniy Alekseev
|
||||||
|
|
||||||
pkgname='ahriman'
|
pkgname='ahriman'
|
||||||
pkgver=1.1.0
|
pkgver=1.2.1
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="ArcHlinux ReposItory MANager"
|
pkgdesc="ArcHlinux ReposItory MANager"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
@ -9,13 +9,13 @@ url="https://github.com/arcan1s/ahriman"
|
|||||||
license=('GPL3')
|
license=('GPL3')
|
||||||
depends=('devtools' 'git' 'pyalpm' 'python-aur' 'python-srcinfo')
|
depends=('devtools' 'git' 'pyalpm' 'python-aur' 'python-srcinfo')
|
||||||
makedepends=('python-pip')
|
makedepends=('python-pip')
|
||||||
optdepends=('aws-cli: sync to s3'
|
optdepends=('breezy: -bzr packages support'
|
||||||
'breezy: -bzr packages support'
|
|
||||||
'darcs: -darcs packages support'
|
'darcs: -darcs packages support'
|
||||||
'gnupg: package and repository sign'
|
'gnupg: package and repository sign'
|
||||||
'mercurial: -hg packages support'
|
'mercurial: -hg packages support'
|
||||||
'python-aiohttp: web server'
|
'python-aiohttp: web server'
|
||||||
'python-aiohttp-jinja2: web server'
|
'python-aiohttp-jinja2: web server'
|
||||||
|
'python-boto3: sync to s3'
|
||||||
'python-jinja: html report generation'
|
'python-jinja: html report generation'
|
||||||
'rsync: sync by using rsync'
|
'rsync: sync by using rsync'
|
||||||
'subversion: -svn packages support')
|
'subversion: -svn packages support')
|
||||||
|
@ -1,2 +1 @@
|
|||||||
d /var/lib/ahriman 0775 ahriman log
|
|
||||||
d /var/log/ahriman 0755 ahriman ahriman
|
d /var/log/ahriman 0755 ahriman ahriman
|
@ -40,8 +40,8 @@ target =
|
|||||||
command = rsync --archive --compress --partial --delete
|
command = rsync --archive --compress --partial --delete
|
||||||
|
|
||||||
[s3]
|
[s3]
|
||||||
command = aws s3 sync --quiet --delete
|
chunk_size = 8388608
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
host = 0.0.0.0
|
host = 127.0.0.1
|
||||||
templates = /usr/share/ahriman
|
templates = /usr/share/ahriman
|
@ -2,10 +2,10 @@
|
|||||||
keys = root,builder,build_details,http
|
keys = root,builder,build_details,http
|
||||||
|
|
||||||
[handlers]
|
[handlers]
|
||||||
keys = console_handler,build_file_handler,file_handler,http_handler
|
keys = console_handler,build_file_handler,file_handler,http_handler,syslog_handler
|
||||||
|
|
||||||
[formatters]
|
[formatters]
|
||||||
keys = generic_format
|
keys = generic_format,syslog_format
|
||||||
|
|
||||||
[handler_console_handler]
|
[handler_console_handler]
|
||||||
class = StreamHandler
|
class = StreamHandler
|
||||||
@ -31,29 +31,39 @@ level = DEBUG
|
|||||||
formatter = generic_format
|
formatter = generic_format
|
||||||
args = ("/var/log/ahriman/http.log", "a", 20971520, 20)
|
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]
|
[formatter_generic_format]
|
||||||
format = [%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
|
format = [%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
|
||||||
datefmt =
|
datefmt =
|
||||||
|
|
||||||
|
[formatter_syslog_format]
|
||||||
|
format = [%(levelname)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
|
||||||
|
datefmt =
|
||||||
|
|
||||||
[logger_root]
|
[logger_root]
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
handlers = file_handler
|
handlers = syslog_handler
|
||||||
qualname = root
|
qualname = root
|
||||||
|
|
||||||
[logger_builder]
|
[logger_builder]
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
handlers = file_handler
|
handlers = syslog_handler
|
||||||
qualname = builder
|
qualname = builder
|
||||||
propagate = 0
|
propagate = 0
|
||||||
|
|
||||||
[logger_build_details]
|
[logger_build_details]
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
handlers = build_file_handler
|
handlers = syslog_handler
|
||||||
qualname = build_details
|
qualname = build_details
|
||||||
propagate = 0
|
propagate = 0
|
||||||
|
|
||||||
[logger_http]
|
[logger_http]
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
handlers = http_handler
|
handlers = syslog_handler
|
||||||
qualname = http
|
qualname = http
|
||||||
propagate = 0
|
propagate = 0
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{ repository|e }}</title>
|
<title>{{ repository }}</title>
|
||||||
|
|
||||||
{% include "style.jinja2" %}
|
{% include "style.jinja2" %}
|
||||||
|
|
||||||
@ -12,9 +12,9 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<h1>ahriman
|
<h1>ahriman
|
||||||
<img src="https://img.shields.io/badge/version-{{ version|e }}-informational" alt="{{ version|e }}">
|
<img src="https://img.shields.io/badge/version-{{ version }}-informational" alt="{{ version }}">
|
||||||
<img src="https://img.shields.io/badge/architecture-{{ architecture|e }}-informational" alt="{{ architecture|e }}">
|
<img src="https://img.shields.io/badge/architecture-{{ architecture }}-informational" alt="{{ architecture }}">
|
||||||
<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/service%20status-{{ service.status }}-{{ service.status_color }}" alt="{{ service.status }}" title="{{ service.timestamp }}">
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{% include "search-line.jinja2" %}
|
{% include "search-line.jinja2" %}
|
||||||
@ -31,11 +31,11 @@
|
|||||||
|
|
||||||
{% for package in packages %}
|
{% for package in packages %}
|
||||||
<tr class="package">
|
<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 class="include-search">{{ package.packages|join("<br>"|safe) }}</td>
|
||||||
<td>{{ package.version|e }}</td>
|
<td>{{ package.version }}</td>
|
||||||
<td>{{ package.timestamp|e }}</td>
|
<td>{{ package.timestamp }}</td>
|
||||||
<td class="status package-{{ package.status|e }}">{{ package.status|e }}</td>
|
<td class="status package-{{ package.status }}">{{ package.status }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{ repository|e }}</title>
|
<title>{{ repository }}</title>
|
||||||
|
|
||||||
{% include "style.jinja2" %}
|
{% include "style.jinja2" %}
|
||||||
|
|
||||||
@ -18,13 +18,13 @@
|
|||||||
|
|
||||||
<section class="element">
|
<section class="element">
|
||||||
{% if pgp_key is not none %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|
||||||
<code>
|
<code>
|
||||||
$ cat /etc/pacman.conf<br>
|
$ cat /etc/pacman.conf<br>
|
||||||
[{{ repository|e }}]<br>
|
[{{ repository }}]<br>
|
||||||
Server = {{ link_path|e }}<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
|
SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly
|
||||||
</code>
|
</code>
|
||||||
</section>
|
</section>
|
||||||
@ -44,11 +44,11 @@
|
|||||||
|
|
||||||
{% for package in packages %}
|
{% for package in packages %}
|
||||||
<tr class="package">
|
<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 class="include-search"><a href="{{ link_path }}/{{ package.filename }}" title="{{ package.name }}">{{ package.name }}</a></td>
|
||||||
<td>{{ package.version|e }}</td>
|
<td>{{ package.version }}</td>
|
||||||
<td>{{ package.archive_size|e }}</td>
|
<td>{{ package.archive_size }}</td>
|
||||||
<td>{{ package.installed_size|e }}</td>
|
<td>{{ package.installed_size }}</td>
|
||||||
<td>{{ package.build_date|e }}</td>
|
<td>{{ package.build_date }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
@ -58,7 +58,7 @@
|
|||||||
<footer>
|
<footer>
|
||||||
<ul class="navigation">
|
<ul class="navigation">
|
||||||
{% if homepage is not none %}
|
{% 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 %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
<section class="element">
|
<section class="element">
|
||||||
<input type="search" id="search" onkeyup="searchInTable()" placeholder="search for package" title="search for package"/>
|
<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");
|
const tables = document.getElementsByClassName("search-table");
|
||||||
|
|
||||||
for (let i = 0; i < tables.length; i++) {
|
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
|
// from 1 coz of header
|
||||||
for (let i = 1; i < tr.length; i++) {
|
for (let i = 1; i < trs.length; i++) {
|
||||||
let td = tr[i].getElementsByClassName("include-search");
|
let tr = trs[i].getElementsByClassName("include-search");
|
||||||
let display = "none";
|
let display = "none";
|
||||||
for (let j = 0; j < td.length; j++) {
|
for (let j = 0; j < tr.length; j++) {
|
||||||
if (td[j].tagName.toLowerCase() === "td") {
|
if (tr[j].tagName.toLowerCase() === "td") {
|
||||||
if (td[j].innerHTML.toLowerCase().indexOf(filter) > -1) {
|
let contains = (element) => tr[j].innerHTML.toLowerCase().indexOf(element) > -1
|
||||||
|
if (filter.some(contains)) {
|
||||||
display = "";
|
display = "";
|
||||||
break;
|
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 {
|
ul.navigation li a:hover {
|
||||||
background-color: rgba(var(--color-hover), 1.0);
|
background-color: rgba(var(--color-hover), 1.0);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
4
setup.py
4
setup.py
@ -74,9 +74,13 @@ setup(
|
|||||||
extras_require={
|
extras_require={
|
||||||
"check": [
|
"check": [
|
||||||
"autopep8",
|
"autopep8",
|
||||||
|
"bandit",
|
||||||
"mypy",
|
"mypy",
|
||||||
"pylint",
|
"pylint",
|
||||||
],
|
],
|
||||||
|
"s3": [
|
||||||
|
"boto3",
|
||||||
|
],
|
||||||
"test": [
|
"test": [
|
||||||
"pytest",
|
"pytest",
|
||||||
"pytest-aiohttp",
|
"pytest-aiohttp",
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
#
|
#
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@ -44,7 +45,14 @@ def _parser() -> argparse.ArgumentParser:
|
|||||||
action="append")
|
action="append")
|
||||||
parser.add_argument("-c", "--configuration", help="configuration path", type=Path, default=Path("/etc/ahriman.ini"))
|
parser.add_argument("-c", "--configuration", help="configuration path", type=Path, default=Path("/etc/ahriman.ini"))
|
||||||
parser.add_argument("--force", help="force run, remove file lock", action="store_true")
|
parser.add_argument("--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-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("--no-report", help="force disable reporting to web service", action="store_true")
|
||||||
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user", action="store_true")
|
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user", action="store_true")
|
||||||
|
@ -185,7 +185,7 @@ class Application:
|
|||||||
continue
|
continue
|
||||||
for archive in package.packages.values():
|
for archive in package.packages.values():
|
||||||
if archive.filepath is None:
|
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
|
continue # avoid mypy warning
|
||||||
src = self.repository.paths.repository / archive.filepath
|
src = self.repository.paths.repository / archive.filepath
|
||||||
dst = self.repository.paths.packages / archive.filepath
|
dst = self.repository.paths.packages / archive.filepath
|
||||||
@ -211,6 +211,8 @@ class Application:
|
|||||||
:param updates: list of packages to update
|
:param updates: list of packages to update
|
||||||
"""
|
"""
|
||||||
def process_update(paths: Iterable[Path]) -> None:
|
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]
|
updated = [Package.load(path, self.repository.pacman, self.repository.aur_url) for path in paths]
|
||||||
self.repository.process_update(paths)
|
self.repository.process_update(paths)
|
||||||
self._finalize(updated)
|
self._finalize(updated)
|
||||||
@ -222,6 +224,6 @@ class Application:
|
|||||||
# process manual packages
|
# process manual packages
|
||||||
tree = Tree.load(updates)
|
tree = Tree.load(updates)
|
||||||
for num, level in enumerate(tree.levels()):
|
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)
|
packages = self.repository.process_build(level)
|
||||||
process_update(packages)
|
process_update(packages)
|
||||||
|
@ -94,8 +94,10 @@ class Lock:
|
|||||||
"""
|
"""
|
||||||
status = self.reporter.get_internal()
|
status = self.reporter.get_internal()
|
||||||
if status.version is not None and status.version != version.__version__:
|
if status.version is not None and status.version != version.__version__:
|
||||||
logging.getLogger("root").warning(f"status watcher version mismatch, "
|
logging.getLogger("root").warning(
|
||||||
f"our {version.__version__}, their {status.version}")
|
"status watcher version mismatch, our %s, their %s",
|
||||||
|
version.__version__,
|
||||||
|
status.version)
|
||||||
|
|
||||||
def check_user(self) -> None:
|
def check_user(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -99,7 +99,7 @@ class Task:
|
|||||||
command.extend(self.archbuild_flags)
|
command.extend(self.archbuild_flags)
|
||||||
command.extend(["--"] + self.makechrootpkg_flags)
|
command.extend(["--"] + self.makechrootpkg_flags)
|
||||||
command.extend(["--"] + self.makepkg_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(
|
Task._check_output(
|
||||||
*command,
|
*command,
|
||||||
|
@ -85,7 +85,7 @@ class JinjaTemplate:
|
|||||||
"""
|
"""
|
||||||
# idea comes from https://stackoverflow.com/a/38642558
|
# idea comes from https://stackoverflow.com/a/38642558
|
||||||
loader = jinja2.FileSystemLoader(searchpath=self.template_path.parent)
|
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)
|
template = environment.get_template(self.template_path.name)
|
||||||
|
|
||||||
content = [
|
content = [
|
||||||
|
@ -61,7 +61,7 @@ class Executor(Cleaner):
|
|||||||
build_single(single)
|
build_single(single)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.reporter.set_failed(single.base)
|
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()
|
self.clear_build()
|
||||||
|
|
||||||
return self.packages_built()
|
return self.packages_built()
|
||||||
@ -76,7 +76,7 @@ class Executor(Cleaner):
|
|||||||
try:
|
try:
|
||||||
self.repo.remove(package, fn)
|
self.repo.remove(package, fn)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception(f"could not remove {package}")
|
self.logger.exception("could not remove %s", package)
|
||||||
|
|
||||||
requested = set(packages)
|
requested = set(packages)
|
||||||
for local in self.packages():
|
for local in self.packages():
|
||||||
@ -132,7 +132,7 @@ class Executor(Cleaner):
|
|||||||
"""
|
"""
|
||||||
def update_single(fn: Optional[str], base: str) -> None:
|
def update_single(fn: Optional[str], base: str) -> None:
|
||||||
if fn is 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
|
return # suppress type checking, it never can be none actually
|
||||||
# in theory it might be NOT packages directory, but we suppose it is
|
# in theory it might be NOT packages directory, but we suppose it is
|
||||||
full_path = self.paths.packages / fn
|
full_path = self.paths.packages / fn
|
||||||
@ -150,7 +150,7 @@ class Executor(Cleaner):
|
|||||||
local = Package.load(filename, self.pacman, self.aur_url)
|
local = Package.load(filename, self.pacman, self.aur_url)
|
||||||
updates.setdefault(local.base, local).packages.update(local.packages)
|
updates.setdefault(local.base, local).packages.update(local.packages)
|
||||||
except Exception:
|
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():
|
for local in updates.values():
|
||||||
try:
|
try:
|
||||||
@ -159,7 +159,7 @@ class Executor(Cleaner):
|
|||||||
self.reporter.set_success(local)
|
self.reporter.set_success(local)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.reporter.set_failed(local.base)
|
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()
|
self.clear_packages()
|
||||||
|
|
||||||
return self.repo.repo_path
|
return self.repo.repo_path
|
||||||
|
@ -42,7 +42,7 @@ class Repository(Executor, UpdateHandler):
|
|||||||
local = Package.load(full_path, self.pacman, self.aur_url)
|
local = Package.load(full_path, self.pacman, self.aur_url)
|
||||||
result.setdefault(local.base, local).packages.update(local.packages)
|
result.setdefault(local.base, local).packages.update(local.packages)
|
||||||
except Exception:
|
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
|
continue
|
||||||
return list(result.values())
|
return list(result.values())
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class UpdateHandler(Cleaner):
|
|||||||
result.append(remote)
|
result.append(remote)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.reporter.set_failed(local.base)
|
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
|
continue
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@ -81,7 +81,7 @@ class UpdateHandler(Cleaner):
|
|||||||
else:
|
else:
|
||||||
self.reporter.set_pending(local.base)
|
self.reporter.set_pending(local.base)
|
||||||
except Exception:
|
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()
|
self.clear_manual()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -104,7 +104,7 @@ class GPG:
|
|||||||
})
|
})
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as e:
|
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
|
raise
|
||||||
return response.text
|
return response.text
|
||||||
|
|
||||||
@ -142,7 +142,7 @@ class GPG:
|
|||||||
return [path]
|
return [path]
|
||||||
key = self.configuration.get("sign", f"key_{base}", fallback=self.default_key)
|
key = self.configuration.get("sign", f"key_{base}", fallback=self.default_key)
|
||||||
if key is None:
|
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 [path]
|
||||||
return self.process(path, key)
|
return self.process(path, key)
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ class Watcher:
|
|||||||
try:
|
try:
|
||||||
parse_single(item)
|
parse_single(item)
|
||||||
except Exception:
|
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:
|
def _cache_save(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -84,9 +84,9 @@ class WebClient(Client):
|
|||||||
response = requests.post(self._package_url(package.base), json=payload)
|
response = requests.post(self._package_url(package.base), json=payload)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as e:
|
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:
|
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]]:
|
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
|
||||||
"""
|
"""
|
||||||
@ -104,9 +104,9 @@ class WebClient(Client):
|
|||||||
for package in status_json
|
for package in status_json
|
||||||
]
|
]
|
||||||
except requests.exceptions.HTTPError as e:
|
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:
|
except Exception:
|
||||||
self.logger.exception(f"could not get {base}")
|
self.logger.exception("could not get %s", base)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_internal(self) -> InternalStatus:
|
def get_internal(self) -> InternalStatus:
|
||||||
@ -121,7 +121,7 @@ class WebClient(Client):
|
|||||||
status_json = response.json()
|
status_json = response.json()
|
||||||
return InternalStatus.from_json(status_json)
|
return InternalStatus.from_json(status_json)
|
||||||
except requests.exceptions.HTTPError as e:
|
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:
|
except Exception:
|
||||||
self.logger.exception("could not get web service status")
|
self.logger.exception("could not get web service status")
|
||||||
return InternalStatus()
|
return InternalStatus()
|
||||||
@ -138,7 +138,7 @@ class WebClient(Client):
|
|||||||
status_json = response.json()
|
status_json = response.json()
|
||||||
return BuildStatus.from_json(status_json)
|
return BuildStatus.from_json(status_json)
|
||||||
except requests.exceptions.HTTPError as e:
|
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:
|
except Exception:
|
||||||
self.logger.exception("could not get service status")
|
self.logger.exception("could not get service status")
|
||||||
return BuildStatus()
|
return BuildStatus()
|
||||||
@ -152,9 +152,9 @@ class WebClient(Client):
|
|||||||
response = requests.delete(self._package_url(base))
|
response = requests.delete(self._package_url(base))
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as e:
|
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:
|
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:
|
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 = requests.post(self._package_url(base), json=payload)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as e:
|
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:
|
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:
|
def update_self(self, status: BuildStatusEnum) -> None:
|
||||||
"""
|
"""
|
||||||
@ -183,6 +183,6 @@ class WebClient(Client):
|
|||||||
response = requests.post(self._ahriman_url(), json=payload)
|
response = requests.post(self._ahriman_url(), json=payload)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as e:
|
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:
|
except Exception:
|
||||||
self.logger.exception("could not update service status")
|
self.logger.exception("could not update service status")
|
||||||
|
@ -17,24 +17,24 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import boto3 # type: ignore
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterable
|
from typing import Any, Dict, Generator, Iterable
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.upload.upload import Upload
|
from ahriman.core.upload.upload import Upload
|
||||||
from ahriman.core.util import check_output
|
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
|
|
||||||
|
|
||||||
class S3(Upload):
|
class S3(Upload):
|
||||||
"""
|
"""
|
||||||
aws-cli wrapper
|
aws-cli wrapper
|
||||||
:ivar bucket: full bucket name
|
:ivar bucket: boto3 S3 bucket object
|
||||||
:ivar command: command arguments for sync
|
:ivar chunk_size: chunk size for calculating checksums
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_check_output = check_output
|
|
||||||
|
|
||||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
@ -42,8 +42,69 @@ class S3(Upload):
|
|||||||
:param configuration: configuration instance
|
:param configuration: configuration instance
|
||||||
"""
|
"""
|
||||||
Upload.__init__(self, architecture, configuration)
|
Upload.__init__(self, architecture, configuration)
|
||||||
self.bucket = configuration.get("s3", "bucket")
|
self.bucket = self.get_bucket(configuration)
|
||||||
self.command = configuration.getlist("s3", "command")
|
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"))
|
||||||
|
|
||||||
|
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:
|
def sync(self, path: Path, built_packages: Iterable[Package]) -> None:
|
||||||
"""
|
"""
|
||||||
@ -51,5 +112,21 @@ class S3(Upload):
|
|||||||
:param path: local path to sync
|
:param path: local path to sync
|
||||||
:param built_packages: list of packages which has just been built
|
:param built_packages: list of packages which has just been built
|
||||||
"""
|
"""
|
||||||
# TODO rewrite to boto, but it is bullshit
|
remote_objects = self.get_remote_objects()
|
||||||
S3._check_output(*self.command, str(path), self.bucket, exception=None, logger=self.logger)
|
local_files = self.get_local_files(path)
|
||||||
|
|
||||||
|
# sync to remotes first
|
||||||
|
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
|
||||||
|
remote_path = Path(self.architecture) / local_file
|
||||||
|
self.bucket.upload_file(str(path / local_file), str(remote_path))
|
||||||
|
|
||||||
|
# remove files which were removed locally
|
||||||
|
for local_file, remote_object in remote_objects.items():
|
||||||
|
if local_file in local_files:
|
||||||
|
continue
|
||||||
|
remote_object.delete()
|
||||||
|
@ -17,4 +17,4 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "1.1.0"
|
__version__ = "1.2.1"
|
||||||
|
@ -13,17 +13,32 @@ from ahriman.models.package import Package
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def application(configuration: Configuration, mocker: MockerFixture) -> Application:
|
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")
|
mocker.patch("pathlib.Path.mkdir")
|
||||||
return Application("x86_64", configuration)
|
return Application("x86_64", configuration)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def args() -> argparse.Namespace:
|
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)
|
return argparse.Namespace(lock=None, force=False, unsafe=False, no_report=True)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def aur_package_ahriman(package_ahriman: Package) -> aur.Package:
|
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(
|
return aur.Package(
|
||||||
num_votes=None,
|
num_votes=None,
|
||||||
description=package_ahriman.packages[package_ahriman.base].description,
|
description=package_ahriman.packages[package_ahriman.base].description,
|
||||||
@ -44,9 +59,19 @@ def aur_package_ahriman(package_ahriman: Package) -> aur.Package:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def lock(args: argparse.Namespace, configuration: Configuration) -> Lock:
|
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)
|
return Lock(args, "x86_64", configuration)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def parser() -> argparse.ArgumentParser:
|
def parser() -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
fixture for command line arguments parser
|
||||||
|
:return: command line arguments parser test instance
|
||||||
|
"""
|
||||||
return _parser()
|
return _parser()
|
||||||
|
@ -7,6 +7,11 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
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.package = []
|
||||||
args.now = False
|
args.now = False
|
||||||
args.without_dependencies = False
|
args.without_dependencies = False
|
||||||
|
@ -7,6 +7,11 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
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_build = False
|
||||||
args.no_cache = False
|
args.no_cache = False
|
||||||
args.no_chroot = False
|
args.no_chroot = False
|
||||||
|
@ -7,6 +7,11 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
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 = "0xE989490C"
|
||||||
args.key_server = "keys.gnupg.net"
|
args.key_server = "keys.gnupg.net"
|
||||||
return args
|
return args
|
||||||
|
@ -8,6 +8,11 @@ from ahriman.models.package import Package
|
|||||||
|
|
||||||
|
|
||||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
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 = []
|
args.depends_on = []
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
@ -7,6 +7,11 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
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.package = []
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
@ -7,6 +7,11 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
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 = []
|
args.target = []
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
@ -8,6 +8,11 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
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"]
|
args.search = ["ahriman"]
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
@ -11,6 +11,11 @@ from ahriman.models.sign_settings import SignSettings
|
|||||||
|
|
||||||
|
|
||||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
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.build_command = "ahriman"
|
||||||
args.from_configuration = Path("/usr/share/devtools/pacman-extra.conf")
|
args.from_configuration = Path("/usr/share/devtools/pacman-extra.conf")
|
||||||
args.no_multilib = False
|
args.no_multilib = False
|
||||||
|
@ -7,6 +7,11 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
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.package = []
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
@ -9,6 +9,11 @@ from ahriman.models.package import Package
|
|||||||
|
|
||||||
|
|
||||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
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.ahriman = True
|
||||||
args.package = []
|
args.package = []
|
||||||
return args
|
return args
|
||||||
|
@ -9,6 +9,11 @@ from ahriman.models.package import Package
|
|||||||
|
|
||||||
|
|
||||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
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.status = BuildStatusEnum.Success
|
||||||
args.package = None
|
args.package = None
|
||||||
args.remove = False
|
args.remove = False
|
||||||
|
@ -7,6 +7,11 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
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 = []
|
args.target = []
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
@ -8,6 +8,11 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
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.package = []
|
||||||
args.dry_run = False
|
args.dry_run = False
|
||||||
args.no_aur = False
|
args.no_aur = False
|
||||||
|
@ -299,5 +299,5 @@ def test_update(application: Application, package_ahriman: Package, mocker: Mock
|
|||||||
|
|
||||||
application.update([package_ahriman])
|
application.update([package_ahriman])
|
||||||
build_mock.assert_called_once()
|
build_mock.assert_called_once()
|
||||||
update_mock.assert_has_calls([mock.call([]), mock.call(paths)])
|
update_mock.assert_called_with(paths)
|
||||||
finalize_mock.assert_has_calls([mock.call([]), mock.call([package_ahriman])])
|
finalize_mock.assert_called_with([package_ahriman])
|
||||||
|
@ -111,7 +111,7 @@ def test_clear(lock: Lock) -> None:
|
|||||||
"""
|
"""
|
||||||
must remove lock file
|
must remove lock file
|
||||||
"""
|
"""
|
||||||
lock.path = Path(tempfile.mktemp())
|
lock.path = Path(tempfile.mktemp()) # nosec
|
||||||
lock.path.touch()
|
lock.path.touch()
|
||||||
|
|
||||||
lock.clear()
|
lock.clear()
|
||||||
@ -122,7 +122,7 @@ def test_clear_missing(lock: Lock) -> None:
|
|||||||
"""
|
"""
|
||||||
must not fail on lock removal if file is missing
|
must not fail on lock removal if file is missing
|
||||||
"""
|
"""
|
||||||
lock.path = Path(tempfile.mktemp())
|
lock.path = Path(tempfile.mktemp()) # nosec
|
||||||
lock.clear()
|
lock.clear()
|
||||||
|
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ def test_create(lock: Lock) -> None:
|
|||||||
"""
|
"""
|
||||||
must create lock
|
must create lock
|
||||||
"""
|
"""
|
||||||
lock.path = Path(tempfile.mktemp())
|
lock.path = Path(tempfile.mktemp()) # nosec
|
||||||
|
|
||||||
lock.create()
|
lock.create()
|
||||||
assert lock.path.is_file()
|
assert lock.path.is_file()
|
||||||
@ -150,7 +150,7 @@ def test_create_exception(lock: Lock) -> None:
|
|||||||
"""
|
"""
|
||||||
must raise exception if file already exists
|
must raise exception if file already exists
|
||||||
"""
|
"""
|
||||||
lock.path = Path(tempfile.mktemp())
|
lock.path = Path(tempfile.mktemp()) # nosec
|
||||||
lock.path.touch()
|
lock.path.touch()
|
||||||
|
|
||||||
with pytest.raises(DuplicateRun):
|
with pytest.raises(DuplicateRun):
|
||||||
@ -172,7 +172,7 @@ def test_create_unsafe(lock: Lock) -> None:
|
|||||||
must not raise exception if force flag set
|
must not raise exception if force flag set
|
||||||
"""
|
"""
|
||||||
lock.force = True
|
lock.force = True
|
||||||
lock.path = Path(tempfile.mktemp())
|
lock.path = Path(tempfile.mktemp()) # nosec
|
||||||
lock.path.touch()
|
lock.path.touch()
|
||||||
|
|
||||||
lock.create()
|
lock.create()
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -10,6 +12,7 @@ from ahriman.models.package import Package
|
|||||||
from ahriman.models.package_description import PackageDescription
|
from ahriman.models.package_description import PackageDescription
|
||||||
from ahriman.models.repository_paths import RepositoryPaths
|
from ahriman.models.repository_paths import RepositoryPaths
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
@ -17,21 +20,47 @@ T = TypeVar("T")
|
|||||||
# https://stackoverflow.com/a/21611963
|
# https://stackoverflow.com/a/21611963
|
||||||
@pytest.helpers.register
|
@pytest.helpers.register
|
||||||
def anyvar(cls: Type[T], strict: bool = False) -> T:
|
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):
|
class AnyVar(cls):
|
||||||
|
"""
|
||||||
|
any value wrapper
|
||||||
|
"""
|
||||||
|
|
||||||
def __eq__(self, other: Any) -> bool:
|
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 not strict or isinstance(other, cls)
|
||||||
|
|
||||||
return AnyVar()
|
return AnyVar()
|
||||||
|
|
||||||
|
|
||||||
# generic fixtures
|
# generic fixtures
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def configuration(resource_path_root: Path) -> Configuration:
|
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"
|
path = resource_path_root / "core" / "ahriman.ini"
|
||||||
return Configuration.from_path(path=path, architecture="x86_64", logfile=False)
|
return Configuration.from_path(path=path, architecture="x86_64", logfile=False)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def package_ahriman(package_description_ahriman: PackageDescription) -> Package:
|
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}
|
packages = {"ahriman": package_description_ahriman}
|
||||||
return Package(
|
return Package(
|
||||||
base="ahriman",
|
base="ahriman",
|
||||||
@ -44,6 +73,12 @@ def package_ahriman(package_description_ahriman: PackageDescription) -> Package:
|
|||||||
def package_python_schedule(
|
def package_python_schedule(
|
||||||
package_description_python_schedule: PackageDescription,
|
package_description_python_schedule: PackageDescription,
|
||||||
package_description_python2_schedule: PackageDescription) -> Package:
|
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 = {
|
packages = {
|
||||||
"python-schedule": package_description_python_schedule,
|
"python-schedule": package_description_python_schedule,
|
||||||
"python2-schedule": package_description_python2_schedule
|
"python2-schedule": package_description_python2_schedule
|
||||||
@ -57,6 +92,10 @@ def package_python_schedule(
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def package_description_ahriman() -> PackageDescription:
|
def package_description_ahriman() -> PackageDescription:
|
||||||
|
"""
|
||||||
|
package description fixture
|
||||||
|
:return: package description test instance
|
||||||
|
"""
|
||||||
return PackageDescription(
|
return PackageDescription(
|
||||||
architecture="x86_64",
|
architecture="x86_64",
|
||||||
archive_size=4200,
|
archive_size=4200,
|
||||||
@ -72,6 +111,10 @@ def package_description_ahriman() -> PackageDescription:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def package_description_python_schedule() -> PackageDescription:
|
def package_description_python_schedule() -> PackageDescription:
|
||||||
|
"""
|
||||||
|
package description fixture
|
||||||
|
:return: package description test instance
|
||||||
|
"""
|
||||||
return PackageDescription(
|
return PackageDescription(
|
||||||
architecture="x86_64",
|
architecture="x86_64",
|
||||||
archive_size=4201,
|
archive_size=4201,
|
||||||
@ -87,6 +130,10 @@ def package_description_python_schedule() -> PackageDescription:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def package_description_python2_schedule() -> PackageDescription:
|
def package_description_python2_schedule() -> PackageDescription:
|
||||||
|
"""
|
||||||
|
package description fixture
|
||||||
|
:return: package description test instance
|
||||||
|
"""
|
||||||
return PackageDescription(
|
return PackageDescription(
|
||||||
architecture="x86_64",
|
architecture="x86_64",
|
||||||
archive_size=4202,
|
archive_size=4202,
|
||||||
@ -102,6 +149,10 @@ def package_description_python2_schedule() -> PackageDescription:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def repository_paths(configuration: Configuration) -> RepositoryPaths:
|
def repository_paths(configuration: Configuration) -> RepositoryPaths:
|
||||||
|
"""
|
||||||
|
repository paths fixture
|
||||||
|
:return: repository paths test instance
|
||||||
|
"""
|
||||||
return RepositoryPaths(
|
return RepositoryPaths(
|
||||||
architecture="x86_64",
|
architecture="x86_64",
|
||||||
root=configuration.getpath("repository", "root"))
|
root=configuration.getpath("repository", "root"))
|
||||||
@ -109,5 +160,11 @@ def repository_paths(configuration: Configuration) -> RepositoryPaths:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def watcher(configuration: Configuration, mocker: MockerFixture) -> Watcher:
|
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")
|
mocker.patch("pathlib.Path.mkdir")
|
||||||
return Watcher("x86_64", configuration)
|
return Watcher("x86_64", configuration)
|
||||||
|
@ -14,4 +14,4 @@ def test_all_packages_with_provides(pacman: Pacman) -> None:
|
|||||||
"""
|
"""
|
||||||
package list must contain provides packages
|
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
|
@pytest.fixture
|
||||||
def leaf_ahriman(package_ahriman: Package) -> Leaf:
|
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())
|
return Leaf(package_ahriman, set())
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def leaf_python_schedule(package_python_schedule: Package) -> Leaf:
|
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())
|
return Leaf(package_python_schedule, set())
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def pacman(configuration: Configuration) -> Pacman:
|
def pacman(configuration: Configuration) -> Pacman:
|
||||||
|
"""
|
||||||
|
fixture for pacman wrapper
|
||||||
|
:param configuration: configuration fixture
|
||||||
|
:return: pacman wrapper test instance
|
||||||
|
"""
|
||||||
return Pacman(configuration)
|
return Pacman(configuration)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def repo(configuration: Configuration, repository_paths: RepositoryPaths) -> Repo:
|
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, [])
|
return Repo(configuration.get("repository", "name"), repository_paths, [])
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def task_ahriman(package_ahriman: Package, configuration: Configuration, repository_paths: RepositoryPaths) -> Task:
|
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)
|
return Task(package_ahriman, configuration, repository_paths)
|
||||||
|
@ -12,12 +12,24 @@ from ahriman.core.repository.update_handler import UpdateHandler
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def cleaner(configuration: Configuration, mocker: MockerFixture) -> Cleaner:
|
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")
|
mocker.patch("pathlib.Path.mkdir")
|
||||||
return Cleaner("x86_64", configuration)
|
return Cleaner("x86_64", configuration)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def executor(configuration: Configuration, mocker: MockerFixture) -> Executor:
|
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("pathlib.Path.mkdir")
|
||||||
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_build")
|
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_build")
|
||||||
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache")
|
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache")
|
||||||
@ -29,17 +41,34 @@ def executor(configuration: Configuration, mocker: MockerFixture) -> Executor:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def repository(configuration: Configuration, mocker: MockerFixture) -> Repository:
|
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")
|
mocker.patch("pathlib.Path.mkdir")
|
||||||
return Repository("x86_64", configuration)
|
return Repository("x86_64", configuration)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def properties(configuration: Configuration) -> Properties:
|
def properties(configuration: Configuration) -> Properties:
|
||||||
|
"""
|
||||||
|
fixture for properties
|
||||||
|
:param configuration: configuration fixture
|
||||||
|
:return: properties test instance
|
||||||
|
"""
|
||||||
return Properties("x86_64", configuration)
|
return Properties("x86_64", configuration)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def update_handler(configuration: Configuration, mocker: MockerFixture) -> UpdateHandler:
|
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("pathlib.Path.mkdir")
|
||||||
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_build")
|
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_build")
|
||||||
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache")
|
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:
|
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("pathlib.Path.iterdir", return_value=[Path("a"), Path("b"), Path("c")])
|
||||||
mocker.patch("shutil.rmtree")
|
mocker.patch("shutil.rmtree")
|
||||||
|
|
||||||
|
|
||||||
def _mock_clear_check() -> None:
|
def _mock_clear_check() -> None:
|
||||||
|
"""
|
||||||
|
mocker helper for clear tests
|
||||||
|
"""
|
||||||
shutil.rmtree.assert_has_calls([
|
shutil.rmtree.assert_has_calls([
|
||||||
mock.call(Path("a")),
|
mock.call(Path("a")),
|
||||||
mock.call(Path("b")),
|
mock.call(Path("b")),
|
||||||
|
@ -6,10 +6,20 @@ from ahriman.core.sign.gpg import GPG
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def gpg(configuration: Configuration) -> GPG:
|
def gpg(configuration: Configuration) -> GPG:
|
||||||
|
"""
|
||||||
|
fixture for empty GPG
|
||||||
|
:param configuration: configuration fixture
|
||||||
|
:return: GPG test instance
|
||||||
|
"""
|
||||||
return GPG("x86_64", configuration)
|
return GPG("x86_64", configuration)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def gpg_with_key(gpg: GPG) -> GPG:
|
def gpg_with_key(gpg: GPG) -> GPG:
|
||||||
|
"""
|
||||||
|
fixture for correct GPG
|
||||||
|
:param gpg: empty GPG fixture
|
||||||
|
:return: GPG test instance
|
||||||
|
"""
|
||||||
gpg.default_key = "key"
|
gpg.default_key = "key"
|
||||||
return gpg
|
return gpg
|
||||||
|
@ -11,20 +11,38 @@ from ahriman.models.package import Package
|
|||||||
# helpers
|
# helpers
|
||||||
@pytest.helpers.register
|
@pytest.helpers.register
|
||||||
def get_package_status(package: Package) -> Dict[str, Any]:
|
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()}
|
return {"status": BuildStatusEnum.Unknown.value, "package": package.view()}
|
||||||
|
|
||||||
|
|
||||||
@pytest.helpers.register
|
@pytest.helpers.register
|
||||||
def get_package_status_extended(package: Package) -> Dict[str, Any]:
|
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()}
|
return {"status": BuildStatus().view(), "package": package.view()}
|
||||||
|
|
||||||
|
|
||||||
# fixtures
|
# fixtures
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def client() -> Client:
|
def client() -> Client:
|
||||||
|
"""
|
||||||
|
fixture for dummy client
|
||||||
|
:return: dummy client test instance
|
||||||
|
"""
|
||||||
return Client()
|
return Client()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def web_client() -> WebClient:
|
def web_client() -> WebClient:
|
||||||
|
"""
|
||||||
|
fixture for web client
|
||||||
|
:return: web client test instance
|
||||||
|
"""
|
||||||
return WebClient("localhost", 8080)
|
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
|
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",
|
mocker.patch("ahriman.core.status.watcher.Watcher.cache_path",
|
||||||
new_callable=PropertyMock, return_value=dump_file)
|
new_callable=PropertyMock, return_value=dump_file)
|
||||||
known_current = {package_ahriman.base: (package_ahriman, BuildStatus())}
|
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,98 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
from typing import Any, List
|
||||||
|
from unittest import mock
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
|
||||||
from ahriman.core.upload.s3 import S3
|
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_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, s3_remote_objects: List[Any], mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must run sync command
|
must run sync command
|
||||||
"""
|
"""
|
||||||
check_output_mock = mocker.patch("ahriman.core.upload.s3.S3._check_output")
|
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}
|
||||||
|
print(local_files)
|
||||||
|
print(remote_objects)
|
||||||
|
|
||||||
upload = S3("x86_64", configuration)
|
local_files_mock = mocker.patch("ahriman.core.upload.s3.S3.get_local_files", return_value=local_files)
|
||||||
upload.sync(Path("path"), [])
|
remote_objects_mock = mocker.patch("ahriman.core.upload.s3.S3.get_remote_objects", return_value=remote_objects)
|
||||||
check_output_mock.assert_called_once()
|
upload_mock = s3.bucket = MagicMock()
|
||||||
|
|
||||||
|
s3.sync(root, [])
|
||||||
|
|
||||||
|
local_files_mock.assert_called_once()
|
||||||
|
remote_objects_mock.assert_called_once()
|
||||||
|
upload_mock.upload_file.assert_has_calls([
|
||||||
|
mock.call(str(root / s3.architecture / "b"), f"{s3.architecture}/{s3.architecture}/b"),
|
||||||
|
mock.call(str(root / s3.architecture / "d"), f"{s3.architecture}/{s3.architecture}/d"),
|
||||||
|
], any_order=True)
|
||||||
|
remote_objects[Path("x86_64/a")].delete.assert_called_once()
|
||||||
|
@ -12,11 +12,19 @@ from ahriman.models.package_description import PackageDescription
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def build_status_failed() -> BuildStatus:
|
def build_status_failed() -> BuildStatus:
|
||||||
|
"""
|
||||||
|
build result fixture with failed status
|
||||||
|
:return: failed build status test instance
|
||||||
|
"""
|
||||||
return BuildStatus(BuildStatusEnum.Failed, 42)
|
return BuildStatus(BuildStatusEnum.Failed, 42)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def counters() -> Counters:
|
def counters() -> Counters:
|
||||||
|
"""
|
||||||
|
counters fixture
|
||||||
|
:return: counters test instance
|
||||||
|
"""
|
||||||
return Counters(total=10,
|
return Counters(total=10,
|
||||||
unknown=1,
|
unknown=1,
|
||||||
pending=2,
|
pending=2,
|
||||||
@ -27,6 +35,11 @@ def counters() -> Counters:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def internal_status(counters: Counters) -> InternalStatus:
|
def internal_status(counters: Counters) -> InternalStatus:
|
||||||
|
"""
|
||||||
|
internal status fixture
|
||||||
|
:param counters: counters fixture
|
||||||
|
:return: internal status test instance
|
||||||
|
"""
|
||||||
return InternalStatus(architecture="x86_64",
|
return InternalStatus(architecture="x86_64",
|
||||||
packages=counters,
|
packages=counters,
|
||||||
version=version.__version__,
|
version=version.__version__,
|
||||||
@ -35,6 +48,10 @@ def internal_status(counters: Counters) -> InternalStatus:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def package_tpacpi_bat_git() -> Package:
|
def package_tpacpi_bat_git() -> Package:
|
||||||
|
"""
|
||||||
|
git package fixture
|
||||||
|
:return: git package test instance
|
||||||
|
"""
|
||||||
return Package(
|
return Package(
|
||||||
base="tpacpi-bat-git",
|
base="tpacpi-bat-git",
|
||||||
version="3.1.r12.g4959b52-1",
|
version="3.1.r12.g4959b52-1",
|
||||||
@ -44,6 +61,11 @@ def package_tpacpi_bat_git() -> Package:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def pyalpm_handle(pyalpm_package_ahriman: MagicMock) -> MagicMock:
|
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 = MagicMock()
|
||||||
mock.handle.load_pkg.return_value = pyalpm_package_ahriman
|
mock.handle.load_pkg.return_value = pyalpm_package_ahriman
|
||||||
return mock
|
return mock
|
||||||
@ -51,6 +73,11 @@ def pyalpm_handle(pyalpm_package_ahriman: MagicMock) -> MagicMock:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def pyalpm_package_ahriman(package_ahriman: Package) -> MagicMock:
|
def pyalpm_package_ahriman(package_ahriman: Package) -> MagicMock:
|
||||||
|
"""
|
||||||
|
mock object for pyalpm package
|
||||||
|
:param package_ahriman: package fixture
|
||||||
|
:return: pyalpm package mock
|
||||||
|
"""
|
||||||
mock = MagicMock()
|
mock = MagicMock()
|
||||||
type(mock).base = PropertyMock(return_value=package_ahriman.base)
|
type(mock).base = PropertyMock(return_value=package_ahriman.base)
|
||||||
type(mock).name = 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
|
@pytest.fixture
|
||||||
def pyalpm_package_description_ahriman(package_description_ahriman: PackageDescription) -> MagicMock:
|
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()
|
mock = MagicMock()
|
||||||
type(mock).arch = PropertyMock(return_value=package_description_ahriman.architecture)
|
type(mock).arch = PropertyMock(return_value=package_description_ahriman.architecture)
|
||||||
type(mock).builddate = PropertyMock(return_value=package_description_ahriman.build_date)
|
type(mock).builddate = PropertyMock(return_value=package_description_ahriman.build_date)
|
||||||
|
@ -9,5 +9,11 @@ from ahriman.web.web import setup_service
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def application(configuration: Configuration, mocker: MockerFixture) -> web.Application:
|
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")
|
mocker.patch("pathlib.Path.mkdir")
|
||||||
return setup_service("x86_64", configuration)
|
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,49 @@
|
|||||||
|
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()
|
||||||
|
request_handler.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()
|
||||||
|
request_handler.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_application_mock = mocker.patch("aiohttp.web.run_app")
|
||||||
|
|
||||||
run_server(application)
|
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))
|
handle_signals=False, access_log=pytest.helpers.anyvar(int))
|
||||||
|
@ -10,5 +10,13 @@ from typing import Any
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def client(application: web.Application, loop: BaseEventLoop,
|
def client(application: web.Application, loop: BaseEventLoop,
|
||||||
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
|
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=[])
|
mocker.patch("pathlib.Path.iterdir", return_value=[])
|
||||||
return loop.run_until_complete(aiohttp_client(application))
|
return loop.run_until_complete(aiohttp_client(application))
|
||||||
|
@ -26,7 +26,7 @@ target =
|
|||||||
target =
|
target =
|
||||||
|
|
||||||
[email]
|
[email]
|
||||||
host = 0.0.0.0
|
host = 127.0.0.1
|
||||||
link_path =
|
link_path =
|
||||||
no_empty_report = no
|
no_empty_report = no
|
||||||
port = 587
|
port = 587
|
||||||
@ -48,9 +48,11 @@ command = rsync --archive --verbose --compress --partial --delete
|
|||||||
remote =
|
remote =
|
||||||
|
|
||||||
[s3]
|
[s3]
|
||||||
bucket =
|
access_key =
|
||||||
command = aws s3 sync --quiet --delete
|
bucket = bucket
|
||||||
|
region = eu-central-1
|
||||||
|
secret_key =
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
host = 0.0.0.0
|
host = 127.0.0.1
|
||||||
templates = ../web/templates
|
templates = ../web/templates
|
@ -2,10 +2,10 @@
|
|||||||
keys = root,builder,build_details,http
|
keys = root,builder,build_details,http
|
||||||
|
|
||||||
[handlers]
|
[handlers]
|
||||||
keys = console_handler,build_file_handler,file_handler,http_handler
|
keys = console_handler,build_file_handler,file_handler,http_handler,syslog_handler
|
||||||
|
|
||||||
[formatters]
|
[formatters]
|
||||||
keys = generic_format
|
keys = generic_format,syslog_format
|
||||||
|
|
||||||
[handler_console_handler]
|
[handler_console_handler]
|
||||||
class = StreamHandler
|
class = StreamHandler
|
||||||
@ -31,29 +31,39 @@ level = DEBUG
|
|||||||
formatter = generic_format
|
formatter = generic_format
|
||||||
args = ("/var/log/ahriman/http.log", "a", 20971520, 20)
|
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]
|
[formatter_generic_format]
|
||||||
format = [%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
|
format = [%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
|
||||||
datefmt =
|
datefmt =
|
||||||
|
|
||||||
|
[formatter_syslog_format]
|
||||||
|
format = [%(levelname)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
|
||||||
|
datefmt =
|
||||||
|
|
||||||
[logger_root]
|
[logger_root]
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
handlers = file_handler
|
handlers = syslog_handler
|
||||||
qualname = root
|
qualname = root
|
||||||
|
|
||||||
[logger_builder]
|
[logger_builder]
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
handlers = file_handler
|
handlers = syslog_handler
|
||||||
qualname = builder
|
qualname = builder
|
||||||
propagate = 0
|
propagate = 0
|
||||||
|
|
||||||
[logger_build_details]
|
[logger_build_details]
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
handlers = build_file_handler
|
handlers = syslog_handler
|
||||||
qualname = build_details
|
qualname = build_details
|
||||||
propagate = 0
|
propagate = 0
|
||||||
|
|
||||||
[logger_http]
|
[logger_http]
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
handlers = http_handler
|
handlers = syslog_handler
|
||||||
qualname = http
|
qualname = http
|
||||||
propagate = 0
|
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