From 2c1c828d8a62bba81aaa6a0f5aa7b87a6fceffcf Mon Sep 17 00:00:00 2001 From: skhomuti Date: Mon, 24 Jan 2022 17:19:37 +0500 Subject: [PATCH 1/5] Add support for steps and attachments in threads --- allure-python-commons/src/_core.py | 13 ++++---- allure-python-commons/src/reporter.py | 48 +++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 9 deletions(-) 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..3ebe56ff 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,51 @@ from allure_commons._core import plugin_manager +class ThreadContextItems: + + _thread_context = defaultdict(OrderedDict) + + @property + def thread_context(self): + context = self._thread_context[threading.current_thread()] + if not context and threading.current_thread() is not threading.main_thread(): + uuid, last_item = next(reversed(self._thread_context[threading.main_thread()].items())) + context[uuid] = last_item + return context + + def __init__(self, *args, **kwargs): + 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 +116,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): From 9774713d91b49755a1fed0b21f6ecf4177fbb66b Mon Sep 17 00:00:00 2001 From: nigredon1991 Date: Thu, 8 Jul 2021 15:21:06 +0500 Subject: [PATCH 2/5] Add tests for thread --- .../attachment/attachment_step_test.py | 32 +++++++++++++++++ ...st_step_with_several_step_inside_thread.py | 34 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 allure-pytest/test/acceptance/step/test_step_with_several_step_inside_thread.py 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..19277d94 --- /dev/null +++ b/allure-pytest/test/acceptance/step/test_step_with_several_step_inside_thread.py @@ -0,0 +1,34 @@ +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_step +from hamcrest import assert_that, not_ + + +def test_step_with_thread(allured_testdir): + allured_testdir.testdir.makepyfile( + """ + from concurrent.futures import ThreadPoolExecutor + + import allure + import pytest + + @allure.step("thread {x}") + def parallel_step(x=1): + pass + + + 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_step("thread 2") + ) + ) + ) From 614a9e6c1a86e717b11d48bbbf16c214ce3a27a3 Mon Sep 17 00:00:00 2001 From: skhomuti Date: Mon, 24 Jan 2022 23:00:46 +0500 Subject: [PATCH 3/5] Add test for reused threads --- ...st_step_with_several_step_inside_thread.py | 50 +++++++++++++++++-- allure-python-commons/src/reporter.py | 6 ++- 2 files changed, 49 insertions(+), 7 deletions(-) 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 index 19277d94..eb4a3d56 100644 --- 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 @@ -1,6 +1,6 @@ from allure_commons_test.report import has_test_case from allure_commons_test.result import has_step -from hamcrest import assert_that, not_ +from hamcrest import assert_that def test_step_with_thread(allured_testdir): @@ -9,17 +9,17 @@ def test_step_with_thread(allured_testdir): from concurrent.futures import ThreadPoolExecutor import allure - import pytest @allure.step("thread {x}") def parallel_step(x=1): - pass + with allure.step("Sub-step in thread"): + pass def test_thread(): with allure.step("Start in thread"): with ThreadPoolExecutor(max_workers=2) as executor: - f_result = executor.map(parallel_step, [1, 2]) + executor.map(parallel_step, [1, 2]) """ ) @@ -28,7 +28,47 @@ def test_thread(): assert_that(allured_testdir.allure_report, has_test_case("test_thread", has_step("Start in thread", - has_step("thread 1"), has_step("thread 2") + 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(f"thread 1"), + has_step(f"thread 2"), + has_step(f"thread 3"), + has_step("Reuse previous threads", + has_step(f"thread 1"), + has_step(f"thread 2"), + has_step(f"thread 3"), + ), + ) + ) diff --git a/allure-python-commons/src/reporter.py b/allure-python-commons/src/reporter.py index 3ebe56ff..55cf80ac 100644 --- a/allure-python-commons/src/reporter.py +++ b/allure-python-commons/src/reporter.py @@ -12,16 +12,18 @@ 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 threading.main_thread(): - uuid, last_item = next(reversed(self._thread_context[threading.main_thread()].items())) + 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): From a1cb964fbdf666c9a9227a8e3920450873416f58 Mon Sep 17 00:00:00 2001 From: skhomuti Date: Sat, 5 Feb 2022 13:28:56 +0500 Subject: [PATCH 4/5] Mute pytest-bdd test due to https://github.com/pytest-dev/pytest-bdd/issues/447 --- allure-pytest-bdd/test/outline_test.py | 2 ++ 1 file changed, 2 insertions(+) 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 From 19bdd24dfdd98bb16877b453a8f9a15b90dac05c Mon Sep 17 00:00:00 2001 From: skhomuti Date: Sat, 5 Feb 2022 13:33:01 +0500 Subject: [PATCH 5/5] Fixed flake8 --- .../test_step_with_several_step_inside_thread.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 index eb4a3d56..5fdc56fd 100644 --- 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 @@ -62,13 +62,13 @@ def test_thread(): assert_that(allured_testdir.allure_report, has_test_case("test_thread", - has_step(f"thread 1"), - has_step(f"thread 2"), - has_step(f"thread 3"), + has_step("thread 1"), + has_step("thread 2"), + has_step("thread 3"), has_step("Reuse previous threads", - has_step(f"thread 1"), - has_step(f"thread 2"), - has_step(f"thread 3"), + has_step("thread 1"), + has_step("thread 2"), + has_step("thread 3"), ), ) )