100% coverage

This commit is contained in:
Evgenii Alekseev 2021-04-03 21:30:57 +03:00
parent 62d55eff19
commit 461883217d
31 changed files with 534 additions and 45 deletions

View File

@ -2,4 +2,4 @@
test = pytest test = pytest
[tool:pytest] [tool:pytest]
addopts = --cov=ahriman --pspec addopts = --cov=ahriman --cov-report term-missing:skip-covered --pspec

View File

@ -274,11 +274,18 @@ def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser return parser
if __name__ == "__main__": def run():
args_parser = _parser() """
args = args_parser.parse_args() run application instance
"""
if __name__ == "__main__":
args_parser = _parser()
args = args_parser.parse_args()
handler: handlers.Handler = args.handler handler: handlers.Handler = args.handler
status = handler.execute(args) status = handler.execute(args)
sys.exit(status) sys.exit(status)
run()

View File

@ -51,6 +51,13 @@ class Application:
self.architecture = architecture self.architecture = architecture
self.repository = Repository(architecture, configuration) 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]: def _known_packages(self) -> Set[str]:
""" """
load packages from repository and pacman repositories load packages from repository and pacman repositories
@ -63,13 +70,6 @@ class Application:
known_packages.update(self.repository.pacman.all_packages()) known_packages.update(self.repository.pacman.all_packages())
return known_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, def get_updates(self, filter_packages: List[str], no_aur: bool, no_manual: bool, no_vcs: bool,
log_fn: Callable[[str], None]) -> List[Package]: log_fn: Callable[[str], None]) -> List[Package]:
""" """
@ -182,6 +182,7 @@ class Application:
continue continue
for archive in package.packages.values(): for archive in package.packages.values():
if archive.filepath is None: if archive.filepath is None:
self.logger.warning(f"filepath is empty for {package.base}")
continue # avoid mypy warning continue # avoid mypy warning
src = self.repository.paths.repository / archive.filepath src = self.repository.paths.repository / archive.filepath
dst = self.repository.paths.packages / archive.filepath dst = self.repository.paths.packages / archive.filepath

View File

@ -19,7 +19,7 @@
# #
import argparse import argparse
from typing import Type from typing import Callable, Type
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
@ -39,13 +39,22 @@ class Update(Handler):
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :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) 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: if args.dry_run:
return return
application.update(packages) 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

View File

@ -45,6 +45,15 @@ class WebClient(Client):
self.host = host self.host = host
self.port = port 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: def _ahriman_url(self) -> str:
""" """
url generator url generator
@ -75,7 +84,7 @@ class WebClient(Client):
response = requests.post(self._package_url(package.base), json=payload) response = requests.post(self._package_url(package.base), json=payload)
response.raise_for_status() response.raise_for_status()
except requests.exceptions.HTTPError as e: 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: except Exception:
self.logger.exception(f"could not add {package.base}") self.logger.exception(f"could not add {package.base}")
@ -95,7 +104,7 @@ class WebClient(Client):
for package in status_json for package in status_json
] ]
except requests.exceptions.HTTPError as e: 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: except Exception:
self.logger.exception(f"could not get {base}") self.logger.exception(f"could not get {base}")
return [] return []
@ -112,7 +121,7 @@ class WebClient(Client):
status_json = response.json() status_json = response.json()
return BuildStatus.from_json(status_json) return BuildStatus.from_json(status_json)
except requests.exceptions.HTTPError as e: 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: except Exception:
self.logger.exception("could not get service status") self.logger.exception("could not get service status")
return BuildStatus() return BuildStatus()
@ -126,7 +135,7 @@ class WebClient(Client):
response = requests.delete(self._package_url(base)) response = requests.delete(self._package_url(base))
response.raise_for_status() response.raise_for_status()
except requests.exceptions.HTTPError as e: 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: except Exception:
self.logger.exception(f"could not delete {base}") 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 = requests.post(self._package_url(base), json=payload)
response.raise_for_status() response.raise_for_status()
except requests.exceptions.HTTPError as e: 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: except Exception:
self.logger.exception(f"could not update {base}") self.logger.exception(f"could not update {base}")
@ -157,6 +166,6 @@ class WebClient(Client):
response = requests.post(self._ahriman_url(), json=payload) response = requests.post(self._ahriman_url(), json=payload)
response.raise_for_status() response.raise_for_status()
except requests.exceptions.HTTPError as e: 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: except Exception:
self.logger.exception("could not update service status") self.logger.exception("could not update service status")

View File

@ -89,6 +89,6 @@ def pretty_size(size: Optional[float], level: int = 0) -> str:
if size is None: if size is None:
return "" return ""
if size < 1024 or level == 3: if size < 1024 or level >= 3:
return f"{size:.1f} {str_level()}" return f"{size:.1f} {str_level()}"
return pretty_size(size / 1024, level + 1) return pretty_size(size / 1024, level + 1)

View File

@ -154,7 +154,7 @@ class Package:
:return: package properties :return: package properties
""" """
packages = { packages = {
key: PackageDescription(**value) key: PackageDescription.from_json(value)
for key, value in dump.get("packages", {}).items() for key, value in dump.get("packages", {}).items()
} }
return Package( return Package(

View File

@ -19,10 +19,10 @@
# #
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field, fields
from pathlib import Path from pathlib import Path
from pyalpm import Package # type: ignore from pyalpm import Package # type: ignore
from typing import List, Optional, Type from typing import Any, Dict, List, Optional, Type
@dataclass @dataclass
@ -59,6 +59,18 @@ class PackageDescription:
""" """
return Path(self.filename) if self.filename is not None else None 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 @classmethod
def from_package(cls: Type[PackageDescription], package: Package, path: Path) -> PackageDescription: def from_package(cls: Type[PackageDescription], package: Package, path: Path) -> PackageDescription:
""" """

View File

@ -31,6 +31,7 @@ class ReportSettings(Enum):
:cvar HTML: html report generation :cvar HTML: html report generation
""" """
Disabled = auto() # for testing purpose
HTML = auto() HTML = auto()
@classmethod @classmethod

View File

@ -32,6 +32,7 @@ class UploadSettings(Enum):
:cvar S3: sync to Amazon S3 :cvar S3: sync to Amazon S3
""" """
Disabled = auto() # for testing purpose
Rsync = auto() Rsync = auto()
S3 = auto() S3 = auto()

View File

@ -1,8 +1,10 @@
import argparse import argparse
import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.application.handlers import Handler from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
def test_call(args: argparse.Namespace, mocker: MockerFixture) -> None: 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()) mocker.patch("ahriman.application.lock.Lock.__enter__", side_effect=Exception())
assert not Handler._call(args, "x86_64") 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)

View File

@ -4,6 +4,8 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Status from ahriman.application.handlers import Status
from ahriman.core.configuration import Configuration 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: def _default_args(args: argparse.Namespace) -> argparse.Namespace:
@ -12,15 +14,33 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
return args 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 must run command
""" """
args = _default_args(args) args = _default_args(args)
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.core.status.client.Client.get_self") 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) Status.run(args, "x86_64", configuration)
application_mock.assert_called_once() application_mock.assert_called_once()
packages_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()

View File

@ -2,6 +2,7 @@ import argparse
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.application.application import Application
from ahriman.application.handlers import Update from ahriman.application.handlers import Update
from ahriman.core.configuration import Configuration 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) Update.run(args, "x86_64", configuration)
updates_mock.assert_called_once() 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()

View File

@ -1,5 +1,9 @@
import argparse import argparse
from pytest_mock import MockerFixture
from ahriman.application.handlers import Handler
def test_parser(parser: argparse.ArgumentParser) -> None: 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"]) args = parser.parse_args(["-a", "x86_64", "web"])
assert args.lock is None assert args.lock is None
assert args.no_report 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()

View File

@ -20,11 +20,22 @@ def test_finalize(application: Application, mocker: MockerFixture) -> None:
sync_mock.assert_called_once() 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 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") 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) 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() 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, def test_sign_specific(application: Application, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """

View File

@ -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: def test_init_with_cache(task_ahriman: Task, mocker: MockerFixture) -> None:
""" """
must copy tree instead of fetch must copy tree instead of fetch

View File

@ -18,6 +18,16 @@ def test_report_failure(configuration: Configuration, mocker: MockerFixture) ->
Report.load("x86_64", configuration, ReportSettings.HTML.name).run(Path("path")) 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: def test_report_html(configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must generate html report must generate html report

View File

@ -1,3 +1,4 @@
import pytest
import shutil import shutil
from pathlib import Path 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: def test_clear_build(cleaner: Cleaner, mocker: MockerFixture) -> None:
""" """
must remove directories with sources must remove directories with sources

View File

@ -1,23 +1,34 @@
import pytest
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest import mock from unittest import mock
from ahriman.core.report.report import Report
from ahriman.core.repository.executor import Executor from ahriman.core.repository.executor import Executor
from ahriman.core.upload.upload import Upload
from ahriman.models.package import Package 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: def test_process_build(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must run build process 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.build", return_value=[Path(package_ahriman.base)])
mocker.patch("ahriman.core.build_tools.task.Task.init") mocker.patch("ahriman.core.build_tools.task.Task.init")
move_mock = mocker.patch("shutil.move") move_mock = mocker.patch("shutil.move")
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building") status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building")
built_packages_mock = mocker.patch("ahriman.core.repository.executor.Executor.packages_built")
# must return list of built packages executor.process_build([package_ahriman])
assert executor.process_build([package_ahriman]) == [package_ahriman]
# must move files (once) # must move files (once)
move_mock.assert_called_once() move_mock.assert_called_once()
# must update status # must update status
@ -25,6 +36,8 @@ def test_process_build(executor: Executor, package_ahriman: Package, mocker: Moc
# must clear directory # must clear directory
from ahriman.core.repository.cleaner import Cleaner from ahriman.core.repository.cleaner import Cleaner
Cleaner.clear_build.assert_called_once() 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: 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]) executor.process_remove([package_python_schedule.base])
# must remove via alpm wrapper # must remove via alpm wrapper
repo_remove_mock.assert_has_calls([ 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() for package, props in package_python_schedule.packages.items()
], any_order=True) ], any_order=True)
# must update status # must update status
@ -91,6 +104,15 @@ def test_process_remove_base_single(executor: Executor, package_python_schedule:
status_client_mock.assert_not_called() 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, def test_process_remove_nothing(executor: Executor, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
@ -103,6 +125,18 @@ def test_process_remove_nothing(executor: Executor, package_ahriman: Package, pa
repo_remove_mock.assert_not_called() 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: def test_process_report_auto(executor: Executor, mocker: MockerFixture) -> None:
""" """
must process report in auto mode if no targets supplied 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() 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 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") status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success")
# must return complete # 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) # must move files (once)
move_mock.assert_called_once() move_mock.assert_called_once()
# must sign package # 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") repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success") 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([ 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() for package in package_python_schedule.packages.values()
], any_order=True) ], any_order=True)
status_client_mock.assert_called_with(package_python_schedule) 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: def test_process_update_failed(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must process update for failed package 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) mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_failed") 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() 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("shutil.move")
mocker.patch("ahriman.models.package.Package.load", side_effect=Exception()) 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()])

View File

@ -31,3 +31,28 @@ def test_packages(package_ahriman: Package, package_python_schedule: Package,
expected = set(package_ahriman.packages.keys()) expected = set(package_ahriman.packages.keys())
expected.update(package_python_schedule.packages.keys()) expected.update(package_python_schedule.packages.keys())
assert set(archives) == expected 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")]

View File

@ -1,9 +1,19 @@
import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.core.repository.update_handler import UpdateHandler from ahriman.core.repository.update_handler import UpdateHandler
from ahriman.models.package import Package 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, def test_updates_aur(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """

View File

@ -53,6 +53,24 @@ def test_repository_sign_args_skip_4(gpg: GPG) -> None:
assert not gpg.repository_sign_args 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: def test_sign_package_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
""" """
must sign package must sign package

View File

@ -51,6 +51,21 @@ def test_cache_load_no_file(watcher: Watcher, mocker: MockerFixture) -> None:
assert not watcher.known 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: def test_cache_load_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must not load unknown package must not load unknown package

View File

@ -1,5 +1,6 @@
import json import json
import pytest import pytest
import requests
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from requests import Response 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) 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: def test_get_all(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must return all packages status must return all packages status
@ -69,6 +78,14 @@ def test_get_failed(web_client: WebClient, mocker: MockerFixture) -> None:
assert web_client.get(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: def test_get_single(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must return single package status 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 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: def test_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must process package removal 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) 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: def test_update(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must process package update 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) 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: def test_update_self(web_client: WebClient, mocker: MockerFixture) -> None:
""" """
must process service update 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()) mocker.patch("requests.post", side_effect=Exception())
web_client.update_self(BuildStatusEnum.Unknown) 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)

View File

@ -105,6 +105,14 @@ def test_load_includes_missing(configuration: Configuration) -> None:
configuration.load_includes() 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: def test_load_logging_fallback(configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must fallback to stderr without errors must fallback to stderr without errors

View File

@ -4,6 +4,7 @@ import subprocess
from pytest_mock import MockerFixture 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.core.util import check_output, package_like, pretty_datetime, pretty_size
from ahriman.models.package import Package from ahriman.models.package import Package
@ -124,6 +125,14 @@ def test_pretty_size_pbytes() -> None:
assert abbrev == "GiB" 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: def test_pretty_size_empty() -> None:
""" """
must generate empty string for None value must generate empty string for None value

View File

@ -18,6 +18,16 @@ def test_upload_failure(configuration: Configuration, mocker: MockerFixture) ->
Upload.load("x86_64", configuration, UploadSettings.Rsync.name).run(Path("path")) 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: def test_upload_rsync(configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must upload via rsync must upload via rsync

View File

@ -1,3 +1,5 @@
import datetime
from ahriman.models.build_status import BuildStatus, BuildStatusEnum 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 must construct same object from json
""" """
assert BuildStatus.from_json(build_status_failed.view()) == build_status_failed 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)

View File

@ -124,6 +124,17 @@ def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_pa
assert package_ahriman == package 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: def test_from_json_view_1(package_ahriman: Package) -> None:
""" """
must construct same object from json 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) 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: def test_dependencies_with_version(mocker: MockerFixture, resource_path_root: Path) -> None:
""" """
must load correct list of dependencies with version 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" 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, def test_actual_version_vcs_failed(package_tpacpi_bat_git: Package, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must return same version in case if exception occurred 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") mocker.patch("ahriman.core.build_tools.task.Task.fetch")
assert package_tpacpi_bat_git.actual_version(repository_paths) == package_tpacpi_bat_git.version 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") other.version = other.version.replace("-1", "-2")
assert package_ahriman.is_outdated(other, repository_paths) 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)

View File

@ -1,3 +1,4 @@
from dataclasses import asdict
from unittest.mock import MagicMock from unittest.mock import MagicMock
from ahriman.models.package_description import PackageDescription 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 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, def test_from_package(package_description_ahriman: PackageDescription,
pyalpm_package_description_ahriman: MagicMock) -> None: pyalpm_package_description_ahriman: MagicMock) -> None:
""" """

View File

@ -1,4 +1,5 @@
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.build_status import BuildStatus, BuildStatusEnum 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={}) post_response = await client.post("/api/v1/ahriman", json={})
assert post_response.status == 400 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