This commit is contained in:
Andrey Berezin 2023-11-24 13:40:11 +03:00
parent f072f88673
commit 436c61f635
6 changed files with 81 additions and 19 deletions

View file

@ -1,16 +1,14 @@
from typing import Callable from typing import Callable
from frostfs_testlib import reporter
from frostfs_testlib.cli.frostfs_cli.cli import FrostfsCli from frostfs_testlib.cli.frostfs_cli.cli import FrostfsCli
from frostfs_testlib.healthcheck.interfaces import Healthcheck 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.resources.cli import FROSTFS_CLI_EXEC
from frostfs_testlib.shell import CommandOptions from frostfs_testlib.shell import CommandOptions
from frostfs_testlib.steps.node_management import storage_node_healthcheck from frostfs_testlib.steps.node_management import storage_node_healthcheck
from frostfs_testlib.storage.cluster import ClusterNode from frostfs_testlib.storage.cluster import ClusterNode
from frostfs_testlib.testing.test_control import wait_for_success from frostfs_testlib.testing.test_control import wait_for_success
reporter = get_reporter()
class BasicHealthcheck(Healthcheck): class BasicHealthcheck(Healthcheck):
def _perform(self, cluster_node: ClusterNode, checks: dict[Callable, dict]): 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}"): with reporter.step(f"Perform storage healthcheck on {cluster_node}"):
self._perform(cluster_node, checks) 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: def _storage_healthcheck(self, cluster_node: ClusterNode) -> str | None:
result = storage_node_healthcheck(cluster_node.storage_node) result = storage_node_healthcheck(cluster_node.storage_node)
self._gather_socket_info(cluster_node) self._gather_socket_info(cluster_node)
if result.health_status != "READY" or result.network_status != "ONLINE": 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}" 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: def _tree_healthcheck(self, cluster_node: ClusterNode) -> str | None:
host = cluster_node.host host = cluster_node.host
service_config = host.get_service_config(cluster_node.storage_node.name) 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}" 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): def _gather_socket_info(self, cluster_node: ClusterNode):
cluster_node.host.get_shell().exec("ss -tuln | grep 8080", CommandOptions(check=False)) cluster_node.host.get_shell().exec("ss -tuln | grep 8080", CommandOptions(check=False))

View file

@ -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.interfaces import ReporterHandler
from frostfs_testlib.reporter.reporter import Reporter from frostfs_testlib.reporter.reporter import Reporter
@ -15,3 +15,7 @@ def get_reporter() -> Reporter:
Singleton reporter instance. Singleton reporter instance.
""" """
return __reporter return __reporter
def step(title: str):
return __reporter.step(title)

View file

@ -1,6 +1,8 @@
import logging
import os import os
from contextlib import AbstractContextManager from contextlib import AbstractContextManager, ContextDecorator
from textwrap import shorten from textwrap import shorten
from types import TracebackType
from typing import Any, Callable from typing import Any, Callable
import allure import allure
@ -9,10 +11,46 @@ from allure import attachment_type
from frostfs_testlib.reporter.interfaces import ReporterHandler 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): class AllureHandler(ReporterHandler):
"""Handler that stores test artifacts in Allure report.""" """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="...") name = shorten(name, width=140, placeholder="...")
return allure.step(name) return allure.step(name)

View file

@ -1,5 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from contextlib import AbstractContextManager from contextlib import AbstractContextManager, ContextDecorator
from typing import Any, Callable from typing import Any, Callable
@ -7,7 +7,7 @@ class ReporterHandler(ABC):
"""Interface of handler that stores test artifacts in some reporting tool.""" """Interface of handler that stores test artifacts in some reporting tool."""
@abstractmethod @abstractmethod
def step(self, name: str) -> AbstractContextManager: def step(self, name: str) -> AbstractContextManager | ContextDecorator:
"""Register a new step in test execution. """Register a new step in test execution.
Args: Args:

View file

@ -81,11 +81,11 @@ class Reporter:
Returns: Returns:
Step context. Step context.
""" """
if not self.handlers:
return _empty_step()
step_contexts = [handler.step(name) for handler in self.handlers] 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: def attach(self, content: Any, file_name: str) -> None:
"""Attach specified content with given file name to the test report. """Attach specified content with given file name to the test report.
@ -104,9 +104,10 @@ class AggregateContextManager(AbstractContextManager):
contexts: list[AbstractContextManager] contexts: list[AbstractContextManager]
def __init__(self, contexts: list[AbstractContextManager]) -> None: def __init__(self, contexts: list[AbstractContextManager], decorated_wrapper: Callable) -> None:
super().__init__() super().__init__()
self.contexts = contexts self.contexts = contexts
self.wrapper = decorated_wrapper
def __enter__(self): def __enter__(self):
for context in self.contexts: for context in self.contexts:
@ -127,3 +128,6 @@ class AggregateContextManager(AbstractContextManager):
# If all context agreed to suppress exception, then suppress it; # If all context agreed to suppress exception, then suppress it;
# otherwise return None to reraise # otherwise return None to reraise
return True if all(suppress_decisions) else None return True if all(suppress_decisions) else None
def __call__(self, *args: Any, **kwds: Any) -> Any:
return self.wrapper(*args, **kwds)

View file

@ -2,11 +2,14 @@ import inspect
import logging import logging
from functools import wraps from functools import wraps
from time import sleep, time from time import sleep, time
from typing import Any from typing import Any, Callable
from _pytest.outcomes import Failed from _pytest.outcomes import Failed
from allure_commons.utils import func_parameters, represent
from pytest import fail from pytest import fail
from frostfs_testlib import reporter
logger = logging.getLogger("NeoLogger") logger = logging.getLogger("NeoLogger")
# TODO: we may consider deprecating some methods here and use tenacity instead # TODO: we may consider deprecating some methods here and use tenacity instead
@ -124,6 +127,7 @@ def wait_for_success(
expected_result: Any = None, expected_result: Any = None,
fail_testcase: bool = False, fail_testcase: bool = False,
fail_message: str = "", fail_message: str = "",
title: str = None,
): ):
""" """
Decorator to wait for some conditions/functions to pass successfully. Decorator to wait for some conditions/functions to pass successfully.
@ -134,8 +138,7 @@ def wait_for_success(
""" """
def wrapper(func): def wrapper(func):
@wraps(func) def call(func, *a, **kw):
def impl(*a, **kw):
start = int(round(time())) start = int(round(time()))
last_exception = None last_exception = None
while start + max_wait_time >= int(round(time())): while start + max_wait_time >= int(round(time())):
@ -160,6 +163,21 @@ def wait_for_success(
if last_exception is not None: if last_exception is not None:
raise last_exception 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 impl
return wrapper 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)