@@ -20,7 +20,11 @@ def wait_for_stack(function: str) -> None:
2020 while time.monotonic() < deadline:
2121 text = path.read_text(encoding="utf-8")
2222 controller = next(Path("diagnostics").glob("controller-*.log")).read_text(encoding="utf-8")
23- if text.count("Timeout (") >= 2 and f"in {function}\\ n" in text and "pytest_runtestloop" in controller:
23+ if (
24+ text.count(" DUMP\\ n") >= 2
25+ and f"in {function}\\ n" in text
26+ and all("pytest_runtestloop" in log for log in (text, controller))
27+ ):
2428 return
2529 time.sleep(0.01)
2630 raise TimeoutError("stack dumps did not capture the blocked pytest process")
@@ -48,7 +52,7 @@ def test_diagnostics_capture_controller_and_workers(diagnostic_pytester: pytest.
4852 }
4953 assert set (logs ) == {"controller" , "gw0" , "gw1" }
5054 for worker , text in logs .items ():
51- assert text .count ("Timeout ( " ) >= 2
55+ assert text .count (" DUMP \n " ) >= 2
5256 assert "pytest_runtestloop" in text
5357 assert "pid=" in text
5458 assert "parent=" in text
@@ -76,7 +80,7 @@ def test_wait_for_stack() -> None:
7680 text : Final [str ] = next ((diagnostic_pytester .path / "diagnostics" ).glob ("*.log" )).read_text (encoding = "utf-8" )
7781 assert "call failed" in text
7882 assert "in test_wait_for_stack\n " in text
79- assert "Timeout ( " in text
83+ assert " DUMP \n " in text
8084
8185
8286def test_diagnostics_survive_abrupt_exit (diagnostic_pytester : pytest .Pytester ) -> None :
@@ -91,7 +95,7 @@ def test_wait_for_stack() -> None:
9195 assert diagnostic_pytester .runpytest_subprocess (timeout = 30 ).ret == 23
9296 text : Final [str ] = next ((diagnostic_pytester .path / "diagnostics" ).glob ("*.log" )).read_text (encoding = "utf-8" )
9397 assert "in test_wait_for_stack\n " in text
94- assert "Timeout ( " in text
98+ assert " DUMP \n " in text
9599 assert "STOP" not in text
96100
97101
@@ -135,10 +139,31 @@ def test_wait_after_nested_pytest() -> None:
135139 assert "in test_wait_after_nested_pytest\n " in text
136140
137141
142+ def test_diagnostics_dump_when_gil_held (diagnostic_pytester : pytest .Pytester ) -> None :
143+ # a backtracking regex keeps the GIL for over a second, longer than the stall timer's 10 intervals
144+ diagnostic_pytester .makepyfile (
145+ """
146+ import re
147+ import time
148+ from pathlib import Path
149+
150+ def test_hold_the_gil() -> None:
151+ log = next(Path("diagnostics").glob("controller-*.log"))
152+ deadline = time.monotonic() + 20
153+ while "Timeout (" not in log.read_text(encoding="utf-8"):
154+ assert time.monotonic() < deadline, "the stall timer never fired"
155+ re.fullmatch(r"(a+)+b", "a" * 26)
156+ """
157+ )
158+ diagnostic_pytester .runpytest_subprocess (timeout = 30 ).assert_outcomes (passed = 1 )
159+ text : Final [str ] = next ((diagnostic_pytester .path / "diagnostics" ).glob ("*.log" )).read_text (encoding = "utf-8" )
160+ _ , header , after = text .partition ("Timeout (" )
161+ assert (header , "in test_hold_the_gil\n " in after .split (" DUMP\n " , 1 )[0 ]) == ("Timeout (" , True )
162+
163+
138164def test_diagnostics_capture_shutdown_wait (diagnostic_pytester : pytest .Pytester ) -> None :
139165 diagnostic_pytester .makeconftest (
140166 """
141- import faulthandler
142167import os
143168import threading
144169import time
@@ -150,17 +175,13 @@ def pytest_unconfigure() -> None:
150175
151176def wait_for_shutdown() -> None:
152177 path: Final[Path] = Path("diagnostics") / f"controller-{os.getpid()}.log"
153- try:
154- deadline: Final[float] = time.monotonic() + 20
155- while time.monotonic() < deadline:
156- tail = path.read_text(encoding="utf-8").rsplit("STOP\\ n", 1)[-1]
157- if tail.count("in _shutdown\\ n") >= 2 and "in wait_for_shutdown\\ n" in tail:
158- return
159- time.sleep(0.01)
160- raise TimeoutError("no stack dump during shutdown")
161- finally:
162- # Keep this thread alive until the native watchdog finishes reading its frames.
163- faulthandler.cancel_dump_traceback_later()
178+ deadline: Final[float] = time.monotonic() + 20
179+ while time.monotonic() < deadline:
180+ tail = path.read_text(encoding="utf-8").rsplit("STOP\\ n", 1)[-1]
181+ if tail.count("in _shutdown\\ n") >= 2 and "in wait_for_shutdown\\ n" in tail:
182+ return
183+ time.sleep(0.01)
184+ raise TimeoutError("no stack dump during shutdown")
164185"""
165186 )
166187 diagnostic_pytester .makepyfile ("def test_empty() -> None: pass" )
0 commit comments