mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-06-29 07:05:50 +00:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
b0d1f3c091 | |||
50e219fda5 | |||
75298d1b8a | |||
8196dcc8a0 | |||
f634f1df58 | |||
32df4fc54f |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: create release
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
|
3
.github/workflows/run-tests.yml
vendored
3
.github/workflows/run-tests.yml
vendored
@ -1,5 +1,4 @@
|
||||
# based on https://github.com/actions/starter-workflows/blob/main/ci/python-app.yml
|
||||
name: check commit
|
||||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Maintainer: Evgeniy Alekseev
|
||||
|
||||
pkgname='ahriman'
|
||||
pkgver=0.22.1
|
||||
pkgver=1.0.0
|
||||
pkgrel=1
|
||||
pkgdesc="ArcHlinux ReposItory MANager"
|
||||
arch=('any')
|
||||
@ -17,7 +17,6 @@ optdepends=('aws-cli: sync to s3'
|
||||
'python-aiohttp: web server'
|
||||
'python-aiohttp-jinja2: web server'
|
||||
'python-jinja: html report generation'
|
||||
'python-requests: web server'
|
||||
'rsync: sync by using rsync'
|
||||
'subversion: -svn packages support')
|
||||
source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz"
|
||||
|
@ -13,7 +13,7 @@ archbuild_flags =
|
||||
build_command = extra-x86_64-build
|
||||
ignore_packages =
|
||||
makechrootpkg_flags =
|
||||
makepkg_flags = --skippgpcheck
|
||||
makepkg_flags =
|
||||
|
||||
[repository]
|
||||
name = aur-clone
|
||||
@ -37,7 +37,7 @@ template_path = /usr/share/ahriman/repo-index.jinja2
|
||||
target =
|
||||
|
||||
[rsync]
|
||||
command = rsync --archive --verbose --compress --partial --delete
|
||||
command = rsync --archive --compress --partial --delete
|
||||
|
||||
[s3]
|
||||
command = aws s3 sync --quiet --delete
|
||||
|
@ -28,9 +28,9 @@
|
||||
SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly
|
||||
</code>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% include "search-line.jinja2" %}
|
||||
{% endif %}
|
||||
|
||||
<section class="element">
|
||||
<table class="sortable search-table">
|
||||
|
2
setup.py
2
setup.py
@ -29,6 +29,7 @@ setup(
|
||||
install_requires=[
|
||||
"aur",
|
||||
"pyalpm",
|
||||
"requests",
|
||||
"srcinfo",
|
||||
],
|
||||
setup_requires=[
|
||||
@ -89,7 +90,6 @@ setup(
|
||||
"Jinja2",
|
||||
"aiohttp",
|
||||
"aiohttp_jinja2",
|
||||
"requests",
|
||||
],
|
||||
},
|
||||
)
|
||||
|
@ -22,9 +22,8 @@ import sys
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import ahriman.application.handlers as handlers
|
||||
import ahriman.version as version
|
||||
|
||||
from ahriman import version
|
||||
from ahriman.application import handlers
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.sign_settings import SignSettings
|
||||
|
||||
@ -57,9 +56,11 @@ def _parser() -> argparse.ArgumentParser:
|
||||
_set_check_parser(subparsers)
|
||||
_set_clean_parser(subparsers)
|
||||
_set_config_parser(subparsers)
|
||||
_set_key_import_parser(subparsers)
|
||||
_set_rebuild_parser(subparsers)
|
||||
_set_remove_parser(subparsers)
|
||||
_set_report_parser(subparsers)
|
||||
_set_search_parser(subparsers)
|
||||
_set_setup_parser(subparsers)
|
||||
_set_sign_parser(subparsers)
|
||||
_set_status_parser(subparsers)
|
||||
@ -131,6 +132,21 @@ def _set_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
return parser
|
||||
|
||||
|
||||
def _set_key_import_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for key import subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("key-import", help="import PGP key",
|
||||
description="import PGP key from public sources to repository user",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("--key-server", help="key server for key import", default="keys.gnupg.net")
|
||||
parser.add_argument("key", help="PGP key to import from public server")
|
||||
parser.set_defaults(handler=handlers.KeyImport, lock=None, no_report=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for rebuild subcommand
|
||||
@ -170,6 +186,18 @@ def _set_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
return parser
|
||||
|
||||
|
||||
def _set_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for search subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("search", help="search for package", description="search for package in AUR using API")
|
||||
parser.add_argument("search", help="search terms, can be specified multiple times", nargs="+")
|
||||
parser.set_defaults(handler=handlers.Search, lock=None, no_report=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for setup subcommand
|
||||
|
@ -22,9 +22,11 @@ from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.application.handlers.add import Add
|
||||
from ahriman.application.handlers.clean import Clean
|
||||
from ahriman.application.handlers.dump import Dump
|
||||
from ahriman.application.handlers.key_import import KeyImport
|
||||
from ahriman.application.handlers.rebuild import Rebuild
|
||||
from ahriman.application.handlers.remove import Remove
|
||||
from ahriman.application.handlers.report import Report
|
||||
from ahriman.application.handlers.search import Search
|
||||
from ahriman.application.handlers.setup import Setup
|
||||
from ahriman.application.handlers.sign import Sign
|
||||
from ahriman.application.handlers.status import Status
|
||||
|
42
src/ahriman/application/handlers/key_import.py
Normal file
42
src/ahriman/application/handlers/key_import.py
Normal file
@ -0,0 +1,42 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class KeyImport(Handler):
|
||||
"""
|
||||
key import packages handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
"""
|
||||
Application(architecture, configuration).repository.sign.import_key(args.key_server, args.key)
|
58
src/ahriman/application/handlers/search.py
Normal file
58
src/ahriman/application/handlers/search.py
Normal file
@ -0,0 +1,58 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
import aur # type: ignore
|
||||
|
||||
from typing import Callable, Type
|
||||
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class Search(Handler):
|
||||
"""
|
||||
packages search handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
"""
|
||||
search = " ".join(args.search)
|
||||
packages = aur.search(search)
|
||||
|
||||
# it actually always should return string
|
||||
# explicit cast to string just to avoid mypy warning for untyped library
|
||||
comparator: Callable[[aur.Package], str] = lambda item: str(item.package_base)
|
||||
for package in sorted(packages, key=comparator):
|
||||
Search.log_fn(package)
|
||||
|
||||
@staticmethod
|
||||
def log_fn(package: aur.Package) -> None:
|
||||
"""
|
||||
log package information
|
||||
:param package: package object as from AUR
|
||||
"""
|
||||
print(f"=> {package.package_base} {package.version}")
|
||||
print(f" {package.description}")
|
@ -20,12 +20,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
from types import TracebackType
|
||||
from typing import Literal, Optional, Type
|
||||
|
||||
from ahriman import version
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import DuplicateRun, UnsafeRun
|
||||
from ahriman.core.status.client import Client
|
||||
@ -61,12 +63,13 @@ class Lock:
|
||||
default workflow is the following:
|
||||
|
||||
check user UID
|
||||
remove lock file if force flag is set
|
||||
check if there is lock file
|
||||
check web status watcher status
|
||||
create lock file
|
||||
report to web if enabled
|
||||
"""
|
||||
self.check_user()
|
||||
self.check_version()
|
||||
self.create()
|
||||
self.reporter.update_self(BuildStatusEnum.Building)
|
||||
return self
|
||||
@ -85,6 +88,15 @@ class Lock:
|
||||
self.reporter.update_self(status)
|
||||
return False
|
||||
|
||||
def check_version(self) -> None:
|
||||
"""
|
||||
check web server version
|
||||
"""
|
||||
status = self.reporter.get_internal()
|
||||
if status.version is not None and status.version != version.__version__:
|
||||
logging.getLogger("root").warning(f"status watcher version mismatch, "
|
||||
f"our {version.__version__}, their {status.version}")
|
||||
|
||||
def check_user(self) -> None:
|
||||
"""
|
||||
check if current user is actually owner of ahriman root
|
||||
|
@ -140,7 +140,7 @@ class Configuration(configparser.RawConfigParser):
|
||||
if path == self.logging_path:
|
||||
continue # we don't want to load logging explicitly
|
||||
self.read(path)
|
||||
except (FileNotFoundError, configparser.NoOptionError):
|
||||
except (FileNotFoundError, configparser.NoOptionError, configparser.NoSectionError):
|
||||
pass
|
||||
|
||||
def load_logging(self, logfile: bool) -> None:
|
||||
|
@ -18,13 +18,14 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Set, Tuple
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import BuildFailed
|
||||
from ahriman.core.util import check_output
|
||||
from ahriman.core.util import check_output, exception_response_text
|
||||
from ahriman.models.sign_settings import SignSettings
|
||||
|
||||
|
||||
@ -87,6 +88,36 @@ class GPG:
|
||||
default_key = configuration.get("sign", "key") if targets else None
|
||||
return targets, default_key
|
||||
|
||||
def download_key(self, server: str, key: str) -> str:
|
||||
"""
|
||||
download key from public PGP server
|
||||
:param server: public PGP server which will be used to download the key
|
||||
:param key: key ID to download
|
||||
:return: key as plain text
|
||||
"""
|
||||
key = key if key.startswith("0x") else f"0x{key}"
|
||||
try:
|
||||
response = requests.get(f"http://{server}/pks/lookup", params={
|
||||
"op": "get",
|
||||
"options": "mr",
|
||||
"search": key
|
||||
})
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not download key {key} from {server}: {exception_response_text(e)}")
|
||||
raise
|
||||
return response.text
|
||||
|
||||
def import_key(self, server: str, key: str) -> None:
|
||||
"""
|
||||
import key to current user and sign it locally
|
||||
:param server: public PGP server which will be used to download the key
|
||||
:param key: key ID to import
|
||||
"""
|
||||
key_body = self.download_key(server, key)
|
||||
GPG._check_output("gpg", "--import", input_data=key_body, exception=None, logger=self.logger)
|
||||
GPG._check_output("gpg", "--quick-lsign-key", key, exception=None, logger=self.logger)
|
||||
|
||||
def process(self, path: Path, key: str) -> List[Path]:
|
||||
"""
|
||||
gpg command wrapper
|
||||
|
@ -23,6 +23,7 @@ from typing import List, Optional, Tuple, Type
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@ -62,6 +63,14 @@ class Client:
|
||||
del base
|
||||
return []
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_internal(self) -> InternalStatus:
|
||||
"""
|
||||
get internal service status
|
||||
:return: current internal (web) service status
|
||||
"""
|
||||
return InternalStatus()
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_self(self) -> BuildStatus:
|
||||
"""
|
||||
|
@ -23,7 +23,9 @@ import requests
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.core.util import exception_response_text
|
||||
from ahriman.models.build_status import BuildStatusEnum, BuildStatus
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@ -45,16 +47,6 @@ class WebClient(Client):
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
@staticmethod
|
||||
def _exception_response_text(exception: requests.exceptions.HTTPError) -> str:
|
||||
"""
|
||||
safe response exception text generation
|
||||
:param exception: exception raised
|
||||
:return: text of the response if it is not None and empty string otherwise
|
||||
"""
|
||||
result: str = exception.response.text if exception.response is not None else ""
|
||||
return result
|
||||
|
||||
def _ahriman_url(self) -> str:
|
||||
"""
|
||||
url generator
|
||||
@ -70,6 +62,13 @@ class WebClient(Client):
|
||||
"""
|
||||
return f"http://{self.host}:{self.port}/api/v1/packages/{base}"
|
||||
|
||||
def _status_url(self) -> str:
|
||||
"""
|
||||
url generator
|
||||
:return: full url for web service for status
|
||||
"""
|
||||
return f"http://{self.host}:{self.port}/api/v1/status"
|
||||
|
||||
def add(self, package: Package, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
add new package with status
|
||||
@ -85,7 +84,7 @@ class WebClient(Client):
|
||||
response = requests.post(self._package_url(package.base), json=payload)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not add {package.base}: {WebClient._exception_response_text(e)}")
|
||||
self.logger.exception(f"could not add {package.base}: {exception_response_text(e)}")
|
||||
except Exception:
|
||||
self.logger.exception(f"could not add {package.base}")
|
||||
|
||||
@ -105,11 +104,28 @@ class WebClient(Client):
|
||||
for package in status_json
|
||||
]
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not get {base}: {WebClient._exception_response_text(e)}")
|
||||
self.logger.exception(f"could not get {base}: {exception_response_text(e)}")
|
||||
except Exception:
|
||||
self.logger.exception(f"could not get {base}")
|
||||
return []
|
||||
|
||||
def get_internal(self) -> InternalStatus:
|
||||
"""
|
||||
get internal service status
|
||||
:return: current internal (web) service status
|
||||
"""
|
||||
try:
|
||||
response = requests.get(self._status_url())
|
||||
response.raise_for_status()
|
||||
|
||||
status_json = response.json()
|
||||
return InternalStatus.from_json(status_json)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not get web service status: {exception_response_text(e)}")
|
||||
except Exception:
|
||||
self.logger.exception("could not get web service status")
|
||||
return InternalStatus()
|
||||
|
||||
def get_self(self) -> BuildStatus:
|
||||
"""
|
||||
get ahriman status itself
|
||||
@ -122,7 +138,7 @@ class WebClient(Client):
|
||||
status_json = response.json()
|
||||
return BuildStatus.from_json(status_json)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not get service status: {WebClient._exception_response_text(e)}")
|
||||
self.logger.exception(f"could not get service status: {exception_response_text(e)}")
|
||||
except Exception:
|
||||
self.logger.exception("could not get service status")
|
||||
return BuildStatus()
|
||||
@ -136,7 +152,7 @@ class WebClient(Client):
|
||||
response = requests.delete(self._package_url(base))
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not delete {base}: {WebClient._exception_response_text(e)}")
|
||||
self.logger.exception(f"could not delete {base}: {exception_response_text(e)}")
|
||||
except Exception:
|
||||
self.logger.exception(f"could not delete {base}")
|
||||
|
||||
@ -152,7 +168,7 @@ class WebClient(Client):
|
||||
response = requests.post(self._package_url(base), json=payload)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not update {base}: {WebClient._exception_response_text(e)}")
|
||||
self.logger.exception(f"could not update {base}: {exception_response_text(e)}")
|
||||
except Exception:
|
||||
self.logger.exception(f"could not update {base}")
|
||||
|
||||
@ -167,6 +183,6 @@ class WebClient(Client):
|
||||
response = requests.post(self._ahriman_url(), json=payload)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not update service status: {WebClient._exception_response_text(e)}")
|
||||
self.logger.exception(f"could not update service status: {exception_response_text(e)}")
|
||||
except Exception:
|
||||
self.logger.exception("could not update service status")
|
||||
|
@ -19,6 +19,7 @@
|
||||
#
|
||||
import datetime
|
||||
import subprocess
|
||||
import requests
|
||||
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
@ -27,29 +28,42 @@ from typing import Optional, Union
|
||||
from ahriman.core.exceptions import InvalidOption
|
||||
|
||||
|
||||
def check_output(*args: str, exception: Optional[Exception],
|
||||
cwd: Optional[Path] = None, logger: Optional[Logger] = None) -> str:
|
||||
def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] = None,
|
||||
input_data: Optional[str] = None, logger: Optional[Logger] = None) -> str:
|
||||
"""
|
||||
subprocess wrapper
|
||||
:param args: command line arguments
|
||||
:param exception: exception which has to be reraised instead of default subprocess exception
|
||||
:param cwd: current working directory
|
||||
:param input_data: data which will be written to command stdin
|
||||
:param logger: logger to log command result if required
|
||||
:return: command output
|
||||
"""
|
||||
try:
|
||||
result = subprocess.check_output(args, cwd=cwd, stderr=subprocess.STDOUT).decode("utf8").strip()
|
||||
# universal_newlines is required to read input from string
|
||||
result: str = subprocess.check_output(args, cwd=cwd, input=input_data, stderr=subprocess.STDOUT, # type: ignore
|
||||
universal_newlines=True).strip()
|
||||
if logger is not None:
|
||||
for line in result.splitlines():
|
||||
logger.debug(line)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.output is not None and logger is not None:
|
||||
for line in e.output.decode("utf8").splitlines():
|
||||
for line in e.output.splitlines():
|
||||
logger.debug(line)
|
||||
raise exception or e
|
||||
return result
|
||||
|
||||
|
||||
def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
|
||||
"""
|
||||
safe response exception text generation
|
||||
:param exception: exception raised
|
||||
:return: text of the response if it is not None and empty string otherwise
|
||||
"""
|
||||
result: str = exception.response.text if exception.response is not None else ""
|
||||
return result
|
||||
|
||||
|
||||
def package_like(filename: Path) -> bool:
|
||||
"""
|
||||
check if file looks like package
|
||||
|
71
src/ahriman/models/counters.py
Normal file
71
src/ahriman/models/counters.py
Normal file
@ -0,0 +1,71 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, fields
|
||||
from typing import Any, Dict, List, Tuple, Type
|
||||
|
||||
from ahriman.models.build_status import BuildStatus
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@dataclass
|
||||
class Counters:
|
||||
"""
|
||||
package counters
|
||||
:ivar total: total packages count
|
||||
:ivar unknown: packages in unknown status count
|
||||
:ivar pending: packages in pending status count
|
||||
:ivar building: packages in building status count
|
||||
:ivar failed: packages in failed status count
|
||||
:ivar success: packages in success status count
|
||||
"""
|
||||
total: int
|
||||
unknown: int = 0
|
||||
pending: int = 0
|
||||
building: int = 0
|
||||
failed: int = 0
|
||||
success: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Counters], dump: Dict[str, Any]) -> Counters:
|
||||
"""
|
||||
construct counters from json dump
|
||||
:param dump: json dump body
|
||||
:return: status counters
|
||||
"""
|
||||
# filter to only known fields
|
||||
known_fields = [pair.name for pair in fields(cls)]
|
||||
dump = {key: value for key, value in dump.items() if key in known_fields}
|
||||
return cls(**dump)
|
||||
|
||||
@classmethod
|
||||
def from_packages(cls: Type[Counters], packages: List[Tuple[Package, BuildStatus]]) -> Counters:
|
||||
"""
|
||||
construct counters from packages statuses
|
||||
:param packages: list of package and their status as per watcher property
|
||||
:return: status counters
|
||||
"""
|
||||
per_status = {"total": len(packages)}
|
||||
for _, status in packages:
|
||||
key = status.status.name.lower()
|
||||
per_status.setdefault(key, 0)
|
||||
per_status[key] += 1
|
||||
return cls(**per_status)
|
60
src/ahriman/models/internal_status.py
Normal file
60
src/ahriman/models/internal_status.py
Normal file
@ -0,0 +1,60 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from typing import Any, Dict, Optional, Type
|
||||
|
||||
from ahriman.models.counters import Counters
|
||||
|
||||
|
||||
@dataclass
|
||||
class InternalStatus:
|
||||
"""
|
||||
internal server status
|
||||
:ivar architecture: repository architecture
|
||||
:ivar packages: packages statuses counter object
|
||||
:ivar repository: repository name
|
||||
:ivar version: service version
|
||||
"""
|
||||
architecture: Optional[str] = None
|
||||
packages: Counters = field(default=Counters(total=0))
|
||||
repository: Optional[str] = None
|
||||
version: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[InternalStatus], dump: Dict[str, Any]) -> InternalStatus:
|
||||
"""
|
||||
construct internal status from json dump
|
||||
:param dump: json dump body
|
||||
:return: internal status
|
||||
"""
|
||||
counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0)
|
||||
return cls(architecture=dump.get("architecture"),
|
||||
packages=counters,
|
||||
repository=dump.get("repository"),
|
||||
version=dump.get("version"))
|
||||
|
||||
def view(self) -> Dict[str, Any]:
|
||||
"""
|
||||
generate json status view
|
||||
:return: json-friendly dictionary
|
||||
"""
|
||||
return asdict(self)
|
@ -17,4 +17,4 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "0.22.1"
|
||||
__version__ = "1.0.0"
|
||||
|
@ -23,6 +23,7 @@ from ahriman.web.views.ahriman import AhrimanView
|
||||
from ahriman.web.views.index import IndexView
|
||||
from ahriman.web.views.package import PackageView
|
||||
from ahriman.web.views.packages import PackagesView
|
||||
from ahriman.web.views.status import StatusView
|
||||
|
||||
|
||||
def setup_routes(application: Application) -> None:
|
||||
@ -44,6 +45,8 @@ def setup_routes(application: Application) -> None:
|
||||
GET /api/v1/package/:base get package base status
|
||||
POST /api/v1/package/:base update package base status
|
||||
|
||||
GET /api/v1/status get web service status itself
|
||||
|
||||
:param application: web application instance
|
||||
"""
|
||||
application.router.add_get("/", IndexView)
|
||||
@ -58,3 +61,5 @@ def setup_routes(application: Application) -> None:
|
||||
application.router.add_delete("/api/v1/packages/{package}", PackageView)
|
||||
application.router.add_get("/api/v1/packages/{package}", PackageView)
|
||||
application.router.add_post("/api/v1/packages/{package}", PackageView)
|
||||
|
||||
application.router.add_get("/api/v1/status", StatusView)
|
||||
|
@ -21,8 +21,7 @@ import aiohttp_jinja2
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
import ahriman.version as version
|
||||
|
||||
from ahriman import version
|
||||
from ahriman.core.util import pretty_datetime
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
45
src/ahriman/web/views/status.py
Normal file
45
src/ahriman/web/views/status.py
Normal file
@ -0,0 +1,45 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp.web import Response, json_response
|
||||
|
||||
from ahriman import version
|
||||
from ahriman.models.counters import Counters
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
class StatusView(BaseView):
|
||||
"""
|
||||
web service status web view
|
||||
"""
|
||||
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
get current service status
|
||||
:return: 200 with service status object
|
||||
"""
|
||||
counters = Counters.from_packages(self.service.packages)
|
||||
status = InternalStatus(
|
||||
architecture=self.service.architecture,
|
||||
packages=counters,
|
||||
repository=self.service.repository.name,
|
||||
version=version.__version__)
|
||||
|
||||
return json_response(status.view())
|
@ -1,4 +1,5 @@
|
||||
import argparse
|
||||
import aur
|
||||
import pytest
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
@ -7,6 +8,7 @@ from ahriman.application.ahriman import _parser
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.lock import Lock
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -20,6 +22,26 @@ def args() -> argparse.Namespace:
|
||||
return argparse.Namespace(lock=None, force=False, unsafe=False, no_report=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def aur_package_ahriman(package_ahriman: Package) -> aur.Package:
|
||||
return aur.Package(
|
||||
num_votes=None,
|
||||
description=package_ahriman.packages[package_ahriman.base].description,
|
||||
url_path=package_ahriman.web_url,
|
||||
last_modified=None,
|
||||
name=package_ahriman.base,
|
||||
out_of_date=None,
|
||||
id=None,
|
||||
first_submitted=None,
|
||||
maintainer=None,
|
||||
version=package_ahriman.version,
|
||||
license=package_ahriman.packages[package_ahriman.base].licenses,
|
||||
url=None,
|
||||
package_base=package_ahriman.base,
|
||||
package_base_id=None,
|
||||
category_id=None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lock(args: argparse.Namespace, configuration: Configuration) -> Lock:
|
||||
return Lock(args, "x86_64", configuration)
|
||||
|
@ -0,0 +1,24 @@
|
||||
import argparse
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers import KeyImport
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
args.key = "0xE989490C"
|
||||
args.key_server = "keys.gnupg.net"
|
||||
return args
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
args = _default_args(args)
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
application_mock = mocker.patch("ahriman.core.sign.gpg.GPG.import_key")
|
||||
|
||||
KeyImport.run(args, "x86_64", configuration)
|
||||
application_mock.assert_called_once()
|
50
tests/ahriman/application/handlers/test_handler_search.py
Normal file
50
tests/ahriman/application/handlers/test_handler_search.py
Normal file
@ -0,0 +1,50 @@
|
||||
import argparse
|
||||
import aur
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers import Search
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
args.search = ["ahriman"]
|
||||
return args
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: aur.Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
args = _default_args(args)
|
||||
mocker.patch("aur.search", return_value=[aur_package_ahriman])
|
||||
log_mock = mocker.patch("ahriman.application.handlers.search.Search.log_fn")
|
||||
|
||||
Search.run(args, "x86_64", configuration)
|
||||
log_mock.assert_called_once()
|
||||
|
||||
|
||||
def test_run_multiple_search(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command with multiple search arguments
|
||||
"""
|
||||
args = _default_args(args)
|
||||
args.search = ["ahriman", "is", "cool"]
|
||||
search_mock = mocker.patch("aur.search")
|
||||
|
||||
Search.run(args, "x86_64", configuration)
|
||||
search_mock.assert_called_with(" ".join(args.search))
|
||||
|
||||
|
||||
def test_log_fn(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: aur.Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
log function must call print built-in
|
||||
"""
|
||||
args = _default_args(args)
|
||||
mocker.patch("aur.search", return_value=[aur_package_ahriman])
|
||||
print_mock = mocker.patch("builtins.print")
|
||||
|
||||
Search.run(args, "x86_64", configuration)
|
||||
print_mock.assert_called() # we don't really care about call details tbh
|
@ -71,6 +71,25 @@ def test_subparsers_config(parser: argparse.ArgumentParser) -> None:
|
||||
assert args.unsafe
|
||||
|
||||
|
||||
def test_subparsers_key_import(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
key-import command must imply lock and no_report
|
||||
"""
|
||||
args = parser.parse_args(["-a", "x86_64", "key-import", "key"])
|
||||
assert args.lock is None
|
||||
assert args.no_report
|
||||
|
||||
|
||||
def test_subparsers_search(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
search command must imply lock, no_report and unsafe
|
||||
"""
|
||||
args = parser.parse_args(["-a", "x86_64", "search", "ahriman"])
|
||||
assert args.lock is None
|
||||
assert args.no_report
|
||||
assert args.unsafe
|
||||
|
||||
|
||||
def test_subparsers_setup(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
setup command must imply lock, no_report and unsafe
|
||||
|
@ -5,9 +5,11 @@ from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest import mock
|
||||
|
||||
from ahriman import version
|
||||
from ahriman.application.lock import Lock
|
||||
from ahriman.core.exceptions import DuplicateRun, UnsafeRun
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
|
||||
|
||||
def test_enter(lock: Lock, mocker: MockerFixture) -> None:
|
||||
@ -15,6 +17,7 @@ def test_enter(lock: Lock, mocker: MockerFixture) -> None:
|
||||
must process with context manager
|
||||
"""
|
||||
check_user_mock = mocker.patch("ahriman.application.lock.Lock.check_user")
|
||||
check_version_mock = mocker.patch("ahriman.application.lock.Lock.check_version")
|
||||
clear_mock = mocker.patch("ahriman.application.lock.Lock.clear")
|
||||
create_mock = mocker.patch("ahriman.application.lock.Lock.create")
|
||||
update_status_mock = mocker.patch("ahriman.core.status.client.Client.update_self")
|
||||
@ -24,6 +27,7 @@ def test_enter(lock: Lock, mocker: MockerFixture) -> None:
|
||||
check_user_mock.assert_called_once()
|
||||
clear_mock.assert_called_once()
|
||||
create_mock.assert_called_once()
|
||||
check_version_mock.assert_called_once()
|
||||
update_status_mock.assert_has_calls([
|
||||
mock.call(BuildStatusEnum.Building),
|
||||
mock.call(BuildStatusEnum.Success)
|
||||
@ -48,6 +52,30 @@ def test_exit_with_exception(lock: Lock, mocker: MockerFixture) -> None:
|
||||
])
|
||||
|
||||
|
||||
def test_check_version(lock: Lock, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must check version correctly
|
||||
"""
|
||||
mocker.patch("ahriman.core.status.client.Client.get_internal",
|
||||
return_value=InternalStatus(version=version.__version__))
|
||||
logging_mock = mocker.patch("logging.Logger.warning")
|
||||
|
||||
lock.check_version()
|
||||
logging_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_check_version_mismatch(lock: Lock, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must check version correctly
|
||||
"""
|
||||
mocker.patch("ahriman.core.status.client.Client.get_internal",
|
||||
return_value=InternalStatus(version="version"))
|
||||
logging_mock = mocker.patch("logging.Logger.warning")
|
||||
|
||||
lock.check_version()
|
||||
logging_mock.assert_called_once()
|
||||
|
||||
|
||||
def test_check_user(lock: Lock, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must check user correctly
|
||||
|
@ -1,5 +1,9 @@
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest import mock
|
||||
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.models.sign_settings import SignSettings
|
||||
@ -60,6 +64,38 @@ def test_sign_command(gpg_with_key: GPG) -> None:
|
||||
assert gpg_with_key.sign_command(Path("a"), gpg_with_key.default_key)
|
||||
|
||||
|
||||
def test_download_key(gpg: GPG, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must download the key from public server
|
||||
"""
|
||||
requests_mock = mocker.patch("requests.get")
|
||||
gpg.download_key("keys.gnupg.net", "0xE989490C")
|
||||
requests_mock.assert_called_once()
|
||||
|
||||
|
||||
def test_download_key_failure(gpg: GPG, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must download the key from public server and log error if any (and raise it again)
|
||||
"""
|
||||
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError())
|
||||
with pytest.raises(requests.exceptions.HTTPError):
|
||||
gpg.download_key("keys.gnupg.net", "0xE989490C")
|
||||
|
||||
|
||||
def test_import_key(gpg: GPG, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must import PGP key from the server
|
||||
"""
|
||||
mocker.patch("ahriman.core.sign.gpg.GPG.download_key", return_value="key")
|
||||
check_output_mock = mocker.patch("ahriman.core.sign.gpg.GPG._check_output")
|
||||
|
||||
gpg.import_key("keys.gnupg.net", "0xE989490C")
|
||||
check_output_mock.assert_has_calls([
|
||||
mock.call("gpg", "--import", input_data="key", exception=None, logger=pytest.helpers.anyvar(int)),
|
||||
mock.call("gpg", "--quick-lsign-key", "0xE989490C", exception=None, logger=pytest.helpers.anyvar(int))
|
||||
])
|
||||
|
||||
|
||||
def test_process(gpg_with_key: GPG, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must call process method correctly
|
||||
|
@ -4,6 +4,7 @@ from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.core.status.web_client import WebClient
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@ -38,6 +39,13 @@ def test_get(client: Client, package_ahriman: Package) -> None:
|
||||
assert client.get(None) == []
|
||||
|
||||
|
||||
def test_get_internal(client: Client) -> None:
|
||||
"""
|
||||
must return dummy status for web service
|
||||
"""
|
||||
assert client.get_internal() == InternalStatus()
|
||||
|
||||
|
||||
def test_get_self(client: Client) -> None:
|
||||
"""
|
||||
must return unknown status for service
|
||||
|
@ -7,6 +7,7 @@ from requests import Response
|
||||
|
||||
from ahriman.core.status.web_client import WebClient
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@ -26,6 +27,14 @@ def test_package_url(web_client: WebClient, package_ahriman: Package) -> None:
|
||||
assert web_client._package_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}")
|
||||
|
||||
|
||||
def test_status_url(web_client: WebClient) -> None:
|
||||
"""
|
||||
must generate service status url correctly
|
||||
"""
|
||||
assert web_client._status_url().startswith(f"http://{web_client.host}:{web_client.port}")
|
||||
assert web_client._status_url().endswith("/api/v1/status")
|
||||
|
||||
|
||||
def test_add(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must process package addition
|
||||
@ -103,6 +112,37 @@ def test_get_single(web_client: WebClient, package_ahriman: Package, mocker: Moc
|
||||
assert (package_ahriman, BuildStatusEnum.Unknown) in [(package, status.status) for package, status in result]
|
||||
|
||||
|
||||
def test_get_internal(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return web service status
|
||||
"""
|
||||
response_obj = Response()
|
||||
response_obj._content = json.dumps(InternalStatus(architecture="x86_64").view()).encode("utf8")
|
||||
response_obj.status_code = 200
|
||||
|
||||
requests_mock = mocker.patch("requests.get", return_value=response_obj)
|
||||
|
||||
result = web_client.get_internal()
|
||||
requests_mock.assert_called_once()
|
||||
assert result.architecture == "x86_64"
|
||||
|
||||
|
||||
def test_get_internal_failed(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress any exception happened during web service status getting
|
||||
"""
|
||||
mocker.patch("requests.get", side_effect=Exception())
|
||||
assert web_client.get_internal() == InternalStatus()
|
||||
|
||||
|
||||
def test_get_internal_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress any exception happened during web service status getting
|
||||
"""
|
||||
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError())
|
||||
assert web_client.get_internal() == InternalStatus()
|
||||
|
||||
|
||||
def test_get_self(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return service status
|
||||
|
@ -113,6 +113,14 @@ def test_load_includes_no_option(configuration: Configuration) -> None:
|
||||
configuration.load_includes()
|
||||
|
||||
|
||||
def test_load_includes_no_section(configuration: Configuration) -> None:
|
||||
"""
|
||||
must not fail if no option set
|
||||
"""
|
||||
configuration.remove_section("settings")
|
||||
configuration.load_includes()
|
||||
|
||||
|
||||
def test_load_logging_fallback(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must fallback to stderr without errors
|
||||
|
@ -2,7 +2,10 @@ import pytest
|
||||
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
from ahriman import version
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.counters import Counters
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
|
||||
@ -12,6 +15,24 @@ def build_status_failed() -> BuildStatus:
|
||||
return BuildStatus(BuildStatusEnum.Failed, 42)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def counters() -> Counters:
|
||||
return Counters(total=10,
|
||||
unknown=1,
|
||||
pending=2,
|
||||
building=3,
|
||||
failed=4,
|
||||
success=0)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def internal_status(counters: Counters) -> InternalStatus:
|
||||
return InternalStatus(architecture="x86_64",
|
||||
packages=counters,
|
||||
version=version.__version__,
|
||||
repository="aur-clone")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def package_tpacpi_bat_git() -> Package:
|
||||
return Package(
|
||||
|
31
tests/ahriman/models/test_counters.py
Normal file
31
tests/ahriman/models/test_counters.py
Normal file
@ -0,0 +1,31 @@
|
||||
from dataclasses import asdict
|
||||
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.counters import Counters
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
def test_counters_from_json_view(counters: Counters) -> None:
|
||||
"""
|
||||
must construct same object from json
|
||||
"""
|
||||
assert Counters.from_json(asdict(counters)) == counters
|
||||
|
||||
|
||||
def test_counters_from_packages(package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||
"""
|
||||
must construct object from list of packages with their statuses
|
||||
"""
|
||||
payload = [
|
||||
(package_ahriman, BuildStatus(status=BuildStatusEnum.Success)),
|
||||
(package_python_schedule, BuildStatus(status=BuildStatusEnum.Failed)),
|
||||
]
|
||||
|
||||
counters = Counters.from_packages(payload)
|
||||
assert counters.total == 2
|
||||
assert counters.success == 1
|
||||
assert counters.failed == 1
|
||||
|
||||
json = asdict(counters)
|
||||
total = json.pop("total")
|
||||
assert total == sum(i for i in json.values())
|
8
tests/ahriman/models/test_internal_status.py
Normal file
8
tests/ahriman/models/test_internal_status.py
Normal file
@ -0,0 +1,8 @@
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
|
||||
|
||||
def test_internal_status_from_json_view(internal_status: InternalStatus) -> None:
|
||||
"""
|
||||
must construct same object from json
|
||||
"""
|
||||
assert InternalStatus.from_json(internal_status.view()) == internal_status
|
22
tests/ahriman/web/views/test_view_status.py
Normal file
22
tests/ahriman/web/views/test_view_status.py
Normal file
@ -0,0 +1,22 @@
|
||||
from pytest_aiohttp import TestClient
|
||||
|
||||
import ahriman.version as version
|
||||
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
async def test_get(client: TestClient, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must generate web service status correctly)
|
||||
"""
|
||||
await client.post(f"/api/v1/packages/{package_ahriman.base}",
|
||||
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
||||
|
||||
response = await client.get("/api/v1/status")
|
||||
assert response.status == 200
|
||||
|
||||
json = await response.json()
|
||||
assert json["version"] == version.__version__
|
||||
assert json["packages"]
|
||||
assert json["packages"]["total"] == 1
|
Reference in New Issue
Block a user