|
12 | 12 | import tempfile |
13 | 13 | from pkgutil import ModuleInfo |
14 | 14 | from unittest import TestCase, skipUnless, skipIf, SkipTest |
15 | | -from unittest.mock import patch |
| 15 | +from unittest.mock import Mock, patch |
16 | 16 | from test.support import force_not_colorized, make_clean_env, Py_DEBUG |
17 | 17 | from test.support import has_subprocess_support, SHORT_TIMEOUT, STDLIB_DIR |
18 | 18 | from test.support.import_helper import import_module |
@@ -2105,3 +2105,75 @@ def test_ctrl_d_single_line_end_no_newline(self): |
2105 | 2105 | ) |
2106 | 2106 | reader, _ = handle_all_events(events) |
2107 | 2107 | self.assertEqual("hello", "".join(reader.buffer)) |
| 2108 | + |
| 2109 | + |
| 2110 | +@skipUnless(sys.platform == "win32", "Windows console VT behavior only") |
| 2111 | +class TestWindowsConsoleVtEolWrap(TestCase): |
| 2112 | + """ |
| 2113 | + When a line exactly fills the terminal width, VT terminals differ on whether |
| 2114 | + the cursor immediately wraps to the next row. In VT mode we must synchronize |
| 2115 | + our logical cursor position with the real console cursor. |
| 2116 | + """ |
| 2117 | + def _make_console_like(self, *, width: int, offset: int, vt: bool): |
| 2118 | + from _pyrepl import windows_console as wc |
| 2119 | + |
| 2120 | + con = object.__new__(wc.WindowsConsole) |
| 2121 | + |
| 2122 | + # Minimal state needed by __write_changed_line() |
| 2123 | + con.width = width |
| 2124 | + con.screen = [] |
| 2125 | + con.posxy = (0, 0) |
| 2126 | + setattr(con, "_WindowsConsole__offset", offset) |
| 2127 | + setattr(con, "_WindowsConsole__vt_support", vt) |
| 2128 | + |
| 2129 | + # Stub out side-effecting methods used by __write_changed_line() |
| 2130 | + con._hide_cursor = Mock() |
| 2131 | + con._erase_to_end = Mock() |
| 2132 | + con._move_relative = Mock() |
| 2133 | + con.move_cursor = Mock() |
| 2134 | + setattr(con, "_WindowsConsole__write", Mock()) |
| 2135 | + |
| 2136 | + return con, wc |
| 2137 | + |
| 2138 | + def test_vt_exact_width_line_did_wrap(self): |
| 2139 | + # Terminal wrapped to next row: posxy should become (0, y+1). |
| 2140 | + width = 10 |
| 2141 | + y = 3 |
| 2142 | + con, wc = self._make_console_like(width=width, offset=0, vt=True) |
| 2143 | + |
| 2144 | + def fake_gcsbi(_h, info): |
| 2145 | + info.dwCursorPosition.X = 0 |
| 2146 | + # Visible window top = 0, cursor now on next visible row |
| 2147 | + info.srWindow.Top = 0 |
| 2148 | + info.dwCursorPosition.Y = y + 1 |
| 2149 | + return True |
| 2150 | + |
| 2151 | + with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \ |
| 2152 | + patch.object(wc, "OutHandle", 1): |
| 2153 | + old = "" |
| 2154 | + new = "a" * width |
| 2155 | + wc.WindowsConsole._WindowsConsole__write_changed_line(con, y, old, new, 0) |
| 2156 | + self.assertEqual(con.posxy, (0, y + 1)) |
| 2157 | + self.assertNotIn((0, y + 1), [c.args for c in con._move_relative.mock_calls]) |
| 2158 | + |
| 2159 | + def test_vt_exact_width_line_did_not_wrap(self): |
| 2160 | + # Terminal did NOT wrap yet: posxy should stay at (width, y). |
| 2161 | + width = 10 |
| 2162 | + y = 3 |
| 2163 | + con, wc = self._make_console_like(width=width, offset=0, vt=True) |
| 2164 | + |
| 2165 | + def fake_gcsbi(_h, info): |
| 2166 | + info.dwCursorPosition.X = width |
| 2167 | + info.srWindow.Top = 0 |
| 2168 | + # Cursor remains on the same visible row |
| 2169 | + info.dwCursorPosition.Y = y |
| 2170 | + return True |
| 2171 | + |
| 2172 | + with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \ |
| 2173 | + patch.object(wc, "OutHandle", 1): |
| 2174 | + old = "" |
| 2175 | + new = "a" * width |
| 2176 | + wc.WindowsConsole._WindowsConsole__write_changed_line(con, y, old, new, 0) |
| 2177 | + |
| 2178 | + self.assertEqual(con.posxy, (width, y)) |
| 2179 | + self.assertNotIn((0, y + 1), [c.args for c in con._move_relative.mock_calls]) |
0 commit comments