expose trigger configuration schema

Note that this commit contains the following breaking changes:

* remote pull and remote push triggers are now enabled by default (with
  empty target list)
* remote pull and remote push triggers now require target option to be
  set (old behaviour had fallback on `gitremote`)
* validation is now considered to be stable, so it is enabled by default
  in docker image (can be disabled however)
This commit is contained in:
2023-01-10 03:14:21 +02:00
parent d942a70272
commit 0239fb50b6
27 changed files with 717 additions and 448 deletions

View File

@ -5,8 +5,9 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Validate
from ahriman.core.configuration import Configuration
from ahriman.core.configuration.schema import CONFIGURATION_SCHEMA, GITREMOTE_REMOTE_PULL_SCHEMA
from ahriman.core.configuration.schema import CONFIGURATION_SCHEMA
from ahriman.core.configuration.validator import Validator
from ahriman.core.gitremote import RemotePullTrigger, RemotePushTrigger
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
@ -60,7 +61,6 @@ def test_schema(configuration: Configuration) -> None:
assert schema.pop("console")
assert schema.pop("email")
assert schema.pop("github")
assert schema.pop("gitremote")
assert schema.pop("html")
assert schema.pop("rsync")
assert schema.pop("s3")
@ -69,6 +69,14 @@ def test_schema(configuration: Configuration) -> None:
assert schema == CONFIGURATION_SCHEMA
def test_schema_invalid_trigger(configuration: Configuration) -> None:
"""
must skip trigger if it caused exception on load
"""
configuration.set_option("build", "triggers", "some.invalid.trigger.path.Trigger")
assert Validate.schema("x86_64", configuration) == CONFIGURATION_SCHEMA
def test_schema_erase_required() -> None:
"""
must remove required field from dictionaries recursively
@ -77,24 +85,18 @@ def test_schema_erase_required() -> None:
assert "required" not in json.dumps(Validate.schema_erase_required(CONFIGURATION_SCHEMA))
def test_schema_insert(configuration: Configuration) -> None:
def test_schema_merge() -> None:
"""
must insert child schema to root
must merge schemas correctly
"""
result = Validate.schema_insert("x86_64", configuration, CONFIGURATION_SCHEMA, "remote-pull",
lambda _: GITREMOTE_REMOTE_PULL_SCHEMA)
assert result["gitremote"] == GITREMOTE_REMOTE_PULL_SCHEMA
erased = Validate.schema_erase_required(CONFIGURATION_SCHEMA)
assert Validate.schema_merge(erased, CONFIGURATION_SCHEMA) == CONFIGURATION_SCHEMA
def test_schema_insert_skip(configuration: Configuration) -> None:
"""
must do nothing in case if there is no such section or option
"""
configuration.remove_section("remote-pull")
result = Validate.schema_insert("x86_64", configuration, CONFIGURATION_SCHEMA, "remote-pull",
lambda _: GITREMOTE_REMOTE_PULL_SCHEMA)
assert result == CONFIGURATION_SCHEMA
merged = Validate.schema_merge(RemotePullTrigger.CONFIGURATION_SCHEMA, RemotePushTrigger.CONFIGURATION_SCHEMA)
for key in RemotePullTrigger.CONFIGURATION_SCHEMA["gitremote"]["schema"]:
assert key in merged["gitremote"]["schema"]
for key in RemotePushTrigger.CONFIGURATION_SCHEMA["gitremote"]["schema"]:
assert key in merged["gitremote"]["schema"]
def test_disallow_auto_architecture_run() -> None:

View File

@ -207,6 +207,15 @@ def test_gettype(configuration: Configuration) -> None:
assert provider == "s3"
def test_gettype_with_fallback(configuration: Configuration) -> None:
"""
must return same provider name as in fallback
"""
section, provider = configuration.gettype("rsync", "x86_64", fallback="abracadabra")
assert section == "rsync"
assert provider == "abracadabra"
def test_gettype_from_section(configuration: Configuration) -> None:
"""
must extract type from section name

View File

@ -4,6 +4,17 @@ from ahriman.core.configuration import Configuration
from ahriman.core.gitremote import RemotePullTrigger
def test_configuration_sections(configuration: Configuration) -> None:
"""
must correctly parse target list
"""
configuration.set_option("remote-pull", "target", "a b c")
assert RemotePullTrigger.configuration_sections(configuration) == ["a", "b", "c"]
configuration.remove_option("remote-pull", "target")
assert RemotePullTrigger.configuration_sections(configuration) == []
def test_on_start(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must clone repo on start

View File

@ -8,6 +8,17 @@ from ahriman.models.package import Package
from ahriman.models.result import Result
def test_configuration_sections(configuration: Configuration) -> None:
"""
must correctly parse target list
"""
configuration.set_option("remote-push", "target", "a b c")
assert RemotePushTrigger.configuration_sections(configuration) == ["a", "b", "c"]
configuration.remove_option("remote-push", "target")
assert RemotePushTrigger.configuration_sections(configuration) == []
def test_on_result(configuration: Configuration, result: Result, package_ahriman: Package,
database: SQLite, mocker: MockerFixture) -> None:
"""

View File

@ -5,6 +5,17 @@ from ahriman.core.report import ReportTrigger
from ahriman.models.result import Result
def test_configuration_sections(configuration: Configuration) -> None:
"""
must correctly parse target list
"""
configuration.set_option("report", "target", "a b c")
assert ReportTrigger.configuration_sections(configuration) == ["a", "b", "c"]
configuration.remove_option("report", "target")
assert ReportTrigger.configuration_sections(configuration) == []
def test_on_result(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run report for specified targets

View File

@ -28,4 +28,4 @@ def trigger_loader(configuration: Configuration) -> TriggerLoader:
Returns:
TriggerLoader: trigger loader test instance
"""
return TriggerLoader("x86_64", configuration)
return TriggerLoader.load("x86_64", configuration)

View File

@ -1,9 +1,64 @@
from unittest.mock import MagicMock
from ahriman.core.configuration import Configuration
from ahriman.core.report import ReportTrigger
from ahriman.core.triggers import Trigger
from ahriman.models.result import Result
def test_configuration_schema(configuration: Configuration) -> None:
"""
must return used configuration schema
"""
section = "console"
configuration.set_option("report", "target", section)
expected = {section: ReportTrigger.CONFIGURATION_SCHEMA[section]}
assert ReportTrigger.configuration_schema("x86_64", configuration) == expected
def test_configuration_schema_no_section(configuration: Configuration) -> None:
"""
must return nothing in case if section doesn't exist
"""
section = "abracadabra"
configuration.set_option("report", "target", section)
assert ReportTrigger.configuration_schema("x86_64", configuration) == {}
def test_configuration_schema_no_schema(configuration: Configuration) -> None:
"""
must return nothing in case if schema doesn't exist
"""
section = "abracadabra"
configuration.set_option("report", "target", section)
configuration.set_option(section, "key", "value")
assert ReportTrigger.configuration_schema("x86_64", configuration) == {}
def test_configuration_schema_empty() -> None:
"""
must return default schema if no configuration set
"""
assert ReportTrigger.configuration_schema("x86_64", None) == ReportTrigger.CONFIGURATION_SCHEMA
def test_configuration_schema_variables(configuration: Configuration) -> None:
"""
must return empty schema
"""
assert Trigger.CONFIGURATION_SCHEMA == {}
assert Trigger.CONFIGURATION_SCHEMA_FALLBACK is None
def test_configuration_sections(configuration: Configuration) -> None:
"""
must return empty section list
"""
assert Trigger.configuration_sections(configuration) == []
def test_on_result(trigger: Trigger) -> None:
"""
must pass execution nto run method

View File

@ -5,75 +5,97 @@ from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ExtensionError
from ahriman.core.report import ReportTrigger
from ahriman.core.triggers import TriggerLoader
from ahriman.models.package import Package
from ahriman.models.result import Result
def test_load_trigger_package(trigger_loader: TriggerLoader) -> None:
def test_selected_triggers(configuration: Configuration) -> None:
"""
must load trigger from package
must return used triggers
"""
assert trigger_loader.load_trigger("ahriman.core.report.ReportTrigger")
configuration.set_option("build", "triggers", "a b c")
assert TriggerLoader.selected_triggers(configuration) == ["a", "b", "c"]
configuration.remove_option("build", "triggers")
assert TriggerLoader.selected_triggers(configuration) == []
def test_load_trigger_package_invalid_import(trigger_loader: TriggerLoader, mocker: MockerFixture) -> None:
def test_load_trigger(trigger_loader: TriggerLoader, configuration: Configuration) -> None:
"""
must raise InvalidExtension on invalid import
must load trigger
"""
mocker.patch("ahriman.core.triggers.trigger_loader.importlib.import_module", side_effect=ModuleNotFoundError())
with pytest.raises(ExtensionError):
trigger_loader.load_trigger("random.module")
loaded = trigger_loader.load_trigger("ahriman.core.report.ReportTrigger", "x86_64", configuration)
assert loaded
assert isinstance(loaded, ReportTrigger)
def test_load_trigger_package_not_trigger(trigger_loader: TriggerLoader) -> None:
"""
must raise InvalidExtension if imported module is not a type
"""
with pytest.raises(ExtensionError):
trigger_loader.load_trigger("ahriman.core.util.check_output")
def test_load_trigger_package_error_on_creation(trigger_loader: TriggerLoader, mocker: MockerFixture) -> None:
def test_load_trigger_package_error_on_creation(trigger_loader: TriggerLoader, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must raise InvalidException on trigger initialization if any exception is thrown
"""
mocker.patch("ahriman.core.triggers.trigger.Trigger.__init__", side_effect=Exception())
with pytest.raises(ExtensionError):
trigger_loader.load_trigger("ahriman.core.report.ReportTrigger")
trigger_loader.load_trigger("ahriman.core.report.ReportTrigger", "x86_64", configuration)
def test_load_trigger_package_is_not_trigger(trigger_loader: TriggerLoader) -> None:
def test_load_trigger_class_package(trigger_loader: TriggerLoader) -> None:
"""
must load trigger class from package
"""
assert trigger_loader.load_trigger_class("ahriman.core.report.ReportTrigger") == ReportTrigger
def test_load_trigger_class_package_invalid_import(trigger_loader: TriggerLoader, mocker: MockerFixture) -> None:
"""
must raise InvalidExtension on invalid import
"""
mocker.patch("ahriman.core.triggers.trigger_loader.importlib.import_module", side_effect=ModuleNotFoundError())
with pytest.raises(ExtensionError):
trigger_loader.load_trigger_class("random.module")
def test_load_trigger_class_package_not_trigger(trigger_loader: TriggerLoader) -> None:
"""
must raise InvalidExtension if imported module is not a type
"""
with pytest.raises(ExtensionError):
trigger_loader.load_trigger_class("ahriman.core.util.check_output")
def test_load_trigger_class_package_is_not_trigger(trigger_loader: TriggerLoader) -> None:
"""
must raise InvalidExtension if loaded class is not a trigger
"""
with pytest.raises(ExtensionError):
trigger_loader.load_trigger("ahriman.core.sign.gpg.GPG")
trigger_loader.load_trigger_class("ahriman.core.sign.gpg.GPG")
def test_load_trigger_path(trigger_loader: TriggerLoader, resource_path_root: Path) -> None:
def test_load_trigger_class_path(trigger_loader: TriggerLoader, resource_path_root: Path) -> None:
"""
must load trigger from path
must load trigger class from path
"""
path = resource_path_root.parent.parent / "src" / "ahriman" / "core" / "report" / "report_trigger.py"
assert trigger_loader.load_trigger(f"{path}.ReportTrigger")
path = resource_path_root.parent.parent / "src" / "ahriman" / "core" / "report" / "__init__.py"
assert trigger_loader.load_trigger_class(f"{path}.ReportTrigger") == ReportTrigger
def test_load_trigger_path_directory(trigger_loader: TriggerLoader, resource_path_root: Path) -> None:
def test_load_trigger_class_path_directory(trigger_loader: TriggerLoader, resource_path_root: Path) -> None:
"""
must raise InvalidExtension if provided import path is directory
"""
path = resource_path_root.parent.parent / "src" / "ahriman" / "core" / "report"
with pytest.raises(ExtensionError):
trigger_loader.load_trigger(f"{path}.ReportTrigger")
trigger_loader.load_trigger_class(f"{path}.ReportTrigger")
def test_load_trigger_path_not_found(trigger_loader: TriggerLoader) -> None:
def test_load_trigger_class_path_not_found(trigger_loader: TriggerLoader) -> None:
"""
must raise InvalidExtension if file cannot be found
"""
with pytest.raises(ExtensionError):
trigger_loader.load_trigger("/some/random/path.py.SomeRandomModule")
trigger_loader.load_trigger_class("/some/random/path.py.SomeRandomModule")
def test_on_result(trigger_loader: TriggerLoader, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -119,9 +141,11 @@ def test_on_stop_with_on_start(configuration: Configuration, mocker: MockerFixtu
"""
must call on_stop on exit if on_start was called
"""
mocker.patch("ahriman.core.upload.UploadTrigger.on_start")
mocker.patch("ahriman.core.report.ReportTrigger.on_start")
on_stop_mock = mocker.patch("ahriman.core.triggers.trigger_loader.TriggerLoader.on_stop")
trigger_loader = TriggerLoader("x86_64", configuration)
trigger_loader = TriggerLoader.load("x86_64", configuration)
trigger_loader.on_start()
del trigger_loader
on_stop_mock.assert_called_once_with()
@ -133,7 +157,7 @@ def test_on_stop_without_on_start(configuration: Configuration, mocker: MockerFi
"""
on_stop_mock = mocker.patch("ahriman.core.triggers.trigger_loader.TriggerLoader.on_stop")
trigger_loader = TriggerLoader("x86_64", configuration)
trigger_loader = TriggerLoader.load("x86_64", configuration)
del trigger_loader
on_stop_mock.assert_not_called()

View File

@ -5,6 +5,17 @@ from ahriman.core.upload import UploadTrigger
from ahriman.models.result import Result
def test_configuration_sections(configuration: Configuration) -> None:
"""
must correctly parse target list
"""
configuration.set_option("upload", "target", "a b c")
assert UploadTrigger.configuration_sections(configuration) == ["a", "b", "c"]
configuration.remove_option("upload", "target")
assert UploadTrigger.configuration_sections(configuration) == []
def test_on_result(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run report for specified targets

View File

@ -45,7 +45,7 @@ push_url = https://github.com/arcan1s/repository.git
pull_url = https://github.com/arcan1s/repository.git
[report]
target =
target = console
[email]
host = 127.0.0.1