From fb87c362632a54167d094b181847ca8ce70d546c Mon Sep 17 00:00:00 2001 From: goodboy Date: Wed, 13 May 2026 19:00:18 -0400 Subject: [PATCH] Add hang-snapshot session index to pytest summary - `_testing/trace.py`: add `_SNAPSHOT_INDEX` session- scoped list populated by `_do_capture_snapshot()` on each successful dump; add TODO for future `TRACTOR_TRACE_HOLD=1` pause-on-hang mode - `_testing/pytest.py`: add `pytest_terminal_summary` hook that prints all captured snapshot dirs at end-of-session so paths don't get buried in scrollback (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tractor/_testing/pytest.py | 34 ++++++++++++++++++++++++++++++++++ tractor/_testing/trace.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/tractor/_testing/pytest.py b/tractor/_testing/pytest.py index 5aca2908..38d9ebec 100644 --- a/tractor/_testing/pytest.py +++ b/tractor/_testing/pytest.py @@ -765,3 +765,37 @@ def set_fork_aware_capture( # request=request, # start_method=start_method, # ) + + +def pytest_terminal_summary( + terminalreporter, + exitstatus: int, + config: pytest.Config, +) -> None: + ''' + End-of-session summary: list all + `fail_after_w_trace`/`afk_alarm_w_trace` snapshot dirs + captured during the run so the human doesn't have to scroll + back through captured-stderr lines to find dump paths. + + Reads from `tractor._testing.trace._SNAPSHOT_INDEX` which is + populated by `_do_capture_snapshot()` on each successful + snapshot capture. + + No-op when zero snapshots were captured (most sessions). + + ''' + from .trace import _SNAPSHOT_INDEX + + if not _SNAPSHOT_INDEX: + return + + tr = terminalreporter + tr.write_sep('=', 'tractor hang-snapshot index') + tr.write_line( + f'{len(_SNAPSHOT_INDEX)} `fail_after_w_trace` / ' + f'`afk_alarm_w_trace` snapshot(s) captured this session:' + ) + for label, path in _SNAPSHOT_INDEX: + tr.write_line(f' {label}') + tr.write_line(f' → {path}') diff --git a/tractor/_testing/trace.py b/tractor/_testing/trace.py index 5073e337..609f00ff 100644 --- a/tractor/_testing/trace.py +++ b/tractor/_testing/trace.py @@ -1000,6 +1000,31 @@ class AFKAlarmTimeout(TimeoutError): ''' +# Session-scoped list of snapshot (label, dump_dir) tuples +# captured by `fail_after_w_trace` / `afk_alarm_w_trace` during +# the current process lifetime. Populated by +# `_do_capture_snapshot()` on each successful dump. The +# `pytest_terminal_summary` hook in `tractor._testing.pytest` +# reads this at end-of-session to print an index of all +# snapshot dirs so the human doesn't have to scroll back through +# captured-stderr lines to find paths. +_SNAPSHOT_INDEX: list[tuple[str, Path]] = [] + + +# TODO: follow-up — `TRACTOR_TRACE_HOLD=1` pause-on-hang mode. +# When env-var-enabled, `_do_capture_snapshot` would block on +# `input('press Enter to continue...')` reading from +# `sys.__stdin__` AFTER the dump succeeds, BEFORE re-raising the +# original exception. This lets a human invoke +# `acli.ptree`/`acli.bindspace_scan` from a second terminal +# while the cancel-cascade is frozen mid-flight — currently +# impossible because the per-test reaper fixture sweeps +# orphans within ~0.6s of the timeout firing. See discussion +# 2026-05-13: orphans visible in snapshot's `trace.txt` +# (depth_3 / depth_1 init-adopted procs) but invisible to any +# post-test `acli.*` invocation. + + def _do_capture_snapshot( *, label: str, @@ -1015,6 +1040,10 @@ def _do_capture_snapshot( Returns the snapshot `Path` on success, `None` if capture itself failed (with a banner printed to stderr). + Appends `(label, dump_dir)` to the session-scoped + `_SNAPSHOT_INDEX` on success so the `pytest_terminal_summary` + hook can render an index at end-of-session. + ''' target_pid: int = pid if pid is not None else os.getpid() # NOTE: print to `sys.__stderr__` (the ORIGINAL unredirected @@ -1054,6 +1083,7 @@ def _do_capture_snapshot( f'pid={target_pid}); snapshot at: {dump_dir}', file=sys.__stderr__, ) + _SNAPSHOT_INDEX.append((label, dump_dir)) return dump_dir