forked from TrueCloudLab/frostfs-testlib
science
This commit is contained in:
parent
f072f88673
commit
436c61f635
6 changed files with 81 additions and 19 deletions
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue