diff --git a/.gitignore b/.gitignore index af0bb13b..652e4153 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ *.egg-info */build -*/dist \ No newline at end of file +*/dist +.eggs diff --git a/allure-pytest/src/listener.py b/allure-pytest/src/listener.py index 70b1e55c..c6bf1fbf 100644 --- a/allure-pytest/src/listener.py +++ b/allure-pytest/src/listener.py @@ -5,7 +5,7 @@ from allure_commons.utils import uuid4 from allure_commons.utils import represent from allure_commons.utils import platform_label -from allure_commons.utils import host_tag, thread_tag +from allure_commons.utils import host_tag, thread_tag, thread_tag_detail from allure_commons.reporter import AllureReporter from allure_commons.model2 import TestStepResult, TestResult, TestBeforeResult, TestAfterResult from allure_commons.model2 import TestResultContainer @@ -36,7 +36,7 @@ def __init__(self, config): @allure_commons.hookimpl def start_step(self, uuid, title, params): parameters = [Parameter(name=name, value=value) for name, value in params.items()] - step = TestStepResult(name=title, start=now(), parameters=parameters) + step = TestStepResult(name=title, start=now(), parameters=parameters, thrd=thread_tag_detail()) self.allure_logger.start_step(None, uuid, step) @allure_commons.hookimpl @@ -48,7 +48,7 @@ def stop_step(self, uuid, exc_type, exc_val, exc_tb): @allure_commons.hookimpl def start_fixture(self, parent_uuid, uuid, name): - after_fixture = TestAfterResult(name=name, start=now()) + after_fixture = TestAfterResult(name=name, start=now(), thrd=thread_tag_detail()) self.allure_logger.start_after_fixture(parent_uuid, uuid, after_fixture) @allure_commons.hookimpl @@ -61,7 +61,7 @@ def stop_fixture(self, parent_uuid, uuid, name, exc_type, exc_val, exc_tb): @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_runtest_protocol(self, item, nextitem): uuid = self._cache.push(item.nodeid) - test_result = TestResult(name=item.name, uuid=uuid, start=now(), stop=now()) + test_result = TestResult(name=item.name, uuid=uuid, start=now(), stop=now(), thrd=thread_tag_detail()) self.allure_logger.schedule_test(uuid, test_result) yield @@ -69,7 +69,7 @@ def pytest_runtest_protocol(self, item, nextitem): def pytest_runtest_setup(self, item): if not self._cache.get(item.nodeid): uuid = self._cache.push(item.nodeid) - test_result = TestResult(name=item.name, uuid=uuid, start=now(), stop=now()) + test_result = TestResult(name=item.name, uuid=uuid, start=now(), stop=now(), thrd=thread_tag_detail()) self.allure_logger.schedule_test(uuid, test_result) yield @@ -80,7 +80,7 @@ def pytest_runtest_setup(self, item): group_uuid = self._cache.get(fixturedef) if not group_uuid: group_uuid = self._cache.push(fixturedef) - group = TestResultContainer(uuid=group_uuid) + group = TestResultContainer(uuid=group_uuid, thrd=thread_tag_detail()) self.allure_logger.start_group(group_uuid, group) self.allure_logger.update_group(group_uuid, children=uuid) params = item.callspec.params if hasattr(item, 'callspec') else {} @@ -130,13 +130,13 @@ def pytest_fixture_setup(self, fixturedef, request): if not container_uuid: container_uuid = self._cache.push(fixturedef) - container = TestResultContainer(uuid=container_uuid) + container = TestResultContainer(uuid=container_uuid, thrd=thread_tag_detail()) self.allure_logger.start_group(container_uuid, container) self.allure_logger.update_group(container_uuid, start=now()) before_fixture_uuid = uuid4() - before_fixture = TestBeforeResult(name=fixture_name, start=now()) + before_fixture = TestBeforeResult(name=fixture_name, start=now(), thrd=thread_tag_detail()) self.allure_logger.start_before_fixture(container_uuid, before_fixture_uuid, before_fixture) outcome = yield 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..8c6302ed --- /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 + + +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") + ) + ) + ) diff --git a/allure-python-commons/src/_core.py b/allure-python-commons/src/_core.py index 22219905..43f20a6b 100644 --- a/allure-python-commons/src/_core.py +++ b/allure-python-commons/src/_core.py @@ -1,11 +1,11 @@ -import threading +import multiprocessing from six import with_metaclass from pluggy import PluginManager from allure_commons import _hooks class MetaPluginManager(type): - _storage = threading.local() + _storage = multiprocessing.Process @staticmethod def get_plugin_manager(): diff --git a/allure-python-commons/src/lifecycle.py b/allure-python-commons/src/lifecycle.py index 951de3d2..7748eaac 100644 --- a/allure-python-commons/src/lifecycle.py +++ b/allure-python-commons/src/lifecycle.py @@ -11,6 +11,7 @@ from allure_commons.utils import uuid4 from allure_commons.utils import now from allure_commons.types import AttachmentType +import threading class AllureLifecycle(object): @@ -52,7 +53,7 @@ def write_test_case(self, uuid=None): @contextmanager def start_step(self, parent_uuid=None, uuid=None): parent = self._get_item(uuid=parent_uuid, item_type=ExecutableItem) - step = TestStepResult() + step = TestStepResult(thrd=threading.current_thread().name) step.start = now() parent.steps.append(step) self._items[uuid or uuid4()] = step diff --git a/allure-python-commons/src/logger.py b/allure-python-commons/src/logger.py index ab326b06..8a004052 100644 --- a/allure-python-commons/src/logger.py +++ b/allure-python-commons/src/logger.py @@ -12,6 +12,19 @@ INDENT = 4 +def delete_step_thrd(data): + if isinstance(data, list): + for field in data: + field = delete_step_thrd(field) + elif isinstance(data, dict): + for field in data.keys(): + if field == 'thrd': + data.pop('thrd') + break + field = delete_step_thrd(data[field]) + return data + + class AllureFileLogger(object): def __init__(self, report_dir, clean=False): @@ -32,6 +45,8 @@ def _report_item(self, item): indent = INDENT if os.environ.get("ALLURE_INDENT_OUTPUT") else None filename = item.file_pattern.format(prefix=uuid.uuid4()) data = asdict(item, filter=lambda attr, value: not (type(value) != bool and not bool(value))) + data = delete_step_thrd(data) + with io.open(os.path.join(self._report_dir, filename), 'w', encoding='utf8') as json_file: if sys.version_info.major < 3: json_file.write( diff --git a/allure-python-commons/src/model2.py b/allure-python-commons/src/model2.py index 074bff94..6a17389f 100644 --- a/allure-python-commons/src/model2.py +++ b/allure-python-commons/src/model2.py @@ -22,6 +22,7 @@ class TestResultContainer(object): links = attrib(default=Factory(list)) start = attrib(default=None) stop = attrib(default=None) + thrd = attrib(default=None) @attrs @@ -37,6 +38,7 @@ class ExecutableItem(object): parameters = attrib(default=Factory(list)) start = attrib(default=None) stop = attrib(default=None) + thrd = attrib(default=None) @attrs diff --git a/allure-python-commons/src/reporter.py b/allure-python-commons/src/reporter.py index ecc1550d..22601fa2 100644 --- a/allure-python-commons/src/reporter.py +++ b/allure-python-commons/src/reporter.py @@ -1,17 +1,17 @@ from collections import OrderedDict -from allure_commons.types import AttachmentType -from allure_commons.model2 import ExecutableItem -from allure_commons.model2 import TestResult -from allure_commons.model2 import Attachment, ATTACHMENT_PATTERN -from allure_commons.utils import now from allure_commons._core import plugin_manager +from allure_commons.model2 import (ATTACHMENT_PATTERN, Attachment, + ExecutableItem, TestResult) +from allure_commons.types import AttachmentType +from allure_commons.utils import now, thread_tag_detail class AllureReporter(object): def __init__(self): self._items = OrderedDict() self._orphan_items = [] + self._thread = thread_tag_detail() def _update_item(self, uuid, **kwargs): item = self._items[uuid] if uuid else self._items[next(reversed(self._items))] @@ -23,7 +23,21 @@ def _update_item(self, uuid, **kwargs): setattr(item, name, value) def _last_executable(self): - for _uuid in reversed(self._items): + copy_items = self._items.copy() + for _uuid in reversed(copy_items): + if ( + hasattr(self._items[_uuid], "thrd") + and self._items[_uuid].thrd != thread_tag_detail() + ): + continue + if isinstance(self._items[_uuid], ExecutableItem): + return _uuid + for _uuid in reversed(copy_items): + if hasattr(self._items[_uuid], "thrd") and self._items[_uuid].thrd != self._thread: + continue + if isinstance(self._items[_uuid], ExecutableItem): + return _uuid + for _uuid in reversed(copy_items): if isinstance(self._items[_uuid], ExecutableItem): return _uuid diff --git a/allure-python-commons/src/utils.py b/allure-python-commons/src/utils.py index 47278c05..14f00e91 100644 --- a/allure-python-commons/src/utils.py +++ b/allure-python-commons/src/utils.py @@ -96,6 +96,10 @@ def thread_tag(): return '{0}-{1}'.format(os.getpid(), threading.current_thread().name) +def thread_tag_detail(): + return '{0}-{1}'.format(threading.get_ident(), threading.current_thread().name) + + def host_tag(): return socket.gethostname()