allow to use multiple upload and report targets with the same name

In this feature target option must allways point to section name instead
of type. Type will be read from type option. In case if type option is
not presented it will try to check if section with architecture exists
(e.g. target = email, section = email:x86_64); if it does, the correct
section name and type will be used. Otherwise it will check if the
specified section exists; if it does, seection name and type will be
returned.
This commit is contained in:
Evgenii Alekseev 2021-10-17 05:33:23 +03:00
parent fd38dfd176
commit 20962f0385
17 changed files with 274 additions and 131 deletions

View File

@ -69,12 +69,19 @@ Settings for signing packages or repository. Group name must refer to architectu
Report generation settings. Report generation settings.
* `target` - list of reports to be generated, space separated list of strings, required. Allowed values are `html`, `email`. * `target` - list of reports to be generated, space separated list of strings, required. It must point to valid section (or to section with architecture), e.g. `somerandomname` must point to existing section, `email` must point to one of `email` of `email:x86_64` (with architecture it has higher priority).
### `email:*` groups Type will be read from several ways:
Group name must refer to architecture, e.g. it should be `email:x86_64` for x86_64 architecture. * In case if `type` option set inside the section, it will be used.
* Otherwise, it will look for type from section name removing architecture name.
* And finally, it will use section name as type.
### `email` type
Section name must be either `email` (plus optional architecture name, e.g. `email:x86_64`) or random name with `type` set.
* `type` - type of the report, string, optional, must be set to `email` if exists.
* `full_template_path` - path to Jinja2 template for full package description index, string, optional. * `full_template_path` - path to Jinja2 template for full package description index, string, optional.
* `homepage` - link to homepage, string, optional. * `homepage` - link to homepage, string, optional.
* `host` - SMTP host for sending emails, string, required. * `host` - SMTP host for sending emails, string, required.
@ -88,10 +95,11 @@ Group name must refer to architecture, e.g. it should be `email:x86_64` for x86_
* `template_path` - path to Jinja2 template, string, required. * `template_path` - path to Jinja2 template, string, required.
* `user` - SMTP user to authenticate, string, optional. * `user` - SMTP user to authenticate, string, optional.
### `html:*` groups ### `html` type
Group name must refer to architecture, e.g. it should be `html:x86_64` for x86_64 architecture. Section name must be either `html` (plus optional architecture name, e.g. `html:x86_64`) or random name with `type` set.
* `type` - type of the report, string, optional, must be set to `html` if exists.
* `path` - path to html report file, string, required. * `path` - path to html report file, string, required.
* `homepage` - link to homepage, string, optional. * `homepage` - link to homepage, string, optional.
* `link_path` - prefix for HTML links, string, required. * `link_path` - prefix for HTML links, string, required.
@ -101,12 +109,19 @@ Group name must refer to architecture, e.g. it should be `html:x86_64` for x86_6
Remote synchronization settings. Remote synchronization settings.
* `target` - list of synchronizations to be used, space separated list of strings, required. Allowed values are `rsync`, `s3`, `github`. * `target` - list of synchronizations to be used, space separated list of strings, required. It must point to valid section (or to section with architecture), e.g. `somerandomname` must point to existing section, `github` must point to one of `github` of `github:x86_64` (with architecture it has higher priority).
### `github:*` groups Type will be read from several ways:
Group name must refer to architecture, e.g. it should be `github:x86_64` for x86_64 architecture. This feature requires Github key creation (see below). * In case if `type` option set inside the section, it will be used.
* Otherwise, it will look for type from section name removing architecture name.
* And finally, it will use section name as type.
### `github` type
This feature requires Github key creation (see below). Section name must be either `github` (plus optional architecture name, e.g. `github:x86_64`) or random name with `type` set.
* `type` - type of the upload, string, optional, must be set to `github` if exists.
* `owner` - Github repository owner, string, required. * `owner` - Github repository owner, string, required.
* `password` - created Github API key. In order to create it do the following: * `password` - created Github API key. In order to create it do the following:
1. Go to [settings page](https://github.com/settings/profile). 1. Go to [settings page](https://github.com/settings/profile).
@ -116,17 +131,19 @@ Group name must refer to architecture, e.g. it should be `github:x86_64` for x86
* `repository` - Github repository name, string, required. Repository must be created before any action and must have active branch (e.g. with readme). * `repository` - Github repository name, string, required. Repository must be created before any action and must have active branch (e.g. with readme).
* `username` - Github authorization user, string, required. Basically the same as `owner`. * `username` - Github authorization user, string, required. Basically the same as `owner`.
### `rsync:*` groups ### `rsync` type
Group name must refer to architecture, e.g. it should be `rsync:x86_64` for x86_64 architecture. Requires `rsync` package to be installed. Do not forget to configure ssh for user `ahriman`. Requires `rsync` package to be installed. Do not forget to configure ssh for user `ahriman`. Section name must be either `rsync` (plus optional architecture name, e.g. `rsync:x86_64`) or random name with `type` set.
* `type` - type of the upload, string, optional, must be set to `rsync` if exists.
* `command` - rsync command to run, space separated list of string, required. * `command` - rsync command to run, space separated list of string, required.
* `remote` - remote server to rsync (e.g. `1.2.3.4:path/to/sync`), string, required. * `remote` - remote server to rsync (e.g. `1.2.3.4:path/to/sync`), string, required.
### `s3:*` groups ### `s3` type
Group name must refer to architecture, e.g. it should be `s3:x86_64` for x86_64 architecture. Requires `boto3` library to be installed. Section name must be either `s3` (plus optional architecture name, e.g. `s3:x86_64`) or random name with `type` set.
* `type` - type of the upload, string, optional, must be set to `github` if exists.
* `access_key` - AWS access key ID, string, required. * `access_key` - AWS access key ID, string, required.
* `bucket` - bucket name (e.g. `bucket`), string, required. * `bucket` - bucket name (e.g. `bucket`), string, required.
* `chunk_size` - chunk size for calculating entity tags, int, optional, default 8 * 1024 * 1024. * `chunk_size` - chunk size for calculating entity tags, int, optional, default 8 * 1024 * 1024.
@ -135,7 +152,7 @@ Group name must refer to architecture, e.g. it should be `s3:x86_64` for x86_64
## `web:*` groups ## `web:*` groups
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name must refer to architecture, e.g. it should be `web:x86_64` for x86_64 architecture. Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name must refer to architecture, e.g. it should be `web:x86_64` for x86_64 architecture. This feature requires `aiohttp` libraries to be installed.
* `address` - optional address in form `proto://host:port` (`port` can be omitted in case of default `proto` ports), will be used instead of `http://{host}:{port}` in case if set, string, optional. This option is required in case if `OAuth` provider is used. * `address` - optional address in form `proto://host:port` (`port` can be omitted in case of default `proto` ports), will be used instead of `http://{host}:{port}` in case if set, string, optional. This option is required in case if `OAuth` provider is used.
* `debug` - enable debug toolbar, boolean, optional, default `no`. * `debug` - enable debug toolbar, boolean, optional, default `no`.

View File

@ -21,6 +21,66 @@ systemctl enable --now ahriman@x86_64.timer
The idea is to install the package as usual, create working directory tree, create configuration for `sudo` and `devtools`. Detailed description of the setup instruction can be found [here](setup.md). The idea is to install the package as usual, create working directory tree, create configuration for `sudo` and `devtools`. Detailed description of the setup instruction can be found [here](setup.md).
### What does "architecture specific" mean? / How to configure for different architectures?
Some sections can be configured per architecture. The service will merge architecture specific values into common settings. In order to specify settings for specific architecture you must point it in section name.
For example, the section
```ini
[build]
build_command = extra-x86_64-build
```
states that default build command is `extra-x86_64-build`. But if there is section
```ini
[build:i686]
build_command = extra-i686-build
```
the `extra-i686-build` command will be used for `i686` architecture.
### How to use reporter/upload settings?
Normally you probably like to generate only one report for the specific type, e.g. only one email report. In order to do it you will need to have the following configuration:
```ini
[report]
target = email
[email]
...
```
or in case of multiple architectures and _different_ reporting settings:
```ini
[report]
target = email
[email:i686]
...
[email:x86_64]
...
```
But for some cases you would like to have multiple different reports with the same type (e.g. sending different templates to different addresses). For these cases you will need to specify section name in target and type in section, e.g. the following configuration can be used:
```ini
[report]
target = email_1 email_2
[email_1]
type = email
...
[email_2]
type = email
...
```
### Okay, I've installed ahriman, how do I add new package? ### Okay, I've installed ahriman, how do I add new package?
```shell ```shell

View File

@ -24,7 +24,7 @@ import logging
from logging.config import fileConfig from logging.config import fileConfig
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Type from typing import Any, Dict, List, Optional, Tuple, Type
from ahriman.core.exceptions import InitializeException from ahriman.core.exceptions import InitializeException
@ -42,7 +42,7 @@ class Configuration(configparser.RawConfigParser):
DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s" DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s"
DEFAULT_LOG_LEVEL = logging.DEBUG DEFAULT_LOG_LEVEL = logging.DEBUG
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "html", "rsync", "s3", "sign", "web"] ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "sign", "web"]
def __init__(self) -> None: def __init__(self) -> None:
""" """
@ -121,6 +121,26 @@ class Configuration(configparser.RawConfigParser):
def getpath(self, *args: Any, **kwargs: Any) -> Path: ... def getpath(self, *args: Any, **kwargs: Any) -> Path: ...
def gettype(self, section: str, architecture: str) -> Tuple[str, str]:
"""
get type variable with fallback to old logic
Despite the fact that it has same semantics as other get* methods, but it has different argument list
:param section: section name
:param architecture: repository architecture
:return: section name and found type name
"""
group_type = self.get(section, "type", fallback=None) # new-style logic
if group_type is not None:
return section, group_type
# okay lets check for the section with architecture name
full_section = self.section_name(section, architecture)
if self.has_section(full_section):
return full_section, section
# okay lets just use section as type
if not self.has_section(section):
raise configparser.NoSectionError(section)
return section, section
def load(self, path: Path) -> None: def load(self, path: Path) -> None:
""" """
fully load configuration fully load configuration

View File

@ -45,27 +45,28 @@ class Email(Report, JinjaTemplate):
:ivar user: username to authenticate via SMTP :ivar user: username to authenticate via SMTP
""" """
def __init__(self, architecture: str, configuration: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param section: settings section name
""" """
Report.__init__(self, architecture, configuration) Report.__init__(self, architecture, configuration)
JinjaTemplate.__init__(self, "email", configuration) JinjaTemplate.__init__(self, section, configuration)
self.full_template_path = configuration.getpath("email", "full_template_path", fallback=None) self.full_template_path = configuration.getpath(section, "full_template_path", fallback=None)
self.template_path = configuration.getpath("email", "template_path") self.template_path = configuration.getpath(section, "template_path")
# base smtp settings # base smtp settings
self.host = configuration.get("email", "host") self.host = configuration.get(section, "host")
self.no_empty_report = configuration.getboolean("email", "no_empty_report", fallback=True) self.no_empty_report = configuration.getboolean(section, "no_empty_report", fallback=True)
self.password = configuration.get("email", "password", fallback=None) self.password = configuration.get(section, "password", fallback=None)
self.port = configuration.getint("email", "port") self.port = configuration.getint(section, "port")
self.receivers = configuration.getlist("email", "receivers") self.receivers = configuration.getlist(section, "receivers")
self.sender = configuration.get("email", "sender") self.sender = configuration.get(section, "sender")
self.ssl = SmtpSSLSettings.from_option(configuration.get("email", "ssl", fallback="disabled")) self.ssl = SmtpSSLSettings.from_option(configuration.get(section, "ssl", fallback="disabled"))
self.user = configuration.get("email", "user", fallback=None) self.user = configuration.get(section, "user", fallback=None)
def _send(self, text: str, attachment: Dict[str, str]) -> None: def _send(self, text: str, attachment: Dict[str, str]) -> None:
""" """

View File

@ -31,17 +31,18 @@ class HTML(Report, JinjaTemplate):
:ivar report_path: output path to html report :ivar report_path: output path to html report
""" """
def __init__(self, architecture: str, configuration: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param section: settings section name
""" """
Report.__init__(self, architecture, configuration) Report.__init__(self, architecture, configuration)
JinjaTemplate.__init__(self, "html", configuration) JinjaTemplate.__init__(self, section, configuration)
self.report_path = configuration.getpath("html", "path") self.report_path = configuration.getpath(section, "path")
self.template_path = configuration.getpath("html", "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], built_packages: Iterable[Package]) -> None:
""" """

View File

@ -53,16 +53,17 @@ class Report:
load client from settings load client from settings
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param target: target to generate report (e.g. html) :param target: target to generate report aka section name (e.g. html)
:return: client according to current settings :return: client according to current settings
""" """
provider = ReportSettings.from_option(target) section, provider_name = configuration.gettype(target, architecture)
provider = ReportSettings.from_option(provider_name)
if provider == ReportSettings.HTML: if provider == ReportSettings.HTML:
from ahriman.core.report.html import HTML from ahriman.core.report.html import HTML
return HTML(architecture, configuration) return HTML(architecture, configuration, section)
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) return Email(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], built_packages: Iterable[Package]) -> None:

View File

@ -36,15 +36,16 @@ class Github(HttpUpload):
:ivar gh_repository: github repository name :ivar gh_repository: github repository name
""" """
def __init__(self, architecture: str, configuration: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param section: settings section name
""" """
HttpUpload.__init__(self, architecture, configuration, "github") HttpUpload.__init__(self, architecture, configuration, section)
self.gh_owner = configuration.get("github", "owner") self.gh_owner = configuration.get(section, "owner")
self.gh_repository = configuration.get("github", "repository") self.gh_repository = configuration.get(section, "repository")
def asset_remove(self, release: Dict[str, Any], name: str) -> None: def asset_remove(self, release: Dict[str, Any], name: str) -> None:
""" """

View File

@ -35,15 +35,16 @@ class Rsync(Upload):
_check_output = check_output _check_output = check_output
def __init__(self, architecture: str, configuration: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param section: settings section name
""" """
Upload.__init__(self, architecture, configuration) Upload.__init__(self, architecture, configuration)
self.command = configuration.getlist("rsync", "command") self.command = configuration.getlist(section, "command")
self.remote = configuration.get("rsync", "remote") self.remote = configuration.get(section, "remote")
def sync(self, path: Path, built_packages: Iterable[Package]) -> None: def sync(self, path: Path, built_packages: Iterable[Package]) -> None:
""" """

View File

@ -37,15 +37,15 @@ class S3(Upload):
:ivar chunk_size: chunk size for calculating checksums :ivar chunk_size: chunk size for calculating checksums
""" """
def __init__(self, architecture: str, configuration: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
""" """
Upload.__init__(self, architecture, configuration) Upload.__init__(self, architecture, configuration)
self.bucket = self.get_bucket(configuration) self.bucket = self.get_bucket(configuration, section)
self.chunk_size = configuration.getint("s3", "chunk_size", fallback=8 * 1024 * 1024) self.chunk_size = configuration.getint(section, "chunk_size", fallback=8 * 1024 * 1024)
@staticmethod @staticmethod
def calculate_etag(path: Path, chunk_size: int) -> str: def calculate_etag(path: Path, chunk_size: int) -> str:
@ -70,17 +70,18 @@ class S3(Upload):
return f"{checksum.hexdigest()}{suffix}" return f"{checksum.hexdigest()}{suffix}"
@staticmethod @staticmethod
def get_bucket(configuration: Configuration) -> Any: def get_bucket(configuration: Configuration, section: str) -> Any:
""" """
create resource client from configuration create resource client from configuration
:param configuration: configuration instance :param configuration: configuration instance
:param section: settings section name
:return: amazon client :return: amazon client
""" """
client = boto3.resource(service_name="s3", client = boto3.resource(service_name="s3",
region_name=configuration.get("s3", "region"), region_name=configuration.get(section, "region"),
aws_access_key_id=configuration.get("s3", "access_key"), aws_access_key_id=configuration.get(section, "access_key"),
aws_secret_access_key=configuration.get("s3", "secret_key")) aws_secret_access_key=configuration.get(section, "secret_key"))
return client.Bucket(configuration.get("s3", "bucket")) return client.Bucket(configuration.get(section, "bucket"))
@staticmethod @staticmethod
def files_remove(local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None: def files_remove(local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None:

View File

@ -57,16 +57,17 @@ class Upload:
:param target: target to run sync (e.g. s3) :param target: target to run sync (e.g. s3)
:return: client according to current settings :return: client according to current settings
""" """
provider = UploadSettings.from_option(target) section, provider_name = configuration.gettype(target, architecture)
provider = UploadSettings.from_option(provider_name)
if provider == UploadSettings.Rsync: if provider == UploadSettings.Rsync:
from ahriman.core.upload.rsync import Rsync from ahriman.core.upload.rsync import Rsync
return Rsync(architecture, configuration) return Rsync(architecture, configuration, section)
if provider == UploadSettings.S3: if provider == UploadSettings.S3:
from ahriman.core.upload.s3 import S3 from ahriman.core.upload.s3 import S3
return S3(architecture, configuration) return S3(architecture, configuration, section)
if provider == UploadSettings.Github: if provider == UploadSettings.Github:
from ahriman.core.upload.github import Github from ahriman.core.upload.github import Github
return Github(architecture, configuration) return Github(architecture, configuration, section)
return cls(architecture, configuration) # should never happen return cls(architecture, configuration) # should never happen
def run(self, path: Path, built_packages: Iterable[Package]) -> None: def run(self, path: Path, built_packages: Iterable[Package]) -> None:

View File

@ -11,7 +11,7 @@ def test_send(configuration: Configuration, mocker: MockerFixture) -> None:
""" """
smtp_mock = mocker.patch("smtplib.SMTP") smtp_mock = mocker.patch("smtplib.SMTP")
report = Email("x86_64", configuration) report = Email("x86_64", configuration, "email")
report._send("a text", {"attachment.html": "an attachment"}) report._send("a text", {"attachment.html": "an attachment"})
smtp_mock.return_value.starttls.assert_not_called() smtp_mock.return_value.starttls.assert_not_called()
smtp_mock.return_value.login.assert_not_called() smtp_mock.return_value.login.assert_not_called()
@ -27,7 +27,7 @@ def test_send_auth(configuration: Configuration, mocker: MockerFixture) -> None:
configuration.set_option("email", "password", "password") configuration.set_option("email", "password", "password")
smtp_mock = mocker.patch("smtplib.SMTP") smtp_mock = mocker.patch("smtplib.SMTP")
report = Email("x86_64", configuration) report = Email("x86_64", configuration, "email")
report._send("a text", {"attachment.html": "an attachment"}) report._send("a text", {"attachment.html": "an attachment"})
smtp_mock.return_value.login.assert_called_once() smtp_mock.return_value.login.assert_called_once()
@ -39,7 +39,7 @@ def test_send_auth_no_password(configuration: Configuration, mocker: MockerFixtu
configuration.set_option("email", "user", "username") configuration.set_option("email", "user", "username")
smtp_mock = mocker.patch("smtplib.SMTP") smtp_mock = mocker.patch("smtplib.SMTP")
report = Email("x86_64", configuration) report = Email("x86_64", configuration, "email")
report._send("a text", {"attachment.html": "an attachment"}) report._send("a text", {"attachment.html": "an attachment"})
smtp_mock.return_value.login.assert_not_called() smtp_mock.return_value.login.assert_not_called()
@ -51,7 +51,7 @@ def test_send_auth_no_user(configuration: Configuration, mocker: MockerFixture)
configuration.set_option("email", "password", "password") configuration.set_option("email", "password", "password")
smtp_mock = mocker.patch("smtplib.SMTP") smtp_mock = mocker.patch("smtplib.SMTP")
report = Email("x86_64", configuration) report = Email("x86_64", configuration, "email")
report._send("a text", {"attachment.html": "an attachment"}) report._send("a text", {"attachment.html": "an attachment"})
smtp_mock.return_value.login.assert_not_called() smtp_mock.return_value.login.assert_not_called()
@ -63,7 +63,7 @@ def test_send_ssl_tls(configuration: Configuration, mocker: MockerFixture) -> No
configuration.set_option("email", "ssl", "ssl") configuration.set_option("email", "ssl", "ssl")
smtp_mock = mocker.patch("smtplib.SMTP_SSL") smtp_mock = mocker.patch("smtplib.SMTP_SSL")
report = Email("x86_64", configuration) report = Email("x86_64", configuration, "email")
report._send("a text", {"attachment.html": "an attachment"}) report._send("a text", {"attachment.html": "an attachment"})
smtp_mock.return_value.starttls.assert_not_called() smtp_mock.return_value.starttls.assert_not_called()
smtp_mock.return_value.login.assert_not_called() smtp_mock.return_value.login.assert_not_called()
@ -78,7 +78,7 @@ def test_send_starttls(configuration: Configuration, mocker: MockerFixture) -> N
configuration.set_option("email", "ssl", "starttls") configuration.set_option("email", "ssl", "starttls")
smtp_mock = mocker.patch("smtplib.SMTP") smtp_mock = mocker.patch("smtplib.SMTP")
report = Email("x86_64", configuration) report = Email("x86_64", configuration, "email")
report._send("a text", {"attachment.html": "an attachment"}) report._send("a text", {"attachment.html": "an attachment"})
smtp_mock.return_value.starttls.assert_called_once() smtp_mock.return_value.starttls.assert_called_once()
@ -89,7 +89,7 @@ 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) report = Email("x86_64", configuration, "email")
report.generate([package_ahriman], []) report.generate([package_ahriman], [])
send_mock.assert_called_once() send_mock.assert_called_once()
@ -100,7 +100,7 @@ def test_generate_with_built(configuration: Configuration, package_ahriman: Pack
""" """
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) report = Email("x86_64", configuration, "email")
report.generate([package_ahriman], [package_ahriman]) report.generate([package_ahriman], [package_ahriman])
send_mock.assert_called_once() send_mock.assert_called_once()
@ -114,7 +114,7 @@ def test_generate_with_built_and_full_path(
""" """
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) 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], [package_ahriman])
send_mock.assert_called_once() send_mock.assert_called_once()
@ -127,7 +127,7 @@ def test_generate_no_empty(configuration: Configuration, package_ahriman: Packag
configuration.set_option("email", "no_empty_report", "yes") configuration.set_option("email", "no_empty_report", "yes")
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) report = Email("x86_64", configuration, "email")
report.generate([package_ahriman], []) report.generate([package_ahriman], [])
send_mock.assert_not_called() send_mock.assert_not_called()
@ -140,6 +140,6 @@ def test_generate_no_empty_with_built(configuration: Configuration, package_ahri
configuration.set_option("email", "no_empty_report", "yes") configuration.set_option("email", "no_empty_report", "yes")
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) report = Email("x86_64", configuration, "email")
report.generate([package_ahriman], [package_ahriman]) report.generate([package_ahriman], [package_ahriman])
send_mock.assert_called_once() send_mock.assert_called_once()

View File

@ -11,6 +11,6 @@ def test_generate(configuration: Configuration, package_ahriman: Package, mocker
""" """
write_mock = mocker.patch("pathlib.Path.write_text") write_mock = mocker.patch("pathlib.Path.write_text")
report = HTML("x86_64", configuration) report = HTML("x86_64", configuration, "html")
report.generate([package_ahriman], []) report.generate([package_ahriman], [])
write_mock.assert_called_once() write_mock.assert_called_once()

View File

@ -1,6 +1,5 @@
import pytest import pytest
from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -15,7 +14,7 @@ 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, ReportSettings.HTML.name).run(Path("path"), []) Report.load("x86_64", configuration, "html").run([], [])
def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> None: def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> None:
@ -24,7 +23,7 @@ def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> No
""" """
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, ReportSettings.Disabled.name).run(Path("path"), []) Report.load("x86_64", configuration, "disabled").run([], [])
report_mock.assert_called_once() report_mock.assert_called_once()
@ -33,7 +32,7 @@ def test_report_email(configuration: Configuration, mocker: MockerFixture) -> No
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, ReportSettings.Email.name).run(Path("path"), []) Report.load("x86_64", configuration, "email").run([], [])
report_mock.assert_called_once() report_mock.assert_called_once()
@ -42,5 +41,5 @@ def test_report_html(configuration: Configuration, mocker: MockerFixture) -> Non
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, ReportSettings.HTML.name).run(Path("path"), []) Report.load("x86_64", configuration, "html").run([], [])
report_mock.assert_called_once() report_mock.assert_called_once()

View File

@ -1,7 +1,7 @@
import configparser import configparser
from pathlib import Path
import pytest import pytest
from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -24,53 +24,6 @@ def test_from_path(mocker: MockerFixture) -> None:
load_logging_mock.assert_called_once() load_logging_mock.assert_called_once()
def test_section_name(configuration: Configuration) -> None:
"""
must return architecture specific group
"""
assert configuration.section_name("build", "x86_64") == "build:x86_64"
def test_absolute_path_for_absolute(configuration: Configuration) -> None:
"""
must not change path for absolute path in settings
"""
path = Path("/a/b/c")
configuration.set_option("build", "path", str(path))
assert configuration.getpath("build", "path") == path
def test_absolute_path_for_relative(configuration: Configuration) -> None:
"""
must prepend root path to relative path
"""
path = Path("a")
configuration.set_option("build", "path", str(path))
result = configuration.getpath("build", "path")
assert result.is_absolute()
assert result.parent == configuration.path.parent
assert result.name == path.name
def test_path_with_fallback(configuration: Configuration) -> None:
"""
must return fallback path
"""
path = Path("a")
assert configuration.getpath("some", "option", fallback=path).name == str(path)
assert configuration.getpath("some", "option", fallback=None) is None
def test_path_without_fallback(configuration: Configuration) -> None:
"""
must raise exception without fallback
"""
with pytest.raises(configparser.NoSectionError):
assert configuration.getpath("some", "option")
with pytest.raises(configparser.NoOptionError):
assert configuration.getpath("build", "option")
def test_dump(configuration: Configuration) -> None: def test_dump(configuration: Configuration) -> None:
""" """
dump must not be empty dump must not be empty
@ -93,6 +46,53 @@ def test_dump_architecture_specific(configuration: Configuration) -> None:
assert dump["build"]["archbuild_flags"] == "hello flag" assert dump["build"]["archbuild_flags"] == "hello flag"
def test_section_name(configuration: Configuration) -> None:
"""
must return architecture specific group
"""
assert configuration.section_name("build", "x86_64") == "build:x86_64"
def test_getpath_absolute_to_absolute(configuration: Configuration) -> None:
"""
must not change path for absolute path in settings
"""
path = Path("/a/b/c")
configuration.set_option("build", "path", str(path))
assert configuration.getpath("build", "path") == path
def test_getpath_absolute_to_relative(configuration: Configuration) -> None:
"""
must prepend root path to relative path
"""
path = Path("a")
configuration.set_option("build", "path", str(path))
result = configuration.getpath("build", "path")
assert result.is_absolute()
assert result.parent == configuration.path.parent
assert result.name == path.name
def test_getpath_with_fallback(configuration: Configuration) -> None:
"""
must return fallback path
"""
path = Path("a")
assert configuration.getpath("some", "option", fallback=path).name == str(path)
assert configuration.getpath("some", "option", fallback=None) is None
def test_getpath_without_fallback(configuration: Configuration) -> None:
"""
must raise exception without fallback
"""
with pytest.raises(configparser.NoSectionError):
assert configuration.getpath("some", "option")
with pytest.raises(configparser.NoOptionError):
assert configuration.getpath("build", "option")
def test_getlist(configuration: Configuration) -> None: def test_getlist(configuration: Configuration) -> None:
""" """
must return list of string correctly must return list of string correctly
@ -119,6 +119,43 @@ def test_getlist_single(configuration: Configuration) -> None:
assert configuration.getlist("build", "test_list") == ["a"] assert configuration.getlist("build", "test_list") == ["a"]
def test_gettype(configuration: Configuration) -> None:
"""
must extract type from variable
"""
section, provider = configuration.gettype("customs3", "x86_64")
assert section == "customs3"
assert provider == "s3"
def test_gettype_from_section(configuration: Configuration) -> None:
"""
must extract type from section name
"""
section, provider = configuration.gettype("rsync", "x86_64")
assert section == "rsync"
assert provider == "rsync"
def test_gettype_from_section_with_architecture(configuration: Configuration) -> None:
"""
must extract type from section name with architecture
"""
section, provider = configuration.gettype("github", "x86_64")
assert section == "github:x86_64"
assert provider == "github"
def test_gettype_from_section_no_section(configuration: Configuration) -> None:
"""
must extract type from section name with architecture
"""
# technically rsync:x86_64 is valid section
# but in current configuration it must be considered as missing section
with pytest.raises(configparser.NoSectionError):
configuration.gettype("rsync:x86_64", "x86_64")
def test_load_includes_missing(configuration: Configuration) -> None: def test_load_includes_missing(configuration: Configuration) -> None:
""" """
must not fail if not include directory found must not fail if not include directory found
@ -137,7 +174,7 @@ def test_load_includes_no_option(configuration: Configuration) -> None:
def test_load_includes_no_section(configuration: Configuration) -> None: def test_load_includes_no_section(configuration: Configuration) -> None:
""" """
must not fail if no option set must not fail if no section set
""" """
configuration.remove_section("settings") configuration.remove_section("settings")
configuration.load_includes() configuration.load_includes()

View File

@ -20,7 +20,7 @@ def github(configuration: Configuration) -> Github:
:param configuration: configuration fixture :param configuration: configuration fixture
:return: github test instance :return: github test instance
""" """
return Github("x86_64", configuration) return Github("x86_64", configuration, "github:x86_64")
@pytest.fixture @pytest.fixture
@ -50,7 +50,7 @@ def rsync(configuration: Configuration) -> Rsync:
:param configuration: configuration fixture :param configuration: configuration fixture
:return: rsync test instance :return: rsync test instance
""" """
return Rsync("x86_64", configuration) return Rsync("x86_64", configuration, "rsync")
@pytest.fixture @pytest.fixture
@ -60,7 +60,7 @@ def s3(configuration: Configuration) -> S3:
:param configuration: configuration fixture :param configuration: configuration fixture
:return: S3 test instance :return: S3 test instance
""" """
return S3("x86_64", configuration) return S3("x86_64", configuration, "customs3")
@pytest.fixture @pytest.fixture

View File

@ -15,7 +15,7 @@ def test_upload_failure(configuration: Configuration, mocker: MockerFixture) ->
""" """
mocker.patch("ahriman.core.upload.rsync.Rsync.sync", side_effect=Exception()) mocker.patch("ahriman.core.upload.rsync.Rsync.sync", side_effect=Exception())
with pytest.raises(SyncFailed): with pytest.raises(SyncFailed):
Upload.load("x86_64", configuration, UploadSettings.Rsync.name).run(Path("path"), []) Upload.load("x86_64", configuration, "rsync").run(Path("path"), [])
def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> None: def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> None:
@ -24,7 +24,7 @@ def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> No
""" """
mocker.patch("ahriman.models.upload_settings.UploadSettings.from_option", return_value=UploadSettings.Disabled) mocker.patch("ahriman.models.upload_settings.UploadSettings.from_option", return_value=UploadSettings.Disabled)
upload_mock = mocker.patch("ahriman.core.upload.upload.Upload.sync") upload_mock = mocker.patch("ahriman.core.upload.upload.Upload.sync")
Upload.load("x86_64", configuration, UploadSettings.Disabled.name).run(Path("path"), []) Upload.load("x86_64", configuration, "disabled").run(Path("path"), [])
upload_mock.assert_called_once() upload_mock.assert_called_once()
@ -33,7 +33,7 @@ def test_upload_rsync(configuration: Configuration, mocker: MockerFixture) -> No
must upload via rsync must upload via rsync
""" """
upload_mock = mocker.patch("ahriman.core.upload.rsync.Rsync.sync") upload_mock = mocker.patch("ahriman.core.upload.rsync.Rsync.sync")
Upload.load("x86_64", configuration, UploadSettings.Rsync.name).run(Path("path"), []) Upload.load("x86_64", configuration, "rsync").run(Path("path"), [])
upload_mock.assert_called_once() upload_mock.assert_called_once()
@ -42,7 +42,7 @@ def test_upload_s3(configuration: Configuration, mocker: MockerFixture) -> None:
must upload via s3 must upload via s3
""" """
upload_mock = mocker.patch("ahriman.core.upload.s3.S3.sync") upload_mock = mocker.patch("ahriman.core.upload.s3.S3.sync")
Upload.load("x86_64", configuration, UploadSettings.S3.name).run(Path("path"), []) Upload.load("x86_64", configuration, "customs3").run(Path("path"), [])
upload_mock.assert_called_once() upload_mock.assert_called_once()
@ -51,5 +51,5 @@ def test_upload_github(configuration: Configuration, mocker: MockerFixture) -> N
must upload via github must upload via github
""" """
upload_mock = mocker.patch("ahriman.core.upload.github.Github.sync") upload_mock = mocker.patch("ahriman.core.upload.github.Github.sync")
Upload.load("x86_64", configuration, UploadSettings.Github.name).run(Path("path"), []) Upload.load("x86_64", configuration, "github").run(Path("path"), [])
upload_mock.assert_called_once() upload_mock.assert_called_once()

View File

@ -55,13 +55,16 @@ target =
command = rsync --archive --verbose --compress --partial --delete command = rsync --archive --verbose --compress --partial --delete
remote = remote =
[s3] [disabled]
[customs3]
type = s3
access_key = access_key =
bucket = bucket bucket = bucket
region = eu-central-1 region = eu-central-1
secret_key = secret_key =
[github] [github:x86_64]
owner = arcan1s owner = arcan1s
password = password =
repository = ahriman repository = ahriman