From 436c61f635aa88572ab7abc442008149804e64bc Mon Sep 17 00:00:00 2001 From: Andrey Berezin Date: Fri, 24 Nov 2023 13:40:11 +0300 Subject: [PATCH] science --- .../healthcheck/basic_healthcheck.py | 10 ++--- src/frostfs_testlib/reporter/__init__.py | 6 ++- .../reporter/allure_handler.py | 42 ++++++++++++++++++- src/frostfs_testlib/reporter/interfaces.py | 4 +- src/frostfs_testlib/reporter/reporter.py | 14 ++++--- src/frostfs_testlib/testing/test_control.py | 24 +++++++++-- 6 files changed, 81 insertions(+), 19 deletions(-) diff --git a/src/frostfs_testlib/healthcheck/basic_healthcheck.py b/src/frostfs_testlib/healthcheck/basic_healthcheck.py index 6f21534..5108ff3 100644 --- a/src/frostfs_testlib/healthcheck/basic_healthcheck.py +++ b/src/frostfs_testlib/healthcheck/basic_healthcheck.py @@ -1,16 +1,14 @@ from typing import Callable +from frostfs_testlib import reporter from frostfs_testlib.cli.frostfs_cli.cli import FrostfsCli from frostfs_testlib.healthcheck.interfaces import Healthcheck -from frostfs_testlib.reporter import get_reporter from frostfs_testlib.resources.cli import FROSTFS_CLI_EXEC from frostfs_testlib.shell import CommandOptions from frostfs_testlib.steps.node_management import storage_node_healthcheck from frostfs_testlib.storage.cluster import ClusterNode from frostfs_testlib.testing.test_control import wait_for_success -reporter = get_reporter() - class BasicHealthcheck(Healthcheck): def _perform(self, cluster_node: ClusterNode, checks: dict[Callable, dict]): @@ -51,14 +49,14 @@ class BasicHealthcheck(Healthcheck): with reporter.step(f"Perform storage healthcheck on {cluster_node}"): self._perform(cluster_node, checks) - @reporter.step_deco("Storage healthcheck on {cluster_node}") + @reporter.step("Storage healthcheck on {cluster_node}") def _storage_healthcheck(self, cluster_node: ClusterNode) -> str | None: result = storage_node_healthcheck(cluster_node.storage_node) self._gather_socket_info(cluster_node) if result.health_status != "READY" or result.network_status != "ONLINE": return f"Node {cluster_node} is not healthy. Health={result.health_status}. Network={result.network_status}" - @reporter.step_deco("Tree healthcheck on {cluster_node}") + @reporter.step("Tree healthcheck on {cluster_node}") def _tree_healthcheck(self, cluster_node: ClusterNode) -> str | None: host = cluster_node.host service_config = host.get_service_config(cluster_node.storage_node.name) @@ -81,6 +79,6 @@ class BasicHealthcheck(Healthcheck): f"Error during tree healthcheck (rc={result.return_code}): {result.stdout}. \n Stderr: {result.stderr}" ) - @reporter.step_deco("Gather socket info for {cluster_node}") + @reporter.step("Gather socket info for {cluster_node}") def _gather_socket_info(self, cluster_node: ClusterNode): cluster_node.host.get_shell().exec("ss -tuln | grep 8080", CommandOptions(check=False)) diff --git a/src/frostfs_testlib/reporter/__init__.py b/src/frostfs_testlib/reporter/__init__.py index 10e4146..706a429 100644 --- a/src/frostfs_testlib/reporter/__init__.py +++ b/src/frostfs_testlib/reporter/__init__.py @@ -1,4 +1,4 @@ -from frostfs_testlib.reporter.allure_handler import AllureHandler +from frostfs_testlib.reporter.allure_handler import AllureHandler, StepLogger from frostfs_testlib.reporter.interfaces import ReporterHandler from frostfs_testlib.reporter.reporter import Reporter @@ -15,3 +15,7 @@ def get_reporter() -> Reporter: Singleton reporter instance. """ return __reporter + + +def step(title: str): + return __reporter.step(title) diff --git a/src/frostfs_testlib/reporter/allure_handler.py b/src/frostfs_testlib/reporter/allure_handler.py index fef815d..1edaca7 100644 --- a/src/frostfs_testlib/reporter/allure_handler.py +++ b/src/frostfs_testlib/reporter/allure_handler.py @@ -1,6 +1,8 @@ +import logging import os -from contextlib import AbstractContextManager +from contextlib import AbstractContextManager, ContextDecorator from textwrap import shorten +from types import TracebackType from typing import Any, Callable import allure @@ -9,10 +11,46 @@ from allure import attachment_type from frostfs_testlib.reporter.interfaces import ReporterHandler +class StepLoggerContext(AbstractContextManager, Callable): + INDENT = 0 + + def __init__(self, title: str): + self.title = title + self.logger = logging.getLogger("NeoLogger") + + def __enter__(self) -> Any: + indent = ">" * StepLoggerContext.INDENT * 2 + self.logger.info(f"{indent}> {self.title}") + StepLoggerContext.INDENT += 1 + + def __exit__( + self, + __exc_type: type[BaseException] | None, + __exc_value: BaseException | None, + __traceback: TracebackType | None, + ) -> bool | None: + StepLoggerContext.INDENT -= 1 + indent = "<" * StepLoggerContext.INDENT * 2 + self.logger.info(f"{indent}< {self.title}") + + +class StepLogger(ReporterHandler): + """Handler that prints steps to log.""" + + def step(self, name: str) -> AbstractContextManager | ContextDecorator: + return StepLoggerContext(name) + + def step_decorator(self, name: str) -> Callable: + return StepLoggerContext(name) + + def attach(self, body: Any, file_name: str) -> None: + pass + + class AllureHandler(ReporterHandler): """Handler that stores test artifacts in Allure report.""" - def step(self, name: str) -> AbstractContextManager: + def step(self, name: str) -> AbstractContextManager | ContextDecorator: name = shorten(name, width=140, placeholder="...") return allure.step(name) diff --git a/src/frostfs_testlib/reporter/interfaces.py b/src/frostfs_testlib/reporter/interfaces.py index b47a3fb..4e24feb 100644 --- a/src/frostfs_testlib/reporter/interfaces.py +++ b/src/frostfs_testlib/reporter/interfaces.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from contextlib import AbstractContextManager +from contextlib import AbstractContextManager, ContextDecorator from typing import Any, Callable @@ -7,7 +7,7 @@ class ReporterHandler(ABC): """Interface of handler that stores test artifacts in some reporting tool.""" @abstractmethod - def step(self, name: str) -> AbstractContextManager: + def step(self, name: str) -> AbstractContextManager | ContextDecorator: """Register a new step in test execution. Args: diff --git a/src/frostfs_testlib/reporter/reporter.py b/src/frostfs_testlib/reporter/reporter.py index d1c75f5..c742ec9 100644 --- a/src/frostfs_testlib/reporter/reporter.py +++ b/src/frostfs_testlib/reporter/reporter.py @@ -81,11 +81,11 @@ class Reporter: Returns: Step context. """ - if not self.handlers: - return _empty_step() - step_contexts = [handler.step(name) for handler in self.handlers] - return AggregateContextManager(step_contexts) + if not step_contexts: + step_contexts = [_empty_step()] + decorated_wrapper = self.step_deco(name) + return AggregateContextManager(step_contexts, decorated_wrapper) def attach(self, content: Any, file_name: str) -> None: """Attach specified content with given file name to the test report. @@ -104,9 +104,10 @@ class AggregateContextManager(AbstractContextManager): contexts: list[AbstractContextManager] - def __init__(self, contexts: list[AbstractContextManager]) -> None: + def __init__(self, contexts: list[AbstractContextManager], decorated_wrapper: Callable) -> None: super().__init__() self.contexts = contexts + self.wrapper = decorated_wrapper def __enter__(self): for context in self.contexts: @@ -127,3 +128,6 @@ class AggregateContextManager(AbstractContextManager): # If all context agreed to suppress exception, then suppress it; # otherwise return None to reraise return True if all(suppress_decisions) else None + + def __call__(self, *args: Any, **kwds: Any) -> Any: + return self.wrapper(*args, **kwds) diff --git a/src/frostfs_testlib/testing/test_control.py b/src/frostfs_testlib/testing/test_control.py index ed74f6a..f4ed146 100644 --- a/src/frostfs_testlib/testing/test_control.py +++ b/src/frostfs_testlib/testing/test_control.py @@ -2,11 +2,14 @@ import inspect import logging from functools import wraps from time import sleep, time -from typing import Any +from typing import Any, Callable from _pytest.outcomes import Failed +from allure_commons.utils import func_parameters, represent from pytest import fail +from frostfs_testlib import reporter + logger = logging.getLogger("NeoLogger") # TODO: we may consider deprecating some methods here and use tenacity instead @@ -124,6 +127,7 @@ def wait_for_success( expected_result: Any = None, fail_testcase: bool = False, fail_message: str = "", + title: str = None, ): """ Decorator to wait for some conditions/functions to pass successfully. @@ -134,8 +138,7 @@ def wait_for_success( """ def wrapper(func): - @wraps(func) - def impl(*a, **kw): + def call(func, *a, **kw): start = int(round(time())) last_exception = None while start + max_wait_time >= int(round(time())): @@ -160,6 +163,21 @@ def wait_for_success( if last_exception is not None: raise last_exception + @wraps(func) + def impl(*a, **kw): + if title is not None: + with reporter.step(_format_title(func, title, *a, **kw)): + return call(func, *a, **kw) + + return call(func, *a, **kw) + return impl return wrapper + + +def _format_title(__func: Callable, __title: str, *a, **kw) -> str: + params = func_parameters(__func, *a, **kw) + args = list(map(lambda x: represent(x), a)) + + return __title.format(*args, **params)