Skip to content

Fix terminal reporter output not appearing with capture active#13848

Open
RonnyPfannschmidt wants to merge 1 commit into
pytest-dev:mainfrom
RonnyPfannschmidt:fix-8973-terminalwriter-use-dup-if-necessary
Open

Fix terminal reporter output not appearing with capture active#13848
RonnyPfannschmidt wants to merge 1 commit into
pytest-dev:mainfrom
RonnyPfannschmidt:fix-8973-terminalwriter-use-dup-if-necessary

Conversation

@RonnyPfannschmidt

@RonnyPfannschmidt RonnyPfannschmidt commented Oct 26, 2025

Copy link
Copy Markdown
Member

Fixes #8973

Output written through the terminal reporter while output capture is active (e.g. by a plugin from within a test, like pytest-print) used to silently disappear into the capture buffers. Plugins could only work around this by reaching into the private CaptureManager API to toggle capture around each write.

Instead of toggling capture off and on around writes, the terminal reporter now sidesteps capture entirely: _prepareconfig() duplicates stdout's file descriptor before any capture can start and wraps it in an unbuffered text file stored in the config stash (owned and closed by the config's cleanup). This file always refers to the original stdout — the same file capture restores on suspend — and remains writable no matter how capture is started, stopped, or reconfigured. Writing to it neither goes through capture nor affects capture state.

To preserve output ordering with writes that legitimately reach the terminal through sys.stdout (prints under -s or capsys.disabled(), which are block-buffered when stdout is not a tty), the wrapper flushes stdout's buffers before each of its own writes; during active capture that flush merely moves pending test output into the capture buffers where it belongs.

The regression test runs in a subprocess: in-process pytester runs replace stdout with an object without a real file descriptor, which takes the sys.stdout fallback path instead of the duplicated-fd path under test (the previous in-process regression test passed even without the fix).

🤖 Generated with Claude Code

@bluetech bluetech left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder how this compares to integrating with capture plugin e.g. using suspend_global_capture (or such)?

Comment thread src/_pytest/config/__init__.py Outdated
# File descriptor for stdout, duplicated before capture starts.
# This allows the terminal reporter to bypass pytest's output capture (#8973).
# The FD is duplicated early in _prepareconfig before any capture can start.
stdout_fd_dup_key = StashKey[int]()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems weird to me that this is in config/ when config doesn't really care about it. It should be in terminal.py or maybe capture.py if we want to take care of it "at the source".

Comment thread testing/test_terminal.py Outdated
plugin = config.pluginmanager.get_plugin("terminalprogress")
assert plugin is not None
# Use a mock file with isatty returning True
from io import StringIO

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to have the imports at the beginning of the file if they don't need to be "lazy". Also below.

Comment thread testing/test_terminal.py Outdated
def test_plugin_registration(self, pytester: pytest.Pytester) -> None:
"""Test that the plugin is registered correctly on TTY output."""
# The plugin module should be registered as a default plugin.
with patch.object(sys.stdout, "isatty", return_value=True):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why need to change this test?

Comment thread testing/test_terminal.py Outdated
assert "\x1b]9;4;0;\x1b\\" in mock_file.getvalue()


def test_terminal_reporter_write_with_capture(pytester: Pytester) -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test seems to pass also in main, so needs some refinement if we want to prevent regression.

@RonnyPfannschmidt
RonnyPfannschmidt force-pushed the fix-8973-terminalwriter-use-dup-if-necessary branch 2 times, most recently from 8ee87af to 5560219 Compare July 24, 2026 16:38
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Jul 24, 2026
Fixes pytest-dev#8973.

Output written through the terminal reporter while output capture is
active (e.g. by a plugin from within a test) used to disappear into the
capture buffers.

Duplicate stdout's file descriptor early in _prepareconfig, before any
capture can start, and wrap it in an unbuffered text file stored in the
config stash. The terminal reporter writes to this file, which always
refers to the original stdout no matter how capture is started, stopped,
or reconfigured - sidestepping capture entirely rather than toggling it.

The regression test runs in a subprocess: in-process pytester runs
replace stdout with an object without a real file descriptor, which
takes the sys.stdout fallback path instead of the one under test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@RonnyPfannschmidt
RonnyPfannschmidt force-pushed the fix-8973-terminalwriter-use-dup-if-necessary branch from 5560219 to 314979a Compare July 24, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Terminal reporter doesn't print

2 participants