Skip to content

Commit b229efb

Browse files
authored
Merge pull request #13990 from pytest-dev/patchback/backports/9.0.x/66834f362e38e96eb7685e21f46bdb83ba52530b/pr-13971
[PR #13971/66834f36 backport][9.0.x] terminal: disable OSC 9;4; terminal progress by default, except on Windows
2 parents 22a2158 + 1c27999 commit b229efb

File tree

6 files changed

+89
-40
lines changed

6 files changed

+89
-40
lines changed

changelog/13896.bugfix.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The terminal progress feature added in pytest 9.0.0 has been disabled by default, except on Windows, due to compatibility issues with some terminal emulators.
2+
3+
You may enable it again by passing ``-p terminalprogress``. We may enable it by default again once compatibility improves in the future.
4+
5+
Additionally, when the environment variable ``TERM`` is ``dumb``, the escape codes are no longer emitted, even if the plugin is enabled.

doc/en/changelog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ New features
178178

179179

180180
- `#13072 <https://github.com/pytest-dev/pytest/issues/13072>`_: Added support for displaying test session **progress in the terminal tab** using the `OSC 9;4; <https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC>`_ ANSI sequence.
181+
182+
**Note**: *This feature has been disabled by default in version 9.0.2, except on Windows, due to compatibility issues with some terminal emulators.
183+
You may enable it again by passing* ``-p terminalprogress``. *We may enable it by default again once compatibility improves in the future.*
184+
181185
When pytest runs in a supported terminal emulator like ConEmu, Gnome Terminal, Ptyxis, Windows Terminal, Kitty or Ghostty,
182186
you'll see the progress in the terminal tab or window,
183187
allowing you to monitor pytest's progress at a glance.

src/_pytest/config/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ def directory_arg(path: str, optname: str) -> str:
299299
*default_plugins,
300300
"pytester",
301301
"pytester_assertions",
302+
"terminalprogress",
302303
}
303304

304305

src/_pytest/terminal.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import datetime
1717
from functools import partial
1818
import inspect
19-
import os
2019
from pathlib import Path
2120
import platform
2221
import sys
@@ -299,16 +298,10 @@ def mywriter(tags, args):
299298

300299
config.trace.root.setprocessor("pytest:config", mywriter)
301300

302-
if reporter.isatty():
303-
# Some terminals interpret OSC 9;4 as desktop notification,
304-
# skip on those we know (#13896).
305-
should_skip_terminal_progress = (
306-
# iTerm2 (reported on version 3.6.5).
307-
"ITERM_SESSION_ID" in os.environ
308-
)
309-
if not should_skip_terminal_progress:
310-
plugin = TerminalProgressPlugin(reporter)
311-
config.pluginmanager.register(plugin, "terminalprogress")
301+
# See terminalprogress.py.
302+
# On Windows it's safe to load by default.
303+
if sys.platform == "win32":
304+
config.pluginmanager.import_plugin("terminalprogress")
312305

313306

314307
def getreportopt(config: Config) -> str:

src/_pytest/terminalprogress.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# A plugin to register the TerminalProgressPlugin plugin.
2+
#
3+
# This plugin is not loaded by default due to compatibility issues (#13896),
4+
# but can be enabled in one of these ways:
5+
# - The terminal plugin enables it in a few cases where it's safe, and not
6+
# blocked by the user (using e.g. `-p no:terminalprogress`).
7+
# - The user explicitly requests it, e.g. using `-p terminalprogress`.
8+
#
9+
# In a few years, if it's safe, we can consider enabling it by default. Then,
10+
# this file will become unnecessary and can be inlined into terminal.py.
11+
12+
from __future__ import annotations
13+
14+
import os
15+
16+
from _pytest.config import Config
17+
from _pytest.config import hookimpl
18+
from _pytest.terminal import TerminalProgressPlugin
19+
from _pytest.terminal import TerminalReporter
20+
21+
22+
@hookimpl(trylast=True)
23+
def pytest_configure(config: Config) -> None:
24+
reporter: TerminalReporter | None = config.pluginmanager.get_plugin(
25+
"terminalreporter"
26+
)
27+
28+
if reporter is not None and reporter.isatty() and os.environ.get("TERM") != "dumb":
29+
plugin = TerminalProgressPlugin(reporter)
30+
config.pluginmanager.register(plugin, name="terminalprogress-plugin")

testing/test_terminal.py

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
from typing import cast
1313
from typing import Literal
1414
from typing import NamedTuple
15-
from unittest.mock import Mock
16-
from unittest.mock import patch
15+
from unittest import mock
1716

1817
import pluggy
1918

@@ -3419,40 +3418,57 @@ def mock_file(self) -> StringIO:
34193418

34203419
@pytest.fixture
34213420
def mock_tr(self, mock_file: StringIO) -> pytest.TerminalReporter:
3422-
tr = Mock(spec=pytest.TerminalReporter)
3421+
tr: pytest.TerminalReporter = mock.create_autospec(pytest.TerminalReporter)
34233422

3424-
def write_raw(s: str, *, flush: bool = False) -> None:
3425-
mock_file.write(s)
3423+
def write_raw(content: str, *, flush: bool = False) -> None:
3424+
mock_file.write(content)
34263425

3427-
tr.write_raw = write_raw
3426+
tr.write_raw = write_raw # type: ignore[method-assign]
34283427
tr._progress_nodeids_reported = set()
34293428
return tr
34303429

3431-
def test_plugin_registration(self, pytester: pytest.Pytester) -> None:
3432-
"""Test that the plugin is registered correctly on TTY output."""
3430+
@pytest.mark.skipif(sys.platform != "win32", reason="#13896")
3431+
def test_plugin_registration_enabled_by_default(
3432+
self, pytester: pytest.Pytester, monkeypatch: MonkeyPatch
3433+
) -> None:
3434+
"""Test that the plugin registration is enabled by default.
3435+
3436+
Currently only on Windows (#13896).
3437+
"""
3438+
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
34333439
# The plugin module should be registered as a default plugin.
3434-
with patch.object(sys.stdout, "isatty", return_value=True):
3435-
config = pytester.parseconfigure()
3436-
plugin = config.pluginmanager.get_plugin("terminalprogress")
3437-
assert plugin is not None
3440+
config = pytester.parseconfigure()
3441+
plugin = config.pluginmanager.get_plugin("terminalprogress")
3442+
assert plugin is not None
34383443

3439-
def test_disabled_for_non_tty(self, pytester: pytest.Pytester) -> None:
3444+
def test_plugin_registred_on_all_platforms_when_explicitly_requested(
3445+
self, pytester: pytest.Pytester, monkeypatch: MonkeyPatch
3446+
) -> None:
3447+
"""Test that the plugin is registered on any platform if explicitly requested."""
3448+
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
3449+
# The plugin module should be registered as a default plugin.
3450+
config = pytester.parseconfigure("-p", "terminalprogress")
3451+
plugin = config.pluginmanager.get_plugin("terminalprogress")
3452+
assert plugin is not None
3453+
3454+
def test_disabled_for_non_tty(
3455+
self, pytester: pytest.Pytester, monkeypatch: MonkeyPatch
3456+
) -> None:
34403457
"""Test that plugin is disabled for non-TTY output."""
3441-
with patch.object(sys.stdout, "isatty", return_value=False):
3442-
config = pytester.parseconfigure()
3443-
plugin = config.pluginmanager.get_plugin("terminalprogress")
3444-
assert plugin is None
3445-
3446-
def test_disabled_for_iterm2(self, pytester: pytest.Pytester, monkeypatch) -> None:
3447-
"""Should not register the plugin on iTerm2 terminal since it interprets
3448-
OSC 9;4 as desktop notifications, not progress (#13896)."""
3449-
monkeypatch.setenv(
3450-
"ITERM_SESSION_ID", "w0t1p0:3DB6DF06-FE11-40C3-9A66-9E10A193A632"
3451-
)
3452-
with patch.object(sys.stdout, "isatty", return_value=True):
3453-
config = pytester.parseconfigure()
3454-
plugin = config.pluginmanager.get_plugin("terminalprogress")
3455-
assert plugin is None
3458+
monkeypatch.setattr(sys.stdout, "isatty", lambda: False)
3459+
config = pytester.parseconfigure("-p", "terminalprogress")
3460+
plugin = config.pluginmanager.get_plugin("terminalprogress-plugin")
3461+
assert plugin is None
3462+
3463+
def test_disabled_for_dumb_terminal(
3464+
self, pytester: pytest.Pytester, monkeypatch: MonkeyPatch
3465+
) -> None:
3466+
"""Test that plugin is disabled when TERM=dumb."""
3467+
monkeypatch.setenv("TERM", "dumb")
3468+
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
3469+
config = pytester.parseconfigure("-p", "terminalprogress")
3470+
plugin = config.pluginmanager.get_plugin("terminalprogress-plugin")
3471+
assert plugin is None
34563472

34573473
@pytest.mark.parametrize(
34583474
["state", "progress", "expected"],
@@ -3484,7 +3500,7 @@ def test_session_lifecycle(
34843500
"""Test progress updates during session lifecycle."""
34853501
plugin = TerminalProgressPlugin(mock_tr)
34863502

3487-
session = Mock(spec=pytest.Session)
3503+
session = mock.create_autospec(pytest.Session)
34883504
session.testscollected = 3
34893505

34903506
# Session start - should emit indeterminate progress.

0 commit comments

Comments
 (0)