add console printer

also add python-requests as explicit dependency and escape symbols in
repository name for badges in default tempate
This commit is contained in:
Evgenii Alekseev 2022-03-19 23:48:43 +03:00
parent 060c7412b1
commit a7c6d95b34
59 changed files with 631 additions and 187 deletions

View File

@ -24,7 +24,7 @@ RUN YAY_DIR="$(runuser -u build -- mktemp -d)" && \
runuser -u build -- makepkg --noconfirm --install && \ runuser -u build -- makepkg --noconfirm --install && \
cd - && rm -r "$YAY_DIR" cd - && rm -r "$YAY_DIR"
## install package dependencies ## install package dependencies
RUN runuser -u build -- yay --noconfirm -Sy devtools git pyalpm python-inflection python-passlib python-srcinfo && \ RUN runuser -u build -- yay --noconfirm -Sy devtools git pyalpm python-inflection python-passlib python-requests python-srcinfo && \
runuser -u build -- yay --noconfirm -Sy python-pip && \ runuser -u build -- yay --noconfirm -Sy python-pip && \
runuser -u build -- yay --noconfirm -Sy breezy darcs mercurial python-aioauth-client python-aiohttp \ runuser -u build -- yay --noconfirm -Sy breezy darcs mercurial python-aioauth-client python-aiohttp \
python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security \ python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security \

View File

@ -7,7 +7,7 @@ pkgdesc="ArcH Linux ReposItory MANager"
arch=('any') arch=('any')
url="https://github.com/arcan1s/ahriman" url="https://github.com/arcan1s/ahriman"
license=('GPL3') license=('GPL3')
depends=('devtools' 'git' 'pyalpm' 'python-inflection' 'python-passlib' 'python-srcinfo') depends=('devtools' 'git' 'pyalpm' 'python-inflection' 'python-passlib' 'python-requests' 'python-srcinfo')
makedepends=('python-pip') makedepends=('python-pip')
optdepends=('breezy: -bzr packages support' optdepends=('breezy: -bzr packages support'
'darcs: -darcs packages support' 'darcs: -darcs packages support'

View File

@ -30,7 +30,10 @@ root = /var/lib/ahriman
target = target =
[report] [report]
target = target = console
[console]
use_utf = yes
[email] [email]
full_template_path = /usr/share/ahriman/repo-index.jinja2 full_template_path = /usr/share/ahriman/repo-index.jinja2

View File

@ -16,7 +16,7 @@
<h1>ahriman <h1>ahriman
{% if auth.authenticated %} {% if auth.authenticated %}
<img src="https://img.shields.io/badge/version-{{ version }}-informational" alt="{{ version }}"> <img src="https://img.shields.io/badge/version-{{ version }}-informational" alt="{{ version }}">
<img src="https://img.shields.io/badge/repository-{{ repository }}-informational" alt="{{ repository }}"> <img src="https://img.shields.io/badge/repository-{{ repository | replace("-", "--") }}-informational" alt="{{ repository }}">
<img src="https://img.shields.io/badge/architecture-{{ architecture }}-informational" alt="{{ architecture }}"> <img src="https://img.shields.io/badge/architecture-{{ architecture }}-informational" alt="{{ architecture }}">
<img src="https://img.shields.io/badge/service%20status-{{ service.status }}-{{ service.status_color }}" alt="{{ service.status }}" title="{{ service.timestamp }}"> <img src="https://img.shields.io/badge/service%20status-{{ service.status }}-{{ service.status_color }}" alt="{{ service.status }}" title="{{ service.timestamp }}">
{% endif %} {% endif %}

View File

@ -17,11 +17,11 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from typing import Iterable, Set from typing import Set
from ahriman.application.application.packages import Packages from ahriman.application.application.packages import Packages
from ahriman.application.application.repository import Repository from ahriman.application.application.repository import Repository
from ahriman.models.package import Package from ahriman.models.result import Result
class Application(Packages, Repository): class Application(Packages, Repository):
@ -29,12 +29,13 @@ class Application(Packages, Repository):
base application class base application class
""" """
def _finalize(self, built_packages: Iterable[Package]) -> None: def _finalize(self, result: Result) -> None:
""" """
generate report and sync to remote server generate report and sync to remote server
:param result: build result
""" """
self.report([], built_packages) self.report([], result)
self.sync([], built_packages) self.sync([], result.success)
def _known_packages(self) -> Set[str]: def _known_packages(self) -> Set[str]:
""" """

View File

@ -28,6 +28,7 @@ from ahriman.core.build_tools.sources import Sources
from ahriman.core.util import package_like from ahriman.core.util import package_like
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
from ahriman.models.result import Result
class Packages(Properties): class Packages(Properties):
@ -35,9 +36,10 @@ class Packages(Properties):
package control class package control class
""" """
def _finalize(self, built_packages: Iterable[Package]) -> None: def _finalize(self, result: Result) -> None:
""" """
generate report and sync to remote server generate report and sync to remote server
:param result: build result
""" """
raise NotImplementedError raise NotImplementedError
@ -141,4 +143,4 @@ class Packages(Properties):
:param names: list of packages (either base or name) to remove :param names: list of packages (either base or name) to remove
""" """
self.repository.process_remove(names) self.repository.process_remove(names)
self._finalize([]) self._finalize(Result())

View File

@ -23,11 +23,11 @@ from pathlib import Path
from typing import Callable, Iterable, List from typing import Callable, Iterable, List
from ahriman.application.application.properties import Properties from ahriman.application.application.properties import Properties
from ahriman.application.formatters.update_printer import UpdatePrinter
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.core.tree import Tree from ahriman.core.tree import Tree
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource from ahriman.models.result import Result
class Repository(Properties): class Repository(Properties):
@ -35,9 +35,10 @@ class Repository(Properties):
repository control class repository control class
""" """
def _finalize(self, built_packages: Iterable[Package]) -> None: def _finalize(self, result: Result) -> None:
""" """
generate report and sync to remote server generate report and sync to remote server
:param result: build result
""" """
raise NotImplementedError raise NotImplementedError
@ -64,14 +65,14 @@ class Repository(Properties):
if patches: if patches:
self.repository.clear_patches() self.repository.clear_patches()
def report(self, target: Iterable[str], built_packages: Iterable[Package]) -> None: def report(self, target: Iterable[str], result: Result) -> None:
""" """
generate report generate report
:param target: list of targets to run (e.g. html) :param target: list of targets to run (e.g. html)
:param built_packages: list of packages which has just been built :param result: build result
""" """
targets = target or None targets = target or None
self.repository.process_report(targets, built_packages) self.repository.process_report(targets, result)
def sign(self, packages: Iterable[str]) -> None: def sign(self, packages: Iterable[str]) -> None:
""" """
@ -94,7 +95,7 @@ class Repository(Properties):
self.update([]) self.update([])
# sign repository database if set # sign repository database if set
self.repository.sign.process_sign_repository(self.repository.repo.repo_path) self.repository.sign.process_sign_repository(self.repository.repo.repo_path)
self._finalize([]) self._finalize(Result())
def sync(self, target: Iterable[str], built_packages: Iterable[Package]) -> None: def sync(self, target: Iterable[str], built_packages: Iterable[Package]) -> None:
""" """
@ -142,26 +143,23 @@ class Repository(Properties):
run package updates run package updates
: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], result: Result) -> None:
if not paths: if not paths:
return # don't need to process if no update supplied return # don't need to process if no update supplied
updated = [ update_result = self.repository.process_update(paths)
Package.load(str(path), PackageSource.Archive, self.repository.pacman, self.repository.aur_url) self._finalize(result.merge(update_result))
for path in paths
]
self.repository.process_update(paths)
self._finalize(updated)
# process built packages # process built packages
packages = self.repository.packages_built() packages = self.repository.packages_built()
process_update(packages) process_update(packages, Result())
# process manual packages # process manual packages
tree = Tree.load(updates, self.repository.paths) tree = Tree.load(updates, self.repository.paths)
for num, level in enumerate(tree.levels()): for num, level in enumerate(tree.levels()):
self.logger.info("processing level #%i %s", 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) build_result = self.repository.process_build(level)
process_update(packages) packages = self.repository.packages_built()
process_update(packages, build_result)
def updates(self, filter_packages: Iterable[str], no_aur: bool, no_local: bool, no_manual: bool, no_vcs: bool, def updates(self, filter_packages: Iterable[str], no_aur: bool, no_local: bool, no_manual: bool, no_vcs: bool,
log_fn: Callable[[str], None]) -> List[Package]: log_fn: Callable[[str], None]) -> List[Package]:

View File

@ -21,9 +21,9 @@ import argparse
from typing import Type from typing import Type
from ahriman.application.formatters.configuration_printer import ConfigurationPrinter
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.formatters.configuration_printer import ConfigurationPrinter
class Dump(Handler): class Dump(Handler):

View File

@ -22,9 +22,9 @@ import argparse
from typing import Type from typing import Type
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.formatters.update_printer import UpdatePrinter
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.formatters.update_printer import UpdatePrinter
class Rebuild(Handler): class Rebuild(Handler):

View File

@ -22,9 +22,9 @@ import argparse
from typing import Type from typing import Type
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.formatters.string_printer import StringPrinter
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.formatters.string_printer import StringPrinter
class RemoveUnknown(Handler): class RemoveUnknown(Handler):

View File

@ -24,6 +24,7 @@ from typing import Type
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.result import Result
class Report(Handler): class Report(Handler):
@ -42,4 +43,4 @@ class Report(Handler):
:param no_report: force disable reporting :param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation :param unsafe: if set no user check will be performed before path creation
""" """
Application(architecture, configuration, no_report, unsafe).report(args.target, []) Application(architecture, configuration, no_report, unsafe).report(args.target, Result())

View File

@ -22,11 +22,11 @@ import argparse
from dataclasses import fields from dataclasses import fields
from typing import Callable, Iterable, List, Tuple, Type from typing import Callable, Iterable, List, Tuple, Type
from ahriman.application.formatters.aur_printer import AurPrinter
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.alpm.aur import AUR from ahriman.core.alpm.aur import AUR
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InvalidOption from ahriman.core.exceptions import InvalidOption
from ahriman.core.formatters.aur_printer import AurPrinter
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage

View File

@ -22,10 +22,10 @@ import argparse
from typing import Callable, Iterable, Tuple, Type from typing import Callable, Iterable, Tuple, Type
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.formatters.package_printer import PackagePrinter
from ahriman.application.formatters.status_printer import StatusPrinter
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.formatters.package_printer import PackagePrinter
from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.models.build_status import BuildStatus from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package from ahriman.models.package import Package

View File

@ -21,9 +21,9 @@ import argparse
from typing import List, Type from typing import List, Type
from ahriman.application.formatters.string_printer import StringPrinter
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.formatters.string_printer import StringPrinter
class UnsafeCommands(Handler): class UnsafeCommands(Handler):

View File

@ -136,6 +136,19 @@ class ReportFailed(RuntimeError):
RuntimeError.__init__(self, "Report failed") RuntimeError.__init__(self, "Report failed")
class SuccessFailed(ValueError):
"""
exception for merging invalid statues
"""
def __init__(self, package_base: str) -> None:
"""
default constructor
:param package_base: package base name
"""
ValueError.__init__(self, f"Package base {package_base} had status failed, but new status is success")
class SyncFailed(RuntimeError): class SyncFailed(RuntimeError):
""" """
remote synchronization exception remote synchronization exception

View File

@ -17,17 +17,18 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from typing import List, Optional from typing import List
from ahriman.application.formatters.printer import Printer from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.core.util import pretty_datetime from ahriman.core.util import pretty_datetime
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
from ahriman.models.property import Property from ahriman.models.property import Property
class AurPrinter(Printer): class AurPrinter(StringPrinter):
""" """
print content of the AUR package print content of the AUR package
:ivar package: AUR package description
""" """
def __init__(self, package: AURPackage) -> None: def __init__(self, package: AURPackage) -> None:
@ -35,7 +36,8 @@ class AurPrinter(Printer):
default constructor default constructor
:param package: AUR package description :param package: AUR package description
""" """
self.content = package StringPrinter.__init__(self, f"{package.name} {package.version} ({package.num_votes})")
self.package = package
def properties(self) -> List[Property]: def properties(self) -> List[Property]:
""" """
@ -43,19 +45,12 @@ class AurPrinter(Printer):
:return: list of content properties :return: list of content properties
""" """
return [ return [
Property("Package base", self.content.package_base), Property("Package base", self.package.package_base),
Property("Description", self.content.description, is_required=True), Property("Description", self.package.description, is_required=True),
Property("Upstream URL", self.content.url or ""), Property("Upstream URL", self.package.url or ""),
Property("Licenses", ",".join(self.content.license)), Property("Licenses", ",".join(self.package.license)),
Property("Maintainer", self.content.maintainer or ""), Property("Maintainer", self.package.maintainer or ""),
Property("First submitted", pretty_datetime(self.content.first_submitted)), Property("First submitted", pretty_datetime(self.package.first_submitted)),
Property("Last updated", pretty_datetime(self.content.last_modified)), Property("Last updated", pretty_datetime(self.package.last_modified)),
Property("Keywords", ",".join(self.content.keywords)), Property("Keywords", ",".join(self.package.keywords)),
] ]
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""
return f"{self.content.name} {self.content.version} ({self.content.num_votes})"

View File

@ -0,0 +1,48 @@
#
# 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 ahriman.core.formatters.string_printer import StringPrinter
from ahriman.models.package import Package
class BuildPrinter(StringPrinter):
"""
print content of the build result
"""
def __init__(self, package: Package, is_success: bool, use_utf: bool) -> None:
"""
default constructor
:param package: built package
:param is_success: True in case if build has success status and False otherwise
:param use_utf: use utf instead of normal symbols
"""
StringPrinter.__init__(self, f"{self.sign(is_success, use_utf)} {package.base}")
@staticmethod
def sign(is_success: bool, use_utf: bool) -> str:
"""
generate sign according to settings
:param use_utf: use utf instead of normal symbols
:param is_success: True in case if build has success status and False otherwise
:return: sign symbol according to current settings
"""
if is_success:
return "[✔]" if use_utf else "[x]"
return "[❌]" if use_utf else "[ ]"

View File

@ -17,15 +17,16 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from typing import Dict, List, Optional from typing import Dict, List
from ahriman.application.formatters.printer import Printer from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.models.property import Property from ahriman.models.property import Property
class ConfigurationPrinter(Printer): class ConfigurationPrinter(StringPrinter):
""" """
print content of the configuration section print content of the configuration section
:ivar values: configuration values dictionary
""" """
def __init__(self, section: str, values: Dict[str, str]) -> None: def __init__(self, section: str, values: Dict[str, str]) -> None:
@ -34,8 +35,8 @@ class ConfigurationPrinter(Printer):
:param section: section name :param section: section name
:param values: configuration values dictionary :param values: configuration values dictionary
""" """
self.section = section StringPrinter.__init__(self, f"[{section}]")
self.content = values self.values = values
def properties(self) -> List[Property]: def properties(self) -> List[Property]:
""" """
@ -44,12 +45,5 @@ class ConfigurationPrinter(Printer):
""" """
return [ return [
Property(key, value, is_required=True) Property(key, value, is_required=True)
for key, value in sorted(self.content.items()) for key, value in sorted(self.values.items())
] ]
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""
return f"[{self.section}]"

View File

@ -17,17 +17,19 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from typing import List, Optional from typing import List
from ahriman.application.formatters.printer import Printer from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.models.build_status import BuildStatus from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.property import Property from ahriman.models.property import Property
class PackagePrinter(Printer): class PackagePrinter(StringPrinter):
""" """
print content of the internal package object print content of the internal package object
:ivar package: package description
:ivar status: build status
""" """
def __init__(self, package: Package, status: BuildStatus) -> None: def __init__(self, package: Package, status: BuildStatus) -> None:
@ -36,7 +38,8 @@ class PackagePrinter(Printer):
:param package: package description :param package: package description
:param status: build status :param status: build status
""" """
self.content = package StringPrinter.__init__(self, package.pretty_print())
self.package = package
self.status = status self.status = status
def properties(self) -> List[Property]: def properties(self) -> List[Property]:
@ -45,16 +48,9 @@ class PackagePrinter(Printer):
:return: list of content properties :return: list of content properties
""" """
return [ return [
Property("Version", self.content.version, is_required=True), Property("Version", self.package.version, is_required=True),
Property("Groups", " ".join(self.content.groups)), Property("Groups", " ".join(self.package.groups)),
Property("Licenses", " ".join(self.content.licenses)), Property("Licenses", " ".join(self.package.licenses)),
Property("Depends", " ".join(self.content.depends)), Property("Depends", " ".join(self.package.depends)),
Property("Status", self.status.pretty_print(), is_required=True), Property("Status", self.status.pretty_print(), is_required=True),
] ]
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""
return self.content.pretty_print()

View File

@ -17,13 +17,11 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from typing import Optional from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.application.formatters.printer import Printer
from ahriman.models.build_status import BuildStatus from ahriman.models.build_status import BuildStatus
class StatusPrinter(Printer): class StatusPrinter(StringPrinter):
""" """
print content of the status object print content of the status object
""" """
@ -33,11 +31,4 @@ class StatusPrinter(Printer):
default constructor default constructor
:param status: build status :param status: build status
""" """
self.content = status StringPrinter.__init__(self, status.pretty_print())
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""
return self.content.pretty_print()

View File

@ -19,7 +19,7 @@
# #
from typing import Optional from typing import Optional
from ahriman.application.formatters.printer import Printer from ahriman.core.formatters.printer import Printer
class StringPrinter(Printer): class StringPrinter(Printer):

View File

@ -19,14 +19,16 @@
# #
from typing import List, Optional from typing import List, Optional
from ahriman.application.formatters.printer import Printer from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.property import Property from ahriman.models.property import Property
class UpdatePrinter(Printer): class UpdatePrinter(StringPrinter):
""" """
print content of the package update print content of the package update
:ivar package: remote (new) package object
:ivar local_version: local version of the package if any
""" """
def __init__(self, remote: Package, local_version: Optional[str]) -> None: def __init__(self, remote: Package, local_version: Optional[str]) -> None:
@ -35,7 +37,8 @@ class UpdatePrinter(Printer):
:param remote: remote (new) package object :param remote: remote (new) package object
:param local_version: local version of the package if any :param local_version: local version of the package if any
""" """
self.content = remote StringPrinter.__init__(self, remote.base)
self.package = remote
self.local_version = local_version or "N/A" self.local_version = local_version or "N/A"
def properties(self) -> List[Property]: def properties(self) -> List[Property]:
@ -43,11 +46,4 @@ class UpdatePrinter(Printer):
convert content into printable data convert content into printable data
:return: list of content properties :return: list of content properties
""" """
return [Property(self.local_version, self.content.version, is_required=True)] return [Property(self.local_version, self.package.version, is_required=True)]
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""
return self.content.base

View File

@ -0,0 +1,54 @@
#
# 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 typing import Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.formatters.build_printer import BuildPrinter
from ahriman.core.report.report import Report
from ahriman.models.package import Package
from ahriman.models.result import Result
class Console(Report):
"""
html report generator
:ivar use_utf: print utf8 symbols instead of ASCII
"""
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param section: settings section name
"""
Report.__init__(self, architecture, configuration)
self.use_utf = configuration.getboolean(section, "use_utf")
def generate(self, packages: Iterable[Package], result: Result) -> None:
"""
generate report for the specified packages
:param packages: list of packages to generate report
:param result: build result
"""
for package in result.success:
BuildPrinter(package, is_success=True, use_utf=self.use_utf).print(verbose=True)
for package in result.failed:
BuildPrinter(package, is_success=True, use_utf=self.use_utf).print(verbose=True)

View File

@ -29,12 +29,14 @@ from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.report.report import Report from ahriman.core.report.report import Report
from ahriman.core.util import pretty_datetime from ahriman.core.util import pretty_datetime
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result
from ahriman.models.smtp_ssl_settings import SmtpSSLSettings from ahriman.models.smtp_ssl_settings import SmtpSSLSettings
class Email(Report, JinjaTemplate): class Email(Report, JinjaTemplate):
""" """
email report generator email report generator
:ivar full_template_path: path to template for full package list
:ivar host: SMTP host to connect :ivar host: SMTP host to connect
:ivar no_empty_report: skip empty report generation :ivar no_empty_report: skip empty report generation
:ivar password: password to authenticate via SMTP :ivar password: password to authenticate via SMTP
@ -42,6 +44,7 @@ class Email(Report, JinjaTemplate):
:ivar receivers: list of receivers emails :ivar receivers: list of receivers emails
:ivar sender: sender email address :ivar sender: sender email address
:ivar ssl: SSL mode for SMTP connection :ivar ssl: SSL mode for SMTP connection
:ivar template_path: path to template for built packages
:ivar user: username to authenticate via SMTP :ivar user: username to authenticate via SMTP
""" """
@ -96,17 +99,17 @@ class Email(Report, JinjaTemplate):
session.sendmail(self.sender, self.receivers, message.as_string()) session.sendmail(self.sender, self.receivers, message.as_string())
session.quit() session.quit()
def generate(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None: def generate(self, packages: Iterable[Package], result: Result) -> None:
""" """
generate report for the specified packages generate report for the specified packages
:param packages: list of packages to generate report :param packages: list of packages to generate report
:param built_packages: list of packages which has just been built :param result: build result
""" """
if self.no_empty_report and not built_packages: if self.no_empty_report and not result.success:
return return
text = self.make_html(built_packages, self.template_path) text = self.make_html(result, self.template_path)
if self.full_template_path is not None: if self.full_template_path is not None:
attachments = {"index.html": self.make_html(packages, self.full_template_path)} attachments = {"index.html": self.make_html(Result(success=packages), self.full_template_path)}
else: else:
attachments = {} attachments = {}
self._send(text, attachments) self._send(text, attachments)

View File

@ -23,12 +23,14 @@ from ahriman.core.configuration import Configuration
from ahriman.core.report.jinja_template import JinjaTemplate from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.report.report import Report from ahriman.core.report.report import Report
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result
class HTML(Report, JinjaTemplate): class HTML(Report, JinjaTemplate):
""" """
html report generator html report generator
:ivar report_path: output path to html report :ivar report_path: output path to html report
:ivar template_path: path to template for full package list
""" """
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None: def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
@ -44,11 +46,11 @@ class HTML(Report, JinjaTemplate):
self.report_path = configuration.getpath(section, "path") self.report_path = configuration.getpath(section, "path")
self.template_path = configuration.getpath(section, "template_path") self.template_path = configuration.getpath(section, "template_path")
def generate(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None: def generate(self, packages: Iterable[Package], result: Result) -> None:
""" """
generate report for the specified packages generate report for the specified packages
:param packages: list of packages to generate report :param packages: list of packages to generate report
:param built_packages: list of packages which has just been built :param result: build result
""" """
html = self.make_html(packages, self.template_path) html = self.make_html(Result(success=packages), self.template_path)
self.report_path.write_text(html) self.report_path.write_text(html)

View File

@ -20,12 +20,12 @@
import jinja2 import jinja2
from pathlib import Path from pathlib import Path
from typing import Callable, Dict, Iterable from typing import Callable, Dict
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.sign.gpg import GPG from ahriman.core.sign.gpg import GPG
from ahriman.core.util import pretty_datetime, pretty_size from ahriman.core.util import pretty_datetime, pretty_size
from ahriman.models.package import Package from ahriman.models.result import Result
from ahriman.models.sign_settings import SignSettings from ahriman.models.sign_settings import SignSettings
@ -76,10 +76,10 @@ class JinjaTemplate:
self.sign_targets, self.default_pgp_key = GPG.sign_options(configuration) self.sign_targets, self.default_pgp_key = GPG.sign_options(configuration)
def make_html(self, packages: Iterable[Package], template_path: Path) -> str: def make_html(self, result: Result, template_path: Path) -> str:
""" """
generate report for the specified packages generate report for the specified packages
:param packages: list of packages to generate report :param result: build result
:param template_path: path to jinja template :param template_path: path to jinja template
""" """
# idea comes from https://stackoverflow.com/a/38642558 # idea comes from https://stackoverflow.com/a/38642558
@ -101,7 +101,7 @@ class JinjaTemplate:
"name": package, "name": package,
"url": properties.url or "", "url": properties.url or "",
"version": base.version "version": base.version
} for base in packages for package, properties in base.packages.items() } for base in result.updated for package, properties in base.packages.items()
] ]
comparator: Callable[[Dict[str, str]], str] = lambda item: item["filename"] comparator: Callable[[Dict[str, str]], str] = lambda item: item["filename"]

View File

@ -27,6 +27,7 @@ from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ReportFailed from ahriman.core.exceptions import ReportFailed
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.report_settings import ReportSettings from ahriman.models.report_settings import ReportSettings
from ahriman.models.result import Result
class Report: class Report:
@ -64,23 +65,26 @@ class Report:
if provider == ReportSettings.Email: if provider == ReportSettings.Email:
from ahriman.core.report.email import Email from ahriman.core.report.email import Email
return Email(architecture, configuration, section) return Email(architecture, configuration, section)
if provider == ReportSettings.Console:
from ahriman.core.report.console import Console
return Console(architecture, configuration, section)
return cls(architecture, configuration) # should never happen return cls(architecture, configuration) # should never happen
def generate(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None: def generate(self, packages: Iterable[Package], result: Result) -> None:
""" """
generate report for the specified packages generate report for the specified packages
:param packages: list of packages to generate report :param packages: list of packages to generate report
:param built_packages: list of packages which has just been built :param result: build result
""" """
def run(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None: def run(self, packages: Iterable[Package], result: Result) -> None:
""" """
run report generation run report generation
:param packages: list of packages to generate report :param packages: list of packages to generate report
:param built_packages: list of packages which has just been built :param result: build result
""" """
try: try:
self.generate(packages, built_packages) self.generate(packages, result)
except Exception: except Exception:
self.logger.exception("report generation failed") self.logger.exception("report generation failed")
raise ReportFailed() raise ReportFailed()

View File

@ -27,6 +27,7 @@ from ahriman.core.report.report import Report
from ahriman.core.repository.cleaner import Cleaner from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.upload.upload import Upload from ahriman.core.upload.upload import Upload
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result
class Executor(Cleaner): class Executor(Cleaner):
@ -49,7 +50,7 @@ class Executor(Cleaner):
""" """
raise NotImplementedError raise NotImplementedError
def process_build(self, updates: Iterable[Package]) -> List[Path]: def process_build(self, updates: Iterable[Package]) -> Result:
""" """
build packages build packages
:param updates: list of packages properties to build :param updates: list of packages properties to build
@ -64,15 +65,18 @@ class Executor(Cleaner):
dst = self.paths.packages / src.name dst = self.paths.packages / src.name
shutil.move(src, dst) shutil.move(src, dst)
result = Result()
for single in updates: for single in updates:
try: try:
build_single(single) build_single(single)
result.add_success(single)
except Exception: except Exception:
self.reporter.set_failed(single.base) self.reporter.set_failed(single.base)
result.add_failed(single)
self.logger.exception("%s (%s) build exception", single.base, self.architecture) self.logger.exception("%s (%s) build exception", single.base, self.architecture)
self.clear_build() self.clear_build()
return self.packages_built() return result
def process_remove(self, packages: Iterable[str]) -> Path: def process_remove(self, packages: Iterable[str]) -> Path:
""" """
@ -116,17 +120,17 @@ class Executor(Cleaner):
return self.repo.repo_path return self.repo.repo_path
def process_report(self, targets: Optional[Iterable[str]], built_packages: Iterable[Package]) -> None: def process_report(self, targets: Optional[Iterable[str]], result: Result) -> None:
""" """
generate reports generate reports
:param targets: list of targets to generate reports. Configuration option will be used if it is not set :param targets: list of targets to generate reports. Configuration option will be used if it is not set
:param built_packages: list of packages which has just been built :param result: build result
""" """
if targets is None: if targets is None:
targets = self.configuration.getlist("report", "target") targets = self.configuration.getlist("report", "target")
for target in targets: for target in targets:
runner = Report.load(self.architecture, self.configuration, target) runner = Report.load(self.architecture, self.configuration, target)
runner.run(self.packages(), built_packages) runner.run(self.packages(), result)
def process_sync(self, targets: Optional[Iterable[str]], built_packages: Iterable[Package]) -> None: def process_sync(self, targets: Optional[Iterable[str]], built_packages: Iterable[Package]) -> None:
""" """
@ -140,7 +144,7 @@ class Executor(Cleaner):
runner = Upload.load(self.architecture, self.configuration, target) runner = Upload.load(self.architecture, self.configuration, target)
runner.run(self.paths.repository, built_packages) runner.run(self.paths.repository, built_packages)
def process_update(self, packages: Iterable[Path]) -> Path: def process_update(self, packages: Iterable[Path]) -> Result:
""" """
sign packages, add them to repository and update repository database sign packages, add them to repository and update repository database
:param packages: list of filenames to run :param packages: list of filenames to run
@ -163,20 +167,23 @@ class Executor(Cleaner):
removed_packages: List[str] = [] # list of packages which have been removed from the base removed_packages: List[str] = [] # list of packages which have been removed from the base
updates = self.load_archives(packages) updates = self.load_archives(packages)
result = Result()
for local in updates: for local in updates:
try: try:
for description in local.packages.values(): for description in local.packages.values():
update_single(description.filename, local.base) update_single(description.filename, local.base)
self.reporter.set_success(local) self.reporter.set_success(local)
result.add_success(local)
current_package_archives: Set[str] = next( current_package_archives: Set[str] = next(
(set(current.packages) for current in current_packages if current.base == local.base), set()) (set(current.packages) for current in current_packages if current.base == local.base), set())
removed_packages.extend(current_package_archives.difference(local.packages)) removed_packages.extend(current_package_archives.difference(local.packages))
except Exception: except Exception:
self.reporter.set_failed(local.base) self.reporter.set_failed(local.base)
result.add_failed(local)
self.logger.exception("could not process %s", local.base) self.logger.exception("could not process %s", local.base)
self.clear_packages() self.clear_packages()
self.process_remove(removed_packages) self.process_remove(removed_packages)
return self.repo.repo_path return result

View File

@ -31,11 +31,13 @@ class ReportSettings(Enum):
:cvar Disabled: option which generates no report for testing purpose :cvar Disabled: option which generates no report for testing purpose
:cvar HTML: html report generation :cvar HTML: html report generation
:cvar Email: email report generation :cvar Email: email report generation
:cvar Console: print result to console
""" """
Disabled = "disabled" # for testing purpose Disabled = "disabled" # for testing purpose
HTML = "html" HTML = "html"
Email = "email" Email = "email"
Console = "console"
@classmethod @classmethod
def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings: def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings:
@ -48,4 +50,6 @@ class ReportSettings(Enum):
return cls.HTML return cls.HTML
if value.lower() in ("email",): if value.lower() in ("email",):
return cls.Email return cls.Email
if value.lower() in ("console",):
return cls.Console
raise InvalidOption(value) raise InvalidOption(value)

View File

@ -0,0 +1,105 @@
#
# 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 typing import Any, List, Optional, Iterable
from ahriman.core.exceptions import SuccessFailed
from ahriman.models.package import Package
class Result:
"""
build result class holder
"""
def __init__(self, success: Optional[Iterable[Package]] = None, failed: Optional[Iterable[Package]] = None) -> None:
"""
default constructor
:param success: initial list of successes packages
:param failed: initial list of failed packages
"""
success = success or []
self._success = {package.base: package for package in success}
failed = failed or []
self._failed = {package.base: package for package in failed}
@property
def failed(self) -> List[Package]:
"""
:return: list of packages which were failed
"""
return list(self._failed.values())
@property
def success(self) -> List[Package]:
"""
:return: list of packages with success result
"""
return list(self._success.values())
@property
def updated(self) -> List[Package]:
"""
:return: list of updated packages inclding both success and failed
"""
return self.success + self.failed
def add_failed(self, package: Package) -> None:
"""
add new package to failed built
:param package: package with errors during build
"""
self._failed[package.base] = package
def add_success(self, package: Package) -> None:
"""
add new package to success built
:param package: package built
"""
self._success[package.base] = package
# pylint: disable=protected-access
def merge(self, other: Result) -> Result:
"""
merge other result into this one. This method assumes that other has fresh info about status and override it
:param other: instance of the newest result
:return: updated instance
"""
for base, package in other._failed.items():
if base in self._success:
del self._success[base]
self.add_failed(package)
for base, package in other._success.items():
if base in self._failed:
raise SuccessFailed(base)
self.add_success(package)
return self
# required for tests at least
def __eq__(self, other: Any) -> bool:
"""
check if other is the same object
:param other: other object instance
:return: True if the other object is the same and False otherwise
"""
if not isinstance(other, Result):
return False
return self.success == other.success and self.failed == other.failed

View File

@ -2,6 +2,7 @@ from pytest_mock import MockerFixture
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result
def test_finalize(application: Application, mocker: MockerFixture) -> None: def test_finalize(application: Application, mocker: MockerFixture) -> None:
@ -11,8 +12,8 @@ def test_finalize(application: Application, mocker: MockerFixture) -> None:
report_mock = mocker.patch("ahriman.application.application.Application.report") report_mock = mocker.patch("ahriman.application.application.Application.report")
sync_mock = mocker.patch("ahriman.application.application.Application.sync") sync_mock = mocker.patch("ahriman.application.application.Application.sync")
application._finalize([]) application._finalize(Result())
report_mock.assert_called_once_with([], []) report_mock.assert_called_once_with([], Result())
sync_mock.assert_called_once_with([], []) sync_mock.assert_called_once_with([], [])

View File

@ -9,6 +9,7 @@ from ahriman.application.application.packages import Packages
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
from ahriman.models.result import Result
def test_finalize(application_packages: Packages) -> None: def test_finalize(application_packages: Packages) -> None:
@ -211,4 +212,4 @@ def test_remove(application_packages: Packages, mocker: MockerFixture) -> None:
application_packages.remove([]) application_packages.remove([])
executor_mock.assert_called_once_with([]) executor_mock.assert_called_once_with([])
finalize_mock.assert_called_once_with([]) finalize_mock.assert_called_once_with(Result())

View File

@ -6,6 +6,7 @@ from unittest import mock
from ahriman.application.application.repository import Repository from ahriman.application.application.repository import Repository
from ahriman.core.tree import Leaf, Tree from ahriman.core.tree import Leaf, Tree
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result
def test_finalize(application_repository: Repository) -> None: def test_finalize(application_repository: Repository) -> None:
@ -98,7 +99,7 @@ def test_sign(application_repository: Repository, package_ahriman: Package, pack
]) ])
update_mock.assert_called_once_with([]) update_mock.assert_called_once_with([])
sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path) sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path)
finalize_mock.assert_called_once_with([]) finalize_mock.assert_called_once_with(Result())
def test_sign_skip(application_repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None: def test_sign_skip(application_repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -132,7 +133,7 @@ def test_sign_specific(application_repository: Repository, package_ahriman: Pack
application_repository.repository.paths.packages / filename.name) application_repository.repository.paths.packages / filename.name)
update_mock.assert_called_once_with([]) update_mock.assert_called_once_with([])
sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path) sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path)
finalize_mock.assert_called_once_with([]) finalize_mock.assert_called_once_with(Result())
def test_sync(application_repository: Repository, mocker: MockerFixture) -> None: def test_sync(application_repository: Repository, mocker: MockerFixture) -> None:
@ -181,7 +182,8 @@ def test_unknown_no_local(application_repository: Repository, package_ahriman: P
assert not application_repository.unknown() assert not application_repository.unknown()
def test_update(application_repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None: def test_update(application_repository: Repository, package_ahriman: Package, result: Result,
mocker: MockerFixture) -> None:
""" """
must process package updates must process package updates
""" """
@ -189,16 +191,33 @@ def test_update(application_repository: Repository, package_ahriman: Package, mo
tree = Tree([Leaf(package_ahriman, set())]) tree = Tree([Leaf(package_ahriman, set())])
mocker.patch("ahriman.core.tree.Tree.load", return_value=tree) mocker.patch("ahriman.core.tree.Tree.load", return_value=tree)
mocker.patch("ahriman.core.repository.repository.Repository.packages_built", return_value=[]) mocker.patch("ahriman.core.repository.repository.Repository.packages_built", return_value=paths)
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
build_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_build", return_value=paths) build_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_build", return_value=result)
update_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_update") update_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_update", return_value=result)
finalize_mock = mocker.patch("ahriman.application.application.repository.Repository._finalize") finalize_mock = mocker.patch("ahriman.application.application.repository.Repository._finalize")
application_repository.update([package_ahriman]) application_repository.update([package_ahriman])
build_mock.assert_called_once_with([package_ahriman]) build_mock.assert_called_once_with([package_ahriman])
update_mock.assert_called_once_with(paths) update_mock.assert_has_calls([mock.call(paths), mock.call(paths)])
finalize_mock.assert_called_once_with([package_ahriman]) finalize_mock.assert_has_calls([mock.call(result), mock.call(result)])
def test_update_empty(application_repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must skip updating repository if no packages supplied
"""
paths = [package.filepath for package in package_ahriman.packages.values()]
tree = Tree([Leaf(package_ahriman, set())])
mocker.patch("ahriman.core.tree.Tree.load", return_value=tree)
mocker.patch("ahriman.core.repository.repository.Repository.packages_built", return_value=[])
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
mocker.patch("ahriman.core.repository.executor.Executor.process_build")
update_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_update")
application_repository.update([package_ahriman])
update_mock.assert_not_called()
def test_updates_all(application_repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None: def test_updates_all(application_repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:

View File

@ -11,7 +11,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
must run command must run command
""" """
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print") print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
application_mock = mocker.patch("ahriman.core.configuration.Configuration.dump", application_mock = mocker.patch("ahriman.core.configuration.Configuration.dump",
return_value=configuration.dump()) return_value=configuration.dump())

View File

@ -45,7 +45,7 @@ def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, pac
application_mock = mocker.patch("ahriman.application.application.Application.unknown", application_mock = mocker.patch("ahriman.application.application.Application.unknown",
return_value=[package_ahriman]) return_value=[package_ahriman])
remove_mock = mocker.patch("ahriman.application.application.Application.remove") remove_mock = mocker.patch("ahriman.application.application.Application.remove")
print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print") print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
RemoveUnknown.run(args, "x86_64", configuration, True, False) RemoveUnknown.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with() application_mock.assert_called_once_with()
@ -65,7 +65,7 @@ def test_run_dry_run_verbose(args: argparse.Namespace, configuration: Configurat
application_mock = mocker.patch("ahriman.application.application.Application.unknown", application_mock = mocker.patch("ahriman.application.application.Application.unknown",
return_value=[package_ahriman]) return_value=[package_ahriman])
remove_mock = mocker.patch("ahriman.application.application.Application.remove") remove_mock = mocker.patch("ahriman.application.application.Application.remove")
print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print") print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
RemoveUnknown.run(args, "x86_64", configuration, True, False) RemoveUnknown.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with() application_mock.assert_called_once_with()

View File

@ -4,6 +4,7 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Report from ahriman.application.handlers import Report
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.result import Result
def _default_args(args: argparse.Namespace) -> argparse.Namespace: def _default_args(args: argparse.Namespace) -> argparse.Namespace:
@ -25,4 +26,4 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
application_mock = mocker.patch("ahriman.application.application.Application.report") application_mock = mocker.patch("ahriman.application.application.Application.report")
Report.run(args, "x86_64", configuration, True, False) Report.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(args.target, []) application_mock.assert_called_once_with(args.target, Result())

View File

@ -29,7 +29,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, aur_package
""" """
args = _default_args(args) args = _default_args(args)
search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[aur_package_ahriman]) search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[aur_package_ahriman])
print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print") print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
Search.run(args, "x86_64", configuration, True, False) Search.run(args, "x86_64", configuration, True, False)
search_mock.assert_called_once_with("ahriman") search_mock.assert_called_once_with("ahriman")

View File

@ -33,7 +33,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, package_ahr
packages_mock = mocker.patch("ahriman.core.status.client.Client.get", packages_mock = mocker.patch("ahriman.core.status.client.Client.get",
return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success)), return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success)),
(package_python_schedule, BuildStatus(BuildStatusEnum.Failed))]) (package_python_schedule, BuildStatus(BuildStatusEnum.Failed))])
print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print") print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
Status.run(args, "x86_64", configuration, True, False) Status.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with() application_mock.assert_called_once_with()
@ -51,7 +51,7 @@ def test_run_verbose(args: argparse.Namespace, configuration: Configuration, pac
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.core.status.client.Client.get", mocker.patch("ahriman.core.status.client.Client.get",
return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success))]) return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success))])
print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print") print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
Status.run(args, "x86_64", configuration, True, False) Status.run(args, "x86_64", configuration, True, False)
print_mock.assert_has_calls([mock.call(True) for _ in range(2)]) print_mock.assert_has_calls([mock.call(True) for _ in range(2)])
@ -83,7 +83,7 @@ def test_run_by_status(args: argparse.Namespace, configuration: Configuration, p
return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success)), return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success)),
(package_python_schedule, BuildStatus(BuildStatusEnum.Failed))]) (package_python_schedule, BuildStatus(BuildStatusEnum.Failed))])
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print") print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
Status.run(args, "x86_64", configuration, True, False) Status.run(args, "x86_64", configuration, True, False)
print_mock.assert_has_calls([mock.call(False) for _ in range(2)]) print_mock.assert_has_calls([mock.call(False) for _ in range(2)])

View File

@ -15,7 +15,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
args.parser = _parser args.parser = _parser
commands_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.get_unsafe_commands", commands_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.get_unsafe_commands",
return_value=["command"]) return_value=["command"])
print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print") print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
UnsafeCommands.run(args, "x86_64", configuration, True, False) UnsafeCommands.run(args, "x86_64", configuration, True, False)
commands_mock.assert_called_once_with(pytest.helpers.anyvar(int)) commands_mock.assert_called_once_with(pytest.helpers.anyvar(int))

View File

@ -14,6 +14,7 @@ from ahriman.models.aur_package import AURPackage
from ahriman.models.package import Package 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
from ahriman.models.result import Result
from ahriman.models.user import User from ahriman.models.user import User
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -234,6 +235,18 @@ def repository_paths(configuration: Configuration) -> RepositoryPaths:
root=configuration.getpath("repository", "root")) root=configuration.getpath("repository", "root"))
@pytest.fixture
def result(package_ahriman: Package) -> Result:
"""
result fixture
:param package_ahriman: package fixture
:return: result test instance
"""
result = Result()
result.add_success(package_ahriman)
return result
@pytest.fixture @pytest.fixture
def spawner(configuration: Configuration) -> Spawn: def spawner(configuration: Configuration) -> Spawn:
""" """

View File

@ -1,11 +1,11 @@
import pytest import pytest
from ahriman.application.formatters.aur_printer import AurPrinter from ahriman.core.formatters.aur_printer import AurPrinter
from ahriman.application.formatters.configuration_printer import ConfigurationPrinter from ahriman.core.formatters.configuration_printer import ConfigurationPrinter
from ahriman.application.formatters.package_printer import PackagePrinter from ahriman.core.formatters.package_printer import PackagePrinter
from ahriman.application.formatters.status_printer import StatusPrinter from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.application.formatters.string_printer import StringPrinter from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.application.formatters.update_printer import UpdatePrinter from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
from ahriman.models.build_status import BuildStatus from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package from ahriman.models.package import Package

View File

@ -1,4 +1,4 @@
from ahriman.application.formatters.aur_printer import AurPrinter from ahriman.core.formatters.aur_printer import AurPrinter
def test_properties(aur_package_ahriman_printer: AurPrinter) -> None: def test_properties(aur_package_ahriman_printer: AurPrinter) -> None:

View File

@ -0,0 +1,36 @@
import pytest
from ahriman.core.formatters.build_printer import BuildPrinter
from ahriman.models.package import Package
def test_properties(package_ahriman: Package) -> None:
"""
must return empty properties list
"""
assert not BuildPrinter(package_ahriman, is_success=True, use_utf=False).properties()
def test_sign_ascii(package_ahriman: Package) -> None:
"""
must correctly generate sign in ascii
"""
BuildPrinter(package_ahriman, is_success=True, use_utf=False).title().encode("ascii")
BuildPrinter(package_ahriman, is_success=False, use_utf=False).title().encode("ascii")
def test_sign_utf8(package_ahriman: Package) -> None:
"""
must correctly generate sign in ascii
"""
with pytest.raises(UnicodeEncodeError):
BuildPrinter(package_ahriman, is_success=True, use_utf=True).title().encode("ascii")
with pytest.raises(UnicodeEncodeError):
BuildPrinter(package_ahriman, is_success=False, use_utf=True).title().encode("ascii")
def test_title(package_ahriman: Package) -> None:
"""
must return non empty title
"""
assert BuildPrinter(package_ahriman, is_success=True, use_utf=False).title() is not None

View File

@ -1,4 +1,4 @@
from ahriman.application.formatters.configuration_printer import ConfigurationPrinter from ahriman.core.formatters.configuration_printer import ConfigurationPrinter
def test_properties(configuration_printer: ConfigurationPrinter) -> None: def test_properties(configuration_printer: ConfigurationPrinter) -> None:

View File

@ -1,4 +1,4 @@
from ahriman.application.formatters.package_printer import PackagePrinter from ahriman.core.formatters.package_printer import PackagePrinter
def test_properties(package_ahriman_printer: PackagePrinter) -> None: def test_properties(package_ahriman_printer: PackagePrinter) -> None:

View File

@ -1,7 +1,7 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
from ahriman.application.formatters.package_printer import PackagePrinter from ahriman.core.formatters.package_printer import PackagePrinter
from ahriman.application.formatters.printer import Printer from ahriman.core.formatters.printer import Printer
def test_print(package_ahriman_printer: PackagePrinter) -> None: def test_print(package_ahriman_printer: PackagePrinter) -> None:

View File

@ -1,4 +1,4 @@
from ahriman.application.formatters.status_printer import StatusPrinter from ahriman.core.formatters.status_printer import StatusPrinter
def test_properties(status_printer: StatusPrinter) -> None: def test_properties(status_printer: StatusPrinter) -> None:

View File

@ -1,4 +1,4 @@
from ahriman.application.formatters.string_printer import StringPrinter from ahriman.core.formatters.string_printer import StringPrinter
def test_properties(string_printer: StringPrinter) -> None: def test_properties(string_printer: StringPrinter) -> None:

View File

@ -1,4 +1,4 @@
from ahriman.application.formatters.update_printer import UpdatePrinter from ahriman.core.formatters.update_printer import UpdatePrinter
def test_properties(update_printer: UpdatePrinter) -> None: def test_properties(update_printer: UpdatePrinter) -> None:

View File

@ -0,0 +1,20 @@
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.core.configuration import Configuration
from ahriman.core.report.console import Console
from ahriman.models.package import Package
from ahriman.models.result import Result
def test_generate(configuration: Configuration, result: Result, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must print result to stdout
"""
print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
result.add_failed(package_python_schedule)
report = Console("x86_64", configuration, "console")
report.generate([], result)
print_mock.assert_has_calls([mock.call(verbose=True), mock.call(verbose=True)])

View File

@ -5,6 +5,7 @@ from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.report.email import Email from ahriman.core.report.email import Email
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result
def test_send(configuration: Configuration, mocker: MockerFixture) -> None: def test_send(configuration: Configuration, mocker: MockerFixture) -> None:
@ -92,24 +93,26 @@ def test_generate(configuration: Configuration, package_ahriman: Package, mocker
send_mock = mocker.patch("ahriman.core.report.email.Email._send") send_mock = mocker.patch("ahriman.core.report.email.Email._send")
report = Email("x86_64", configuration, "email") report = Email("x86_64", configuration, "email")
report.generate([package_ahriman], []) report.generate([package_ahriman], Result())
send_mock.assert_called_once_with(pytest.helpers.anyvar(int), {}) send_mock.assert_called_once_with(pytest.helpers.anyvar(int), {})
def test_generate_with_built(configuration: Configuration, package_ahriman: Package, mocker: MockerFixture) -> None: def test_generate_with_built(configuration: Configuration, package_ahriman: Package, result: Result,
mocker: MockerFixture) -> None:
""" """
must generate report with built packages must generate report with built packages
""" """
send_mock = mocker.patch("ahriman.core.report.email.Email._send") send_mock = mocker.patch("ahriman.core.report.email.Email._send")
report = Email("x86_64", configuration, "email") report = Email("x86_64", configuration, "email")
report.generate([package_ahriman], [package_ahriman]) report.generate([package_ahriman], result)
send_mock.assert_called_once_with(pytest.helpers.anyvar(int), {}) send_mock.assert_called_once_with(pytest.helpers.anyvar(int), {})
def test_generate_with_built_and_full_path( def test_generate_with_built_and_full_path(
configuration: Configuration, configuration: Configuration,
package_ahriman: Package, package_ahriman: Package,
result: Result,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must generate report with built packages must generate report with built packages
@ -118,7 +121,7 @@ def test_generate_with_built_and_full_path(
report = Email("x86_64", configuration, "email") report = Email("x86_64", configuration, "email")
report.full_template_path = report.template_path report.full_template_path = report.template_path
report.generate([package_ahriman], [package_ahriman]) report.generate([package_ahriman], result)
send_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int)) send_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int))
@ -130,11 +133,11 @@ def test_generate_no_empty(configuration: Configuration, package_ahriman: Packag
send_mock = mocker.patch("ahriman.core.report.email.Email._send") send_mock = mocker.patch("ahriman.core.report.email.Email._send")
report = Email("x86_64", configuration, "email") report = Email("x86_64", configuration, "email")
report.generate([package_ahriman], []) report.generate([package_ahriman], Result())
send_mock.assert_not_called() send_mock.assert_not_called()
def test_generate_no_empty_with_built(configuration: Configuration, package_ahriman: Package, def test_generate_no_empty_with_built(configuration: Configuration, package_ahriman: Package, result: Result,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must generate report with built packages if no_empty_report is set must generate report with built packages if no_empty_report is set
@ -143,5 +146,5 @@ def test_generate_no_empty_with_built(configuration: Configuration, package_ahri
send_mock = mocker.patch("ahriman.core.report.email.Email._send") send_mock = mocker.patch("ahriman.core.report.email.Email._send")
report = Email("x86_64", configuration, "email") report = Email("x86_64", configuration, "email")
report.generate([package_ahriman], [package_ahriman]) report.generate([package_ahriman], result)
send_mock.assert_called_once_with(pytest.helpers.anyvar(int), {}) send_mock.assert_called_once_with(pytest.helpers.anyvar(int), {})

View File

@ -1,6 +1,7 @@
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.report.jinja_template import JinjaTemplate from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result
def test_generate(configuration: Configuration, package_ahriman: Package) -> None: def test_generate(configuration: Configuration, package_ahriman: Package) -> None:
@ -9,4 +10,4 @@ def test_generate(configuration: Configuration, package_ahriman: Package) -> Non
""" """
path = configuration.getpath("html", "template_path") path = configuration.getpath("html", "template_path")
report = JinjaTemplate("html", configuration) report = JinjaTemplate("html", configuration)
assert report.make_html([package_ahriman], path) assert report.make_html(Result(success=[package_ahriman]), path)

View File

@ -6,6 +6,7 @@ from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ReportFailed from ahriman.core.exceptions import ReportFailed
from ahriman.core.report.report import Report from ahriman.core.report.report import Report
from ahriman.models.report_settings import ReportSettings from ahriman.models.report_settings import ReportSettings
from ahriman.models.result import Result
def test_report_failure(configuration: Configuration, mocker: MockerFixture) -> None: def test_report_failure(configuration: Configuration, mocker: MockerFixture) -> None:
@ -14,32 +15,41 @@ def test_report_failure(configuration: Configuration, mocker: MockerFixture) ->
""" """
mocker.patch("ahriman.core.report.html.HTML.generate", side_effect=Exception()) mocker.patch("ahriman.core.report.html.HTML.generate", side_effect=Exception())
with pytest.raises(ReportFailed): with pytest.raises(ReportFailed):
Report.load("x86_64", configuration, "html").run([], []) Report.load("x86_64", configuration, "html").run([], Result())
def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> None: def test_report_dummy(configuration: Configuration, result: Result, mocker: MockerFixture) -> None:
""" """
must construct dummy report class must construct dummy report class
""" """
mocker.patch("ahriman.models.report_settings.ReportSettings.from_option", return_value=ReportSettings.Disabled) mocker.patch("ahriman.models.report_settings.ReportSettings.from_option", return_value=ReportSettings.Disabled)
report_mock = mocker.patch("ahriman.core.report.report.Report.generate") report_mock = mocker.patch("ahriman.core.report.report.Report.generate")
Report.load("x86_64", configuration, "disabled").run([], []) Report.load("x86_64", configuration, "disabled").run([], result)
report_mock.assert_called_once_with([], []) report_mock.assert_called_once_with([], result)
def test_report_email(configuration: Configuration, mocker: MockerFixture) -> None: def test_report_console(configuration: Configuration, result: Result, mocker: MockerFixture) -> None:
"""
must generate console report
"""
report_mock = mocker.patch("ahriman.core.report.console.Console.generate")
Report.load("x86_64", configuration, "console").run([], result)
report_mock.assert_called_once_with([], result)
def test_report_email(configuration: Configuration, result: Result, mocker: MockerFixture) -> None:
""" """
must generate email report must generate email report
""" """
report_mock = mocker.patch("ahriman.core.report.email.Email.generate") report_mock = mocker.patch("ahriman.core.report.email.Email.generate")
Report.load("x86_64", configuration, "email").run([], []) Report.load("x86_64", configuration, "email").run([], result)
report_mock.assert_called_once_with([], []) report_mock.assert_called_once_with([], result)
def test_report_html(configuration: Configuration, mocker: MockerFixture) -> None: def test_report_html(configuration: Configuration, result: Result, mocker: MockerFixture) -> None:
""" """
must generate html report must generate html report
""" """
report_mock = mocker.patch("ahriman.core.report.html.HTML.generate") report_mock = mocker.patch("ahriman.core.report.html.HTML.generate")
Report.load("x86_64", configuration, "html").run([], []) Report.load("x86_64", configuration, "html").run([], result)
report_mock.assert_called_once_with([], []) report_mock.assert_called_once_with([], result)

View File

@ -35,7 +35,6 @@ def test_process_build(executor: Executor, package_ahriman: Package, mocker: Moc
mocker.patch("ahriman.core.build_tools.task.Task.init") mocker.patch("ahriman.core.build_tools.task.Task.init")
move_mock = mocker.patch("shutil.move") move_mock = mocker.patch("shutil.move")
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building") status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building")
built_packages_mock = mocker.patch("ahriman.core.repository.executor.Executor.packages_built")
executor.process_build([package_ahriman]) executor.process_build([package_ahriman])
# must move files (once) # must move files (once)
@ -45,8 +44,6 @@ def test_process_build(executor: Executor, package_ahriman: Package, mocker: Moc
# must clear directory # must clear directory
from ahriman.core.repository.cleaner import Cleaner from ahriman.core.repository.cleaner import Cleaner
Cleaner.clear_build.assert_called_once_with() Cleaner.clear_build.assert_called_once_with()
# must return build packages after all
built_packages_mock.assert_called_once_with()
def test_process_build_failure(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None: def test_process_build_failure(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:

View File

@ -21,3 +21,6 @@ def test_from_option_valid() -> None:
assert ReportSettings.from_option("email") == ReportSettings.Email assert ReportSettings.from_option("email") == ReportSettings.Email
assert ReportSettings.from_option("EmAil") == ReportSettings.Email assert ReportSettings.from_option("EmAil") == ReportSettings.Email
assert ReportSettings.from_option("console") == ReportSettings.Console
assert ReportSettings.from_option("conSOle") == ReportSettings.Console

View File

@ -0,0 +1,119 @@
import pytest
from ahriman.core.exceptions import SuccessFailed
from ahriman.models.package import Package
from ahriman.models.result import Result
def test_add_failed(package_ahriman: Package) -> None:
"""
must add package to failed list
"""
result = Result()
result.add_failed(package_ahriman)
assert result.failed == [package_ahriman]
assert not result.success
def test_add_success(package_ahriman: Package) -> None:
"""
must add package to success list
"""
result = Result()
result.add_success(package_ahriman)
assert result.success == [package_ahriman]
assert not result.failed
def test_merge(package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must merge success packages
"""
left = Result()
left.add_success(package_ahriman)
right = Result()
right.add_success(package_python_schedule)
result = left.merge(right)
assert result.success == [package_ahriman, package_python_schedule]
assert not left.failed
def test_merge_failed(package_ahriman: Package) -> None:
"""
must merge and remove failed packages from success list
"""
left = Result()
left.add_success(package_ahriman)
right = Result()
right.add_failed(package_ahriman)
result = left.merge(right)
assert result.failed == [package_ahriman]
assert not left.success
def test_merge_exception(package_ahriman: Package) -> None:
"""
must raise exception in case if package was failed
"""
left = Result()
left.add_failed(package_ahriman)
right = Result()
right.add_success(package_ahriman)
with pytest.raises(SuccessFailed):
left.merge(right)
def test_eq(package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must return True for same objects
"""
left = Result()
left.add_success(package_ahriman)
left.add_failed(package_python_schedule)
right = Result()
right.add_success(package_ahriman)
right.add_failed(package_python_schedule)
assert left == right
def test_eq_false(package_ahriman: Package) -> None:
"""
must return False in case if lists do not match
"""
left = Result()
left.add_success(package_ahriman)
right = Result()
right.add_failed(package_ahriman)
assert left != right
def test_eq_false_failed(package_ahriman: Package) -> None:
"""
must return False in case if failed does not match
"""
left = Result()
left.add_failed(package_ahriman)
assert left != Result()
def test_eq_false_success(package_ahriman: Package) -> None:
"""
must return False in case if success does not match
"""
left = Result()
left.add_success(package_ahriman)
assert left != Result()
def test_eq_other() -> None:
"""
must return False in case if object is not an instance of result
"""
assert Result() != 42

View File

@ -42,6 +42,9 @@ receivers = mail@example.com
sender = mail@example.com sender = mail@example.com
template_path = ../web/templates/repo-index.jinja2 template_path = ../web/templates/repo-index.jinja2
[console]
use_utf = yes
[html] [html]
path = path =
homepage = homepage =