diff --git a/allure-pytest/src/listener.py b/allure-pytest/src/listener.py index 10ec29df..62edf2ea 100644 --- a/allure-pytest/src/listener.py +++ b/allure-pytest/src/listener.py @@ -26,6 +26,7 @@ from allure_pytest.utils import get_pytest_report_status from allure_pytest.utils import format_allure_link from allure_pytest.utils import get_history_id +from allure_pytest.utils import is_internal_finalizer from allure_pytest.compat import getfixturedefs @@ -180,6 +181,8 @@ def pytest_fixture_setup(self, fixturedef, request): finalizers = getattr(fixturedef, "_finalizers", []) for index, finalizer in enumerate(finalizers): + if is_internal_finalizer(finalizer): + continue finalizer_name = getattr(finalizer, "__name__", index) name = f"{fixture_name}::{finalizer_name}" finalizers[index] = allure_commons.fixture(finalizer, parent_uuid=container_uuid, name=name) diff --git a/allure-pytest/src/utils.py b/allure-pytest/src/utils.py index 31ffb63f..c2437020 100644 --- a/allure-pytest/src/utils.py +++ b/allure-pytest/src/utils.py @@ -221,3 +221,20 @@ def get_history_id(full_name, parameters, original_values): key=lambda p: p.name )) ) + + +def is_internal_finalizer(finalizer): + """True for finalizers that belong to pytest itself rather than to user code. + + pytest 9.1 attaches its own finalizer inside ``FixtureDef.execute`` + (``FixtureDef.execute..``). Wrapping it produces an + ``::`` afterStage that pytest never invokes through the + wrapper, so it is reported with no status at all - which Allure renders as + "unknown", once per fixture, in every report. + + The check is on which module owns the callable, not on its name: a lambda + passed to ``request.addfinalizer`` by a test author is a real teardown and + must still be reported. + """ + module = getattr(finalizer, "__module__", None) or "" + return module == "_pytest" or module.startswith("_pytest.") diff --git a/tests/allure_pytest/acceptance/fixture/internal_finalizer_test.py b/tests/allure_pytest/acceptance/fixture/internal_finalizer_test.py new file mode 100644 index 00000000..c45d1476 --- /dev/null +++ b/tests/allure_pytest/acceptance/fixture/internal_finalizer_test.py @@ -0,0 +1,69 @@ +import allure +from hamcrest import assert_that +from tests.allure_pytest.pytest_runner import AllurePytestRunner + +from allure_commons_test.report import has_test_case +from allure_commons_test.container import has_container +from allure_commons_test.container import has_after + + +def afters_of(allure_results): + return [ + after + for container in allure_results.test_containers + for after in container.get("afters", []) + ] + + +@allure.feature("Fixture") +@allure.story("Fixture finalizer") +def test_pytest_internal_finalizer_is_not_reported(allure_pytest_runner: AllurePytestRunner): + """ + >>> import pytest + + pytest attaches its own finalizer to every fixture from 9.1 onwards, inside + ``FixtureDef.execute``. It is machinery, not a teardown anyone wrote: + >>> @pytest.fixture + ... def plain_fixture(): + ... yield + + >>> def test_plain_fixture_example(plain_fixture): + ... pass + """ + + allure_results = allure_pytest_runner.run_docstring() + + unreported = [after for after in afters_of(allure_results) if after.get("status") is None] + + assert_that(unreported == [], f"expected no statusless afterStage, got {unreported}") + + +@allure.feature("Fixture") +@allure.story("Fixture finalizer") +def test_user_lambda_finalizer_is_still_reported(allure_pytest_runner: AllurePytestRunner): + """ + A finalizer is skipped based on which module owns it, never on its name, so a + lambda registered by the test author is still a real teardown and is reported. + + >>> import pytest + + >>> @pytest.fixture + ... def fixture_with_lambda_finalizer(request): + ... request.addfinalizer(lambda: None) + + >>> def test_fixture_with_lambda_finalizer_example(fixture_with_lambda_finalizer): + ... pass + """ + + allure_results = allure_pytest_runner.run_docstring() + + assert_that( + allure_results, + has_test_case( + "test_fixture_with_lambda_finalizer_example", + has_container( + allure_results, + has_after("fixture_with_lambda_finalizer::") + ) + ) + )