-
-
Notifications
You must be signed in to change notification settings - Fork 34k
gh-144259: Fix Windows EOL wrap by syncing real console cursor #144297
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
cfc3423
0acecdc
a2d1786
e085733
22a8e9e
e13d1dd
460dfb3
f66910e
4647e00
fb63940
4a84421
b2b78a8
9a168a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,7 @@ | |
| import tempfile | ||
| from pkgutil import ModuleInfo | ||
| from unittest import TestCase, skipUnless, skipIf, SkipTest | ||
| from unittest.mock import patch | ||
| from unittest.mock import Mock, patch | ||
| from test.support import force_not_colorized, make_clean_env, Py_DEBUG | ||
| from test.support import has_subprocess_support, SHORT_TIMEOUT, STDLIB_DIR | ||
| from test.support.import_helper import import_module | ||
|
|
@@ -2105,3 +2105,96 @@ def test_ctrl_d_single_line_end_no_newline(self): | |
| ) | ||
| reader, _ = handle_all_events(events) | ||
| self.assertEqual("hello", "".join(reader.buffer)) | ||
|
|
||
|
|
||
| @skipUnless(sys.platform == "win32", "Windows console behavior only") | ||
| class TestWindowsConsoleEolWrap(TestCase): | ||
| """ | ||
| When a line exactly fills the terminal width, Windows consoles differ on | ||
| whether the cursor immediately wraps to the next row (depends on host/mode). | ||
| We use _has_wrapped_to_next_row() to determine the actual behavior. | ||
| """ | ||
|
|
||
| def _make_console_like(self, *, width: int, offset: int, vt: bool): | ||
| from _pyrepl import windows_console as wc | ||
|
|
||
| con = object.__new__(wc.WindowsConsole) | ||
|
|
||
| # Minimal state needed by __write_changed_line() | ||
| con.width = width | ||
| con.screen = [] | ||
| con.posxy = (0, 0) | ||
| setattr(con, "_WindowsConsole__offset", offset) | ||
| setattr(con, "_WindowsConsole__vt_support", vt) | ||
|
|
||
| # Stub out side-effecting methods used by __write_changed_line() | ||
| con._hide_cursor = Mock() | ||
| con._erase_to_end = Mock() | ||
| con._move_relative = Mock() | ||
| con.move_cursor = Mock() | ||
| setattr(con, "_WindowsConsole__write", Mock()) | ||
|
|
||
| return con, wc | ||
|
|
||
| def _run_exact_width_case(self, *, vt: bool, did_wrap: bool): | ||
| width = 10 | ||
| y = 3 | ||
| con, wc = self._make_console_like(width=width, offset=0, vt=vt) | ||
|
|
||
| with patch.object(con, "_has_wrapped_to_next_row", return_value=did_wrap): | ||
| old = "" | ||
| new = "a" * width | ||
| wc.WindowsConsole._WindowsConsole__write_changed_line(con, y, old, new, 0) | ||
|
|
||
| if did_wrap: | ||
| self.assertEqual(con.posxy, (0, y + 1)) | ||
| self.assertNotIn((0, y + 1), [c.args for c in con._move_relative.mock_calls]) | ||
| else: | ||
| self.assertEqual(con.posxy, (width, y)) | ||
| self.assertNotIn((0, y + 1), [c.args for c in con._move_relative.mock_calls]) | ||
|
|
||
| def test_exact_width_line_did_wrap_vt_and_legacy(self): | ||
| for vt in (True, False): | ||
| with self.subTest(vt=vt): | ||
| self._run_exact_width_case(vt=vt, did_wrap=True) | ||
|
|
||
| def test_exact_width_line_did_not_wrap_vt_and_legacy(self): | ||
| for vt in (True, False): | ||
| with self.subTest(vt=vt): | ||
| self._run_exact_width_case(vt=vt, did_wrap=False) | ||
|
|
||
|
|
||
| @skipUnless(sys.platform == "win32", "Windows console behavior only") | ||
| class TestHasWrappedToNextRow(TestCase): | ||
| def _make_console_like(self, *, offset: int): | ||
| from _pyrepl import windows_console as wc | ||
|
|
||
| con = object.__new__(wc.WindowsConsole) | ||
| setattr(con, "_WindowsConsole__offset", offset) | ||
| return con, wc | ||
|
|
||
| def test_returns_true_when_wrapped(self): | ||
| con, wc = self._make_console_like(offset=0) | ||
| y = 3 | ||
|
|
||
| def fake_gcsbi(_h, info): | ||
| info.srWindow.Top = 0 | ||
| info.dwCursorPosition.Y = y + 1 | ||
| return True | ||
|
|
||
| with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \ | ||
| patch.object(wc, "OutHandle", 1): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| self.assertIs(con._has_wrapped_to_next_row(y), True) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use |
||
|
|
||
| def test_returns_false_when_not_wrapped(self): | ||
| con, wc = self._make_console_like(offset=0) | ||
| y = 3 | ||
|
|
||
| def fake_gcsbi(_h, info): | ||
| info.srWindow.Top = 0 | ||
| info.dwCursorPosition.Y = y | ||
| return True | ||
|
|
||
| with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \ | ||
| patch.object(wc, "OutHandle", 1): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| self.assertIs(con._has_wrapped_to_next_row(y), False) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Fix Windows REPL cursor desynchronization when a line exactly fills the terminal width. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No parameters are here needed anymore, since always
offset=0is passed.