diff --git a/utils/_decorators.py b/utils/_decorators.py index 6fec427d5d..d2a54196c9 100644 --- a/utils/_decorators.py +++ b/utils/_decorators.py @@ -1,6 +1,8 @@ import inspect import os import re +from functools import partial +import enum import pytest import semantic_version as semver @@ -14,6 +16,14 @@ def configure(config: pytest.Config): pass # nothing to do right now +class _DecoratorType(enum.StrEnum): + BUG = "bug" + FLAKY = "flaky" + IRRELEVANT = "irrelevant" + MISSING_FEATURE = "missing_feature" + INCOMPLETE_TEST_APP = "incomplete_test_app" + + # semver module offers two spec engine : # 1. SimpleSpec : not a good fit because it does not allows OR clause # 2. NpmSpec : not a good fit because it disallow prerelease version by default (6.0.0-pre is not in ">=5.0.0") @@ -48,36 +58,20 @@ def _ensure_jira_ticket_as_reason(item, reason: str): pytest.exit(f"Please set a jira ticket for {nodeid}, instead of reason: {reason}", 1) -def _get_skipped_item(item, skip_reason): +def _add_pytest_marker(item, reason, marker): if inspect.isfunction(item) or inspect.isclass(item): if not hasattr(item, "pytestmark"): item.pytestmark = [] - item.pytestmark.append(pytest.mark.skip(reason=skip_reason)) - + item.pytestmark.append(marker(reason=reason)) else: raise ValueError(f"Unexpected skipped object: {item}") return item -def _get_expected_failure_item(item, skip_reason, force_skip: bool = False): - if inspect.isfunction(item) or inspect.isclass(item): - if not hasattr(item, "pytestmark"): - item.pytestmark = [] - - if force_skip: - item.pytestmark.append(pytest.mark.skip(reason=skip_reason)) - else: - item.pytestmark.append(pytest.mark.xfail(reason=skip_reason)) - else: - raise ValueError(f"Unexpected skipped object: {item}") - - return item - - -def _should_skip(condition=None, library=None, weblog_variant=None): - if condition is not None and not condition: +def _expected_to_fail(condition=None, library=None, weblog_variant=None): + if condition is False: return False if weblog_variant is not None and weblog_variant != context.weblog_variant: @@ -105,104 +99,88 @@ def _should_skip(condition=None, library=None, weblog_variant=None): return True -def missing_feature(condition: bool = None, library=None, weblog_variant=None, reason=None, force_skip: bool = False): - """decorator, allow to mark a test function/class as missing""" - - skip = _should_skip(library=library, weblog_variant=weblog_variant, condition=condition) - - def decorator(function_or_class): - if inspect.isclass(function_or_class): - assert condition is not None or (library is None and weblog_variant is None), _MANIFEST_ERROR_MESSAGE - - if not skip: - return function_or_class - - full_reason = "missing_feature" if reason is None else f"missing_feature ({reason})" - - return _get_expected_failure_item(function_or_class, full_reason, force_skip=force_skip) - - return decorator - - -def incomplete_test_app( - condition: bool = None, library=None, weblog_variant=None, reason=None, force_skip: bool = False -): - """decorator, marks tests that rely on endpoints in test apps that are incomplete or missing""" - - skip = _should_skip(library=library, weblog_variant=weblog_variant, condition=condition) +def _decorator(function_or_class, marker, decorator_type, condition, library, weblog_variant, reason): + expected_to_fail = _expected_to_fail(library=library, weblog_variant=weblog_variant, condition=condition) - def decorator(function_or_class): - if inspect.isclass(function_or_class): - assert condition is not None or (library is None and weblog_variant is None), _MANIFEST_ERROR_MESSAGE + if inspect.isclass(function_or_class): + assert condition is not None or (library is None and weblog_variant is None), _MANIFEST_ERROR_MESSAGE - if not skip: - return function_or_class + if decorator_type in (_DecoratorType.BUG, _DecoratorType.FLAKY): + _ensure_jira_ticket_as_reason(function_or_class, reason) - full_reason = "incomplete_test_app" if reason is None else f"incomplete_test_app ({reason})" + full_reason = decorator_type.value if reason is None else f"{decorator_type.value} ({reason})" + if not expected_to_fail: + return function_or_class + return _add_pytest_marker(function_or_class, full_reason, marker) - return _get_expected_failure_item(function_or_class, full_reason, force_skip=force_skip) - return decorator +def missing_feature(condition=None, library=None, weblog_variant=None, reason=None, force_skip: bool = False): + """decorator, allow to mark a test function/class as missing""" + marker = pytest.mark.skip if force_skip else pytest.mark.xfail + return partial( + _decorator, + marker=marker, + decorator_type=_DecoratorType.MISSING_FEATURE, + condition=condition, + library=library, + weblog_variant=weblog_variant, + reason=reason, + ) + + +def incomplete_test_app(condition=None, library=None, weblog_variant=None, reason=None): + """Decorator, allow to mark a test function/class as not compatible with the tested application""" + return partial( + _decorator, + marker=pytest.mark.xfail, + decorator_type=_DecoratorType.INCOMPLETE_TEST_APP, + condition=condition, + library=library, + weblog_variant=weblog_variant, + reason=reason, + ) def irrelevant(condition=None, library=None, weblog_variant=None, reason=None): """decorator, allow to mark a test function/class as not relevant""" - - skip = _should_skip(library=library, weblog_variant=weblog_variant, condition=condition) - - def decorator(function_or_class): - if inspect.isclass(function_or_class): - assert condition is not None, _MANIFEST_ERROR_MESSAGE - - if not skip: - return function_or_class - - full_reason = "irrelevant" if reason is None else f"irrelevant ({reason})" - return _get_skipped_item(function_or_class, full_reason) - - return decorator + return partial( + _decorator, + marker=pytest.mark.skip, + decorator_type=_DecoratorType.IRRELEVANT, + condition=condition, + library=library, + weblog_variant=weblog_variant, + reason=reason, + ) def bug(condition=None, library=None, weblog_variant=None, reason=None, force_skip: bool = False): """Decorator, allow to mark a test function/class as an known bug. The test is executed, and if it passes, and warning is reported """ - - expected_to_fail = _should_skip(library=library, weblog_variant=weblog_variant, condition=condition) - - def decorator(function_or_class): - if inspect.isclass(function_or_class): - assert condition is not None, _MANIFEST_ERROR_MESSAGE - - _ensure_jira_ticket_as_reason(function_or_class, reason) - - if not expected_to_fail: - return function_or_class - - full_reason = "bug" if reason is None else f"bug ({reason})" - return _get_expected_failure_item(function_or_class, full_reason, force_skip=force_skip) - - return decorator + marker = pytest.mark.skip if force_skip else pytest.mark.xfail + return partial( + _decorator, + marker=marker, + decorator_type=_DecoratorType.BUG, + condition=condition, + library=library, + weblog_variant=weblog_variant, + reason=reason, + ) def flaky(condition=None, library=None, weblog_variant=None, reason=None): """Decorator, allow to mark a test function/class as a known bug, and skip it""" - - skip = _should_skip(library=library, weblog_variant=weblog_variant, condition=condition) - - def decorator(function_or_class): - if inspect.isclass(function_or_class): - assert condition is not None, _MANIFEST_ERROR_MESSAGE - - _ensure_jira_ticket_as_reason(function_or_class, reason) - - if not skip: - return function_or_class - - full_reason = "flaky" if reason is None else f"flaky ({reason})" - return _get_skipped_item(function_or_class, full_reason) - - return decorator + return partial( + _decorator, + marker=pytest.mark.skip, + decorator_type=_DecoratorType.FLAKY, + condition=condition, + library=library, + weblog_variant=weblog_variant, + reason=reason, + ) def released( @@ -282,16 +260,16 @@ def compute_declaration(only_for_library, component_name, declaration, tested_ve for reason in skip_reasons: if reason.startswith("flaky"): _ensure_jira_ticket_as_reason(test_class, reason[7:-1]) - return _get_skipped_item(test_class, reason) + return _add_pytest_marker(test_class, reason, pytest.mark.skip) if reason.startswith("irrelevant"): - return _get_skipped_item(test_class, reason) + return _add_pytest_marker(test_class, reason, pytest.mark.skip) # Otherwise, it's either bug, or missing_feature. Take the first one if reason.startswith("bug"): _ensure_jira_ticket_as_reason(test_class, reason[5:-1]) - return _get_expected_failure_item(test_class, reason) + return _add_pytest_marker(test_class, reason, pytest.mark.xfail) return test_class