Skip to content

Commit 4b93535

Browse files
committed
Add unit tests for terminal restoration within ppaged
1 parent 06fe45e commit 4b93535

File tree

1 file changed

+174
-0
lines changed

1 file changed

+174
-0
lines changed

tests/test_cmd2.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2804,6 +2804,180 @@ def test_ppaged_no_pager(outsim_app) -> None:
28042804
assert out == msg + end
28052805

28062806

2807+
@pytest.mark.parametrize('has_tcsetpgrp', [True, False])
2808+
def test_ppaged_terminal_restoration(outsim_app, monkeypatch, has_tcsetpgrp) -> None:
2809+
"""Test terminal restoration in ppaged() after pager exits."""
2810+
# Make it look like we're in a terminal
2811+
stdin_mock = mock.MagicMock()
2812+
stdin_mock.isatty.return_value = True
2813+
stdin_mock.fileno.return_value = 0
2814+
monkeypatch.setattr(outsim_app, "stdin", stdin_mock)
2815+
2816+
stdout_mock = mock.MagicMock()
2817+
stdout_mock.isatty.return_value = True
2818+
monkeypatch.setattr(outsim_app, "stdout", stdout_mock)
2819+
2820+
if not sys.platform.startswith('win') and os.environ.get("TERM") is None:
2821+
monkeypatch.setenv('TERM', 'simulated')
2822+
2823+
# Mock termios and signal since they are imported within the method
2824+
termios_mock = mock.MagicMock()
2825+
# The error attribute needs to be the actual exception for isinstance checks
2826+
import termios
2827+
2828+
termios_mock.error = termios.error
2829+
monkeypatch.setitem(sys.modules, 'termios', termios_mock)
2830+
2831+
signal_mock = mock.MagicMock()
2832+
monkeypatch.setitem(sys.modules, 'signal', signal_mock)
2833+
2834+
# Mock os.tcsetpgrp and os.getpgrp
2835+
if has_tcsetpgrp:
2836+
monkeypatch.setattr(os, "tcsetpgrp", mock.Mock(), raising=False)
2837+
monkeypatch.setattr(os, "getpgrp", mock.Mock(return_value=123), raising=False)
2838+
else:
2839+
monkeypatch.delattr(os, "tcsetpgrp", raising=False)
2840+
2841+
# Mock subprocess.Popen
2842+
popen_mock = mock.MagicMock(name='Popen')
2843+
monkeypatch.setattr("subprocess.Popen", popen_mock)
2844+
2845+
# Set initial termios settings so the logic will run
2846+
dummy_settings = ["dummy settings"]
2847+
outsim_app._initial_termios_settings = dummy_settings
2848+
2849+
# Call ppaged
2850+
outsim_app.ppaged("Test")
2851+
2852+
# Verify restoration logic
2853+
if has_tcsetpgrp:
2854+
os.tcsetpgrp.assert_called_once_with(0, 123)
2855+
signal_mock.signal.assert_any_call(signal_mock.SIGTTOU, signal_mock.SIG_IGN)
2856+
2857+
termios_mock.tcsetattr.assert_called_once_with(0, termios_mock.TCSANOW, dummy_settings)
2858+
2859+
2860+
def test_ppaged_terminal_restoration_exceptions(outsim_app, monkeypatch) -> None:
2861+
"""Test that terminal restoration in ppaged() handles exceptions gracefully."""
2862+
# Make it look like we're in a terminal
2863+
stdin_mock = mock.MagicMock()
2864+
stdin_mock.isatty.return_value = True
2865+
stdin_mock.fileno.return_value = 0
2866+
monkeypatch.setattr(outsim_app, "stdin", stdin_mock)
2867+
2868+
stdout_mock = mock.MagicMock()
2869+
stdout_mock.isatty.return_value = True
2870+
monkeypatch.setattr(outsim_app, "stdout", stdout_mock)
2871+
2872+
if not sys.platform.startswith('win') and os.environ.get("TERM") is None:
2873+
monkeypatch.setenv('TERM', 'simulated')
2874+
2875+
# Mock termios and make it raise an error
2876+
termios_mock = mock.MagicMock()
2877+
import termios
2878+
2879+
termios_mock.error = termios.error
2880+
termios_mock.tcsetattr.side_effect = termios.error("Restoration failed")
2881+
monkeypatch.setitem(sys.modules, 'termios', termios_mock)
2882+
2883+
monkeypatch.setitem(sys.modules, 'signal', mock.MagicMock())
2884+
2885+
# Mock os.tcsetpgrp and os.getpgrp to prevent OSError before tcsetattr
2886+
monkeypatch.setattr(os, "tcsetpgrp", mock.Mock(), raising=False)
2887+
monkeypatch.setattr(os, "getpgrp", mock.Mock(return_value=123), raising=False)
2888+
2889+
# Mock subprocess.Popen
2890+
popen_mock = mock.MagicMock(name='Popen')
2891+
monkeypatch.setattr("subprocess.Popen", popen_mock)
2892+
2893+
# Set initial termios settings
2894+
outsim_app._initial_termios_settings = ["dummy settings"]
2895+
2896+
# Call ppaged - should not raise exception
2897+
outsim_app.ppaged("Test")
2898+
2899+
# Verify tcsetattr was attempted
2900+
assert termios_mock.tcsetattr.called
2901+
2902+
2903+
def test_ppaged_terminal_restoration_no_settings(outsim_app, monkeypatch) -> None:
2904+
"""Test that terminal restoration in ppaged() is skipped if no settings are saved."""
2905+
# Make it look like we're in a terminal
2906+
stdin_mock = mock.MagicMock()
2907+
stdin_mock.isatty.return_value = True
2908+
stdin_mock.fileno.return_value = 0
2909+
monkeypatch.setattr(outsim_app, "stdin", stdin_mock)
2910+
2911+
stdout_mock = mock.MagicMock()
2912+
stdout_mock.isatty.return_value = True
2913+
monkeypatch.setattr(outsim_app, "stdout", stdout_mock)
2914+
2915+
if not sys.platform.startswith('win') and os.environ.get("TERM") is None:
2916+
monkeypatch.setenv('TERM', 'simulated')
2917+
2918+
# Mock termios
2919+
termios_mock = mock.MagicMock()
2920+
monkeypatch.setitem(sys.modules, 'termios', termios_mock)
2921+
2922+
# Mock subprocess.Popen
2923+
popen_mock = mock.MagicMock(name='Popen')
2924+
monkeypatch.setattr("subprocess.Popen", popen_mock)
2925+
2926+
# Ensure initial termios settings is None
2927+
outsim_app._initial_termios_settings = None
2928+
2929+
# Call ppaged
2930+
outsim_app.ppaged("Test")
2931+
2932+
# Verify tcsetattr was NOT called
2933+
assert not termios_mock.tcsetattr.called
2934+
2935+
2936+
def test_ppaged_terminal_restoration_oserror(outsim_app, monkeypatch) -> None:
2937+
"""Test that terminal restoration in ppaged() handles OSError gracefully."""
2938+
# Make it look like we're in a terminal
2939+
stdin_mock = mock.MagicMock()
2940+
stdin_mock.isatty.return_value = True
2941+
stdin_mock.fileno.return_value = 0
2942+
monkeypatch.setattr(outsim_app, "stdin", stdin_mock)
2943+
2944+
stdout_mock = mock.MagicMock()
2945+
stdout_mock.isatty.return_value = True
2946+
monkeypatch.setattr(outsim_app, "stdout", stdout_mock)
2947+
2948+
if not sys.platform.startswith('win') and os.environ.get("TERM") is None:
2949+
monkeypatch.setenv('TERM', 'simulated')
2950+
2951+
# Mock signal
2952+
monkeypatch.setitem(sys.modules, 'signal', mock.MagicMock())
2953+
2954+
# Mock os.tcsetpgrp to raise OSError
2955+
monkeypatch.setattr(os, "tcsetpgrp", mock.Mock(side_effect=OSError("Permission denied")), raising=False)
2956+
monkeypatch.setattr(os, "getpgrp", mock.Mock(return_value=123), raising=False)
2957+
2958+
# Mock termios
2959+
termios_mock = mock.MagicMock()
2960+
import termios
2961+
2962+
termios_mock.error = termios.error
2963+
monkeypatch.setitem(sys.modules, 'termios', termios_mock)
2964+
2965+
# Mock subprocess.Popen
2966+
popen_mock = mock.MagicMock(name='Popen')
2967+
monkeypatch.setattr("subprocess.Popen", popen_mock)
2968+
2969+
# Set initial termios settings
2970+
outsim_app._initial_termios_settings = ["dummy settings"]
2971+
2972+
# Call ppaged - should not raise exception
2973+
outsim_app.ppaged("Test")
2974+
2975+
# Verify tcsetpgrp was attempted and OSError was caught
2976+
assert os.tcsetpgrp.called
2977+
# tcsetattr should have been skipped due to OSError being raised before it
2978+
assert not termios_mock.tcsetattr.called
2979+
2980+
28072981
# we override cmd.parseline() so we always get consistent
28082982
# command parsing by parent methods we don't override
28092983
# don't need to test all the parsing logic here, because

0 commit comments

Comments
 (0)