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 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))

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

View file

@ -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)

View file

@ -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:

View file

@ -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)

View file

@ -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)