From e5e82b550520f0c1992c94b755611eff515411a8 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:11:43 +0400 Subject: [PATCH 1/2] fix(pytest-bdd): do not reuse uuid --- allure-pytest-bdd/src/pytest_bdd_listener.py | 21 ++++++---- allure-pytest-bdd/src/steps.py | 25 ++++++----- allure-pytest-bdd/src/utils.py | 4 -- .../acceptance/scenario_test.py | 42 ++++++++++++++++++- 4 files changed, 68 insertions(+), 24 deletions(-) diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index f91e34ae..5a96731d 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -1,5 +1,7 @@ import pytest +from collections import defaultdict + from allure_commons.utils import now from allure_commons.model2 import Label from allure_commons.model2 import Status @@ -7,6 +9,7 @@ from allure_commons.utils import platform_label from allure_commons.utils import host_tag, thread_tag from allure_commons.utils import md5 +from allure_commons.utils import uuid4 from .steps import get_step_uuid from .steps import process_gherkin_step_args @@ -29,7 +32,6 @@ from .utils import get_pytest_report_status from .utils import get_scenario_status_details from .utils import get_test_name -from .utils import get_uuid from .utils import post_process_test_result from functools import partial @@ -40,11 +42,12 @@ def __init__(self, lifecycle): self.lifecycle = lifecycle self.host = host_tag() self.thread = thread_tag() + self.nodeid_to_uuid = defaultdict(uuid4) @pytest.hookimpl def pytest_bdd_before_scenario(self, request, feature, scenario): item = request.node - uuid = get_uuid(item.nodeid) + uuid = self.nodeid_to_uuid[item.nodeid] outline_params = get_outline_params(item) pytest_params = get_pytest_params(item) @@ -74,18 +77,19 @@ def pytest_bdd_before_scenario(self, request, feature, scenario): test_result.links.extend(get_allure_links(item)) test_result.parameters.extend(convert_params(outline_params, pytest_params)) - finalizer = partial(report_remaining_steps, self.lifecycle, item) + finalizer = partial(report_remaining_steps, self.lifecycle, item, uuid) item.addfinalizer(finalizer) @pytest.hookimpl def pytest_bdd_after_scenario(self, request, feature, scenario): - uuid = get_uuid(request.node.nodeid) + uuid = self.nodeid_to_uuid[request.node.nodeid] with self.lifecycle.update_test_case(uuid=uuid) as test_result: test_result.stop = now() @pytest.hookimpl def pytest_bdd_before_step(self, request, feature, scenario, step, step_func): - start_gherkin_step(self.lifecycle, request.node, step, step_func) + uuid = self.nodeid_to_uuid[request.node.nodeid] + start_gherkin_step(self.lifecycle, request.node, uuid, step, step_func) @pytest.hookimpl def pytest_bdd_before_step_call(self, request, feature, scenario, step, step_func, step_func_args): @@ -101,7 +105,8 @@ def pytest_bdd_step_error(self, request, feature, scenario, step, step_func, ste @pytest.hookimpl def pytest_bdd_step_func_lookup_error(self, request, feature, scenario, step, exception): - report_undefined_step(self.lifecycle, request.node, step, exception) + uuid = self.nodeid_to_uuid[request.node.nodeid] + report_undefined_step(self.lifecycle, request.node, uuid, step, exception) @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(self, item, call): @@ -112,7 +117,8 @@ def pytest_runtest_makereport(self, item, call): status = get_pytest_report_status(report, excinfo) status_details = get_scenario_status_details(report, excinfo) - uuid = get_uuid(report.nodeid) + uuid = self.nodeid_to_uuid[report.nodeid] + with self.lifecycle.update_test_case(uuid=uuid) as test_result: if test_result and report.when == "setup": @@ -143,3 +149,4 @@ def pytest_runtest_makereport(self, item, call): if report.when == "teardown": self.lifecycle.write_test_case(uuid=uuid) + del self.nodeid_to_uuid[report.nodeid] diff --git a/allure-pytest-bdd/src/steps.py b/allure-pytest-bdd/src/steps.py index cfc2741b..0667a88e 100644 --- a/allure-pytest-bdd/src/steps.py +++ b/allure-pytest-bdd/src/steps.py @@ -1,9 +1,12 @@ +from uuid import UUID + from allure import attachment_type from allure_commons.model2 import StatusDetails from allure_commons.model2 import Status from allure_commons.model2 import Parameter from allure_commons.utils import format_exception from allure_commons.utils import represent +from allure_commons.utils import md5 from .storage import get_saved_params from .storage import get_test_data @@ -11,7 +14,6 @@ from .utils import attach_data from .utils import format_csv from .utils import get_allure_title -from .utils import get_uuid from .utils import get_status from .utils import get_status_details @@ -32,7 +34,7 @@ def get_allure_title_of_step(item, step_func, step_func_args): def get_step_uuid(step): - return get_uuid(str(id(step))) + return str(UUID(md5(id(step)))) def start_step(lifecycle, step_uuid, title, params=None, parent_uuid=None): @@ -57,7 +59,7 @@ def stop_step(lifecycle, uuid, status=None, status_details=None, exception=None, return True -def start_gherkin_step(lifecycle, item, step, step_func=None, step_uuid=None): +def start_gherkin_step(lifecycle, item, test_uuid, step, step_func=None, step_uuid=None): if step_uuid is None: step_uuid = get_step_uuid(step) @@ -65,7 +67,7 @@ def start_gherkin_step(lifecycle, item, step, step_func=None, step_uuid=None): lifecycle, step_uuid=step_uuid, title=get_step_name(item, step, step_func), - parent_uuid=get_uuid(item.nodeid), + parent_uuid=test_uuid, ) @@ -152,7 +154,7 @@ def stop_gherkin_step(lifecycle, item, step_uuid, **kwargs): return res -def ensure_gherkin_step_reported(lifecycle, item, step, step_uuid=None, **kwargs): +def ensure_gherkin_step_reported(lifecycle, item, test_uuid, step, step_uuid=None, **kwargs): if not step_uuid: step_uuid = get_step_uuid(step) @@ -160,14 +162,15 @@ def ensure_gherkin_step_reported(lifecycle, item, step, step_uuid=None, **kwargs if stop_gherkin_step(lifecycle, item, step_uuid, **kwargs): return - start_gherkin_step(lifecycle, item, step, step_uuid=step_uuid) + start_gherkin_step(lifecycle, item, test_uuid, step, step_uuid=step_uuid) stop_gherkin_step(lifecycle, item, step_uuid, **kwargs) -def report_undefined_step(lifecycle, item, step, exception): +def report_undefined_step(lifecycle, item, test_uuid, step, exception): ensure_gherkin_step_reported( lifecycle, item, + test_uuid, step, status=Status.BROKEN, status_details=StatusDetails( @@ -176,7 +179,7 @@ def report_undefined_step(lifecycle, item, step, exception): ) -def report_remaining_steps(lifecycle, item): +def report_remaining_steps(lifecycle, item, test_uuid): test_data = get_test_data(item) scenario = test_data.scenario excinfo = test_data.excinfo @@ -185,12 +188,12 @@ def report_remaining_steps(lifecycle, item): for step in scenario.steps: step_uuid = get_step_uuid(step) if step_uuid not in reported_steps: - __report_remaining_step(lifecycle, item, step, step_uuid, excinfo) + __report_remaining_step(lifecycle, item, test_uuid, step, step_uuid, excinfo) excinfo = None # Only show the full message and traceback once -def __report_remaining_step(lifecycle, item, step, step_uuid, excinfo): - args = [lifecycle, item, step, step_uuid] +def __report_remaining_step(lifecycle, item, test_uuid, step, step_uuid, excinfo): + args = [lifecycle, item, test_uuid, step, step_uuid] kwargs = { "exception": excinfo.value, "exception_type": excinfo.type, diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index 43bc17a1..63d482b3 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -182,10 +182,6 @@ def get_title_path(request, feature): return [*parts[:-1], feature.name or parts[-1]] -def get_uuid(*args): - return str(UUID(md5(*args))) - - def get_status(exception): if exception: if isinstance(exception, (pytest.skip.Exception, pytest.xfail.Exception)): diff --git a/tests/allure_pytest_bdd/acceptance/scenario_test.py b/tests/allure_pytest_bdd/acceptance/scenario_test.py index a381f088..fe648eed 100644 --- a/tests/allure_pytest_bdd/acceptance/scenario_test.py +++ b/tests/allure_pytest_bdd/acceptance/scenario_test.py @@ -1,6 +1,7 @@ """ ./allure-pytest-bdd/examples/simple-scenario """ -from hamcrest import assert_that +from allure import issue +from hamcrest import assert_that, not_, equal_to from tests.allure_pytest.pytest_runner import AllurePytestRunner from allure_commons_test.report import has_test_case from allure_commons_test.result import with_status @@ -42,7 +43,7 @@ def then_the_postconditions_are_held(): output = allure_pytest_bdd_runner.run_pytest( ("scenario.feature", feature_content), - steps_content + steps_content, ) assert_that( @@ -56,3 +57,40 @@ def then_the_postconditions_are_held(): has_history_id() ) ) + +@issue("925") +def test_uuid_not_reused_across_runs(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given, when, then + + @scenario("foo.feature", "Bar") + def test_bar(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + output1 = allure_pytest_bdd_runner.run_pytest( + ("foo.feature", feature_content), + steps_content + ) + output2 = allure_pytest_bdd_runner.run_pytest( + ("foo.feature", feature_content), + steps_content + ) + + uuid1 = output1.test_cases[0]["uuid"] + uuid2 = output2.test_cases[0]["uuid"] + + assert_that(uuid1, not_(equal_to(uuid2))) From fc9e3cf067b6ffe33ebbd563b51c81635f5484c7 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:49:38 +0400 Subject: [PATCH 2/2] refactor(pytest-bdd): remove unused import --- allure-pytest-bdd/src/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index 63d482b3..cc35d9e0 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -2,7 +2,6 @@ import io import os from urllib.parse import urlparse -from uuid import UUID from pathlib import Path import pytest