@@ -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