forked from TrueCloudLab/frostfs-testlib
Compare commits
1 commit
master
...
feature-sc
Author | SHA1 | Date | |
---|---|---|---|
436c61f635 |
6 changed files with 81 additions and 19 deletions
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue