Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

remove duplicate code in decorators #3647

Merged
merged 11 commits into from
Dec 16, 2024
182 changes: 80 additions & 102 deletions utils/_decorators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import inspect
import os
import re
from functools import partial
import enum

import pytest
import semantic_version as semver
Expand All @@ -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")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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

Expand Down
Loading