diff --git a/changelog/13896.bugfix.rst b/changelog/13896.bugfix.rst new file mode 100644 index 00000000000..528556d7a5b --- /dev/null +++ b/changelog/13896.bugfix.rst @@ -0,0 +1,3 @@ +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. + +You may enable it again by passing ``-p terminalprogress``. We may enable it by default again once compatibility improves in the future. diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 85a509dff3f..422b68c9e29 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -178,6 +178,10 @@ New features - `#13072 `_: Added support for displaying test session **progress in the terminal tab** using the `OSC 9;4; `_ ANSI sequence. + + **Note**: *This feature has been disabled by default in version 9.0.2, except on Windows, due to compatibility issues with some terminal emulators. + You may enable it again by passing* ``-p terminalprogress``. *We may enable it by default again once compatibility improves in the future.* + When pytest runs in a supported terminal emulator like ConEmu, Gnome Terminal, Ptyxis, Windows Terminal, Kitty or Ghostty, you'll see the progress in the terminal tab or window, allowing you to monitor pytest's progress at a glance. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index a17e246845a..63bcdad3f11 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -303,6 +303,7 @@ def directory_arg(path: str, optname: str) -> str: *default_plugins, "pytester", "pytester_assertions", + "terminalprogress", } diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 4517b05bdee..e66e4f48dd6 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -16,7 +16,6 @@ import datetime from functools import partial import inspect -import os from pathlib import Path import platform import sys @@ -299,16 +298,10 @@ def mywriter(tags, args): config.trace.root.setprocessor("pytest:config", mywriter) - if reporter.isatty(): - # Some terminals interpret OSC 9;4 as desktop notification, - # skip on those we know (#13896). - should_skip_terminal_progress = ( - # iTerm2 (reported on version 3.6.5). - "ITERM_SESSION_ID" in os.environ - ) - if not should_skip_terminal_progress: - plugin = TerminalProgressPlugin(reporter) - config.pluginmanager.register(plugin, "terminalprogress") + # See terminalprogress.py. + # On Windows it's safe to load by default. + if sys.platform == "win32": + config.pluginmanager.import_plugin("terminalprogress") def getreportopt(config: Config) -> str: diff --git a/src/_pytest/terminalprogress.py b/src/_pytest/terminalprogress.py new file mode 100644 index 00000000000..4d5d321c490 --- /dev/null +++ b/src/_pytest/terminalprogress.py @@ -0,0 +1,28 @@ +# A plugin to register the TerminalProgressPlugin plugin. +# +# This plugin is not loaded by default due to compatibility issues (#13896), +# but can be enabled in one of these ways: +# - The terminal plugin enables it in a few cases where it's safe, and not +# blocked by the user (using e.g. `-p no:terminalprogress`). +# - The user explicitly requests it, e.g. using `-p terminalprogress`. +# +# In a few years, if it's safe, we can consider enabling it by default. Then, +# this file will become unnecessary and can be inlined into terminal.py. + +from __future__ import annotations + +from _pytest.config import Config +from _pytest.config import hookimpl +from _pytest.terminal import TerminalProgressPlugin +from _pytest.terminal import TerminalReporter + + +@hookimpl(trylast=True) +def pytest_configure(config: Config) -> None: + reporter: TerminalReporter | None = config.pluginmanager.get_plugin( + "terminalreporter" + ) + + if reporter is not None and reporter.isatty(): + plugin = TerminalProgressPlugin(reporter) + config.pluginmanager.register(plugin, name="terminalprogress-plugin") diff --git a/testing/test_terminal.py b/testing/test_terminal.py index a981e14f0a2..9b63484fdbe 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -3428,30 +3428,35 @@ def write_raw(s: str, *, flush: bool = False) -> None: tr._progress_nodeids_reported = set() return tr - def test_plugin_registration(self, pytester: pytest.Pytester) -> None: - """Test that the plugin is registered correctly on TTY output.""" + @pytest.mark.skipif(sys.platform != "win32", reason="#13896") + def test_plugin_registration_enabled_by_default( + self, pytester: pytest.Pytester + ) -> None: + """Test that the plugin registration is enabled by default. + + Currently only on Windows (#13896). + """ # The plugin module should be registered as a default plugin. with patch.object(sys.stdout, "isatty", return_value=True): config = pytester.parseconfigure() plugin = config.pluginmanager.get_plugin("terminalprogress") assert plugin is not None + def test_plugin_registred_on_all_platforms_when_explicitly_requested( + self, pytester: pytest.Pytester + ) -> None: + """Test that the plugin is registered on any platform if explicitly requested.""" + # The plugin module should be registered as a default plugin. + with patch.object(sys.stdout, "isatty", return_value=True): + config = pytester.parseconfigure("-p", "terminalprogress") + plugin = config.pluginmanager.get_plugin("terminalprogress") + assert plugin is not None + def test_disabled_for_non_tty(self, pytester: pytest.Pytester) -> None: """Test that plugin is disabled for non-TTY output.""" with patch.object(sys.stdout, "isatty", return_value=False): - config = pytester.parseconfigure() - plugin = config.pluginmanager.get_plugin("terminalprogress") - assert plugin is None - - def test_disabled_for_iterm2(self, pytester: pytest.Pytester, monkeypatch) -> None: - """Should not register the plugin on iTerm2 terminal since it interprets - OSC 9;4 as desktop notifications, not progress (#13896).""" - monkeypatch.setenv( - "ITERM_SESSION_ID", "w0t1p0:3DB6DF06-FE11-40C3-9A66-9E10A193A632" - ) - with patch.object(sys.stdout, "isatty", return_value=True): - config = pytester.parseconfigure() - plugin = config.pluginmanager.get_plugin("terminalprogress") + config = pytester.parseconfigure("-p", "terminalprogress") + plugin = config.pluginmanager.get_plugin("terminalprogress-plugin") assert plugin is None @pytest.mark.parametrize(