Skip to content

Add support for steps and attachments in threads #647

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

Merged
merged 5 commits into from
Jul 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions allure-pytest-bdd/test/outline_test.py
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions allure-pytest/test/acceptance/attachment/attachment_step_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
)
)
)
Original file line number Diff line number Diff line change
@@ -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"),
),
)
)
13 changes: 6 additions & 7 deletions allure-python-commons/src/_core.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
50 changes: 48 additions & 2 deletions allure-python-commons/src/reporter.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down