Skip to content

Commit 75e5334

Browse files
committed
Isolate tests from terminal geometry, clipboard state, and shell timing
1 parent 395a23a commit 75e5334

3 files changed

Lines changed: 52 additions & 22 deletions

File tree

‎tests/conftest.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Cmd2 unit/functional testing"""
22

3+
import os
34
import sys
45
from collections.abc import Callable
56
from contextlib import redirect_stderr
@@ -97,6 +98,18 @@ def neutral_color_environment(monkeypatch: pytest.MonkeyPatch) -> None:
9798
monkeypatch.delenv(name, raising=False)
9899

99100

101+
@pytest.fixture(autouse=True)
102+
def fixed_terminal_size(monkeypatch: pytest.MonkeyPatch) -> None:
103+
"""Keep wrapping independent of the caller's terminal and xdist worker environment.
104+
105+
Clear explicit dimensions as well as fixing the OS query: Rich and argparse
106+
consult both. Individual tests can still override geometry with monkeypatch.
107+
"""
108+
monkeypatch.delenv("COLUMNS", raising=False)
109+
monkeypatch.delenv("LINES", raising=False)
110+
monkeypatch.setattr(os, "get_terminal_size", lambda fd=1: os.terminal_size((80, 24)))
111+
112+
100113
@pytest.fixture
101114
def base_app() -> cmd2.Cmd:
102115
return cmd2.Cmd(include_py=True, include_ipy=True)

‎tests/test_cmd2.py‎

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
CommandSet,
3131
Completions,
3232
SubcommandRecord,
33-
clipboard,
3433
constants,
3534
exceptions,
3635
plugin,
@@ -864,29 +863,38 @@ def test_pipe_to_shell_and_redirect(redirection_app) -> None:
864863
os.remove(filename)
865864

866865

867-
def test_pipe_to_shell_error(redirection_app) -> None:
868-
# Try to pipe command output to a shell command that doesn't exist in order to produce an error
866+
def test_pipe_to_shell_error(redirection_app, mocker, capsys) -> None:
867+
"""An already-exited pipe process must be reported before the command runs.
868+
869+
A real nonexistent command may take longer than the startup probe under load.
870+
In that case the command runs and writes to a closing pipe instead, exercising
871+
a different path (including EINVAL on Windows). Model the early exit explicitly.
872+
"""
873+
popen = mocker.patch("subprocess.Popen", autospec=True)
874+
process = popen.return_value
875+
process.returncode = 127
876+
process.wait.return_value = 127
877+
869878
out, err = run_cmd(redirection_app, "print_output | foobarbaz.this_does_not_exist")
870879
assert not out
871-
assert "Pipe process exited with code" in err[0]
880+
assert "Pipe process exited with code 127 before command could run" in " ".join(err)
881+
assert capsys.readouterr().out == ""
882+
process.wait.assert_called_once()
883+
assert popen.call_args.kwargs["stdin"].closed
884+
872885

886+
def test_send_to_paste_buffer(redirection_app: RedirectionApp, capsys: pytest.CaptureFixture[str], mocker) -> None:
887+
# Exercise cmd2's real clipboard redirection against a private backend, not the
888+
# shared OS clipboard (which another test run or desktop application can alter).
889+
contents = "previous clipboard contents\n"
873890

874-
try:
875-
# try getting the contents of the clipboard
876-
_ = clipboard.get_paste_buffer()
877-
# pyperclip raises at least the following types of exceptions
878-
# FileNotFoundError on Windows Subsystem for Linux (WSL) when Windows paths are removed from $PATH
879-
# ValueError for headless Linux systems without Gtk installed
880-
# AssertionError can be raised by paste_klipper().
881-
# PyperclipException for pyperclip-specific exceptions
882-
except Exception: # noqa: BLE001
883-
can_paste = False
884-
else:
885-
can_paste = True
891+
def copy(text: str) -> None:
892+
nonlocal contents
893+
contents = text
886894

895+
mocker.patch("pyperclip.copy", autospec=True, side_effect=copy)
896+
mocker.patch("pyperclip.paste", autospec=True, side_effect=lambda: contents)
887897

888-
@pytest.mark.skipif(not can_paste, reason="Pyperclip could not find a copy/paste mechanism for your system")
889-
def test_send_to_paste_buffer(redirection_app: RedirectionApp, capsys: pytest.CaptureFixture[str]) -> None:
890898
# Test writing to the PasteBuffer/Clipboard
891899
run_cmd(redirection_app, "print_output >")
892900

@@ -932,10 +940,7 @@ def test_allow_clipboard_initializer(redirection_app) -> None:
932940
assert noclipcmd.allow_clipboard is False
933941

934942

935-
# if clipboard access is not allowed, cmd2 should check that first
936-
# before it tries to do anything with pyperclip, that's why we can
937-
# safely run this test without skipping it if pyperclip doesn't
938-
# work in the test environment, like we do for test_send_to_paste_buffer()
943+
# Disallowing clipboard access must be checked before contacting the backend.
939944
def test_allow_clipboard(base_app) -> None:
940945
base_app.allow_clipboard = False
941946
out, err = run_cmd(base_app, "help >")

‎tests/test_suite_environment.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ def test_color_environment_does_not_leak_into_tests(name: str) -> None:
2525
)
2626

2727

28+
def test_terminal_dimensions_do_not_leak_into_tests() -> None:
29+
"""Both Rich and argparse must see the geometry used by wrapping assertions."""
30+
import shutil
31+
32+
from rich.console import Console
33+
34+
assert "COLUMNS" not in os.environ
35+
assert "LINES" not in os.environ
36+
assert shutil.get_terminal_size() == (80, 24)
37+
assert Console(force_terminal=False, legacy_windows=False).size == (80, 24)
38+
39+
2840
class EncodingProbe(cmd2.Cmd):
2941
"""Reports the encoding of whatever stream output is currently going to."""
3042

0 commit comments

Comments
 (0)