From bdcfeb47ea6f2b5e6ce75b0ee9998dd66122e6da Mon Sep 17 00:00:00 2001 From: Stamatis-Christos Saridakis <34583142+SaridakisStamatisChristos@users.noreply.github.com> Date: Mon, 19 Oct 2026 01:33:53 +0300 Subject: [PATCH 1/6] fixtures: announce fixture setup before user code (#13169) AI-assisted draft prepared with ChatGPT; human review required before upstream PR. --- AUTHORS | 1 + changelog/13169.improvement.rst | 2 + src/_pytest/setuponly.py | 9 ++++- testing/test_setuponly.py | 68 +++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 changelog/13169.improvement.rst diff --git a/AUTHORS b/AUTHORS index bd1e27cb37d..654b94df826 100644 --- a/AUTHORS +++ b/AUTHORS @@ -466,6 +466,7 @@ Simon Holesch Simon Kerr Skylar Downes Srinivas Reddy Thatiparthy +Stamatis-Christos Saridakis Stefaan Lippens Stefan Farmbauer Stefan Scherfke diff --git a/changelog/13169.improvement.rst b/changelog/13169.improvement.rst new file mode 100644 index 00000000000..981171d6207 --- /dev/null +++ b/changelog/13169.improvement.rst @@ -0,0 +1,2 @@ +When using ``--setup-show`` or ``--setup-only``, show a ``SETUP START`` +line before each fixture starts executing, so a stalled fixture setup can be identified. diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index 7e6b46bcdb4..8b806c16dee 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -32,6 +32,9 @@ def pytest_addoption(parser: Parser) -> None: def pytest_fixture_setup( fixturedef: FixtureDef[object], request: SubRequest ) -> Generator[None, object, object]: + if request.config.option.setupshow: + # Report this fixture before entering user code, including if it hangs. + _show_fixture_action(fixturedef, request.config, "SETUP START") try: return (yield) finally: @@ -85,7 +88,11 @@ def _show_fixture_action( if hasattr(fixturedef, "cached_param"): tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]") - tw.flush() + # End the early marker before any output from the fixture body. + if msg == "SETUP START": + tw.line() + else: + tw.flush() if capman: capman.resume_global_capture() diff --git a/testing/test_setuponly.py b/testing/test_setuponly.py index 1d2261f75a8..625659e9041 100644 --- a/testing/test_setuponly.py +++ b/testing/test_setuponly.py @@ -7,6 +7,74 @@ from _pytest.pytester import Pytester import pytest +@pytest.mark.parametrize("mode", ["--setup-show", "--setup-only"]) +def test_setup_show_announces_fixture_before_body( + pytester: Pytester, mode: str +) -> None: + p = pytester.makepyfile( + """ + import pytest + + @pytest.fixture(params=["example"], ids=["sample"]) + def slow(): + print("BODY-ENTERED", flush=True) + yield "value" + + def test_value(slow): + assert slow == "value" + """ + ) + + result = pytester.runpytest(mode, "-s", p) + assert result.ret == 0 + output = result.stdout.str() + assert "SETUP START F slow" in output + assert "SETUP F slow['sample']" in output + assert "TEARDOWN F slow['sample']" in output + assert output.index("SETUP START F slow") < output.index("BODY-ENTERED") + assert output.index("BODY-ENTERED") < output.index("SETUP F slow") + assert "SETUP START F slow\\n" in output + + +def test_setup_show_announces_failed_fixture(pytester: Pytester) -> None: + p = pytester.makepyfile( + """ + import pytest + + @pytest.fixture + def broken(): + raise ValueError("fixture setup failed") + + def test_failure(broken): + pass + """ + ) + + result = pytester.runpytest("--setup-show", p) + assert result.ret == 1 + output = result.stdout.str() + assert "SETUP START F broken" in output + assert "SETUP F broken" in output + assert output.index("SETUP START F broken") < output.index("SETUP F broken") + + +def test_no_setup_start_when_not_requested(pytester: Pytester) -> None: + p = pytester.makepyfile( + """ + import pytest + + @pytest.fixture + def normal(): + return 1 + + def test_normal(normal): + assert normal == 1 + """ + ) + result = pytester.runpytest(p) + assert result.ret == 0 + assert "SETUP START" not in result.stdout.str() + @pytest.fixture(params=["--setup-only", "--setup-plan", "--setup-show"], scope="module") def mode(request): From 1dd5f8db87ed7a674ce33f109cdf164f20537078 Mon Sep 17 00:00:00 2001 From: Stamatis-Christos Saridakis <34583142+SaridakisStamatisChristos@users.noreply.github.com> Date: Mon, 19 Oct 2026 01:34:31 +0300 Subject: [PATCH 2/6] test: assert actual newline after setup-start marker --- testing/test_setuponly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_setuponly.py b/testing/test_setuponly.py index 625659e9041..945c8029c9a 100644 --- a/testing/test_setuponly.py +++ b/testing/test_setuponly.py @@ -33,7 +33,7 @@ def test_value(slow): assert "TEARDOWN F slow['sample']" in output assert output.index("SETUP START F slow") < output.index("BODY-ENTERED") assert output.index("BODY-ENTERED") < output.index("SETUP F slow") - assert "SETUP START F slow\\n" in output + assert "SETUP START F slow\n" in output def test_setup_show_announces_failed_fixture(pytester: Pytester) -> None: From 66858bf7f72bbfac737ad33f910872900b8f9594 Mon Sep 17 00:00:00 2001 From: Stamatis-Christos Saridakis <34583142+SaridakisStamatisChristos@users.noreply.github.com> Date: Mon, 19 Oct 2026 01:35:34 +0300 Subject: [PATCH 3/6] style: separate test imports from module-level definitions --- testing/test_setuponly.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/test_setuponly.py b/testing/test_setuponly.py index 945c8029c9a..b1e4e6adaff 100644 --- a/testing/test_setuponly.py +++ b/testing/test_setuponly.py @@ -7,6 +7,7 @@ from _pytest.pytester import Pytester import pytest + @pytest.mark.parametrize("mode", ["--setup-show", "--setup-only"]) def test_setup_show_announces_fixture_before_body( pytester: Pytester, mode: str From 56776e4a22d33322f7d9892dd0e225f62436915e Mon Sep 17 00:00:00 2001 From: Stamatis-Christos Saridakis <34583142+SaridakisStamatisChristos@users.noreply.github.com> Date: Mon, 19 Oct 2026 02:07:31 +0300 Subject: [PATCH 4/6] test: account for pre-setup marker in fixture finalizer output --- testing/python/fixtures.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index cb2f1daab63..eb37a91ec6e 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4626,10 +4626,12 @@ def test_second(my_fixture): result.stdout.fnmatch_lines( [ "test_fixtures.py::test_first ", + " SETUP START F my_fixture", " SETUP F my_fixture", " test_fixtures.py::test_first (fixtures used: my_fixture, request) PASSED", "test_fixtures.py::test_first ERROR", "test_fixtures.py::test_second ", + " SETUP START F my_fixture", " SETUP F my_fixture", " test_fixtures.py::test_second (fixtures used: my_fixture, request) PASSED", " TEARDOWN F my_fixture", From 616c22c176e2bcf9699d31f352d0bf28db024e76 Mon Sep 17 00:00:00 2001 From: Stamatis-Christos Saridakis <34583142+SaridakisStamatisChristos@users.noreply.github.com> Date: Mon, 19 Oct 2026 02:11:41 +0300 Subject: [PATCH 5/6] test: preserve exact blank-line output around setup-start diagnostics --- testing/python/fixtures.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index eb37a91ec6e..8626beb7315 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4627,11 +4627,13 @@ def test_second(my_fixture): [ "test_fixtures.py::test_first ", " SETUP START F my_fixture", + "", " SETUP F my_fixture", " test_fixtures.py::test_first (fixtures used: my_fixture, request) PASSED", "test_fixtures.py::test_first ERROR", "test_fixtures.py::test_second ", " SETUP START F my_fixture", + "", " SETUP F my_fixture", " test_fixtures.py::test_second (fixtures used: my_fixture, request) PASSED", " TEARDOWN F my_fixture", From 23500aaad18242cd3654c9e1aabc39b3586eb37d Mon Sep 17 00:00:00 2001 From: Stamatis-Christos Saridakis <34583142+SaridakisStamatisChristos@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:59:38 +0300 Subject: [PATCH 6/6] refactor: make fixture marker line termination explicit --- src/_pytest/setuponly.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index 8b806c16dee..8ecd5ff9900 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -34,7 +34,7 @@ def pytest_fixture_setup( ) -> Generator[None, object, object]: if request.config.option.setupshow: # Report this fixture before entering user code, including if it hangs. - _show_fixture_action(fixturedef, request.config, "SETUP START") + _show_fixture_action(fixturedef, request.config, "SETUP START", end_line=True) try: return (yield) finally: @@ -65,7 +65,7 @@ def pytest_fixture_post_finalizer( def _show_fixture_action( - fixturedef: FixtureDef[object], config: Config, msg: str + fixturedef: FixtureDef[object], config: Config, msg: str, *, end_line: bool = False ) -> None: capman = config.pluginmanager.getplugin("capturemanager") if capman: @@ -89,7 +89,7 @@ def _show_fixture_action( tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]") # End the early marker before any output from the fixture body. - if msg == "SETUP START": + if end_line: tw.line() else: tw.flush()