Skip to content

Commit

Permalink
collect_all on fixture wrapper end
Browse files Browse the repository at this point in the history
  • Loading branch information
eliorerz committed Aug 4, 2021
1 parent b4342b3 commit e25f510
Show file tree
Hide file tree
Showing 22 changed files with 255 additions and 722 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,4 @@ class TestSomeThing:
| Variable | Description |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| JUNIT_REPORT_DIR | Reports directory where the reports will be extracted. If it does not exist - create it. |
| FAIL_ON_MISSING_SUITE | Decide whether the test will throw a SuiteNotExistError exception if a JunitTestCase does not have a suite parent. <br/> Available values: 'True', 'true', 'y', 'Y', '1' |
3 changes: 2 additions & 1 deletion src/junit_report/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from ._junit_fixture_test_case import JunitFixtureTestCase
from ._junit_test_case import CaseFailure, JunitTestCase, TestCaseCategories
from ._junit_test_suite import DuplicateSuiteError, JunitTestSuite
from ._junit_test_suite import DuplicateSuiteError, JunitTestSuite, SuiteNotExistError

__all__ = [
"JunitTestCase",
"CaseFailure",
"TestCaseCategories",
"JunitFixtureTestCase",
"JunitTestSuite",
"SuiteNotExistError",
"DuplicateSuiteError",
]
36 changes: 4 additions & 32 deletions src/junit_report/_junit_decorator.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
import inspect
import re
import time
from abc import ABC, abstractmethod
from contextlib import suppress
from typing import Any, Callable, List, Tuple
from typing import Any, Callable

import decorator
import pytest

from ._test_case_data import MainRunner


class JunitDecorator(ABC):
def __init__(self) -> None:
self._func = None
self._start_time = None
self._stack_locals = list()
self._pytest_function = None
self._main_runner = MainRunner.NONE

def __call__(self, function: Callable) -> Callable:
"""
Expand Down Expand Up @@ -45,8 +36,7 @@ def _wrapper(self, function: Callable, *args, **kwargs):
except BaseException as e:
self._on_exception(e)
finally:
with suppress(BaseException):
self._on_wrapper_end()
self._on_wrapper_end()
return value

def _get_class_name(self) -> str:
Expand All @@ -63,11 +53,10 @@ def _get_class_name(self) -> str:
return module.__name__

@abstractmethod
def _on_wrapper_end(self) -> bool:
def _on_wrapper_end(self) -> None:
"""
Executed after execution finished (successfully or not)
:return: if success return True, else return False
:raises None - this function must not raise exception
:return: None
"""

def _on_call(self) -> None:
Expand Down Expand Up @@ -99,20 +88,3 @@ def _on_wrapper_start(self, function) -> None:
This function executed when wrapper function starts
:return: None
"""
self._start_time = time.time()
self._stack_locals = [frame_info.frame.f_locals for frame_info in inspect.stack()]
try:
self._pytest_function = [
stack_local
for stack_local in self._stack_locals
if "self" in stack_local and isinstance(stack_local["self"], pytest.Function)][0]["self"]
self._main_runner = MainRunner.PYTEST
except IndexError:
self._main_runner = MainRunner.PYTHON

def get_pytest_parameterized(self, pytest_function: pytest.Function) -> List[Tuple[str, Any]]:
return (
list(sorted(pytest_function.callspec.params.items()))
if pytest_function and pytest_function.own_markers
else list()
)
90 changes: 79 additions & 11 deletions src/junit_report/_junit_fixture_test_case.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from contextlib import suppress
import re
from types import GeneratorType
from typing import Callable, Union
from typing import Callable, List, Union

import decorator
from _pytest.mark import Mark
from _pytest.python import Function

from ._junit_test_case import JunitTestCase, TestCaseCategories
from ._junit_test_suite import JunitTestSuite
Expand Down Expand Up @@ -39,8 +41,7 @@ def wrapper(_, *args, **kwargs):
self._add_failure(e, self.AFTER_YIELD_EXCEPTION_MESSAGE_PREFIX)
raise
finally:
suite_key = self.get_suite_key()
JunitTestSuite.fixture_cleanup(self._case_data, suite_key)
JunitTestSuite.fixture_cleanup(self._data, self.get_suite_key())

return decorator.decorator(wrapper, function)

Expand All @@ -49,8 +50,10 @@ def _teardown_yield_fixture(cls, it) -> None:
"""Execute the teardown of a fixture function by advancing the iterator
after the yield and ensure the iteration ends (if not it means there is
more than one yield in the function)."""
with suppress(StopIteration):
try:
next(it)
except StopIteration:
pass

def _execute_wrapped_function(self, *args, **kwargs):
generator = super()._execute_wrapped_function(*args, **kwargs)
Expand All @@ -67,11 +70,76 @@ def _on_wrapper_end(self):
collect_all that trigger the suite to collect all cases and export them into xml
:return: None
"""
self._case_data.case.category = TestCaseCategories.FIXTURE.value
self._data.case.category = TestCaseCategories.FIXTURE

super(JunitFixtureTestCase, self)._on_wrapper_end()
if len(self._data.case.failures) > 0:
JunitTestSuite.collect_all()

def _get_suite(self) -> Union[Callable, None]:
"""
Get suite function as unique key.
This function handles also on case that the test suite function decorated with pytest.mark.parametrize
:return: Wrapped Suite function instance
"""

for f_locals in [
stack_local
for stack_local in self._stack_locals
if "self" in stack_local and isinstance(stack_local["self"], Function)
]:
func = f_locals["self"]

try:
# if pytest.mark.parametrize exist, get actual function from class while ignoring the add parameters
if hasattr(func, "own_markers") and len(func.own_markers) > 0:
mark_function = self._get_mark_function(func.own_markers, func)
if mark_function:
return mark_function

if func.cls and self._is_suite_exist(func.cls, func.name):
return getattr(func.cls, func.name).__wrapped__

if func.cls is None and self._is_suite_exist(func.module, func.name):
return getattr(func.module, func.name).__wrapped__
except AttributeError:
pass

return None

@classmethod
def _is_suite_exist(cls, obj: Union, func_name: str):
func = getattr(obj, func_name)
if hasattr(func, "__wrapped__"):
return JunitTestSuite.is_suite_exist(func.__wrapped__)
return False

def _get_mark_function(self, own_markers: List[Mark], func: Function) -> Union[Callable, None]:
"""
If mark parameterize decorate test suite with given fixture _get_mark_function is searching
for all parametrize arguments and permutations.
If mark type is parametrize, set self._parametrize with the current argument permutation.
The representation of function name and arguments for N parametrize marks are as followed:
func.name = some_func_name[param1-param2-...-paramN]
:param own_markers: Pytest markers taken from func stack_locals
:param func: wrapped function contained with pytest Function object
:return: wrapped function itself
"""
marks = [m for m in own_markers if m.name == "parametrize"]
marks_count = len(marks)

if marks_count == 0:
return None

params_regex = "-".join(["(.*?)"] * marks_count)
args = list(re.compile(r"(.*?)\[{0}]".format(params_regex)).findall(func.name).pop())
func_name = args.pop(0)

success = super(JunitFixtureTestCase, self)._on_wrapper_end()
if not success:
return success
JunitTestSuite.collect_all()
params = list()
for i in range(marks_count):
params.append((marks[i].args[0], args[i]))

return success
if func.cls:
return getattr(func.cls, func_name).__wrapped__
else:
return getattr(func.module, func_name).__wrapped__
Loading

0 comments on commit e25f510

Please sign in to comment.