Repository: tox-dev/tox

Issue:
Set --parallel-no-spinner if in CI
## What's the problem this feature will solve?

`--parallel-no-spinner` is fairly required for CI runs to prevent the spinner from messing the output. It seems reasonable to set the flag on by default if CI is detected.

## Describe the solution you'd like

Set the `--parallel-no-spinner` default based on `is_ci()` utility.

## Alternative Solutions

No alternatives.

## Caveats
There's no `--parallel-spinner` which would force the spinner if it was disabled by the CI detection. `TOX_PARALLEL_NO_SPINNER=0` should still work in those (hopefully rare) cases.


Discussion:

Patch:
diff --git a/docs/changelog/3318.feature.rst b/docs/changelog/3318.feature.rst
new file mode 100644
index 00000000..eaa5ba35
--- /dev/null
+++ b/docs/changelog/3318.feature.rst
@@ -0,0 +1,1 @@
+Suppress spinner in parallel runs in CI - by :user:`ziima`.
diff --git a/src/tox/session/cmd/run/parallel.py b/src/tox/session/cmd/run/parallel.py
index 3ba22af0..d83d72a2 100644
--- a/src/tox/session/cmd/run/parallel.py
+++ b/src/tox/session/cmd/run/parallel.py
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
 
 from tox.plugin import impl
 from tox.session.env_select import CliEnv, register_env_select_flags
+from tox.util.ci import is_ci
 from tox.util.cpu import auto_detect_cpus
 
 from .common import env_run_create_flags, execute
@@ -28,7 +29,7 @@ def tox_add_option(parser: ToxParser) -> None:
     our = parser.add_command("run-parallel", ["p"], "run environments in parallel", run_parallel)
     register_env_select_flags(our, default=CliEnv())
     env_run_create_flags(our, mode="run-parallel")
-    parallel_flags(our, default_parallel=DEFAULT_PARALLEL)
+    parallel_flags(our, default_parallel=DEFAULT_PARALLEL, default_spinner=is_ci())
 
 
 def parse_num_processes(str_value: str) -> int | None:
@@ -51,6 +52,8 @@ def parallel_flags(
     our: ArgumentParser,
     default_parallel: int | str,
     no_args: bool = False,  # noqa: FBT001, FBT002
+    *,
+    default_spinner: bool = False,
 ) -> None:
     our.add_argument(
         "-p",
@@ -75,7 +78,11 @@ def parallel_flags(
         "--parallel-no-spinner",
         action="store_true",
         dest="parallel_no_spinner",
-        help="run tox environments in parallel, but don't show the spinner, implies --parallel",
+        default=default_spinner,
+        help=(
+            "run tox environments in parallel, but don't show the spinner, implies --parallel. "
+            "Disabled by default if CI is detected (not in legacy API)."
+        ),
     )
 
 


Test patch:
diff --git a/tests/session/cmd/test_legacy.py b/tests/session/cmd/test_legacy.py
index 6b4bd243..508e4ec9 100644
--- a/tests/session/cmd/test_legacy.py
+++ b/tests/session/cmd/test_legacy.py
@@ -121,6 +121,18 @@ def test_legacy_run_sequential(tox_project: ToxProjectCreator, mocker: MockerFix
     assert run_sequential.call_count == 1
 
 
+def test_legacy_run_sequential_ci(
+    tox_project: ToxProjectCreator, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    """Test legacy run sequential in CI by default."""
+    run_sequential = mocker.patch("tox.session.cmd.legacy.run_sequential")
+    monkeypatch.setenv("CI", "1")
+
+    tox_project({"tox.ini": ""}).run("le", "-e", "py")
+
+    assert run_sequential.call_count == 1
+
+
 def test_legacy_help(tox_project: ToxProjectCreator) -> None:
     outcome = tox_project({"tox.ini": ""}).run("le", "-h")
     outcome.assert_success()
diff --git a/tests/session/cmd/test_parallel.py b/tests/session/cmd/test_parallel.py
index 01b5b6bc..89c007fb 100644
--- a/tests/session/cmd/test_parallel.py
+++ b/tests/session/cmd/test_parallel.py
@@ -186,6 +186,23 @@ def test_parallel_no_spinner(tox_project: ToxProjectCreator) -> None:
     )
 
 
+def test_parallel_no_spinner_ci(
+    tox_project: ToxProjectCreator, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    """Ensure spinner is disabled by default in CI."""
+    mocked = mocker.patch.object(parallel, "execute")
+    monkeypatch.setenv("CI", "1")
+
+    tox_project({"tox.ini": ""}).run("p")
+
+    mocked.assert_called_once_with(
+        mock.ANY,
+        max_workers=None,
+        has_spinner=False,
+        live=False,
+    )
+
+
 def test_parallel_no_spinner_legacy(tox_project: ToxProjectCreator) -> None:
     with mock.patch.object(parallel, "execute") as mocked:
         tox_project({"tox.ini": ""}).run("--parallel-no-spinner")