Skip to content

fix(pytest): stop reporting pytest's own finalizer as a phantom teardown - #921

Open
Mohanad49 wants to merge 1 commit into
allure-framework:masterfrom
Mohanad49:fix/internal-finalizer-phantom-teardown
Open

fix(pytest): stop reporting pytest's own finalizer as a phantom teardown#921
Mohanad49 wants to merge 1 commit into
allure-framework:masterfrom
Mohanad49:fix/internal-finalizer-phantom-teardown

Conversation

@Mohanad49

Copy link
Copy Markdown

Description

Closes #918.

From pytest 9.1, FixtureDef.execute attaches an internal finalizer to every fixture. pytest_fixture_setup wrapped it along with the real ones, so every fixture produced an extra afterStage:

fixture_with_finalizer::teardown    status=passed    <- real
fixture_with_finalizer::<lambda>    status=None      <- pytest's own
simple_fixture::1                   status=passed    <- real
simple_fixture::<lambda>            status=None      <- pytest's own

pytest never calls that finalizer through Allure's wrapper, so nothing ever closes the stage and it is written out with no status — which the report renders as unknown, once per fixture, on every run.

Confirmed as FixtureDef.execute.<locals>.<lambda>, owned by _pytest.fixtures:

[probe] simple_fixture
   name='<lambda>'  module='_pytest.fixtures'  qualname=FixtureDef.execute.<locals>.<lambda>
   name='?'         module='functools'         (the yield-fixture teardown)
[probe] fixture_with_finalizer
   name='<lambda>'  module='_pytest.fixtures'  qualname=FixtureDef.execute.<locals>.<lambda>
   name='teardown'  module='test_phantom'      (the author's finalizer)

The fix

Skip finalizers owned by _pytest.

The check is on the owning module, not on the name. Filtering <lambda> would have been shorter and wrong: a lambda passed to request.addfinalizer by a test author is a real teardown and has to keep being reported. The functools.partial that implements yield-fixture teardown is likewise untouched, so simple_fixture::1 still appears.

Testing

tests/allure_pytest273 passed, 2 xfailed (271 before, plus the two added here). ruff clean.

The two tests do different jobs, deliberately:

Test Without the fix With the fix
test_pytest_internal_finalizer_is_not_reported fails passes
test_user_lambda_finalizer_is_still_reported passes passes

The first is the regression test — I checked it fails on unpatched main, otherwise it would be asserting nothing. The second passes either way on purpose: it guards the fix against becoming too aggressive, and would start failing if someone later narrowed the filter to the name.

I ran the existing suite on unpatched main too — 271 passed there as well, so nothing currently covers this, which is presumably how it shipped.

Verified against pytest 9.1.1 / Python 3.11. The filter is a no-op on older pytest, which has no such finalizer.

From pytest 9.1, FixtureDef.execute attaches an internal finalizer to every
fixture. The listener wrapped it along with the real ones, producing a
<fixture>::<lambda> afterStage that pytest never invokes through the wrapper -
so it was written out with no status and rendered as 'unknown', once per
fixture, in every report.

Finalizers owned by _pytest are now skipped. The check is on the owning module
rather than on the name, because a lambda passed to request.addfinalizer by a
test author is a real teardown and must still be reported.

Two tests: one fails without the fix, and one guards against the fix being too
aggressive by asserting a user's own lambda finalizer is still reported.
@CLAassistant

CLAassistant commented Aug 16, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phantom <lambda> teardown entries with unknown status when using pytest 9.1+

2 participants