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..8ecd5ff9900 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", end_line=True) try: return (yield) finally: @@ -62,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: @@ -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 end_line: + tw.line() + else: + tw.flush() if capman: capman.resume_global_capture() diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index cb2f1daab63..8626beb7315 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4626,10 +4626,14 @@ 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", diff --git a/testing/test_setuponly.py b/testing/test_setuponly.py index 1d2261f75a8..b1e4e6adaff 100644 --- a/testing/test_setuponly.py +++ b/testing/test_setuponly.py @@ -8,6 +8,75 @@ 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): return request.param