diff --git a/allure-pytest-bdd/test/outline_test.py b/allure-pytest-bdd/test/outline_test.py index d176148a..fddfd11d 100644 --- a/allure-pytest-bdd/test/outline_test.py +++ b/allure-pytest-bdd/test/outline_test.py @@ -1,6 +1,8 @@ from pytest_bdd import scenario +import pytest +@pytest.mark.skip(reason="https://github.com/pytest-dev/pytest-bdd/issues/447") @scenario("../features/outline.feature", "Scenario outline") def test_scenario_outline(): pass diff --git a/allure-pytest/test/acceptance/attachment/attachment_step_test.py b/allure-pytest/test/acceptance/attachment/attachment_step_test.py index 41a85173..bdb14d44 100644 --- a/allure-pytest/test/acceptance/attachment/attachment_step_test.py +++ b/allure-pytest/test/acceptance/attachment/attachment_step_test.py @@ -14,3 +14,35 @@ def test_step_with_attachment(executed_docstring_path): ), ) ) + + +def test_step_with_thread_and_attachment(allured_testdir): + allured_testdir.testdir.makepyfile( + """ + from concurrent.futures import ThreadPoolExecutor + + import allure + import pytest + + @allure.step("thread {x}") + def parallel_step(x=1): + allure.attach("text", str(x), allure.attachment_type.TEXT) + + + def test_thread(): + with allure.step("Start in thread"): + with ThreadPoolExecutor(max_workers=2) as executor: + f_result = executor.map(parallel_step, [1, 2]) + """ + ) + + allured_testdir.run_with_allure() + + assert_that(allured_testdir.allure_report, + has_test_case("test_thread", + has_step("Start in thread", + has_step("thread 1", has_attachment(name="1")), + has_step("thread 2", has_attachment(name="2")), + ) + ) + ) diff --git a/allure-pytest/test/acceptance/step/test_step_with_several_step_inside_thread.py b/allure-pytest/test/acceptance/step/test_step_with_several_step_inside_thread.py new file mode 100644 index 00000000..5fdc56fd --- /dev/null +++ b/allure-pytest/test/acceptance/step/test_step_with_several_step_inside_thread.py @@ -0,0 +1,74 @@ +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_step +from hamcrest import assert_that + + +def test_step_with_thread(allured_testdir): + allured_testdir.testdir.makepyfile( + """ + from concurrent.futures import ThreadPoolExecutor + + import allure + + @allure.step("thread {x}") + def parallel_step(x=1): + with allure.step("Sub-step in thread"): + pass + + + def test_thread(): + with allure.step("Start in thread"): + with ThreadPoolExecutor(max_workers=2) as executor: + executor.map(parallel_step, [1, 2]) + """ + ) + + allured_testdir.run_with_allure() + + assert_that(allured_testdir.allure_report, + has_test_case("test_thread", + has_step("Start in thread", + has_step("thread 1", has_step("Sub-step in thread")), + has_step("thread 2") + ) + ) + ) + + +def test_step_with_reused_threads(allured_testdir): + allured_testdir.testdir.makepyfile( + """ + from concurrent.futures import ThreadPoolExecutor + + import allure + import random + from time import sleep + + @allure.step("thread {x}") + def parallel_step(x=1): + sleep(random.randint(0, 3)) + + def test_thread(): + with ThreadPoolExecutor(max_workers=2) as executor: + executor.map(parallel_step, range(1, 4)) + with allure.step("Reuse previous threads"): + with ThreadPoolExecutor(max_workers=2) as executor: + executor.map(parallel_step, range(1, 4)) + + """ + ) + + allured_testdir.run_with_allure() + + assert_that(allured_testdir.allure_report, + has_test_case("test_thread", + has_step("thread 1"), + has_step("thread 2"), + has_step("thread 3"), + has_step("Reuse previous threads", + has_step("thread 1"), + has_step("thread 2"), + has_step("thread 3"), + ), + ) + ) diff --git a/allure-python-commons/src/_core.py b/allure-python-commons/src/_core.py index 22219905..7f79d114 100644 --- a/allure-python-commons/src/_core.py +++ b/allure-python-commons/src/_core.py @@ -1,20 +1,19 @@ -import threading from six import with_metaclass from pluggy import PluginManager from allure_commons import _hooks class MetaPluginManager(type): - _storage = threading.local() + _plugin_manager: PluginManager = None @staticmethod def get_plugin_manager(): - if not hasattr(MetaPluginManager._storage, 'plugin_manager'): - MetaPluginManager._storage.plugin_manager = PluginManager('allure') - MetaPluginManager._storage.plugin_manager.add_hookspecs(_hooks.AllureUserHooks) - MetaPluginManager._storage.plugin_manager.add_hookspecs(_hooks.AllureDeveloperHooks) + if not MetaPluginManager._plugin_manager: + MetaPluginManager._plugin_manager = PluginManager('allure') + MetaPluginManager._plugin_manager.add_hookspecs(_hooks.AllureUserHooks) + MetaPluginManager._plugin_manager.add_hookspecs(_hooks.AllureDeveloperHooks) - return MetaPluginManager._storage.plugin_manager + return MetaPluginManager._plugin_manager def __getattr__(cls, attr): pm = MetaPluginManager.get_plugin_manager() diff --git a/allure-python-commons/src/reporter.py b/allure-python-commons/src/reporter.py index ecc1550d..55cf80ac 100644 --- a/allure-python-commons/src/reporter.py +++ b/allure-python-commons/src/reporter.py @@ -1,4 +1,5 @@ -from collections import OrderedDict +import threading +from collections import OrderedDict, defaultdict from allure_commons.types import AttachmentType from allure_commons.model2 import ExecutableItem @@ -8,9 +9,53 @@ from allure_commons._core import plugin_manager +class ThreadContextItems: + + _thread_context = defaultdict(OrderedDict) + _init_thread: threading.Thread + + @property + def thread_context(self): + context = self._thread_context[threading.current_thread()] + if not context and threading.current_thread() is not self._init_thread: + uuid, last_item = next(reversed(self._thread_context[self._init_thread].items())) + context[uuid] = last_item + return context + + def __init__(self, *args, **kwargs): + self._init_thread = threading.current_thread() + super().__init__(*args, **kwargs) + + def __setitem__(self, key, value): + self.thread_context.__setitem__(key, value) + + def __getitem__(self, item): + return self.thread_context.__getitem__(item) + + def __iter__(self): + return self.thread_context.__iter__() + + def __reversed__(self): + return self.thread_context.__reversed__() + + def get(self, key): + return self.thread_context.get(key) + + def pop(self, key): + return self.thread_context.pop(key) + + def cleanup(self): + stopped_threads = [] + for thread in self._thread_context.keys(): + if not thread.is_alive(): + stopped_threads.append(thread) + for thread in stopped_threads: + del self._thread_context[thread] + + class AllureReporter(object): def __init__(self): - self._items = OrderedDict() + self._items = ThreadContextItems() self._orphan_items = [] def _update_item(self, uuid, **kwargs): @@ -73,6 +118,7 @@ def get_test(self, uuid): def close_test(self, uuid): test_case = self._items.pop(uuid) + self._items.cleanup() plugin_manager.hook.report_result(result=test_case) def drop_test(self, uuid):