diff --git a/setup.cfg b/setup.cfg index f3e013e1..36bcbfff 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,4 +2,4 @@ test = pytest [tool:pytest] -addopts = --cov=ahriman --pspec +addopts = --cov=ahriman --cov-report term-missing:skip-covered --pspec diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index 0ce4b26f..ceca9885 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -274,11 +274,18 @@ def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser: return parser -if __name__ == "__main__": - args_parser = _parser() - args = args_parser.parse_args() +def run(): + """ + run application instance + """ + if __name__ == "__main__": + args_parser = _parser() + args = args_parser.parse_args() - handler: handlers.Handler = args.handler - status = handler.execute(args) + handler: handlers.Handler = args.handler + status = handler.execute(args) - sys.exit(status) + sys.exit(status) + + +run() diff --git a/src/ahriman/application/application.py b/src/ahriman/application/application.py index d28edb9a..31293c72 100644 --- a/src/ahriman/application/application.py +++ b/src/ahriman/application/application.py @@ -51,6 +51,13 @@ class Application: self.architecture = architecture self.repository = Repository(architecture, configuration) + def _finalize(self) -> None: + """ + generate report and sync to remote server + """ + self.report([]) + self.sync([]) + def _known_packages(self) -> Set[str]: """ load packages from repository and pacman repositories @@ -63,13 +70,6 @@ class Application: known_packages.update(self.repository.pacman.all_packages()) return known_packages - def _finalize(self) -> None: - """ - generate report and sync to remote server - """ - self.report([]) - self.sync([]) - def get_updates(self, filter_packages: List[str], no_aur: bool, no_manual: bool, no_vcs: bool, log_fn: Callable[[str], None]) -> List[Package]: """ @@ -182,6 +182,7 @@ class Application: continue for archive in package.packages.values(): if archive.filepath is None: + self.logger.warning(f"filepath is empty for {package.base}") continue # avoid mypy warning src = self.repository.paths.repository / archive.filepath dst = self.repository.paths.packages / archive.filepath diff --git a/src/ahriman/application/handlers/update.py b/src/ahriman/application/handlers/update.py index a6c58f4f..08febe66 100644 --- a/src/ahriman/application/handlers/update.py +++ b/src/ahriman/application/handlers/update.py @@ -19,7 +19,7 @@ # import argparse -from typing import Type +from typing import Callable, Type from ahriman.application.application import Application from ahriman.application.handlers.handler import Handler @@ -39,13 +39,22 @@ class Update(Handler): :param architecture: repository architecture :param configuration: configuration instance """ - # typing workaround - def log_fn(line: str) -> None: - return print(line) if args.dry_run else application.logger.info(line) - application = Application(architecture, configuration) - packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn) + packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, + Update.log_fn(application, args.dry_run)) if args.dry_run: return application.update(packages) + + @staticmethod + def log_fn(application: Application, dry_run: bool) -> Callable[[str], None]: + """ + package updates log function + :param application: application instance + :param dry_run: do not perform update itself + :return: in case if dry_run is set it will return print, logger otherwise + """ + def inner(line: str) -> None: + return print(line) if dry_run else application.logger.info(line) + return inner diff --git a/src/ahriman/core/status/web_client.py b/src/ahriman/core/status/web_client.py index 2a3c2ea3..81413742 100644 --- a/src/ahriman/core/status/web_client.py +++ b/src/ahriman/core/status/web_client.py @@ -45,6 +45,15 @@ class WebClient(Client): self.host = host self.port = port + @staticmethod + def _exception_response_text(exception: requests.exceptions.HTTPError) -> str: + """ + safe response exception text generation + :param exception: exception raised + :return: text of the response if it is not None and empty string otherwise + """ + return exception.response.text if exception.response is not None else '' + def _ahriman_url(self) -> str: """ url generator @@ -75,7 +84,7 @@ class WebClient(Client): response = requests.post(self._package_url(package.base), json=payload) response.raise_for_status() except requests.exceptions.HTTPError as e: - self.logger.exception(f"could not add {package.base}: {e.response.text}") + self.logger.exception(f"could not add {package.base}: {WebClient._exception_response_text(e)}") except Exception: self.logger.exception(f"could not add {package.base}") @@ -95,7 +104,7 @@ class WebClient(Client): for package in status_json ] except requests.exceptions.HTTPError as e: - self.logger.exception(f"could not get {base}: {e.response.text}") + self.logger.exception(f"could not get {base}: {WebClient._exception_response_text(e)}") except Exception: self.logger.exception(f"could not get {base}") return [] @@ -112,7 +121,7 @@ class WebClient(Client): status_json = response.json() return BuildStatus.from_json(status_json) except requests.exceptions.HTTPError as e: - self.logger.exception(f"could not get service status: {e.response.text}") + self.logger.exception(f"could not get service status: {WebClient._exception_response_text(e)}") except Exception: self.logger.exception("could not get service status") return BuildStatus() @@ -126,7 +135,7 @@ class WebClient(Client): response = requests.delete(self._package_url(base)) response.raise_for_status() except requests.exceptions.HTTPError as e: - self.logger.exception(f"could not delete {base}: {e.response.text}") + self.logger.exception(f"could not delete {base}: {WebClient._exception_response_text(e)}") except Exception: self.logger.exception(f"could not delete {base}") @@ -142,7 +151,7 @@ class WebClient(Client): response = requests.post(self._package_url(base), json=payload) response.raise_for_status() except requests.exceptions.HTTPError as e: - self.logger.exception(f"could not update {base}: {e.response.text}") + self.logger.exception(f"could not update {base}: {WebClient._exception_response_text(e)}") except Exception: self.logger.exception(f"could not update {base}") @@ -157,6 +166,6 @@ class WebClient(Client): response = requests.post(self._ahriman_url(), json=payload) response.raise_for_status() except requests.exceptions.HTTPError as e: - self.logger.exception(f"could not update service status: {e.response.text}") + self.logger.exception(f"could not update service status: {WebClient._exception_response_text(e)}") except Exception: self.logger.exception("could not update service status") diff --git a/src/ahriman/core/util.py b/src/ahriman/core/util.py index 6045b280..93b19ed2 100644 --- a/src/ahriman/core/util.py +++ b/src/ahriman/core/util.py @@ -89,6 +89,6 @@ def pretty_size(size: Optional[float], level: int = 0) -> str: if size is None: return "" - if size < 1024 or level == 3: + if size < 1024 or level >= 3: return f"{size:.1f} {str_level()}" return pretty_size(size / 1024, level + 1) diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py index cce86329..f9ac734a 100644 --- a/src/ahriman/models/package.py +++ b/src/ahriman/models/package.py @@ -154,7 +154,7 @@ class Package: :return: package properties """ packages = { - key: PackageDescription(**value) + key: PackageDescription.from_json(value) for key, value in dump.get("packages", {}).items() } return Package( diff --git a/src/ahriman/models/package_description.py b/src/ahriman/models/package_description.py index 0944a112..91d9ebe9 100644 --- a/src/ahriman/models/package_description.py +++ b/src/ahriman/models/package_description.py @@ -19,10 +19,10 @@ # from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass, field, fields from pathlib import Path from pyalpm import Package # type: ignore -from typing import List, Optional, Type +from typing import Any, Dict, List, Optional, Type @dataclass @@ -59,6 +59,18 @@ class PackageDescription: """ return Path(self.filename) if self.filename is not None else None + @classmethod + def from_json(cls: Type[PackageDescription], dump: Dict[str, Any]) -> PackageDescription: + """ + construct package properties from json dump + :param dump: json dump body + :return: package properties + """ + # filter to only known fields + known_fields = [pair.name for pair in fields(cls)] + dump = {key: value for key, value in dump.items() if key in known_fields} + return cls(**dump) + @classmethod def from_package(cls: Type[PackageDescription], package: Package, path: Path) -> PackageDescription: """ diff --git a/src/ahriman/models/report_settings.py b/src/ahriman/models/report_settings.py index d850e268..cfa3e739 100644 --- a/src/ahriman/models/report_settings.py +++ b/src/ahriman/models/report_settings.py @@ -31,6 +31,7 @@ class ReportSettings(Enum): :cvar HTML: html report generation """ + Disabled = auto() # for testing purpose HTML = auto() @classmethod diff --git a/src/ahriman/models/upload_settings.py b/src/ahriman/models/upload_settings.py index 1fec402c..c1aeaf6f 100644 --- a/src/ahriman/models/upload_settings.py +++ b/src/ahriman/models/upload_settings.py @@ -32,6 +32,7 @@ class UploadSettings(Enum): :cvar S3: sync to Amazon S3 """ + Disabled = auto() # for testing purpose Rsync = auto() S3 = auto() diff --git a/tests/ahriman/application/handlers/test_handler.py b/tests/ahriman/application/handlers/test_handler.py index 0569490c..89d71a18 100644 --- a/tests/ahriman/application/handlers/test_handler.py +++ b/tests/ahriman/application/handlers/test_handler.py @@ -1,8 +1,10 @@ import argparse +import pytest from pytest_mock import MockerFixture from ahriman.application.handlers import Handler +from ahriman.core.configuration import Configuration def test_call(args: argparse.Namespace, mocker: MockerFixture) -> None: @@ -27,3 +29,22 @@ def test_call_exception(args: argparse.Namespace, mocker: MockerFixture) -> None """ mocker.patch("ahriman.application.lock.Lock.__enter__", side_effect=Exception()) assert not Handler._call(args, "x86_64") + + +def test_execute(args: argparse.Namespace, mocker: MockerFixture) -> None: + """ + must run execution in multiple processes + """ + args.architecture = ["i686", "x86_64"] + starmap_mock = mocker.patch("multiprocessing.pool.Pool.starmap") + + Handler.execute(args) + starmap_mock.assert_called_once() + + +def test_packages(args: argparse.Namespace, configuration: Configuration) -> None: + """ + must raise NotImplemented for missing method + """ + with pytest.raises(NotImplementedError): + Handler.run(args, "x86_64", configuration) diff --git a/tests/ahriman/application/handlers/test_handler_status.py b/tests/ahriman/application/handlers/test_handler_status.py index a1cac924..1fc32783 100644 --- a/tests/ahriman/application/handlers/test_handler_status.py +++ b/tests/ahriman/application/handlers/test_handler_status.py @@ -4,6 +4,8 @@ from pytest_mock import MockerFixture from ahriman.application.handlers import Status from ahriman.core.configuration import Configuration +from ahriman.models.build_status import BuildStatus +from ahriman.models.package import Package def _default_args(args: argparse.Namespace) -> argparse.Namespace: @@ -12,15 +14,33 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace: return args -def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: +def test_run(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package, + mocker: MockerFixture) -> None: """ must run command """ args = _default_args(args) mocker.patch("pathlib.Path.mkdir") application_mock = mocker.patch("ahriman.core.status.client.Client.get_self") - 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())]) Status.run(args, "x86_64", configuration) application_mock.assert_called_once() packages_mock.assert_called_once() + + +def test_run_with_package_filter(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package, + mocker: MockerFixture) -> None: + """ + must run command + """ + args = _default_args(args) + args.package = [package_ahriman.base] + mocker.patch("pathlib.Path.mkdir") + packages_mock = mocker.patch("ahriman.core.status.client.Client.get", + return_value=[(package_ahriman, BuildStatus())]) + + Status.run(args, "x86_64", configuration) + packages_mock.assert_called_once() + diff --git a/tests/ahriman/application/handlers/test_handler_update.py b/tests/ahriman/application/handlers/test_handler_update.py index a4bbafd2..fd3a9094 100644 --- a/tests/ahriman/application/handlers/test_handler_update.py +++ b/tests/ahriman/application/handlers/test_handler_update.py @@ -2,6 +2,7 @@ import argparse from pytest_mock import MockerFixture +from ahriman.application.application import Application from ahriman.application.handlers import Update from ahriman.core.configuration import Configuration @@ -40,3 +41,12 @@ def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, moc Update.run(args, "x86_64", configuration) updates_mock.assert_called_once() + + +def test_log_fn(application: Application, mocker: MockerFixture) -> None: + """ + must print package updates + """ + logger_mock = mocker.patch("logging.Logger.info") + Update.log_fn(application, False)("hello") + logger_mock.assert_called_once() diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py index 9475c289..435a7ff9 100644 --- a/tests/ahriman/application/test_ahriman.py +++ b/tests/ahriman/application/test_ahriman.py @@ -1,5 +1,9 @@ import argparse +from pytest_mock import MockerFixture + +from ahriman.application.handlers import Handler + def test_parser(parser: argparse.ArgumentParser) -> None: """ @@ -81,3 +85,19 @@ def test_subparsers_web(parser: argparse.ArgumentParser) -> None: args = parser.parse_args(["-a", "x86_64", "web"]) assert args.lock is None assert args.no_report + + +def test_run(args: argparse.Namespace, mocker: MockerFixture) -> None: + """ + application must be run + """ + args.architecture = "x86_64" + args.handler = Handler + + from ahriman.application import ahriman + mocker.patch.object(ahriman, "__name__", "__main__") + mocker.patch("argparse.ArgumentParser.parse_args", return_value=args) + exit_mock = mocker.patch("sys.exit") + + ahriman.run() + exit_mock.assert_called_once() diff --git a/tests/ahriman/application/test_application.py b/tests/ahriman/application/test_application.py index 69e3772b..ecd5728d 100644 --- a/tests/ahriman/application/test_application.py +++ b/tests/ahriman/application/test_application.py @@ -20,11 +20,22 @@ def test_finalize(application: Application, mocker: MockerFixture) -> None: sync_mock.assert_called_once() -def test_get_updates_all(application: Application, mocker: MockerFixture) -> None: +def test_known_packages(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must return not empty list of known packages + """ + mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman]) + packages = application._known_packages() + assert len(packages) > 1 + assert package_ahriman.base in packages + + +def test_get_updates_all(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: """ must get updates for all """ - updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur") + updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur", + return_value=[package_ahriman]) updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual") application.get_updates([], no_aur=False, no_manual=False, no_vcs=False, log_fn=print) @@ -233,6 +244,17 @@ def test_sign(application: Application, package_ahriman: Package, package_python finalize_mock.assert_called_once() +def test_sign_skip(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must skip sign packages with empty filename + """ + package_ahriman.packages[package_ahriman.base].filename = None + mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman]) + mocker.patch("ahriman.application.application.Application.update") + + application.sign([]) + + def test_sign_specific(application: Application, package_ahriman: Package, package_python_schedule: Package, mocker: MockerFixture) -> None: """ diff --git a/tests/ahriman/core/build_tools/test_task.py b/tests/ahriman/core/build_tools/test_task.py index f21bf970..8dffd758 100644 --- a/tests/ahriman/core/build_tools/test_task.py +++ b/tests/ahriman/core/build_tools/test_task.py @@ -51,6 +51,15 @@ def test_fetch_new(mocker: MockerFixture) -> None: ]) +def test_build(task_ahriman: Task, mocker: MockerFixture) -> None: + """ + must build package + """ + check_output_mock = mocker.patch("ahriman.core.build_tools.task.Task._check_output") + task_ahriman.build() + check_output_mock.assert_called() + + def test_init_with_cache(task_ahriman: Task, mocker: MockerFixture) -> None: """ must copy tree instead of fetch diff --git a/tests/ahriman/core/report/test_report.py b/tests/ahriman/core/report/test_report.py index 2d1acbb8..8971d504 100644 --- a/tests/ahriman/core/report/test_report.py +++ b/tests/ahriman/core/report/test_report.py @@ -18,6 +18,16 @@ def test_report_failure(configuration: Configuration, mocker: MockerFixture) -> Report.load("x86_64", configuration, ReportSettings.HTML.name).run(Path("path")) +def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> None: + """ + must construct dummy report class + """ + mocker.patch("ahriman.models.report_settings.ReportSettings.from_option", return_value=ReportSettings.Disabled) + report_mock = mocker.patch("ahriman.core.report.report.Report.generate") + Report.load("x86_64", configuration, ReportSettings.Disabled.name).run(Path("path")) + report_mock.assert_called_once() + + def test_report_html(configuration: Configuration, mocker: MockerFixture) -> None: """ must generate html report diff --git a/tests/ahriman/core/repository/test_cleaner.py b/tests/ahriman/core/repository/test_cleaner.py index 052644b7..f848fb8b 100644 --- a/tests/ahriman/core/repository/test_cleaner.py +++ b/tests/ahriman/core/repository/test_cleaner.py @@ -1,3 +1,4 @@ +import pytest import shutil from pathlib import Path @@ -20,6 +21,14 @@ def _mock_clear_check() -> None: ]) +def test_packages_built(cleaner: Cleaner) -> None: + """ + must raise NotImplemented for missing method + """ + with pytest.raises(NotImplementedError): + cleaner.packages_built() + + def test_clear_build(cleaner: Cleaner, mocker: MockerFixture) -> None: """ must remove directories with sources diff --git a/tests/ahriman/core/repository/test_executor.py b/tests/ahriman/core/repository/test_executor.py index 4a853fc1..7a7ddef8 100644 --- a/tests/ahriman/core/repository/test_executor.py +++ b/tests/ahriman/core/repository/test_executor.py @@ -1,23 +1,34 @@ +import pytest + from pathlib import Path from pytest_mock import MockerFixture from unittest import mock +from ahriman.core.report.report import Report from ahriman.core.repository.executor import Executor +from ahriman.core.upload.upload import Upload from ahriman.models.package import Package +def test_packages(executor: Executor) -> None: + """ + must raise NotImplemented for missing method + """ + with pytest.raises(NotImplementedError): + executor.packages() + + def test_process_build(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None: """ must run build process """ - mocker.patch("ahriman.core.repository.executor.Executor.packages_built", return_value=[package_ahriman]) mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)]) mocker.patch("ahriman.core.build_tools.task.Task.init") move_mock = mocker.patch("shutil.move") status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building") + built_packages_mock = mocker.patch("ahriman.core.repository.executor.Executor.packages_built") - # must return list of built packages - assert executor.process_build([package_ahriman]) == [package_ahriman] + executor.process_build([package_ahriman]) # must move files (once) move_mock.assert_called_once() # must update status @@ -25,6 +36,8 @@ def test_process_build(executor: Executor, package_ahriman: Package, mocker: Moc # must clear directory from ahriman.core.repository.cleaner import Cleaner Cleaner.clear_build.assert_called_once() + # must return build packages after all + built_packages_mock.assert_called_once() def test_process_build_failure(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None: @@ -68,7 +81,7 @@ def test_process_remove_base_multiple(executor: Executor, package_python_schedul executor.process_remove([package_python_schedule.base]) # must remove via alpm wrapper repo_remove_mock.assert_has_calls([ - mock.call(package, Path(props.filename)) + mock.call(package, props.filepath) for package, props in package_python_schedule.packages.items() ], any_order=True) # must update status @@ -91,6 +104,15 @@ def test_process_remove_base_single(executor: Executor, package_python_schedule: status_client_mock.assert_not_called() +def test_process_remove_failed(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must suppress remove errors + """ + mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman]) + mocker.patch("ahriman.core.alpm.repo.Repo.remove", side_effect=Exception()) + executor.process_remove([package_ahriman.base]) + + def test_process_remove_nothing(executor: Executor, package_ahriman: Package, package_python_schedule: Package, mocker: MockerFixture) -> None: """ @@ -103,6 +125,18 @@ def test_process_remove_nothing(executor: Executor, package_ahriman: Package, pa repo_remove_mock.assert_not_called() +def test_process_report(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must process report + """ + mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman]) + mocker.patch("ahriman.core.report.report.Report.load", return_value=Report("x86_64", executor.configuration)) + report_mock = mocker.patch("ahriman.core.report.report.Report.run") + + executor.process_report(["dummy"]) + report_mock.assert_called_once() + + def test_process_report_auto(executor: Executor, mocker: MockerFixture) -> None: """ must process report in auto mode if no targets supplied @@ -113,7 +147,18 @@ def test_process_report_auto(executor: Executor, mocker: MockerFixture) -> None: configuration_getlist_mock.assert_called_once() -def test_process_sync_auto(executor: Executor, mocker: MockerFixture) -> None: +def test_process_upload(executor: Executor, mocker: MockerFixture) -> None: + """ + must process sync in auto mode if no targets supplied + """ + mocker.patch("ahriman.core.upload.upload.Upload.load", return_value=Upload("x86_64", executor.configuration)) + upload_mock = mocker.patch("ahriman.core.upload.upload.Upload.run") + + executor.process_sync(["dummy"]) + upload_mock.assert_called_once() + + +def test_process_upload_auto(executor: Executor, mocker: MockerFixture) -> None: """ must process sync in auto mode if no targets supplied """ @@ -134,7 +179,7 @@ def test_process_update(executor: Executor, package_ahriman: Package, mocker: Mo status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success") # must return complete - assert executor.process_update([Path(package.filename) for package in package_ahriman.packages.values()]) + assert executor.process_update([package.filepath for package in package_ahriman.packages.values()]) # must move files (once) move_mock.assert_called_once() # must sign package @@ -158,14 +203,23 @@ def test_process_update_group(executor: Executor, package_python_schedule: Packa repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add") status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success") - executor.process_update([Path(package.filename) for package in package_python_schedule.packages.values()]) + executor.process_update([package.filepath for package in package_python_schedule.packages.values()]) repo_add_mock.assert_has_calls([ - mock.call(executor.paths.repository / package.filename) + mock.call(executor.paths.repository / package.filepath) for package in package_python_schedule.packages.values() ], any_order=True) status_client_mock.assert_called_with(package_python_schedule) +def test_process_empty_filename(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must skip update for package which does not have path + """ + package_ahriman.packages[package_ahriman.base].filename = None + mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) + executor.process_update([package.filepath for package in package_ahriman.packages.values()]) + + def test_process_update_failed(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None: """ must process update for failed package @@ -174,7 +228,7 @@ def test_process_update_failed(executor: Executor, package_ahriman: Package, moc mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_failed") - executor.process_update([Path(package.filename) for package in package_ahriman.packages.values()]) + executor.process_update([package.filepath for package in package_ahriman.packages.values()]) status_client_mock.assert_called_once() @@ -185,4 +239,4 @@ def test_process_update_failed_on_load(executor: Executor, package_ahriman: Pack mocker.patch("shutil.move") mocker.patch("ahriman.models.package.Package.load", side_effect=Exception()) - assert executor.process_update([Path(package.filename) for package in package_ahriman.packages.values()]) + assert executor.process_update([package.filepath for package in package_ahriman.packages.values()]) diff --git a/tests/ahriman/core/repository/test_repository.py b/tests/ahriman/core/repository/test_repository.py index e2d56b2f..5995b608 100644 --- a/tests/ahriman/core/repository/test_repository.py +++ b/tests/ahriman/core/repository/test_repository.py @@ -31,3 +31,28 @@ def test_packages(package_ahriman: Package, package_python_schedule: Package, expected = set(package_ahriman.packages.keys()) expected.update(package_python_schedule.packages.keys()) assert set(archives) == expected + + +def test_packages_failed(repository: Repository, mocker: MockerFixture) -> None: + """ + must skip packages which cannot be loaded + """ + mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.pkg.tar.xz")]) + mocker.patch("ahriman.models.package.Package.load", side_effect=Exception()) + assert not repository.packages() + + +def test_packages_not_package(repository: Repository, mocker: MockerFixture) -> None: + """ + must skip not packages from iteration + """ + mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.tar.xz")]) + assert not repository.packages() + + +def test_packages_built(repository: Repository, mocker: MockerFixture) -> None: + """ + must return build packages + """ + mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.tar.xz"), Path("b.pkg.tar.xz")]) + assert repository.packages_built() == [Path("b.pkg.tar.xz")] diff --git a/tests/ahriman/core/repository/test_update_handler.py b/tests/ahriman/core/repository/test_update_handler.py index 125abfa1..07d28d53 100644 --- a/tests/ahriman/core/repository/test_update_handler.py +++ b/tests/ahriman/core/repository/test_update_handler.py @@ -1,9 +1,19 @@ +import pytest + from pytest_mock import MockerFixture from ahriman.core.repository.update_handler import UpdateHandler from ahriman.models.package import Package +def test_packages(update_handler: UpdateHandler) -> None: + """ + must raise NotImplemented for missing method + """ + with pytest.raises(NotImplementedError): + update_handler.packages() + + def test_updates_aur(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None: """ diff --git a/tests/ahriman/core/sign/test_gpg.py b/tests/ahriman/core/sign/test_gpg.py index f545671f..b5659747 100644 --- a/tests/ahriman/core/sign/test_gpg.py +++ b/tests/ahriman/core/sign/test_gpg.py @@ -53,6 +53,24 @@ def test_repository_sign_args_skip_4(gpg: GPG) -> None: assert not gpg.repository_sign_args +def test_sign_command(gpg_with_key: GPG) -> None: + """ + must generate sign command + """ + assert gpg_with_key.sign_command(Path("a"), gpg_with_key.default_key) + + +def test_process(gpg_with_key: GPG, mocker: MockerFixture) -> None: + """ + must call process method correctly + """ + result = [Path("a"), Path("a.sig")] + check_output_mock = mocker.patch("ahriman.core.sign.gpg.GPG._check_output") + + assert gpg_with_key.process(Path("a"), gpg_with_key.default_key) == result + check_output_mock.assert_called() + + def test_sign_package_1(gpg_with_key: GPG, mocker: MockerFixture) -> None: """ must sign package diff --git a/tests/ahriman/core/status/test_watcher.py b/tests/ahriman/core/status/test_watcher.py index 114c2489..5baadb1f 100644 --- a/tests/ahriman/core/status/test_watcher.py +++ b/tests/ahriman/core/status/test_watcher.py @@ -51,6 +51,21 @@ def test_cache_load_no_file(watcher: Watcher, mocker: MockerFixture) -> None: assert not watcher.known +def test_cache_load_package_load_error(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must not fail on json errors + """ + response = {"packages": [pytest.helpers.get_package_status_extended(package_ahriman)]} + + mocker.patch("pathlib.Path.is_file", return_value=True) + mocker.patch("pathlib.Path.open") + mocker.patch("ahriman.models.package.Package.from_json", side_effect=Exception()) + mocker.patch("json.load", return_value=response) + + watcher._cache_load() + assert not watcher.known + + def test_cache_load_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: """ must not load unknown package diff --git a/tests/ahriman/core/status/test_web_client.py b/tests/ahriman/core/status/test_web_client.py index 8d5cc2e7..c42f410a 100644 --- a/tests/ahriman/core/status/test_web_client.py +++ b/tests/ahriman/core/status/test_web_client.py @@ -1,5 +1,6 @@ import json import pytest +import requests from pytest_mock import MockerFixture from requests import Response @@ -44,6 +45,14 @@ def test_add_failed(web_client: WebClient, package_ahriman: Package, mocker: Moc web_client.add(package_ahriman, BuildStatusEnum.Unknown) +def test_add_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must suppress any exception happened during addition + """ + mocker.patch("requests.post", side_effect=requests.exceptions.HTTPError()) + web_client.add(package_ahriman, BuildStatusEnum.Unknown) + + def test_get_all(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: """ must return all packages status @@ -69,6 +78,14 @@ def test_get_failed(web_client: WebClient, mocker: MockerFixture) -> None: assert web_client.get(None) == [] +def test_get_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None: + """ + must suppress any exception happened during status getting + """ + mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError()) + assert web_client.get(None) == [] + + def test_get_single(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: """ must return single package status @@ -109,6 +126,14 @@ def test_get_self_failed(web_client: WebClient, mocker: MockerFixture) -> None: assert web_client.get_self().status == BuildStatusEnum.Unknown +def test_get_self_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None: + """ + must suppress any exception happened during service status getting + """ + mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError()) + assert web_client.get_self().status == BuildStatusEnum.Unknown + + def test_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: """ must process package removal @@ -127,6 +152,14 @@ def test_remove_failed(web_client: WebClient, package_ahriman: Package, mocker: web_client.remove(package_ahriman.base) +def test_remove_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must suppress any exception happened during removal + """ + mocker.patch("requests.delete", side_effect=requests.exceptions.HTTPError()) + web_client.remove(package_ahriman.base) + + def test_update(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: """ must process package update @@ -145,6 +178,14 @@ def test_update_failed(web_client: WebClient, package_ahriman: Package, mocker: web_client.update(package_ahriman.base, BuildStatusEnum.Unknown) +def test_update_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must suppress any exception happened during update + """ + mocker.patch("requests.post", side_effect=requests.exceptions.HTTPError()) + web_client.update(package_ahriman.base, BuildStatusEnum.Unknown) + + def test_update_self(web_client: WebClient, mocker: MockerFixture) -> None: """ must process service update @@ -161,3 +202,11 @@ def test_update_self_failed(web_client: WebClient, mocker: MockerFixture) -> Non """ mocker.patch("requests.post", side_effect=Exception()) web_client.update_self(BuildStatusEnum.Unknown) + + +def test_update_self_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None: + """ + must suppress any exception happened during service update + """ + mocker.patch("requests.post", side_effect=requests.exceptions.HTTPError()) + web_client.update_self(BuildStatusEnum.Unknown) diff --git a/tests/ahriman/core/test_configuration.py b/tests/ahriman/core/test_configuration.py index 198dae0d..ebb7eda2 100644 --- a/tests/ahriman/core/test_configuration.py +++ b/tests/ahriman/core/test_configuration.py @@ -105,6 +105,14 @@ def test_load_includes_missing(configuration: Configuration) -> None: configuration.load_includes() +def test_load_includes_no_option(configuration: Configuration) -> None: + """ + must not fail if no option set + """ + configuration.remove_option("settings", "include") + configuration.load_includes() + + def test_load_logging_fallback(configuration: Configuration, mocker: MockerFixture) -> None: """ must fallback to stderr without errors diff --git a/tests/ahriman/core/test_util.py b/tests/ahriman/core/test_util.py index e79ce29e..7f82f3b7 100644 --- a/tests/ahriman/core/test_util.py +++ b/tests/ahriman/core/test_util.py @@ -4,6 +4,7 @@ import subprocess from pytest_mock import MockerFixture +from ahriman.core.exceptions import InvalidOption from ahriman.core.util import check_output, package_like, pretty_datetime, pretty_size from ahriman.models.package import Package @@ -124,6 +125,14 @@ def test_pretty_size_pbytes() -> None: assert abbrev == "GiB" +def test_pretty_size_pbytes_failure() -> None: + """ + must raise exception if level >= 4 supplied + """ + with pytest.raises(InvalidOption): + pretty_size(42 * 1024 * 1024 * 1024 * 1024, 4).split() + + def test_pretty_size_empty() -> None: """ must generate empty string for None value diff --git a/tests/ahriman/core/upload/test_upload.py b/tests/ahriman/core/upload/test_upload.py index 99815c57..37664fc1 100644 --- a/tests/ahriman/core/upload/test_upload.py +++ b/tests/ahriman/core/upload/test_upload.py @@ -18,6 +18,16 @@ def test_upload_failure(configuration: Configuration, mocker: MockerFixture) -> Upload.load("x86_64", configuration, UploadSettings.Rsync.name).run(Path("path")) +def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> None: + """ + must construct dummy upload class + """ + mocker.patch("ahriman.models.upload_settings.UploadSettings.from_option", return_value=UploadSettings.Disabled) + upload_mock = mocker.patch("ahriman.core.upload.upload.Upload.sync") + Upload.load("x86_64", configuration, UploadSettings.Disabled.name).run(Path("path")) + upload_mock.assert_called_once() + + def test_upload_rsync(configuration: Configuration, mocker: MockerFixture) -> None: """ must upload via rsync diff --git a/tests/ahriman/models/test_build_status.py b/tests/ahriman/models/test_build_status.py index f7c78cf2..fe08a94a 100644 --- a/tests/ahriman/models/test_build_status.py +++ b/tests/ahriman/models/test_build_status.py @@ -1,3 +1,5 @@ +import datetime + from ahriman.models.build_status import BuildStatus, BuildStatusEnum @@ -36,3 +38,59 @@ def test_build_status_from_json_view(build_status_failed: BuildStatus) -> None: must construct same object from json """ assert BuildStatus.from_json(build_status_failed.view()) == build_status_failed + + +def test_build_status_pretty_print(build_status_failed: BuildStatus) -> None: + """ + must return string in pretty print function + """ + assert build_status_failed.pretty_print() + assert isinstance(build_status_failed.pretty_print(), str) + + +def test_build_status_eq(build_status_failed: BuildStatus) -> None: + """ + must be equal + """ + other = BuildStatus.from_json(build_status_failed.view()) + assert other == build_status_failed + + +def test_build_status_eq_self(build_status_failed: BuildStatus) -> None: + """ + must be equal itself + """ + assert build_status_failed == build_status_failed + + +def test_build_status_ne_by_status(build_status_failed: BuildStatus) -> None: + """ + must be not equal by status + """ + other = BuildStatus.from_json(build_status_failed.view()) + other.status = BuildStatusEnum.Success + assert build_status_failed != other + + +def test_build_status_ne_by_timestamp(build_status_failed: BuildStatus) -> None: + """ + must be not equal by timestamp + """ + other = BuildStatus.from_json(build_status_failed.view()) + other.timestamp = datetime.datetime.utcnow().timestamp() + assert build_status_failed != other + + +def test_build_status_ne_other(build_status_failed: BuildStatus) -> None: + """ + must be not equal to random object + """ + assert build_status_failed != object() + + +def test_build_status_repr(build_status_failed: BuildStatus) -> None: + """ + must return string in __repr__ function + """ + assert build_status_failed.__repr__() + assert isinstance(build_status_failed.__repr__(), str) diff --git a/tests/ahriman/models/test_package.py b/tests/ahriman/models/test_package.py index bb5276c0..44ef7006 100644 --- a/tests/ahriman/models/test_package.py +++ b/tests/ahriman/models/test_package.py @@ -124,6 +124,17 @@ def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_pa assert package_ahriman == package +def test_from_build_failed(package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must raise exception if there are errors during srcinfo load + """ + mocker.patch("pathlib.Path.read_text", return_value="") + mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"])) + + with pytest.raises(InvalidPackageInfo): + Package.from_build(Path("path"), package_ahriman.aur_url) + + def test_from_json_view_1(package_ahriman: Package) -> None: """ must construct same object from json @@ -190,6 +201,17 @@ def test_load_failure(package_ahriman: Package, pyalpm_handle: MagicMock, mocker Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url) +def test_dependencies_failed(mocker: MockerFixture) -> None: + """ + must raise exception if there are errors during srcinfo load + """ + mocker.patch("pathlib.Path.read_text", return_value="") + mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"])) + + with pytest.raises(InvalidPackageInfo): + Package.dependencies(Path("path")) + + def test_dependencies_with_version(mocker: MockerFixture, resource_path_root: Path) -> None: """ must load correct list of dependencies with version @@ -227,12 +249,25 @@ def test_actual_version_vcs(package_tpacpi_bat_git: Package, repository_paths: R assert package_tpacpi_bat_git.actual_version(repository_paths) == "3.1.r13.g4959b52-1" +def test_actual_version_srcinfo_failed(package_tpacpi_bat_git: Package, repository_paths: RepositoryPaths, + mocker: MockerFixture) -> None: + """ + must return same version in case if exception occurred + """ + mocker.patch("ahriman.models.package.Package._check_output", side_effect=Exception()) + mocker.patch("ahriman.core.build_tools.task.Task.fetch") + + assert package_tpacpi_bat_git.actual_version(repository_paths) == package_tpacpi_bat_git.version + + def test_actual_version_vcs_failed(package_tpacpi_bat_git: Package, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: """ must return same version in case if exception occurred """ - mocker.patch("ahriman.models.package.Package._check_output", side_effect=Exception()) + mocker.patch("pathlib.Path.read_text", return_value="") + mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"])) + mocker.patch("ahriman.models.package.Package._check_output") mocker.patch("ahriman.core.build_tools.task.Task.fetch") assert package_tpacpi_bat_git.actual_version(repository_paths) == package_tpacpi_bat_git.version @@ -253,3 +288,11 @@ def test_is_outdated_true(package_ahriman: Package, repository_paths: Repository other.version = other.version.replace("-1", "-2") assert package_ahriman.is_outdated(other, repository_paths) + + +def test_build_status_pretty_print(package_ahriman: Package) -> None: + """ + must return string in pretty print function + """ + assert package_ahriman.pretty_print() + assert isinstance(package_ahriman.pretty_print(), str) diff --git a/tests/ahriman/models/test_package_desciption.py b/tests/ahriman/models/test_package_desciption.py index 4fb020e5..3229704a 100644 --- a/tests/ahriman/models/test_package_desciption.py +++ b/tests/ahriman/models/test_package_desciption.py @@ -1,3 +1,4 @@ +from dataclasses import asdict from unittest.mock import MagicMock from ahriman.models.package_description import PackageDescription @@ -19,6 +20,22 @@ def test_filepath_empty(package_description_ahriman: PackageDescription) -> None assert package_description_ahriman.filepath is None +def test_from_json(package_description_ahriman: PackageDescription) -> None: + """ + must construct description from json object + """ + assert PackageDescription.from_json(asdict(package_description_ahriman)) == package_description_ahriman + + +def test_from_json_with_unknown_fields(package_description_ahriman: PackageDescription) -> None: + """ + must construct description from json object containing unknown fields + """ + dump = asdict(package_description_ahriman) + dump.update(unknown_field="value") + assert PackageDescription.from_json(dump) == package_description_ahriman + + def test_from_package(package_description_ahriman: PackageDescription, pyalpm_package_description_ahriman: MagicMock) -> None: """ diff --git a/tests/ahriman/web/views/test_view_ahriman.py b/tests/ahriman/web/views/test_view_ahriman.py index 250eb731..258dc635 100644 --- a/tests/ahriman/web/views/test_view_ahriman.py +++ b/tests/ahriman/web/views/test_view_ahriman.py @@ -1,4 +1,5 @@ from aiohttp.test_utils import TestClient +from pytest_mock import MockerFixture from ahriman.models.build_status import BuildStatus, BuildStatusEnum @@ -35,3 +36,14 @@ async def test_post_exception(client: TestClient) -> None: """ post_response = await client.post("/api/v1/ahriman", json={}) assert post_response.status == 400 + + +async def test_post_exception_inside(client: TestClient, mocker: MockerFixture) -> None: + """ + exception handler must handle 500 errors + """ + payload = {"status": BuildStatusEnum.Success.value} + mocker.patch("ahriman.core.status.watcher.Watcher.update_self", side_effect=Exception()) + + post_response = await client.post("/api/v1/ahriman", json=payload) + assert post_response.status == 500