diff --git a/src/pytest_html/fixtures.py b/src/pytest_html/fixtures.py
index 88ce828d..6d5e40b2 100644
--- a/src/pytest_html/fixtures.py
+++ b/src/pytest_html/fixtures.py
@@ -27,7 +27,10 @@ def test_foo(extra):
)
pytestconfig.stash[extras_stash_key] = []
yield pytestconfig.stash[extras_stash_key]
- del pytestconfig.stash[extras_stash_key][:]
+ # Deliberately not cleared here: a fixture that depends on this one may
+ # still append to the list during its own teardown, which runs after
+ # this point. pytest_runtest_makereport reads it once more for the
+ # "teardown" phase and clears it there instead.
@pytest.fixture
@@ -44,4 +47,7 @@ def test_foo(extras):
"""
pytestconfig.stash[extras_stash_key] = []
yield pytestconfig.stash[extras_stash_key]
- del pytestconfig.stash[extras_stash_key][:]
+ # Deliberately not cleared here: a fixture that depends on this one may
+ # still append to the list during its own teardown, which runs after
+ # this point. pytest_runtest_makereport reads it once more for the
+ # "teardown" phase and clears it there instead.
diff --git a/src/pytest_html/plugin.py b/src/pytest_html/plugin.py
index 949a6ffa..be6e583e 100644
--- a/src/pytest_html/plugin.py
+++ b/src/pytest_html/plugin.py
@@ -138,3 +138,18 @@ def pytest_runtest_makereport(item, call):
fixture_extras = item.config.stash.get(extras_stash_key, [])
plugin_extras = getattr(report, "extras", [])
report.extras = fixture_extras + plugin_extras + deprecated_extra
+ # Remember how many stash entries "call" already claimed, so that
+ # anything a fixture appends during its own teardown (which the
+ # stash still holds at this point) isn't attached twice below.
+ item._html_report_extras_seen = len(fixture_extras)
+ elif report.when == "teardown":
+ fixture_extras = item.config.stash.get(extras_stash_key, [])
+ already_seen = getattr(item, "_html_report_extras_seen", 0)
+ new_extras = fixture_extras[already_seen:]
+ if new_extras:
+ report.extras = new_extras + getattr(report, "extras", [])
+ # The "extras"/"extra" fixtures no longer clear the stash on their
+ # own teardown, since a fixture depending on them may still append
+ # to it during its own teardown afterwards. Do it here instead,
+ # once nothing else is left to read it for this test.
+ del fixture_extras[:]
diff --git a/testing/legacy_test_pytest_html.py b/testing/legacy_test_pytest_html.py
index 08ad208d..1fa71c56 100644
--- a/testing/legacy_test_pytest_html.py
+++ b/testing/legacy_test_pytest_html.py
@@ -8,6 +8,7 @@
import re
import sys
from base64 import b64encode
+from html import unescape
import pkg_resources
import pytest
@@ -706,6 +707,38 @@ def test_pass(extra):
src = f"data:image/png;base64,{content}"
assert f'
' in html
+ def test_extra_fixture_teardown(self, testdir):
+ content = str(random.random())
+ testdir.makepyfile(
+ f"""
+ import pytest
+
+ @pytest.fixture
+ def cleanup(extras):
+ yield
+ from pytest_html import extras as extras_module
+ extras.append(extras_module.text('{content}'))
+
+ def test_pass(cleanup):
+ pass
+ """
+ )
+ result, html = run(testdir, "report.html", "--self-contained-html")
+ assert result.ret == 0
+
+ match = re.search(r'data-jsonblob="([^"]*)"', html)
+ assert match
+ report_data = json.loads(unescape(match.group(1)))
+ test_extras = report_data["tests"]["test_extra_fixture_teardown.py::test_pass"][
+ 0
+ ]["extras"]
+ assert len(test_extras) == 1
+ encoded = b64encode(content.encode("utf-8")).decode("ascii")
+ assert (
+ test_extras[0]["content"]
+ == f"data:text/plain;charset=utf-8;base64,{encoded}"
+ )
+
def test_no_invalid_characters_in_filename(self, testdir):
testdir.makeconftest(
"""