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( """