@@ -2112,7 +2112,8 @@ class TestWindowsConsoleEolWrap(TestCase):
21122112 """
21132113 When a line exactly fills the terminal width, Windows consoles differ on
21142114 whether the cursor immediately wraps to the next row (depends on host/mode).
2115- We must synchronize our logical cursor position with the real console cursor.
2115+ We must synchronize our logical cursor position with the real console cursor
2116+ when possible, and degrade gracefully when it cannot be queried.
21162117 """
21172118
21182119 def _make_console_like (self , * , width : int , offset : int , vt : bool ):
@@ -2136,28 +2137,17 @@ def _make_console_like(self, *, width: int, offset: int, vt: bool):
21362137
21372138 return con , wc
21382139
2139- def _run_exact_width_case (self , * , vt : bool , did_wrap : bool ):
2140+ def _run_exact_width_case (self , * , vt : bool , did_wrap : bool | None ):
21402141 width = 10
21412142 y = 3
21422143 con , wc = self ._make_console_like (width = width , offset = 0 , vt = vt )
21432144
2144- def fake_gcsbi (_h , info ):
2145- info .srWindow .Top = 0
2146- if did_wrap :
2147- info .dwCursorPosition .X = 0
2148- info .dwCursorPosition .Y = y + 1
2149- else :
2150- info .dwCursorPosition .X = width
2151- info .dwCursorPosition .Y = y
2152- return True
2153-
2154- with patch .object (wc , "GetConsoleScreenBufferInfo" , side_effect = fake_gcsbi ), \
2155- patch .object (wc , "OutHandle" , 1 ):
2145+ with patch .object (con , "_has_wrapped_to_next_row" , return_value = did_wrap ):
21562146 old = ""
21572147 new = "a" * width
21582148 wc .WindowsConsole ._WindowsConsole__write_changed_line (con , y , old , new , 0 )
21592149
2160- if did_wrap :
2150+ if did_wrap is True :
21612151 self .assertEqual (con .posxy , (0 , y + 1 ))
21622152 self .assertNotIn ((0 , y + 1 ), [c .args for c in con ._move_relative .mock_calls ])
21632153 else :
@@ -2173,3 +2163,70 @@ def test_exact_width_line_did_not_wrap_vt_and_legacy(self):
21732163 for vt in (True , False ):
21742164 with self .subTest (vt = vt ):
21752165 self ._run_exact_width_case (vt = vt , did_wrap = False )
2166+
2167+ def test_exact_width_line_unknown_wrap_vt_and_legacy (self ):
2168+ # real cursor may be unavailable.
2169+ for vt in (True , False ):
2170+ with self .subTest (vt = vt ):
2171+ self ._run_exact_width_case (vt = vt , did_wrap = None )
2172+
2173+
2174+ @skipUnless (sys .platform == "win32" , "Windows console behavior only" )
2175+ class TestHasWrappedToNextRow (TestCase ):
2176+ def _make_console_like (self , * , offset : int ):
2177+ from _pyrepl import windows_console as wc
2178+
2179+ con = object .__new__ (wc .WindowsConsole )
2180+ setattr (con , "_WindowsConsole__offset" , offset )
2181+ return con , wc
2182+
2183+ def test_returns_true_when_wrapped (self ):
2184+ con , wc = self ._make_console_like (offset = 0 )
2185+ y = 3
2186+
2187+ def fake_gcsbi (_h , info ):
2188+ info .srWindow .Top = 0
2189+ info .dwCursorPosition .Y = y + 1
2190+ return True
2191+
2192+ with patch .object (wc , "GetConsoleScreenBufferInfo" , side_effect = fake_gcsbi ), \
2193+ patch .object (wc , "OutHandle" , 1 ):
2194+ self .assertIs (con ._has_wrapped_to_next_row (y ), True )
2195+
2196+ def test_returns_false_when_not_wrapped (self ):
2197+ con , wc = self ._make_console_like (offset = 0 )
2198+ y = 3
2199+
2200+ def fake_gcsbi (_h , info ):
2201+ info .srWindow .Top = 0
2202+ info .dwCursorPosition .Y = y
2203+ return True
2204+
2205+ with patch .object (wc , "GetConsoleScreenBufferInfo" , side_effect = fake_gcsbi ), \
2206+ patch .object (wc , "OutHandle" , 1 ):
2207+ self .assertIs (con ._has_wrapped_to_next_row (y ), False )
2208+
2209+ def test_returns_none_on_invalid_handle (self ):
2210+ con , wc = self ._make_console_like (offset = 0 )
2211+ y = 3
2212+
2213+ def fake_gcsbi (_h , info ):
2214+ return False
2215+
2216+ with patch .object (wc , "GetConsoleScreenBufferInfo" , side_effect = fake_gcsbi ), \
2217+ patch .object (wc , "OutHandle" , 1 ), \
2218+ patch .object (wc , "get_last_error" , return_value = 6 ): # ERROR_INVALID_HANDLE
2219+ self .assertIs (con ._has_wrapped_to_next_row (y ), None )
2220+
2221+ def test_raises_on_unexpected_error (self ):
2222+ con , wc = self ._make_console_like (offset = 0 )
2223+ y = 3
2224+
2225+ def fake_gcsbi (_h , info ):
2226+ return False
2227+
2228+ with patch .object (wc , "GetConsoleScreenBufferInfo" , side_effect = fake_gcsbi ), \
2229+ patch .object (wc , "OutHandle" , 1 ), \
2230+ patch .object (wc , "get_last_error" , return_value = 5 ): # ERROR_ACCESS_DENIED
2231+ with self .assertRaises (OSError ):
2232+ con ._has_wrapped_to_next_row (y )
0 commit comments