diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9dbc8876..e9ae316b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -85,7 +85,7 @@ Again, the most checks can be performed by `make check` command, though some add
Args:
*args(Any): positional arguments
- **kwargs(Any): keyword arguments
+ **kwargs(Any): keyword arguments
"""
self.instance_attribute = ""
```
diff --git a/src/ahriman/application/handlers/status.py b/src/ahriman/application/handlers/status.py
index 61266636..477d0a3f 100644
--- a/src/ahriman/application/handlers/status.py
+++ b/src/ahriman/application/handlers/status.py
@@ -50,14 +50,14 @@ class Status(Handler):
# we are using reporter here
client = Application(architecture, configuration, report=True).repository.reporter
if args.ahriman:
- service_status = client.get_internal()
+ service_status = client.status_get()
StatusPrinter(service_status.status).print(verbose=args.info)
if args.package:
packages: list[tuple[Package, BuildStatus]] = sum(
- (client.get(base) for base in args.package),
+ (client.package_get(base) for base in args.package),
start=[])
else:
- packages = client.get(None)
+ packages = client.package_get(None)
Status.check_if_empty(args.exit_code, not packages)
diff --git a/src/ahriman/application/handlers/status_update.py b/src/ahriman/application/handlers/status_update.py
index 06b9ec68..ef3ebecf 100644
--- a/src/ahriman/application/handlers/status_update.py
+++ b/src/ahriman/application/handlers/status_update.py
@@ -49,10 +49,10 @@ class StatusUpdate(Handler):
if args.action == Action.Update and args.package:
# update packages statuses
for package in args.package:
- client.update(package, args.status)
+ client.package_update(package, args.status)
elif args.action == Action.Update:
# update service status
- client.update_self(args.status)
+ client.status_update(args.status)
elif args.action == Action.Remove:
for package in args.package:
- client.remove(package)
+ client.package_remove(package)
diff --git a/src/ahriman/application/lock.py b/src/ahriman/application/lock.py
index d8b0fcc8..d2eb22d0 100644
--- a/src/ahriman/application/lock.py
+++ b/src/ahriman/application/lock.py
@@ -18,7 +18,6 @@
# along with this program. If not, see .
#
import argparse
-import time
from pathlib import Path
from types import TracebackType
@@ -31,6 +30,7 @@ from ahriman.core.log import LazyLogging
from ahriman.core.status.client import Client
from ahriman.core.util import check_user
from ahriman.models.build_status import BuildStatusEnum
+from ahriman.models.waiter import Waiter
class Lock(LazyLogging):
@@ -81,7 +81,7 @@ class Lock(LazyLogging):
"""
check web server version
"""
- status = self.reporter.get_internal()
+ status = self.reporter.status_get()
if status.version is not None and status.version != __version__:
self.logger.warning("status watcher version mismatch, our %s, their %s",
__version__, status.version)
@@ -115,26 +115,18 @@ class Lock(LazyLogging):
except FileExistsError:
raise DuplicateRunError()
- def watch(self, interval: int = 10) -> None:
+ def watch(self) -> None:
"""
watch until lock disappear
-
- Args:
- interval(int, optional): interval to check in seconds (Default value = 10)
"""
- def is_timed_out(start: float) -> bool:
- since_start: float = time.monotonic() - start
- return self.wait_timeout != 0 and since_start > self.wait_timeout
-
# there are reasons why we are not using inotify here. First of all, if we would use it, it would bring to
# race conditions because multiple processes will be notified in the same time. Secondly, it is good library,
# but platform-specific, and we only need to check if file exists
if self.path is None:
return
- start_time = time.monotonic()
- while not is_timed_out(start_time) and self.path.is_file():
- time.sleep(interval)
+ waiter = Waiter(self.wait_timeout)
+ waiter.wait(self.path.is_file)
def __enter__(self) -> Self:
"""
@@ -154,7 +146,7 @@ class Lock(LazyLogging):
self.check_version()
self.watch()
self.create()
- self.reporter.update_self(BuildStatusEnum.Building)
+ self.reporter.status_update(BuildStatusEnum.Building)
return self
def __exit__(self, exc_type: type[Exception] | None, exc_val: Exception | None,
@@ -172,5 +164,5 @@ class Lock(LazyLogging):
"""
self.clear()
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
- self.reporter.update_self(status)
+ self.reporter.status_update(status)
return False
diff --git a/src/ahriman/core/log/http_log_handler.py b/src/ahriman/core/log/http_log_handler.py
index bff807a5..0b19dea1 100644
--- a/src/ahriman/core/log/http_log_handler.py
+++ b/src/ahriman/core/log/http_log_handler.py
@@ -31,17 +31,15 @@ class HttpLogHandler(logging.Handler):
Attributes:
reporter(Client): build status reporter instance
- suppress_errors(bool): suppress logging errors (e.g. if no web server available)
"""
- def __init__(self, configuration: Configuration, *, report: bool, suppress_errors: bool) -> None:
+ def __init__(self, configuration: Configuration, *, report: bool) -> None:
"""
default constructor
Args:
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
- suppress_errors(bool): suppress logging errors (e.g. if no web server available)
"""
# we don't really care about those parameters because they will be handled by the reporter
logging.Handler.__init__(self)
@@ -49,7 +47,6 @@ class HttpLogHandler(logging.Handler):
# client has to be imported here because of circular imports
from ahriman.core.status.client import Client
self.reporter = Client.load(configuration, report=report)
- self.suppress_errors = suppress_errors
@classmethod
def load(cls, configuration: Configuration, *, report: bool) -> Self:
@@ -68,8 +65,7 @@ class HttpLogHandler(logging.Handler):
if (handler := next((handler for handler in root.handlers if isinstance(handler, cls)), None)) is not None:
return handler # there is already registered instance
- suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False)
- handler = cls(configuration, report=report, suppress_errors=suppress_errors)
+ handler = cls(configuration, report=report)
root.addHandler(handler)
return handler
@@ -85,9 +81,4 @@ class HttpLogHandler(logging.Handler):
if package_base is None:
return # in case if no package base supplied we need just skip log message
- try:
- self.reporter.logs(package_base, record)
- except Exception:
- if self.suppress_errors:
- return
- self.handleError(record)
+ self.reporter.package_logs(package_base, record)
diff --git a/src/ahriman/core/report/remote_call.py b/src/ahriman/core/report/remote_call.py
new file mode 100644
index 00000000..222b365b
--- /dev/null
+++ b/src/ahriman/core/report/remote_call.py
@@ -0,0 +1,123 @@
+#
+# Copyright (c) 2021-2023 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from ahriman.core.configuration import Configuration
+from ahriman.core.report.report import Report
+from ahriman.core.status.web_client import WebClient
+from ahriman.models.package import Package
+from ahriman.models.result import Result
+from ahriman.models.waiter import Waiter
+
+
+class RemoteCall(Report):
+ """
+ trigger implementation which call remote service with update
+
+ Attributes:
+ client(WebClient): web client instance
+ update_aur(bool): check for AUR updates
+ update_local(bool): check for local packages update
+ update_manual(bool): check for manually built packages
+ wait_timeout(int): timeout to wait external process
+ """
+
+ def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
+ """
+ default constructor
+
+ Args:
+ architecture(str): repository architecture
+ configuration(Configuration): configuration instance
+ section(str): settings section name
+ """
+ Report.__init__(self, architecture, configuration)
+
+ self.client = WebClient(configuration)
+
+ self.update_aur = configuration.getboolean(section, "aur", fallback=False)
+ self.update_local = configuration.getboolean(section, "local", fallback=False)
+ self.update_manual = configuration.getboolean(section, "manual", fallback=False)
+
+ self.wait_timeout = configuration.getint(section, "wait_timeout", fallback=-1)
+
+ def generate(self, packages: list[Package], result: Result) -> None:
+ """
+ generate report for the specified packages
+
+ Args:
+ packages(list[Package]): list of packages to generate report
+ result(Result): build result
+ """
+ process_id = self.remote_update()
+ self.remote_wait(process_id)
+
+ def is_process_alive(self, process_id: str) -> bool:
+ """
+ check if process is alive
+
+ Args:
+ process_id(str): remote process id
+
+ Returns:
+ bool: True in case if remote process is alive and False otherwise
+ """
+ response = self.client.make_request("GET", f"{self.client.address}/api/v1/service/process/{process_id}")
+ if response is None:
+ return False
+
+ response_json = response.json()
+ is_alive: bool = response_json["is_alive"]
+
+ return is_alive
+
+ def remote_update(self) -> str | None:
+ """
+ call remote server for update
+
+ Returns:
+ str | None: remote process id on success and ``None`` otherwise
+ """
+ response = self.client.make_request(
+ "POST",
+ f"{self.client.address}/api/v1/service/update",
+ json={
+ "aur": self.update_aur,
+ "local": self.update_local,
+ "manual": self.update_manual,
+ }
+ )
+ if response is None:
+ return None # request terminated with error
+
+ response_json = response.json()
+ process_id: str = response_json["process_id"]
+ return process_id
+
+ def remote_wait(self, process_id: str | None) -> None:
+ """
+ wait for remote process termination
+
+ Args:
+ process_id(str | None): remote process id
+ """
+ if process_id is None:
+ return # nothing to track
+
+ waiter = Waiter(self.wait_timeout)
+ waiter.wait(self.is_process_alive, process_id)
diff --git a/src/ahriman/core/report/report.py b/src/ahriman/core/report/report.py
index f512a5d7..94a7d361 100644
--- a/src/ahriman/core/report/report.py
+++ b/src/ahriman/core/report/report.py
@@ -93,6 +93,9 @@ class Report(LazyLogging):
if provider == ReportSettings.Telegram:
from ahriman.core.report.telegram import Telegram
return Telegram(architecture, configuration, section)
+ if provider == ReportSettings.RemoteCall:
+ from ahriman.core.report.remote_call import RemoteCall
+ return RemoteCall(architecture, configuration, section)
return Report(architecture, configuration) # should never happen
def generate(self, packages: list[Package], result: Result) -> None:
diff --git a/src/ahriman/core/report/report_trigger.py b/src/ahriman/core/report/report_trigger.py
index 9a4691af..e46a4b1b 100644
--- a/src/ahriman/core/report/report_trigger.py
+++ b/src/ahriman/core/report/report_trigger.py
@@ -191,6 +191,31 @@ class ReportTrigger(Trigger):
},
},
},
+ "remote-call": {
+ "type": "dict",
+ "schema": {
+ "type": {
+ "type": "string",
+ "allowed": ["remote-call"],
+ },
+ "aur": {
+ "type": "boolean",
+ "coerce": "boolean",
+ },
+ "local": {
+ "type": "boolean",
+ "coerce": "boolean",
+ },
+ "manual": {
+ "type": "boolean",
+ "coerce": "boolean",
+ },
+ "wait_timeout": {
+ "type": "integer",
+ "coerce": "integer",
+ },
+ },
+ }
}
def __init__(self, architecture: str, configuration: Configuration) -> None:
diff --git a/src/ahriman/core/repository/executor.py b/src/ahriman/core/repository/executor.py
index 47f6b19d..d22d4301 100644
--- a/src/ahriman/core/repository/executor.py
+++ b/src/ahriman/core/repository/executor.py
@@ -121,7 +121,7 @@ class Executor(Cleaner):
self.database.build_queue_clear(package_base)
self.database.patches_remove(package_base, [])
self.database.logs_remove(package_base, None)
- self.reporter.remove(package_base) # we only update status page in case of base removal
+ self.reporter.package_remove(package_base) # we only update status page in case of base removal
except Exception:
self.logger.exception("could not remove base %s", package_base)
diff --git a/src/ahriman/core/status/client.py b/src/ahriman/core/status/client.py
index d3efafca..ccecd794 100644
--- a/src/ahriman/core/status/client.py
+++ b/src/ahriman/core/status/client.py
@@ -60,7 +60,7 @@ class Client:
return WebClient(configuration)
return Client()
- def add(self, package: Package, status: BuildStatusEnum) -> None:
+ def package_add(self, package: Package, status: BuildStatusEnum) -> None:
"""
add new package with status
@@ -69,7 +69,7 @@ class Client:
status(BuildStatusEnum): current package build status
"""
- def get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
+ def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
"""
get package status
@@ -82,16 +82,7 @@ class Client:
del package_base
return []
- def get_internal(self) -> InternalStatus:
- """
- get internal service status
-
- Returns:
- InternalStatus: current internal (web) service status
- """
- return InternalStatus(status=BuildStatus())
-
- def logs(self, package_base: str, record: logging.LogRecord) -> None:
+ def package_logs(self, package_base: str, record: logging.LogRecord) -> None:
"""
post log record
@@ -100,7 +91,7 @@ class Client:
record(logging.LogRecord): log record to post to api
"""
- def remove(self, package_base: str) -> None:
+ def package_remove(self, package_base: str) -> None:
"""
remove packages from watcher
@@ -108,7 +99,7 @@ class Client:
package_base(str): package base to remove
"""
- def update(self, package_base: str, status: BuildStatusEnum) -> None:
+ def package_update(self, package_base: str, status: BuildStatusEnum) -> None:
"""
update package build status. Unlike ``add`` it does not update package properties
@@ -117,14 +108,6 @@ class Client:
status(BuildStatusEnum): current package build status
"""
- def update_self(self, status: BuildStatusEnum) -> None:
- """
- update ahriman status itself
-
- Args:
- status(BuildStatusEnum): current ahriman status
- """
-
def set_building(self, package_base: str) -> None:
"""
set package status to building
@@ -132,7 +115,7 @@ class Client:
Args:
package_base(str): package base to update
"""
- return self.update(package_base, BuildStatusEnum.Building)
+ return self.package_update(package_base, BuildStatusEnum.Building)
def set_failed(self, package_base: str) -> None:
"""
@@ -141,7 +124,7 @@ class Client:
Args:
package_base(str): package base to update
"""
- return self.update(package_base, BuildStatusEnum.Failed)
+ return self.package_update(package_base, BuildStatusEnum.Failed)
def set_pending(self, package_base: str) -> None:
"""
@@ -150,7 +133,7 @@ class Client:
Args:
package_base(str): package base to update
"""
- return self.update(package_base, BuildStatusEnum.Pending)
+ return self.package_update(package_base, BuildStatusEnum.Pending)
def set_success(self, package: Package) -> None:
"""
@@ -159,7 +142,7 @@ class Client:
Args:
package(Package): current package properties
"""
- return self.add(package, BuildStatusEnum.Success)
+ return self.package_add(package, BuildStatusEnum.Success)
def set_unknown(self, package: Package) -> None:
"""
@@ -168,4 +151,21 @@ class Client:
Args:
package(Package): current package properties
"""
- return self.add(package, BuildStatusEnum.Unknown)
+ return self.package_add(package, BuildStatusEnum.Unknown)
+
+ def status_get(self) -> InternalStatus:
+ """
+ get internal service status
+
+ Returns:
+ InternalStatus: current internal (web) service status
+ """
+ return InternalStatus(status=BuildStatus())
+
+ def status_update(self, status: BuildStatusEnum) -> None:
+ """
+ update ahriman status itself
+
+ Args:
+ status(BuildStatusEnum): current ahriman status
+ """
diff --git a/src/ahriman/core/status/web_client.py b/src/ahriman/core/status/web_client.py
index 55e1cdc4..a69fa047 100644
--- a/src/ahriman/core/status/web_client.py
+++ b/src/ahriman/core/status/web_client.py
@@ -22,6 +22,7 @@ import logging
import requests
from collections.abc import Generator
+from typing import Any, Literal
from urllib.parse import quote_plus as urlencode
from ahriman import __version__
@@ -164,10 +165,7 @@ class WebClient(Client, LazyLogging):
"username": self.user.username,
"password": self.user.password
}
-
- with self.__get_session(session):
- response = session.post(self._login_url, json=payload)
- response.raise_for_status()
+ self.make_request("POST", self._login_url, json=payload, session=session)
def _logs_url(self, package_base: str) -> str:
"""
@@ -195,7 +193,31 @@ class WebClient(Client, LazyLogging):
suffix = f"/{package_base}" if package_base else ""
return f"{self.address}/api/v1/packages{suffix}"
- def add(self, package: Package, status: BuildStatusEnum) -> None:
+ def make_request(self, method: Literal["DELETE", "GET", "POST"], url: str,
+ params: list[tuple[str, str]] | None = None, json: dict[str, Any] | None = None,
+ session: requests.Session | None = None) -> requests.Response | None:
+ """
+ perform request with specified parameters
+
+ Args:
+ method(Literal["DELETE", "GET", "POST"]): HTTP method to call
+ url(str): remote url to call
+ params(list[tuple[str, str]] | None, optional): request query parameters (Default value = None)
+ json(dict[str, Any] | None, optional): request json parameters (Default value = None)
+ session(requests.Session | None, optional): session object if any (Default value = None)
+
+ Returns:
+ requests.Response | None: response object or None in case of errors
+ """
+ with self.__get_session(session) as _session:
+ response = _session.request(method, url, params=params, json=json)
+ response.raise_for_status()
+ return response
+
+ # noinspection PyUnreachableCode
+ return None
+
+ def package_add(self, package: Package, status: BuildStatusEnum) -> None:
"""
add new package with status
@@ -207,12 +229,9 @@ class WebClient(Client, LazyLogging):
"status": status.value,
"package": package.view()
}
+ self.make_request("POST", self._package_url(package.base), json=payload)
- with self.__get_session() as session:
- response = session.post(self._package_url(package.base), json=payload)
- response.raise_for_status()
-
- def get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
+ def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
"""
get package status
@@ -222,37 +241,17 @@ class WebClient(Client, LazyLogging):
Returns:
list[tuple[Package, BuildStatus]]: list of current package description and status if it has been found
"""
- with self.__get_session() as session:
- response = session.get(self._package_url(package_base or ""))
- response.raise_for_status()
+ response = self.make_request("GET", self._package_url(package_base or ""))
+ if response is None:
+ return []
- status_json = response.json()
- return [
- (Package.from_json(package["package"]), BuildStatus.from_json(package["status"]))
- for package in status_json
- ]
+ response_json = response.json()
+ return [
+ (Package.from_json(package["package"]), BuildStatus.from_json(package["status"]))
+ for package in response_json
+ ]
- # noinspection PyUnreachableCode
- return []
-
- def get_internal(self) -> InternalStatus:
- """
- get internal service status
-
- Returns:
- InternalStatus: current internal (web) service status
- """
- with self.__get_session() as session:
- response = session.get(self._status_url)
- response.raise_for_status()
-
- status_json = response.json()
- return InternalStatus.from_json(status_json)
-
- # noinspection PyUnreachableCode
- return InternalStatus(status=BuildStatus())
-
- def logs(self, package_base: str, record: logging.LogRecord) -> None:
+ def package_logs(self, package_base: str, record: logging.LogRecord) -> None:
"""
post log record
@@ -265,23 +264,18 @@ class WebClient(Client, LazyLogging):
"message": record.getMessage(),
"process_id": record.process,
}
+ self.make_request("POST", self._logs_url(package_base), json=payload)
- # in this method exception has to be handled outside in logger handler
- response = self.__session.post(self._logs_url(package_base), json=payload)
- response.raise_for_status()
-
- def remove(self, package_base: str) -> None:
+ def package_remove(self, package_base: str) -> None:
"""
remove packages from watcher
Args:
package_base(str): basename to remove
"""
- with self.__get_session() as session:
- response = session.delete(self._package_url(package_base))
- response.raise_for_status()
+ self.make_request("DELETE", self._package_url(package_base))
- def update(self, package_base: str, status: BuildStatusEnum) -> None:
+ def package_update(self, package_base: str, status: BuildStatusEnum) -> None:
"""
update package build status. Unlike ``add`` it does not update package properties
@@ -290,12 +284,23 @@ class WebClient(Client, LazyLogging):
status(BuildStatusEnum): current package build status
"""
payload = {"status": status.value}
+ self.make_request("POST", self._package_url(package_base), json=payload)
- with self.__get_session() as session:
- response = session.post(self._package_url(package_base), json=payload)
- response.raise_for_status()
+ def status_get(self) -> InternalStatus:
+ """
+ get internal service status
- def update_self(self, status: BuildStatusEnum) -> None:
+ Returns:
+ InternalStatus: current internal (web) service status
+ """
+ response = self.make_request("GET", self._status_url)
+ if response is None:
+ return InternalStatus(status=BuildStatus())
+
+ response_json = response.json()
+ return InternalStatus.from_json(response_json)
+
+ def status_update(self, status: BuildStatusEnum) -> None:
"""
update ahriman status itself
@@ -303,7 +308,4 @@ class WebClient(Client, LazyLogging):
status(BuildStatusEnum): current ahriman status
"""
payload = {"status": status.value}
-
- with self.__get_session() as session:
- response = session.post(self._status_url, json=payload)
- response.raise_for_status()
+ self.make_request("POST", self._status_url, json=payload)
diff --git a/src/ahriman/models/report_settings.py b/src/ahriman/models/report_settings.py
index 258e0386..5b37473b 100644
--- a/src/ahriman/models/report_settings.py
+++ b/src/ahriman/models/report_settings.py
@@ -32,6 +32,7 @@ class ReportSettings(str, Enum):
Email(ReportSettings): (class attribute) email report generation
Console(ReportSettings): (class attribute) print result to console
Telegram(ReportSettings): (class attribute) markdown report to telegram channel
+ RemoteCall(ReportSettings): (class attribute) remote server call
"""
Disabled = "disabled" # for testing purpose
@@ -39,6 +40,7 @@ class ReportSettings(str, Enum):
Email = "email"
Console = "console"
Telegram = "telegram"
+ RemoteCall = "remote-call"
@staticmethod
def from_option(value: str) -> ReportSettings:
@@ -59,4 +61,6 @@ class ReportSettings(str, Enum):
return ReportSettings.Console
if value.lower() in ("telegram",):
return ReportSettings.Telegram
+ if value.lower() in ("remote-call",):
+ return ReportSettings.RemoteCall
return ReportSettings.Disabled
diff --git a/src/ahriman/models/waiter.py b/src/ahriman/models/waiter.py
new file mode 100644
index 00000000..18ec7efc
--- /dev/null
+++ b/src/ahriman/models/waiter.py
@@ -0,0 +1,72 @@
+#
+# Copyright (c) 2021-2023 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+import time
+
+from collections.abc import Callable
+from dataclasses import dataclass, field
+from typing import ParamSpec
+
+
+Params = ParamSpec("Params")
+
+
+@dataclass(frozen=True)
+class Waiter:
+ """
+ simple waiter implementation
+
+ Attributes:
+ interval(int): interval in seconds between checks
+ start_time(float): monotonic time of the waiter start. More likely must not be assigned explicitly
+ wait_timeout(int): timeout in seconds to wait for. Negative value will result in immediate exit. Zero value
+ means infinite timeout
+ """
+
+ wait_timeout: int
+ start_time: float = field(default_factory=time.monotonic, kw_only=True)
+ interval: int = field(default=10, kw_only=True)
+
+ def is_timed_out(self) -> bool:
+ """
+ check if timer is out
+
+ Returns:
+ bool: True in case current monotonic time is more than ``Waiter.start_time`` and
+ ``Waiter.wait_timeout`` doesn't equal to 0
+ """
+ since_start: float = time.monotonic() - self.start_time
+ return self.wait_timeout != 0 and since_start > self.wait_timeout
+
+ def wait(self, in_progress: Callable[Params, bool], *args: Params.args, **kwargs: Params.kwargs) -> float:
+ """
+ wait until requirements are not met
+
+ Args:
+ in_progress(Callable[Params, bool]): function to check if timer should wait for another cycle
+ *args(Params.args): positional arguments for check call
+ **kwargs(Params.kwargs): keyword arguments for check call
+
+ Returns:
+ float: consumed time in seconds
+ """
+ while not self.is_timed_out() and in_progress(*args, **kwargs):
+ time.sleep(self.interval)
+
+ return time.monotonic() - self.start_time
diff --git a/tests/ahriman/application/handlers/test_handler_status.py b/tests/ahriman/application/handlers/test_handler_status.py
index 695c031b..4c2bf40e 100644
--- a/tests/ahriman/application/handlers/test_handler_status.py
+++ b/tests/ahriman/application/handlers/test_handler_status.py
@@ -36,8 +36,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
"""
args = _default_args(args)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
- application_mock = mocker.patch("ahriman.core.status.client.Client.get_internal")
- packages_mock = mocker.patch("ahriman.core.status.client.Client.get",
+ application_mock = mocker.patch("ahriman.core.status.client.Client.status_get")
+ packages_mock = mocker.patch("ahriman.core.status.client.Client.package_get",
return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success)),
(package_python_schedule, BuildStatus(BuildStatusEnum.Failed))])
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
@@ -58,8 +58,8 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat
args = _default_args(args)
args.exit_code = True
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
- mocker.patch("ahriman.core.status.client.Client.get_internal")
- mocker.patch("ahriman.core.status.client.Client.get", return_value=[])
+ mocker.patch("ahriman.core.status.client.Client.status_get")
+ mocker.patch("ahriman.core.status.client.Client.package_get", return_value=[])
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
Status.run(args, "x86_64", configuration, report=False)
@@ -74,7 +74,7 @@ def test_run_verbose(args: argparse.Namespace, configuration: Configuration, rep
args = _default_args(args)
args.info = True
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
- mocker.patch("ahriman.core.status.client.Client.get",
+ mocker.patch("ahriman.core.status.client.Client.package_get",
return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success))])
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
@@ -90,7 +90,7 @@ def test_run_with_package_filter(args: argparse.Namespace, configuration: Config
args = _default_args(args)
args.package = [package_ahriman.base]
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
- packages_mock = mocker.patch("ahriman.core.status.client.Client.get",
+ packages_mock = mocker.patch("ahriman.core.status.client.Client.package_get",
return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success))])
Status.run(args, "x86_64", configuration, report=False)
@@ -104,7 +104,7 @@ def test_run_by_status(args: argparse.Namespace, configuration: Configuration, r
"""
args = _default_args(args)
args.status = BuildStatusEnum.Failed
- mocker.patch("ahriman.core.status.client.Client.get",
+ mocker.patch("ahriman.core.status.client.Client.package_get",
return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success)),
(package_python_schedule, BuildStatus(BuildStatusEnum.Failed))])
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
diff --git a/tests/ahriman/application/handlers/test_handler_status_update.py b/tests/ahriman/application/handlers/test_handler_status_update.py
index 679fbb37..5e307115 100644
--- a/tests/ahriman/application/handlers/test_handler_status_update.py
+++ b/tests/ahriman/application/handlers/test_handler_status_update.py
@@ -34,7 +34,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
"""
args = _default_args(args)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
- update_self_mock = mocker.patch("ahriman.core.status.client.Client.update_self")
+ update_self_mock = mocker.patch("ahriman.core.status.client.Client.status_update")
StatusUpdate.run(args, "x86_64", configuration, report=False)
update_self_mock.assert_called_once_with(args.status)
@@ -48,7 +48,7 @@ def test_run_packages(args: argparse.Namespace, configuration: Configuration, re
args = _default_args(args)
args.package = [package_ahriman.base]
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
- update_mock = mocker.patch("ahriman.core.status.client.Client.update")
+ update_mock = mocker.patch("ahriman.core.status.client.Client.package_update")
StatusUpdate.run(args, "x86_64", configuration, report=False)
update_mock.assert_called_once_with(package_ahriman.base, args.status)
@@ -63,7 +63,7 @@ def test_run_remove(args: argparse.Namespace, configuration: Configuration, repo
args.package = [package_ahriman.base]
args.action = Action.Remove
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
- update_mock = mocker.patch("ahriman.core.status.client.Client.remove")
+ update_mock = mocker.patch("ahriman.core.status.client.Client.package_remove")
StatusUpdate.run(args, "x86_64", configuration, report=False)
update_mock.assert_called_once_with(package_ahriman.base)
diff --git a/tests/ahriman/application/handlers/test_handler_validate.py b/tests/ahriman/application/handlers/test_handler_validate.py
index cab07531..62391380 100644
--- a/tests/ahriman/application/handlers/test_handler_validate.py
+++ b/tests/ahriman/application/handlers/test_handler_validate.py
@@ -67,6 +67,7 @@ def test_schema(configuration: Configuration) -> None:
assert schema.pop("keyring-generator")
assert schema.pop("mirrorlist")
assert schema.pop("mirrorlist-generator")
+ assert schema.pop("remote-call")
assert schema.pop("remote-pull")
assert schema.pop("remote-push")
assert schema.pop("report")
diff --git a/tests/ahriman/application/test_lock.py b/tests/ahriman/application/test_lock.py
index ae1ef2c0..088f6b5c 100644
--- a/tests/ahriman/application/test_lock.py
+++ b/tests/ahriman/application/test_lock.py
@@ -32,7 +32,7 @@ def test_check_version(lock: Lock, mocker: MockerFixture) -> None:
"""
must check version correctly
"""
- mocker.patch("ahriman.core.status.client.Client.get_internal",
+ mocker.patch("ahriman.core.status.client.Client.status_get",
return_value=InternalStatus(status=BuildStatus(), version=__version__))
logging_mock = mocker.patch("logging.Logger.warning")
@@ -44,7 +44,7 @@ def test_check_version_mismatch(lock: Lock, mocker: MockerFixture) -> None:
"""
must check mismatched version correctly
"""
- mocker.patch("ahriman.core.status.client.Client.get_internal",
+ mocker.patch("ahriman.core.status.client.Client.status_get",
return_value=InternalStatus(status=BuildStatus(), version="version"))
logging_mock = mocker.patch("logging.Logger.warning")
@@ -156,30 +156,13 @@ def test_create_unsafe(lock: Lock) -> None:
def test_watch(lock: Lock, mocker: MockerFixture) -> None:
"""
- must check if lock file exists in cycle
+ must check if lock file exists
"""
- mocker.patch("pathlib.Path.is_file", return_value=False)
- lock.watch()
-
-
-def test_watch_wait(lock: Lock, mocker: MockerFixture) -> None:
- """
- must wait until file will disappear
- """
- mocker.patch("pathlib.Path.is_file", side_effect=[True, False])
+ wait_mock = mocker.patch("ahriman.models.waiter.Waiter.wait")
lock.path = Path(tempfile.mktemp()) # nosec
- lock.wait_timeout = 1
- lock.watch(1)
-
-
-def test_watch_empty_timeout(lock: Lock, mocker: MockerFixture) -> None:
- """
- must skip watch on empty timeout
- """
- mocker.patch("pathlib.Path.is_file", return_value=True)
- lock.path = Path(tempfile.mktemp()) # nosec
lock.watch()
+ wait_mock.assert_called_once_with(lock.path.is_file)
def test_watch_skip(lock: Lock, mocker: MockerFixture) -> None:
@@ -199,7 +182,7 @@ def test_enter(lock: Lock, mocker: MockerFixture) -> None:
watch_mock = mocker.patch("ahriman.application.lock.Lock.watch")
clear_mock = mocker.patch("ahriman.application.lock.Lock.clear")
create_mock = mocker.patch("ahriman.application.lock.Lock.create")
- update_status_mock = mocker.patch("ahriman.core.status.client.Client.update_self")
+ update_status_mock = mocker.patch("ahriman.core.status.client.Client.status_update")
with lock:
pass
@@ -218,7 +201,7 @@ def test_exit_with_exception(lock: Lock, mocker: MockerFixture) -> None:
mocker.patch("ahriman.application.lock.Lock.check_user")
mocker.patch("ahriman.application.lock.Lock.clear")
mocker.patch("ahriman.application.lock.Lock.create")
- update_status_mock = mocker.patch("ahriman.core.status.client.Client.update_self")
+ update_status_mock = mocker.patch("ahriman.core.status.client.Client.status_update")
with pytest.raises(Exception):
with lock:
diff --git a/tests/ahriman/core/log/test_http_log_handler.py b/tests/ahriman/core/log/test_http_log_handler.py
index 5927b1fc..340b33bc 100644
--- a/tests/ahriman/core/log/test_http_log_handler.py
+++ b/tests/ahriman/core/log/test_http_log_handler.py
@@ -40,48 +40,20 @@ def test_emit(configuration: Configuration, log_record: logging.LogRecord, packa
must emit log record to reporter
"""
log_record.package_base = package_ahriman.base
- log_mock = mocker.patch("ahriman.core.status.client.Client.logs")
+ log_mock = mocker.patch("ahriman.core.status.client.Client.package_logs")
- handler = HttpLogHandler(configuration, report=False, suppress_errors=False)
+ handler = HttpLogHandler(configuration, report=False)
handler.emit(log_record)
log_mock.assert_called_once_with(package_ahriman.base, log_record)
-def test_emit_failed(configuration: Configuration, log_record: logging.LogRecord, package_ahriman: Package,
- mocker: MockerFixture) -> None:
- """
- must call handle error on exception
- """
- log_record.package_base = package_ahriman.base
- mocker.patch("ahriman.core.status.client.Client.logs", side_effect=Exception())
- handle_error_mock = mocker.patch("logging.Handler.handleError")
- handler = HttpLogHandler(configuration, report=False, suppress_errors=False)
-
- handler.emit(log_record)
- handle_error_mock.assert_called_once_with(log_record)
-
-
-def test_emit_suppress_failed(configuration: Configuration, log_record: logging.LogRecord, package_ahriman: Package,
- mocker: MockerFixture) -> None:
- """
- must not call handle error on exception if suppress flag is set
- """
- log_record.package_base = package_ahriman.base
- mocker.patch("ahriman.core.status.client.Client.logs", side_effect=Exception())
- handle_error_mock = mocker.patch("logging.Handler.handleError")
- handler = HttpLogHandler(configuration, report=False, suppress_errors=True)
-
- handler.emit(log_record)
- handle_error_mock.assert_not_called()
-
-
def test_emit_skip(configuration: Configuration, log_record: logging.LogRecord, mocker: MockerFixture) -> None:
"""
must skip log record posting if no package base set
"""
- log_mock = mocker.patch("ahriman.core.status.client.Client.logs")
- handler = HttpLogHandler(configuration, report=False, suppress_errors=False)
+ log_mock = mocker.patch("ahriman.core.status.client.Client.package_logs")
+ handler = HttpLogHandler(configuration, report=False)
handler.emit(log_record)
log_mock.assert_not_called()
diff --git a/tests/ahriman/core/report/conftest.py b/tests/ahriman/core/report/conftest.py
new file mode 100644
index 00000000..a2927ba0
--- /dev/null
+++ b/tests/ahriman/core/report/conftest.py
@@ -0,0 +1,20 @@
+import pytest
+
+from ahriman.core.configuration import Configuration
+from ahriman.core.report.remote_call import RemoteCall
+
+
+@pytest.fixture
+def remote_call(configuration: Configuration) -> RemoteCall:
+ """
+ fixture for remote update trigger
+
+ Args:
+ configuration(Configuration): configuration fixture
+
+ Returns:
+ RemoteCall: remote update trigger test instance
+ """
+ configuration.set_option("web", "host", "localhost")
+ configuration.set_option("web", "port", "8080")
+ return RemoteCall("x86_64", configuration, "remote-call")
diff --git a/tests/ahriman/core/report/test_remote_call.py b/tests/ahriman/core/report/test_remote_call.py
new file mode 100644
index 00000000..666df97c
--- /dev/null
+++ b/tests/ahriman/core/report/test_remote_call.py
@@ -0,0 +1,85 @@
+import pytest
+import requests
+
+from pytest_mock import MockerFixture
+
+from ahriman.core.report.remote_call import RemoteCall
+from ahriman.models.result import Result
+
+
+def test_generate(remote_call: RemoteCall, mocker: MockerFixture) -> None:
+ """
+ must correctly call client
+ """
+ update_mock = mocker.patch("ahriman.core.report.remote_call.RemoteCall.remote_update", return_value="id")
+ wait_mock = mocker.patch("ahriman.core.report.remote_call.RemoteCall.remote_wait")
+
+ remote_call.generate([], Result())
+ update_mock.assert_called_once_with()
+ wait_mock.assert_called_once_with("id")
+
+
+def test_is_process_alive(remote_call: RemoteCall, mocker: MockerFixture) -> None:
+ """
+ must correctly define if process is alive
+ """
+ response_obj = requests.Response()
+ response_obj._content = """{"is_alive": true}""".encode("utf8")
+ response_obj.status_code = 200
+
+ request_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request", return_value=response_obj)
+
+ assert remote_call.is_process_alive("id")
+ request_mock.assert_called_once_with("GET", f"{remote_call.client.address}/api/v1/service/process/id")
+
+
+def test_is_process_alive_unknown(remote_call: RemoteCall, mocker: MockerFixture) -> None:
+ """
+ must correctly define if process is unknown
+ """
+ mocker.patch("ahriman.core.status.web_client.WebClient.make_request", return_value=None)
+ assert not remote_call.is_process_alive("id")
+
+
+def test_remote_update(remote_call: RemoteCall, mocker: MockerFixture) -> None:
+ """
+ must call remote server for update process
+ """
+ response_obj = requests.Response()
+ response_obj._content = """{"process_id": "id"}""".encode("utf8")
+ response_obj.status_code = 200
+
+ request_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request", return_value=response_obj)
+
+ assert remote_call.remote_update() == "id"
+ request_mock.assert_called_once_with("POST", f"{remote_call.client.address}/api/v1/service/update", json={
+ "aur": False,
+ "local": False,
+ "manual": True,
+ })
+
+
+def test_remote_update_failed(remote_call: RemoteCall, mocker: MockerFixture) -> None:
+ """
+ must return empty process id in case of errors
+ """
+ mocker.patch("ahriman.core.status.web_client.WebClient.make_request", return_value=None)
+ assert remote_call.generate([], Result()) is None
+
+
+def test_remote_wait(remote_call: RemoteCall, mocker: MockerFixture) -> None:
+ """
+ must wait for remote process to success
+ """
+ wait_mock = mocker.patch("ahriman.models.waiter.Waiter.wait")
+ remote_call.remote_wait("id")
+ wait_mock.assert_called_once_with(pytest.helpers.anyvar(int), "id")
+
+
+def test_remote_wait_skip(remote_call: RemoteCall, mocker: MockerFixture) -> None:
+ """
+ must skip wait if process id is unknown
+ """
+ wait_mock = mocker.patch("ahriman.models.waiter.Waiter.wait")
+ remote_call.remote_wait(None)
+ wait_mock.assert_not_called()
diff --git a/tests/ahriman/core/report/test_report.py b/tests/ahriman/core/report/test_report.py
index 5715c712..c47f97a3 100644
--- a/tests/ahriman/core/report/test_report.py
+++ b/tests/ahriman/core/report/test_report.py
@@ -24,6 +24,7 @@ def test_report_dummy(configuration: Configuration, result: Result, mocker: Mock
"""
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, "disabled").run(result, [])
report_mock.assert_called_once_with([], result)
@@ -55,6 +56,18 @@ def test_report_html(configuration: Configuration, result: Result, mocker: Mocke
report_mock.assert_called_once_with([], result)
+def test_report_remote_call(configuration: Configuration, result: Result, mocker: MockerFixture) -> None:
+ """
+ must instantiate remote call trigger
+ """
+ configuration.set_option("web", "host", "localhost")
+ configuration.set_option("web", "port", "8080")
+ report_mock = mocker.patch("ahriman.core.report.remote_call.RemoteCall.generate")
+
+ Report.load("x86_64", configuration, "remote-call").run(result, [])
+ report_mock.assert_called_once_with([], result)
+
+
def test_report_telegram(configuration: Configuration, result: Result, mocker: MockerFixture) -> None:
"""
must generate telegram report
diff --git a/tests/ahriman/core/repository/test_executor.py b/tests/ahriman/core/repository/test_executor.py
index b093a3aa..768258c1 100644
--- a/tests/ahriman/core/repository/test_executor.py
+++ b/tests/ahriman/core/repository/test_executor.py
@@ -85,7 +85,7 @@ def test_process_remove_base(executor: Executor, package_ahriman: Package, mocke
build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_clear")
patches_mock = mocker.patch("ahriman.core.database.SQLite.patches_remove")
logs_mock = mocker.patch("ahriman.core.database.SQLite.logs_remove")
- status_client_mock = mocker.patch("ahriman.core.status.client.Client.remove")
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.package_remove")
executor.process_remove([package_ahriman.base])
# must remove via alpm wrapper
@@ -106,7 +106,7 @@ def test_process_remove_base_multiple(executor: Executor, package_python_schedul
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_python_schedule])
repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
- status_client_mock = mocker.patch("ahriman.core.status.client.Client.remove")
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.package_remove")
executor.process_remove([package_python_schedule.base])
# must remove via alpm wrapper
@@ -125,7 +125,7 @@ def test_process_remove_base_single(executor: Executor, package_python_schedule:
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_python_schedule])
repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
- status_client_mock = mocker.patch("ahriman.core.status.client.Client.remove")
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.package_remove")
executor.process_remove(["python2-schedule"])
# must remove via alpm wrapper
@@ -171,7 +171,7 @@ def test_process_remove_unknown(executor: Executor, package_ahriman: Package, mo
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[])
repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
- status_client_mock = mocker.patch("ahriman.core.status.client.Client.remove")
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.package_remove")
executor.process_remove([package_ahriman.base])
repo_remove_mock.assert_not_called()
diff --git a/tests/ahriman/core/status/test_client.py b/tests/ahriman/core/status/test_client.py
index de9a91da..f6c0c20a 100644
--- a/tests/ahriman/core/status/test_client.py
+++ b/tests/ahriman/core/status/test_client.py
@@ -51,64 +51,47 @@ def test_load_full_client_from_unix_socket(configuration: Configuration) -> None
assert isinstance(Client.load(configuration, report=True), WebClient)
-def test_add(client: Client, package_ahriman: Package) -> None:
+def test_package_add(client: Client, package_ahriman: Package) -> None:
"""
must process package addition without errors
"""
- client.add(package_ahriman, BuildStatusEnum.Unknown)
+ client.package_add(package_ahriman, BuildStatusEnum.Unknown)
-def test_get(client: Client, package_ahriman: Package) -> None:
+def test_package_get(client: Client, package_ahriman: Package) -> None:
"""
must return empty package list
"""
- assert client.get(package_ahriman.base) == []
- assert client.get(None) == []
+ assert client.package_get(package_ahriman.base) == []
+ assert client.package_get(None) == []
-def test_get_internal(client: Client) -> None:
- """
- must return dummy status for web service
- """
- actual = client.get_internal()
- expected = InternalStatus(status=BuildStatus(timestamp=actual.status.timestamp))
-
- assert actual == expected
-
-
-def test_log(client: Client, package_ahriman: Package, log_record: logging.LogRecord) -> None:
+def test_package_log(client: Client, package_ahriman: Package, log_record: logging.LogRecord) -> None:
"""
must process log record without errors
"""
- client.logs(package_ahriman.base, log_record)
+ client.package_logs(package_ahriman.base, log_record)
-def test_remove(client: Client, package_ahriman: Package) -> None:
+def test_package_remove(client: Client, package_ahriman: Package) -> None:
"""
must process remove without errors
"""
- client.remove(package_ahriman.base)
+ client.package_remove(package_ahriman.base)
-def test_update(client: Client, package_ahriman: Package) -> None:
+def test_package_update(client: Client, package_ahriman: Package) -> None:
"""
must update package status without errors
"""
- client.update(package_ahriman.base, BuildStatusEnum.Unknown)
-
-
-def test_update_self(client: Client) -> None:
- """
- must update self status without errors
- """
- client.update_self(BuildStatusEnum.Unknown)
+ client.package_update(package_ahriman.base, BuildStatusEnum.Unknown)
def test_set_building(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must set building status to the package
"""
- update_mock = mocker.patch("ahriman.core.status.client.Client.update")
+ update_mock = mocker.patch("ahriman.core.status.client.Client.package_update")
client.set_building(package_ahriman.base)
update_mock.assert_called_once_with(package_ahriman.base, BuildStatusEnum.Building)
@@ -118,7 +101,7 @@ def test_set_failed(client: Client, package_ahriman: Package, mocker: MockerFixt
"""
must set failed status to the package
"""
- update_mock = mocker.patch("ahriman.core.status.client.Client.update")
+ update_mock = mocker.patch("ahriman.core.status.client.Client.package_update")
client.set_failed(package_ahriman.base)
update_mock.assert_called_once_with(package_ahriman.base, BuildStatusEnum.Failed)
@@ -128,7 +111,7 @@ def test_set_pending(client: Client, package_ahriman: Package, mocker: MockerFix
"""
must set building status to the package
"""
- update_mock = mocker.patch("ahriman.core.status.client.Client.update")
+ update_mock = mocker.patch("ahriman.core.status.client.Client.package_update")
client.set_pending(package_ahriman.base)
update_mock.assert_called_once_with(package_ahriman.base, BuildStatusEnum.Pending)
@@ -138,7 +121,7 @@ def test_set_success(client: Client, package_ahriman: Package, mocker: MockerFix
"""
must set success status to the package
"""
- add_mock = mocker.patch("ahriman.core.status.client.Client.add")
+ add_mock = mocker.patch("ahriman.core.status.client.Client.package_add")
client.set_success(package_ahriman)
add_mock.assert_called_once_with(package_ahriman, BuildStatusEnum.Success)
@@ -148,7 +131,24 @@ def test_set_unknown(client: Client, package_ahriman: Package, mocker: MockerFix
"""
must add new package with unknown status
"""
- add_mock = mocker.patch("ahriman.core.status.client.Client.add")
+ add_mock = mocker.patch("ahriman.core.status.client.Client.package_add")
client.set_unknown(package_ahriman)
add_mock.assert_called_once_with(package_ahriman, BuildStatusEnum.Unknown)
+
+
+def test_status_get(client: Client) -> None:
+ """
+ must return dummy status for web service
+ """
+ actual = client.status_get()
+ expected = InternalStatus(status=BuildStatus(timestamp=actual.status.timestamp))
+
+ assert actual == expected
+
+
+def test_status_update(client: Client) -> None:
+ """
+ must update self status without errors
+ """
+ client.status_update(BuildStatusEnum.Unknown)
diff --git a/tests/ahriman/core/status/test_web_client.py b/tests/ahriman/core/status/test_web_client.py
index b4756be9..7b2b9712 100644
--- a/tests/ahriman/core/status/test_web_client.py
+++ b/tests/ahriman/core/status/test_web_client.py
@@ -5,7 +5,7 @@ import requests
import requests_unixsocket
from pytest_mock import MockerFixture
-from requests import Response
+from unittest.mock import call as MockCall
from ahriman.core.configuration import Configuration
from ahriman.core.status.web_client import WebClient
@@ -74,14 +74,14 @@ def test_login(web_client: WebClient, user: User, mocker: MockerFixture) -> None
must login user
"""
web_client.user = user
- requests_mock = mocker.patch("requests.Session.post")
+ requests_mock = mocker.patch("requests.Session.request")
payload = {
"username": user.username,
"password": user.password
}
web_client._login(requests.Session())
- requests_mock.assert_called_once_with(pytest.helpers.anyvar(str, True), json=payload)
+ requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), params=None, json=payload)
def test_login_failed(web_client: WebClient, user: User, mocker: MockerFixture) -> None:
@@ -89,7 +89,7 @@ def test_login_failed(web_client: WebClient, user: User, mocker: MockerFixture)
must suppress any exception happened during login
"""
web_client.user = user
- mocker.patch("requests.Session.post", side_effect=Exception())
+ mocker.patch("requests.Session.request", side_effect=Exception())
web_client._login(requests.Session())
@@ -98,7 +98,7 @@ def test_login_failed_http_error(web_client: WebClient, user: User, mocker: Mock
must suppress HTTP exception happened during login
"""
web_client.user = user
- mocker.patch("requests.Session.post", side_effect=requests.exceptions.HTTPError())
+ mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError())
web_client._login(requests.Session())
@@ -106,7 +106,7 @@ def test_login_skip(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must skip login if no user set
"""
- requests_mock = mocker.patch("requests.Session.post")
+ requests_mock = mocker.patch("requests.Session.request")
web_client._login(requests.Session())
requests_mock.assert_not_called()
@@ -130,241 +130,292 @@ def test_package_url(web_client: WebClient, package_ahriman: Package) -> None:
assert web_client._package_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}")
-def test_add(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+def test_make_request(web_client: WebClient, mocker: MockerFixture) -> None:
+ """
+ must make HTTP request
+ """
+ request_mock = mocker.patch("requests.Session.request")
+
+ assert web_client.make_request("GET", "url") is not None
+ assert web_client.make_request("GET", "url", params=[("param", "value")]) is not None
+
+ assert web_client.make_request("POST", "url") is not None
+ assert web_client.make_request("POST", "url", json={"param": "value"}) is not None
+
+ assert web_client.make_request("DELETE", "url") is not None
+
+ request_mock.assert_has_calls([
+ MockCall("GET", "url", params=None, json=None),
+ MockCall().raise_for_status(),
+ MockCall("GET", "url", params=[("param", "value")], json=None),
+ MockCall().raise_for_status(),
+ MockCall("POST", "url", params=None, json=None),
+ MockCall().raise_for_status(),
+ MockCall("POST", "url", params=None, json={"param": "value"}),
+ MockCall().raise_for_status(),
+ MockCall("DELETE", "url", params=None, json=None),
+ MockCall().raise_for_status(),
+ ])
+
+
+def test_make_request_failed(web_client: WebClient, mocker: MockerFixture) -> None:
+ """
+ must make HTTP request
+ """
+ mocker.patch("requests.Session.request", side_effect=Exception())
+ assert web_client.make_request("GET", "url") is None
+
+
+def test_package_add(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must process package addition
"""
- requests_mock = mocker.patch("requests.Session.post")
+ requests_mock = mocker.patch("requests.Session.request")
payload = pytest.helpers.get_package_status(package_ahriman)
- web_client.add(package_ahriman, BuildStatusEnum.Unknown)
- requests_mock.assert_called_once_with(pytest.helpers.anyvar(str, True), json=payload)
+ web_client.package_add(package_ahriman, BuildStatusEnum.Unknown)
+ requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), params=None, json=payload)
-def test_add_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+def test_package_add_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during addition
"""
- mocker.patch("requests.Session.post", side_effect=Exception())
- web_client.add(package_ahriman, BuildStatusEnum.Unknown)
+ mocker.patch("requests.Session.request", side_effect=Exception())
+ web_client.package_add(package_ahriman, BuildStatusEnum.Unknown)
-def test_add_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+def test_package_add_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress HTTP exception happened during addition
"""
- mocker.patch("requests.Session.post", side_effect=requests.exceptions.HTTPError())
- web_client.add(package_ahriman, BuildStatusEnum.Unknown)
+ mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError())
+ web_client.package_add(package_ahriman, BuildStatusEnum.Unknown)
-def test_add_failed_suppress(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+def test_package_add_failed_suppress(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during addition and don't log
"""
web_client.suppress_errors = True
- mocker.patch("requests.Session.post", side_effect=Exception())
+ mocker.patch("requests.Session.request", side_effect=Exception())
logging_mock = mocker.patch("logging.exception")
- web_client.add(package_ahriman, BuildStatusEnum.Unknown)
+ web_client.package_add(package_ahriman, BuildStatusEnum.Unknown)
logging_mock.assert_not_called()
-def test_add_failed_http_error_suppress(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+def test_package_add_failed_http_error_suppress(web_client: WebClient, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
"""
must suppress HTTP exception happened during addition and don't log
"""
web_client.suppress_errors = True
- mocker.patch("requests.Session.post", side_effect=requests.exceptions.HTTPError())
+ mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError())
logging_mock = mocker.patch("logging.exception")
- web_client.add(package_ahriman, BuildStatusEnum.Unknown)
+ web_client.package_add(package_ahriman, BuildStatusEnum.Unknown)
logging_mock.assert_not_called()
-def test_get_all(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+def test_package_get_all(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return all packages status
"""
response = [pytest.helpers.get_package_status_extended(package_ahriman)]
- response_obj = Response()
+ response_obj = requests.Response()
response_obj._content = json.dumps(response).encode("utf8")
response_obj.status_code = 200
- requests_mock = mocker.patch("requests.Session.get", return_value=response_obj)
+ requests_mock = mocker.patch("requests.Session.request", return_value=response_obj)
- result = web_client.get(None)
- requests_mock.assert_called_once_with(web_client._package_url())
+ result = web_client.package_get(None)
+ requests_mock.assert_called_once_with("GET", web_client._package_url(), params=None, json=None)
assert len(result) == len(response)
assert (package_ahriman, BuildStatusEnum.Unknown) in [(package, status.status) for package, status in result]
-def test_get_failed(web_client: WebClient, mocker: MockerFixture) -> None:
+def test_package_get_failed(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during status getting
"""
- mocker.patch("requests.Session.get", side_effect=Exception())
- assert web_client.get(None) == []
+ mocker.patch("requests.Session.request", side_effect=Exception())
+ assert web_client.package_get(None) == []
-def test_get_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
+def test_package_get_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must suppress HTTP exception happened during status getting
"""
- mocker.patch("requests.Session.get", side_effect=requests.exceptions.HTTPError())
- assert web_client.get(None) == []
+ mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError())
+ assert web_client.package_get(None) == []
-def test_get_single(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+def test_package_get_single(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return single package status
"""
response = [pytest.helpers.get_package_status_extended(package_ahriman)]
- response_obj = Response()
+ response_obj = requests.Response()
response_obj._content = json.dumps(response).encode("utf8")
response_obj.status_code = 200
- requests_mock = mocker.patch("requests.Session.get", return_value=response_obj)
+ requests_mock = mocker.patch("requests.Session.request", return_value=response_obj)
- result = web_client.get(package_ahriman.base)
- requests_mock.assert_called_once_with(web_client._package_url(package_ahriman.base))
+ result = web_client.package_get(package_ahriman.base)
+ requests_mock.assert_called_once_with("GET", web_client._package_url(package_ahriman.base),
+ params=None, json=None)
assert len(result) == len(response)
assert (package_ahriman, BuildStatusEnum.Unknown) in [(package, status.status) for package, status in result]
-def test_get_internal(web_client: WebClient, mocker: MockerFixture) -> None:
- """
- must return web service status
- """
- status = InternalStatus(status=BuildStatus(), architecture="x86_64")
- response_obj = Response()
- response_obj._content = json.dumps(status.view()).encode("utf8")
- response_obj.status_code = 200
-
- requests_mock = mocker.patch("requests.Session.get", return_value=response_obj)
-
- result = web_client.get_internal()
- requests_mock.assert_called_once_with(web_client._status_url)
- assert result.architecture == "x86_64"
-
-
-def test_get_internal_failed(web_client: WebClient, mocker: MockerFixture) -> None:
- """
- must suppress any exception happened during web service status getting
- """
- mocker.patch("requests.Session.get", side_effect=Exception())
- assert web_client.get_internal().architecture is None
-
-
-def test_get_internal_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
- """
- must suppress HTTP exception happened during web service status getting
- """
- mocker.patch("requests.Session.get", side_effect=requests.exceptions.HTTPError())
- assert web_client.get_internal().architecture is None
-
-
-def test_logs(web_client: WebClient, log_record: logging.LogRecord, package_ahriman: Package,
- mocker: MockerFixture) -> None:
+def test_package_logs(web_client: WebClient, log_record: logging.LogRecord, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
"""
must process log record
"""
- requests_mock = mocker.patch("requests.Session.post")
+ requests_mock = mocker.patch("requests.Session.request")
payload = {
"created": log_record.created,
"message": log_record.getMessage(),
"process_id": log_record.process,
}
- web_client.logs(package_ahriman.base, log_record)
- requests_mock.assert_called_once_with(pytest.helpers.anyvar(str, True), json=payload)
+ web_client.package_logs(package_ahriman.base, log_record)
+ requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), params=None, json=payload)
-def test_log_failed(web_client: WebClient, log_record: logging.LogRecord, package_ahriman: Package,
- mocker: MockerFixture) -> None:
+def test_package_logs_failed(web_client: WebClient, log_record: logging.LogRecord, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
"""
must pass exception during log post
"""
- mocker.patch("requests.Session.post", side_effect=Exception())
+ mocker.patch("requests.Session.request", side_effect=Exception())
log_record.package_base = package_ahriman.base
- with pytest.raises(Exception):
- web_client.logs(package_ahriman.base, log_record)
+ web_client.package_logs(package_ahriman.base, log_record)
-def test_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+def test_package_logs_failed_http_error(web_client: WebClient, log_record: logging.LogRecord, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
+ """
+ must pass exception during log post
+ """
+ mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError())
+ log_record.package_base = package_ahriman.base
+ web_client.package_logs(package_ahriman.base, log_record)
+
+
+def test_package_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must process package removal
"""
- requests_mock = mocker.patch("requests.Session.delete")
+ requests_mock = mocker.patch("requests.Session.request")
- web_client.remove(package_ahriman.base)
- requests_mock.assert_called_once_with(pytest.helpers.anyvar(str, True))
+ web_client.package_remove(package_ahriman.base)
+ requests_mock.assert_called_once_with("DELETE", pytest.helpers.anyvar(str, True), params=None, json=None)
-def test_remove_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+def test_package_remove_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during removal
"""
- mocker.patch("requests.Session.delete", side_effect=Exception())
- web_client.remove(package_ahriman.base)
+ mocker.patch("requests.Session.request", side_effect=Exception())
+ web_client.package_remove(package_ahriman.base)
-def test_remove_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+def test_package_remove_failed_http_error(web_client: WebClient, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
"""
must suppress HTTP exception happened during removal
"""
- mocker.patch("requests.Session.delete", side_effect=requests.exceptions.HTTPError())
- web_client.remove(package_ahriman.base)
+ mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError())
+ web_client.package_remove(package_ahriman.base)
-def test_update(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+def test_package_update(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must process package update
"""
- requests_mock = mocker.patch("requests.Session.post")
+ requests_mock = mocker.patch("requests.Session.request")
- web_client.update(package_ahriman.base, BuildStatusEnum.Unknown)
- requests_mock.assert_called_once_with(pytest.helpers.anyvar(str, True), json={
- "status": BuildStatusEnum.Unknown.value})
+ web_client.package_update(package_ahriman.base, BuildStatusEnum.Unknown)
+ requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), params=None, json={
+ "status": BuildStatusEnum.Unknown.value
+ })
-def test_update_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+def test_package_update_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during update
"""
- mocker.patch("requests.Session.post", side_effect=Exception())
- web_client.update(package_ahriman.base, BuildStatusEnum.Unknown)
+ mocker.patch("requests.Session.request", side_effect=Exception())
+ web_client.package_update(package_ahriman.base, BuildStatusEnum.Unknown)
-def test_update_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+def test_package_update_failed_http_error(web_client: WebClient, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
"""
must suppress HTTP exception happened during update
"""
- mocker.patch("requests.Session.post", side_effect=requests.exceptions.HTTPError())
- web_client.update(package_ahriman.base, BuildStatusEnum.Unknown)
+ mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError())
+ web_client.package_update(package_ahriman.base, BuildStatusEnum.Unknown)
-def test_update_self(web_client: WebClient, mocker: MockerFixture) -> None:
+def test_status_get(web_client: WebClient, mocker: MockerFixture) -> None:
+ """
+ must return web service status
+ """
+ status = InternalStatus(status=BuildStatus(), architecture="x86_64")
+ response_obj = requests.Response()
+ response_obj._content = json.dumps(status.view()).encode("utf8")
+ response_obj.status_code = 200
+
+ requests_mock = mocker.patch("requests.Session.request", return_value=response_obj)
+
+ result = web_client.status_get()
+ requests_mock.assert_called_once_with("GET", web_client._status_url, params=None, json=None)
+ assert result.architecture == "x86_64"
+
+
+def test_status_get_failed(web_client: WebClient, mocker: MockerFixture) -> None:
+ """
+ must suppress any exception happened during web service status getting
+ """
+ mocker.patch("requests.Session.request", side_effect=Exception())
+ assert web_client.status_get().architecture is None
+
+
+def test_status_get_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
+ """
+ must suppress HTTP exception happened during web service status getting
+ """
+ mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError())
+ assert web_client.status_get().architecture is None
+
+
+def test_status_update(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must process service update
"""
- requests_mock = mocker.patch("requests.Session.post")
+ requests_mock = mocker.patch("requests.Session.request")
- web_client.update_self(BuildStatusEnum.Unknown)
- requests_mock.assert_called_once_with(pytest.helpers.anyvar(str, True), json={
- "status": BuildStatusEnum.Unknown.value})
+ web_client.status_update(BuildStatusEnum.Unknown)
+ requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), params=None, json={
+ "status": BuildStatusEnum.Unknown.value
+ })
-def test_update_self_failed(web_client: WebClient, mocker: MockerFixture) -> None:
+def test_status_update_self_failed(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during service update
"""
- mocker.patch("requests.Session.post", side_effect=Exception())
- web_client.update_self(BuildStatusEnum.Unknown)
+ mocker.patch("requests.Session.request", side_effect=Exception())
+ web_client.status_update(BuildStatusEnum.Unknown)
-def test_update_self_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
+def test_status_update_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must suppress HTTP exception happened during service update
"""
- mocker.patch("requests.Session.post", side_effect=requests.exceptions.HTTPError())
- web_client.update_self(BuildStatusEnum.Unknown)
+ mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError())
+ web_client.status_update(BuildStatusEnum.Unknown)
diff --git a/tests/ahriman/models/test_report_settings.py b/tests/ahriman/models/test_report_settings.py
index 1b14caea..527eb1ee 100644
--- a/tests/ahriman/models/test_report_settings.py
+++ b/tests/ahriman/models/test_report_settings.py
@@ -23,3 +23,6 @@ def test_from_option_valid() -> None:
assert ReportSettings.from_option("telegram") == ReportSettings.Telegram
assert ReportSettings.from_option("TElegraM") == ReportSettings.Telegram
+
+ assert ReportSettings.from_option("remote-call") == ReportSettings.RemoteCall
+ assert ReportSettings.from_option("reMOte-cALL") == ReportSettings.RemoteCall
diff --git a/tests/ahriman/models/test_waiter.py b/tests/ahriman/models/test_waiter.py
new file mode 100644
index 00000000..b98e757c
--- /dev/null
+++ b/tests/ahriman/models/test_waiter.py
@@ -0,0 +1,29 @@
+import time
+
+from ahriman.models.waiter import Waiter
+
+
+def test_is_timed_out() -> None:
+ """
+ must correctly check if timer runs out
+ """
+ assert Waiter(-1).is_timed_out()
+ assert Waiter(1, start_time=time.monotonic() - 10.0).is_timed_out()
+ assert not Waiter(1, start_time=time.monotonic() + 10.0).is_timed_out()
+
+
+def test_is_timed_out_infinite() -> None:
+ """
+ must treat 0 wait timeout as infinite
+ """
+ assert not Waiter(0).is_timed_out()
+ assert not Waiter(0, start_time=time.monotonic() - 10.0).is_timed_out()
+
+
+def test_wait() -> None:
+ """
+ must wait until file will disappear
+ """
+ results = iter([True, False])
+ waiter = Waiter(1, interval=1)
+ assert waiter.wait(lambda: next(results)) > 0
diff --git a/tests/testresources/core/ahriman.ini b/tests/testresources/core/ahriman.ini
index b614fe30..cac44f1c 100644
--- a/tests/testresources/core/ahriman.ini
+++ b/tests/testresources/core/ahriman.ini
@@ -74,6 +74,9 @@ homepage =
link_path =
template_path = ../web/templates/repo-index.jinja2
+[remote-call]
+manual = yes
+
[telegram]
api_key = apikey
chat_id = @ahrimantestchat