Skip to content

Commit d3bfabb

Browse files
authored
alternate realization of threads support (fixes #563, fixes #605, via #647)
1 parent 3c74fd5 commit d3bfabb

File tree

5 files changed

+162
-9
lines changed

5 files changed

+162
-9
lines changed
+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from pytest_bdd import scenario
2+
import pytest
23

34

5+
@pytest.mark.skip(reason="https://github.com/pytest-dev/pytest-bdd/issues/447")
46
@scenario("../features/outline.feature", "Scenario outline")
57
def test_scenario_outline():
68
pass

allure-pytest/test/acceptance/attachment/attachment_step_test.py

+32
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,35 @@ def test_step_with_attachment(executed_docstring_path):
1414
),
1515
)
1616
)
17+
18+
19+
def test_step_with_thread_and_attachment(allured_testdir):
20+
allured_testdir.testdir.makepyfile(
21+
"""
22+
from concurrent.futures import ThreadPoolExecutor
23+
24+
import allure
25+
import pytest
26+
27+
@allure.step("thread {x}")
28+
def parallel_step(x=1):
29+
allure.attach("text", str(x), allure.attachment_type.TEXT)
30+
31+
32+
def test_thread():
33+
with allure.step("Start in thread"):
34+
with ThreadPoolExecutor(max_workers=2) as executor:
35+
f_result = executor.map(parallel_step, [1, 2])
36+
"""
37+
)
38+
39+
allured_testdir.run_with_allure()
40+
41+
assert_that(allured_testdir.allure_report,
42+
has_test_case("test_thread",
43+
has_step("Start in thread",
44+
has_step("thread 1", has_attachment(name="1")),
45+
has_step("thread 2", has_attachment(name="2")),
46+
)
47+
)
48+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from allure_commons_test.report import has_test_case
2+
from allure_commons_test.result import has_step
3+
from hamcrest import assert_that
4+
5+
6+
def test_step_with_thread(allured_testdir):
7+
allured_testdir.testdir.makepyfile(
8+
"""
9+
from concurrent.futures import ThreadPoolExecutor
10+
11+
import allure
12+
13+
@allure.step("thread {x}")
14+
def parallel_step(x=1):
15+
with allure.step("Sub-step in thread"):
16+
pass
17+
18+
19+
def test_thread():
20+
with allure.step("Start in thread"):
21+
with ThreadPoolExecutor(max_workers=2) as executor:
22+
executor.map(parallel_step, [1, 2])
23+
"""
24+
)
25+
26+
allured_testdir.run_with_allure()
27+
28+
assert_that(allured_testdir.allure_report,
29+
has_test_case("test_thread",
30+
has_step("Start in thread",
31+
has_step("thread 1", has_step("Sub-step in thread")),
32+
has_step("thread 2")
33+
)
34+
)
35+
)
36+
37+
38+
def test_step_with_reused_threads(allured_testdir):
39+
allured_testdir.testdir.makepyfile(
40+
"""
41+
from concurrent.futures import ThreadPoolExecutor
42+
43+
import allure
44+
import random
45+
from time import sleep
46+
47+
@allure.step("thread {x}")
48+
def parallel_step(x=1):
49+
sleep(random.randint(0, 3))
50+
51+
def test_thread():
52+
with ThreadPoolExecutor(max_workers=2) as executor:
53+
executor.map(parallel_step, range(1, 4))
54+
with allure.step("Reuse previous threads"):
55+
with ThreadPoolExecutor(max_workers=2) as executor:
56+
executor.map(parallel_step, range(1, 4))
57+
58+
"""
59+
)
60+
61+
allured_testdir.run_with_allure()
62+
63+
assert_that(allured_testdir.allure_report,
64+
has_test_case("test_thread",
65+
has_step("thread 1"),
66+
has_step("thread 2"),
67+
has_step("thread 3"),
68+
has_step("Reuse previous threads",
69+
has_step("thread 1"),
70+
has_step("thread 2"),
71+
has_step("thread 3"),
72+
),
73+
)
74+
)

allure-python-commons/src/_core.py

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1-
import threading
21
from six import with_metaclass
32
from pluggy import PluginManager
43
from allure_commons import _hooks
54

65

76
class MetaPluginManager(type):
8-
_storage = threading.local()
7+
_plugin_manager: PluginManager = None
98

109
@staticmethod
1110
def get_plugin_manager():
12-
if not hasattr(MetaPluginManager._storage, 'plugin_manager'):
13-
MetaPluginManager._storage.plugin_manager = PluginManager('allure')
14-
MetaPluginManager._storage.plugin_manager.add_hookspecs(_hooks.AllureUserHooks)
15-
MetaPluginManager._storage.plugin_manager.add_hookspecs(_hooks.AllureDeveloperHooks)
11+
if not MetaPluginManager._plugin_manager:
12+
MetaPluginManager._plugin_manager = PluginManager('allure')
13+
MetaPluginManager._plugin_manager.add_hookspecs(_hooks.AllureUserHooks)
14+
MetaPluginManager._plugin_manager.add_hookspecs(_hooks.AllureDeveloperHooks)
1615

17-
return MetaPluginManager._storage.plugin_manager
16+
return MetaPluginManager._plugin_manager
1817

1918
def __getattr__(cls, attr):
2019
pm = MetaPluginManager.get_plugin_manager()

allure-python-commons/src/reporter.py

+48-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from collections import OrderedDict
1+
import threading
2+
from collections import OrderedDict, defaultdict
23

34
from allure_commons.types import AttachmentType
45
from allure_commons.model2 import ExecutableItem
@@ -8,9 +9,53 @@
89
from allure_commons._core import plugin_manager
910

1011

12+
class ThreadContextItems:
13+
14+
_thread_context = defaultdict(OrderedDict)
15+
_init_thread: threading.Thread
16+
17+
@property
18+
def thread_context(self):
19+
context = self._thread_context[threading.current_thread()]
20+
if not context and threading.current_thread() is not self._init_thread:
21+
uuid, last_item = next(reversed(self._thread_context[self._init_thread].items()))
22+
context[uuid] = last_item
23+
return context
24+
25+
def __init__(self, *args, **kwargs):
26+
self._init_thread = threading.current_thread()
27+
super().__init__(*args, **kwargs)
28+
29+
def __setitem__(self, key, value):
30+
self.thread_context.__setitem__(key, value)
31+
32+
def __getitem__(self, item):
33+
return self.thread_context.__getitem__(item)
34+
35+
def __iter__(self):
36+
return self.thread_context.__iter__()
37+
38+
def __reversed__(self):
39+
return self.thread_context.__reversed__()
40+
41+
def get(self, key):
42+
return self.thread_context.get(key)
43+
44+
def pop(self, key):
45+
return self.thread_context.pop(key)
46+
47+
def cleanup(self):
48+
stopped_threads = []
49+
for thread in self._thread_context.keys():
50+
if not thread.is_alive():
51+
stopped_threads.append(thread)
52+
for thread in stopped_threads:
53+
del self._thread_context[thread]
54+
55+
1156
class AllureReporter(object):
1257
def __init__(self):
13-
self._items = OrderedDict()
58+
self._items = ThreadContextItems()
1459
self._orphan_items = []
1560

1661
def _update_item(self, uuid, **kwargs):
@@ -73,6 +118,7 @@ def get_test(self, uuid):
73118

74119
def close_test(self, uuid):
75120
test_case = self._items.pop(uuid)
121+
self._items.cleanup()
76122
plugin_manager.hook.report_result(result=test_case)
77123

78124
def drop_test(self, uuid):

0 commit comments

Comments
 (0)