fix: guard FDCapture.snap() against prematurely closed tmpfile#14531
fix: guard FDCapture.snap() against prematurely closed tmpfile#14531RonnyPfannschmidt wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Guards FDCapture.snap() and FDCaptureBinary.snap() against the case where the underlying tmpfile has been closed externally (notably by Python 3.14.0–3.14.4's incremental GC finalizing the TemporaryFile before pytest's capture teardown runs). Instead of raising ValueError: I/O operation on closed file during _ensure_unconfigure → stop_global_capturing, snap() now returns EMPTY_BUFFER, preserving the intended exit code (e.g., 5 for no-tests-collected) and avoiding confusing tracebacks in test output.
Changes:
- Add early-return on
self.tmpfile.closedin bothFDCaptureBinary.snap()andFDCapture.snap(). - Add changelog entry for issue #14528.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/_pytest/capture.py | Returns empty buffer from snap() when tmpfile is already closed, avoiding the seek(0) crash. |
| changelog/14528.bugfix.rst | New changelog entry describing the fix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -588,6 +590,8 @@ class FDCapture(FDCaptureBase[str]): | |||
|
|
|||
| def snap(self) -> str: | |||
| self._assert_state("snap", ("started", "suspended")) | |||
| if self.tmpfile.closed: | |||
| return self.EMPTY_BUFFER | |||
9e43244 to
594e4f5
Compare
|
running another expeirment first - im of the impression the initial explanation doesnt fit and only coincides |
adf46dd to
be36867
Compare
…t-dev#14528) Reported on Windows 11 + Python 3.14.3 + pytest 9.0.3: when collection short-circuits inside a dot-prefix path with import errors, the capture tmpfile is already closed by the time snap() runs during teardown, causing ``ValueError: I/O operation on closed file`` and corrupting the exit code from 2/5 to 1. Root cause is unclear. Reproduction was attempted on Windows CI with Python 3.14.3 (incremental GC active), the reporter's exact plugin set (pytest-asyncio 1.3.0, anyio 4.13.0, pytest-httpx 0.36.2), and an identical dot-prefix tree layout — the issue did not reproduce. All EncodedFile.close() calls traced to explicit teardown paths; no GC-triggered file closure was observed. The trigger appears to depend on additional environmental factors specific to the reporter's machine. Guard snap() in both FDCapture and FDCaptureBinary to return EMPTY_BUFFER when the tmpfile is already closed, with a PytestWarning so the lost capture is discoverable. The captured data is unrecoverable regardless, so crashing on teardown only makes things worse. Closes pytest-dev#14528 Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Claude Opus 4 <claude@anthropic.com>
be36867 to
77940ef
Compare
Summary
Fixes #14528 —
ValueError: I/O operation on closed fileduring capture teardown on Windows + Python 3.14.3.Guards
FDCapture.snap()andFDCaptureBinary.snap()against a prematurely closedtmpfile, returningEMPTY_BUFFERand emitting aPytestWarninginstead of crashing. The captured data is unrecoverable regardless, so crashing only makes things worse.Reproduction attempts
The issue was reported on Windows 11 + Python 3.14.3 + pytest 9.0.3 with
pytest-asyncio,anyio, andpytest-httpx. Collection short-circuits inside a dot-prefix path (.claude/skills/actionmail/tests/) with import errors, and the capture tmpfile is already closed by the timesnap()runs.We attempted to reproduce on Windows CI with:
gc.get_threshold() = (2000, 10, 0))pytest-asyncio==1.3.0,anyio==4.13.0,pytest-httpx==0.36.2)__init__.py, two test files withsys.path.insert+ failedimport run, and afixtures/siblingThe issue did not reproduce. All
EncodedFile.close()calls traced to explicit teardown paths (pytest_keyboard_interrupt → stop_global_capturing → done → tmpfile.close()). No GC-triggered file closure was observed. The trigger appears to depend on additional environmental factors specific to the reporter's machine.Notably, Python 3.14.5 (which reverts the incremental GC) also showed no issue, consistent with the reporter's observation that the problem is 3.14-specific.
Changes
src/_pytest/capture.py: Guardsnap()in bothFDCaptureandFDCaptureBinary— checkself.tmpfile.closedbefore seeking, emitPytestWarning, returnEMPTY_BUFFERtesting/test_capture.py: Parametrized regression test usingos.pipe()for isolationchangelog/14528.bugfix.rst: Changelog entryTest plan
TestFDCaptureClosedTmpfile— deterministically closes tmpfile beforesnap(), assertsEMPTY_BUFFERandPytestWarning