mirror of https://github.com/python/cpython.git
582 lines
20 KiB
Python
582 lines
20 KiB
Python
import sys
|
|
import unittest
|
|
|
|
if sys.platform != "win32":
|
|
raise unittest.SkipTest("test only relevant on win32")
|
|
|
|
|
|
import itertools
|
|
from functools import partial
|
|
from test.support import force_not_colorized_test_class
|
|
from typing import Iterable
|
|
from unittest import TestCase
|
|
from unittest.mock import MagicMock, call
|
|
|
|
from .support import handle_all_events, code_to_events
|
|
from .support import prepare_reader as default_prepare_reader
|
|
|
|
try:
|
|
from _pyrepl.console import Event, Console
|
|
from _pyrepl.windows_console import (
|
|
WindowsConsole,
|
|
MOVE_LEFT,
|
|
MOVE_RIGHT,
|
|
MOVE_UP,
|
|
MOVE_DOWN,
|
|
ERASE_IN_LINE,
|
|
)
|
|
import _pyrepl.windows_console as wc
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
@force_not_colorized_test_class
|
|
class WindowsConsoleTests(TestCase):
|
|
def console(self, events, **kwargs) -> Console:
|
|
console = WindowsConsole()
|
|
console.get_event = MagicMock(side_effect=events)
|
|
console.getpending = MagicMock(return_value=Event("key", ""))
|
|
console.wait = MagicMock()
|
|
console._scroll = MagicMock()
|
|
console._hide_cursor = MagicMock()
|
|
console._show_cursor = MagicMock()
|
|
console._getscrollbacksize = MagicMock(42)
|
|
console.out = MagicMock()
|
|
|
|
height = kwargs.get("height", 25)
|
|
width = kwargs.get("width", 80)
|
|
console.getheightwidth = MagicMock(side_effect=lambda: (height, width))
|
|
|
|
console.prepare()
|
|
for key, val in kwargs.items():
|
|
setattr(console, key, val)
|
|
return console
|
|
|
|
def handle_events(
|
|
self,
|
|
events: Iterable[Event],
|
|
prepare_console=None,
|
|
prepare_reader=None,
|
|
**kwargs,
|
|
):
|
|
prepare_console = prepare_console or partial(self.console, **kwargs)
|
|
prepare_reader = prepare_reader or default_prepare_reader
|
|
return handle_all_events(events, prepare_console, prepare_reader)
|
|
|
|
def handle_events_narrow(self, events):
|
|
return self.handle_events(events, width=5)
|
|
|
|
def handle_events_short(self, events, **kwargs):
|
|
return self.handle_events(events, height=1, **kwargs)
|
|
|
|
def handle_events_height_3(self, events):
|
|
return self.handle_events(events, height=3)
|
|
|
|
def test_simple_addition(self):
|
|
code = "12+34"
|
|
events = code_to_events(code)
|
|
_, con = self.handle_events(events)
|
|
con.out.write.assert_any_call(b"1")
|
|
con.out.write.assert_any_call(b"2")
|
|
con.out.write.assert_any_call(b"+")
|
|
con.out.write.assert_any_call(b"3")
|
|
con.out.write.assert_any_call(b"4")
|
|
con.restore()
|
|
|
|
def test_wrap(self):
|
|
code = "12+34"
|
|
events = code_to_events(code)
|
|
_, con = self.handle_events_narrow(events)
|
|
con.out.write.assert_any_call(b"1")
|
|
con.out.write.assert_any_call(b"2")
|
|
con.out.write.assert_any_call(b"+")
|
|
con.out.write.assert_any_call(b"3")
|
|
con.out.write.assert_any_call(b"\\")
|
|
con.out.write.assert_any_call(b"\n")
|
|
con.out.write.assert_any_call(b"4")
|
|
con.restore()
|
|
|
|
def test_resize_wider(self):
|
|
code = "1234567890"
|
|
events = code_to_events(code)
|
|
reader, console = self.handle_events_narrow(events)
|
|
|
|
console.height = 20
|
|
console.width = 80
|
|
console.getheightwidth = MagicMock(lambda _: (20, 80))
|
|
|
|
def same_reader(_):
|
|
return reader
|
|
|
|
def same_console(events):
|
|
console.get_event = MagicMock(side_effect=events)
|
|
return console
|
|
|
|
_, con = handle_all_events(
|
|
[Event(evt="resize", data=None)],
|
|
prepare_reader=same_reader,
|
|
prepare_console=same_console,
|
|
)
|
|
|
|
con.out.write.assert_any_call(self.move_right(2))
|
|
con.out.write.assert_any_call(self.move_up(2))
|
|
con.out.write.assert_any_call(b"567890")
|
|
|
|
con.restore()
|
|
|
|
def test_resize_narrower(self):
|
|
code = "1234567890"
|
|
events = code_to_events(code)
|
|
reader, console = self.handle_events(events)
|
|
|
|
console.height = 20
|
|
console.width = 4
|
|
console.getheightwidth = MagicMock(lambda _: (20, 4))
|
|
|
|
def same_reader(_):
|
|
return reader
|
|
|
|
def same_console(events):
|
|
console.get_event = MagicMock(side_effect=events)
|
|
return console
|
|
|
|
_, con = handle_all_events(
|
|
[Event(evt="resize", data=None)],
|
|
prepare_reader=same_reader,
|
|
prepare_console=same_console,
|
|
)
|
|
|
|
con.out.write.assert_any_call(b"456\\")
|
|
con.out.write.assert_any_call(b"789\\")
|
|
|
|
con.restore()
|
|
|
|
def test_cursor_left(self):
|
|
code = "1"
|
|
events = itertools.chain(
|
|
code_to_events(code),
|
|
[Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))],
|
|
)
|
|
_, con = self.handle_events(events)
|
|
con.out.write.assert_any_call(self.move_left())
|
|
con.restore()
|
|
|
|
def test_cursor_left_right(self):
|
|
code = "1"
|
|
events = itertools.chain(
|
|
code_to_events(code),
|
|
[
|
|
Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
|
|
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
|
|
],
|
|
)
|
|
_, con = self.handle_events(events)
|
|
con.out.write.assert_any_call(self.move_left())
|
|
con.out.write.assert_any_call(self.move_right())
|
|
con.restore()
|
|
|
|
def test_cursor_up(self):
|
|
code = "1\n2+3"
|
|
events = itertools.chain(
|
|
code_to_events(code),
|
|
[Event(evt="key", data="up", raw=bytearray(b"\x1bOA"))],
|
|
)
|
|
_, con = self.handle_events(events)
|
|
con.out.write.assert_any_call(self.move_up())
|
|
con.restore()
|
|
|
|
def test_cursor_up_down(self):
|
|
code = "1\n2+3"
|
|
events = itertools.chain(
|
|
code_to_events(code),
|
|
[
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
|
|
],
|
|
)
|
|
_, con = self.handle_events(events)
|
|
con.out.write.assert_any_call(self.move_up())
|
|
con.out.write.assert_any_call(self.move_down())
|
|
con.restore()
|
|
|
|
def test_cursor_back_write(self):
|
|
events = itertools.chain(
|
|
code_to_events("1"),
|
|
[Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))],
|
|
code_to_events("2"),
|
|
)
|
|
_, con = self.handle_events(events)
|
|
con.out.write.assert_any_call(b"1")
|
|
con.out.write.assert_any_call(self.move_left())
|
|
con.out.write.assert_any_call(b"21")
|
|
con.restore()
|
|
|
|
def test_multiline_function_move_up_short_terminal(self):
|
|
# fmt: off
|
|
code = (
|
|
"def f():\n"
|
|
" foo"
|
|
)
|
|
# fmt: on
|
|
|
|
events = itertools.chain(
|
|
code_to_events(code),
|
|
[
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
Event(evt="scroll", data=None),
|
|
],
|
|
)
|
|
_, con = self.handle_events_short(events)
|
|
con.out.write.assert_any_call(self.move_left(5))
|
|
con.out.write.assert_any_call(self.move_up())
|
|
con.restore()
|
|
|
|
def test_multiline_function_move_up_down_short_terminal(self):
|
|
# fmt: off
|
|
code = (
|
|
"def f():\n"
|
|
" foo"
|
|
)
|
|
# fmt: on
|
|
|
|
events = itertools.chain(
|
|
code_to_events(code),
|
|
[
|
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
|
Event(evt="scroll", data=None),
|
|
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
|
|
Event(evt="scroll", data=None),
|
|
],
|
|
)
|
|
_, con = self.handle_events_short(events)
|
|
con.out.write.assert_any_call(self.move_left(8))
|
|
con.out.write.assert_any_call(self.erase_in_line())
|
|
con.restore()
|
|
|
|
def test_resize_bigger_on_multiline_function(self):
|
|
# fmt: off
|
|
code = (
|
|
"def f():\n"
|
|
" foo"
|
|
)
|
|
# fmt: on
|
|
|
|
events = itertools.chain(code_to_events(code))
|
|
reader, console = self.handle_events_short(events)
|
|
|
|
console.height = 2
|
|
console.getheightwidth = MagicMock(lambda _: (2, 80))
|
|
|
|
def same_reader(_):
|
|
return reader
|
|
|
|
def same_console(events):
|
|
console.get_event = MagicMock(side_effect=events)
|
|
return console
|
|
|
|
_, con = handle_all_events(
|
|
[Event(evt="resize", data=None)],
|
|
prepare_reader=same_reader,
|
|
prepare_console=same_console,
|
|
)
|
|
con.out.write.assert_has_calls(
|
|
[
|
|
call(self.move_left(5)),
|
|
call(self.move_up()),
|
|
call(b"def f():"),
|
|
call(self.move_left(3)),
|
|
call(self.move_down()),
|
|
]
|
|
)
|
|
console.restore()
|
|
con.restore()
|
|
|
|
def test_resize_smaller_on_multiline_function(self):
|
|
# fmt: off
|
|
code = (
|
|
"def f():\n"
|
|
" foo"
|
|
)
|
|
# fmt: on
|
|
|
|
events = itertools.chain(code_to_events(code))
|
|
reader, console = self.handle_events_height_3(events)
|
|
|
|
console.height = 1
|
|
console.getheightwidth = MagicMock(lambda _: (1, 80))
|
|
|
|
def same_reader(_):
|
|
return reader
|
|
|
|
def same_console(events):
|
|
console.get_event = MagicMock(side_effect=events)
|
|
return console
|
|
|
|
_, con = handle_all_events(
|
|
[Event(evt="resize", data=None)],
|
|
prepare_reader=same_reader,
|
|
prepare_console=same_console,
|
|
)
|
|
con.out.write.assert_has_calls(
|
|
[
|
|
call(self.move_left(5)),
|
|
call(self.move_up()),
|
|
call(self.erase_in_line()),
|
|
call(b" foo"),
|
|
]
|
|
)
|
|
console.restore()
|
|
con.restore()
|
|
|
|
def move_up(self, lines=1):
|
|
return MOVE_UP.format(lines).encode("utf8")
|
|
|
|
def move_down(self, lines=1):
|
|
return MOVE_DOWN.format(lines).encode("utf8")
|
|
|
|
def move_left(self, cols=1):
|
|
return MOVE_LEFT.format(cols).encode("utf8")
|
|
|
|
def move_right(self, cols=1):
|
|
return MOVE_RIGHT.format(cols).encode("utf8")
|
|
|
|
def erase_in_line(self):
|
|
return ERASE_IN_LINE.encode("utf8")
|
|
|
|
def test_multiline_ctrl_z(self):
|
|
# see gh-126332
|
|
code = "abcdefghi"
|
|
|
|
events = itertools.chain(
|
|
code_to_events(code),
|
|
[
|
|
Event(evt="key", data='\x1a', raw=bytearray(b'\x1a')),
|
|
Event(evt="key", data='\x1a', raw=bytearray(b'\x1a')),
|
|
],
|
|
)
|
|
reader, con = self.handle_events_narrow(events)
|
|
self.assertEqual(reader.cxy, (2, 3))
|
|
con.restore()
|
|
|
|
|
|
class WindowsConsoleGetEventTests(TestCase):
|
|
# Virtual-Key Codes: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
|
VK_BACK = 0x08
|
|
VK_RETURN = 0x0D
|
|
VK_LEFT = 0x25
|
|
VK_7 = 0x37
|
|
VK_M = 0x4D
|
|
# Used for miscellaneous characters; it can vary by keyboard.
|
|
# For the US standard keyboard, the '" key.
|
|
# For the German keyboard, the Ä key.
|
|
VK_OEM_7 = 0xDE
|
|
|
|
# State of control keys: https://learn.microsoft.com/en-us/windows/console/key-event-record-str
|
|
RIGHT_ALT_PRESSED = 0x0001
|
|
RIGHT_CTRL_PRESSED = 0x0004
|
|
LEFT_ALT_PRESSED = 0x0002
|
|
LEFT_CTRL_PRESSED = 0x0008
|
|
ENHANCED_KEY = 0x0100
|
|
SHIFT_PRESSED = 0x0010
|
|
|
|
|
|
def get_event(self, input_records, **kwargs) -> Console:
|
|
self.console = WindowsConsole(encoding='utf-8')
|
|
self.mock = MagicMock(side_effect=input_records)
|
|
self.console._read_input = self.mock
|
|
self.console._WindowsConsole__vt_support = kwargs.get("vt_support",
|
|
False)
|
|
self.console.wait = MagicMock(return_value=True)
|
|
event = self.console.get_event(block=False)
|
|
return event
|
|
|
|
def get_input_record(self, unicode_char, vcode=0, control=0):
|
|
return wc.INPUT_RECORD(
|
|
wc.KEY_EVENT,
|
|
wc.ConsoleEvent(KeyEvent=
|
|
wc.KeyEvent(
|
|
bKeyDown=True,
|
|
wRepeatCount=1,
|
|
wVirtualKeyCode=vcode,
|
|
wVirtualScanCode=0, # not used
|
|
uChar=wc.Char(unicode_char),
|
|
dwControlKeyState=control
|
|
)))
|
|
|
|
def test_EmptyBuffer(self):
|
|
self.assertEqual(self.get_event([None]), None)
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_WINDOW_BUFFER_SIZE_EVENT(self):
|
|
ir = wc.INPUT_RECORD(
|
|
wc.WINDOW_BUFFER_SIZE_EVENT,
|
|
wc.ConsoleEvent(WindowsBufferSizeEvent=
|
|
wc.WindowsBufferSizeEvent(
|
|
wc._COORD(0, 0))))
|
|
self.assertEqual(self.get_event([ir]), Event("resize", ""))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_KEY_EVENT_up_ignored(self):
|
|
ir = wc.INPUT_RECORD(
|
|
wc.KEY_EVENT,
|
|
wc.ConsoleEvent(KeyEvent=
|
|
wc.KeyEvent(bKeyDown=False)))
|
|
self.assertEqual(self.get_event([ir]), None)
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_unhandled_events(self):
|
|
for event in (wc.FOCUS_EVENT, wc.MENU_EVENT, wc.MOUSE_EVENT):
|
|
ir = wc.INPUT_RECORD(
|
|
event,
|
|
# fake data, nothing is read except bKeyDown
|
|
wc.ConsoleEvent(KeyEvent=
|
|
wc.KeyEvent(bKeyDown=False)))
|
|
self.assertEqual(self.get_event([ir]), None)
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_enter(self):
|
|
ir = self.get_input_record("\r", self.VK_RETURN)
|
|
self.assertEqual(self.get_event([ir]), Event("key", "\n"))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_backspace(self):
|
|
ir = self.get_input_record("\x08", self.VK_BACK)
|
|
self.assertEqual(
|
|
self.get_event([ir]), Event("key", "backspace"))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_m(self):
|
|
ir = self.get_input_record("m", self.VK_M)
|
|
self.assertEqual(self.get_event([ir]), Event("key", "m"))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_M(self):
|
|
ir = self.get_input_record("M", self.VK_M, self.SHIFT_PRESSED)
|
|
self.assertEqual(self.get_event([ir]), Event("key", "M"))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_left(self):
|
|
# VK_LEFT is sent as ENHANCED_KEY
|
|
ir = self.get_input_record("\x00", self.VK_LEFT, self.ENHANCED_KEY)
|
|
self.assertEqual(self.get_event([ir]), Event("key", "left"))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_left_RIGHT_CTRL_PRESSED(self):
|
|
ir = self.get_input_record(
|
|
"\x00", self.VK_LEFT, self.RIGHT_CTRL_PRESSED | self.ENHANCED_KEY)
|
|
self.assertEqual(
|
|
self.get_event([ir]), Event("key", "ctrl left"))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_left_LEFT_CTRL_PRESSED(self):
|
|
ir = self.get_input_record(
|
|
"\x00", self.VK_LEFT, self.LEFT_CTRL_PRESSED | self.ENHANCED_KEY)
|
|
self.assertEqual(
|
|
self.get_event([ir]), Event("key", "ctrl left"))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_left_RIGHT_ALT_PRESSED(self):
|
|
ir = self.get_input_record(
|
|
"\x00", self.VK_LEFT, self.RIGHT_ALT_PRESSED | self.ENHANCED_KEY)
|
|
self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
|
|
self.assertEqual(
|
|
self.console.get_event(), Event("key", "left"))
|
|
# self.mock is not called again, since the second time we read from the
|
|
# command queue
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_left_LEFT_ALT_PRESSED(self):
|
|
ir = self.get_input_record(
|
|
"\x00", self.VK_LEFT, self.LEFT_ALT_PRESSED | self.ENHANCED_KEY)
|
|
self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
|
|
self.assertEqual(
|
|
self.console.get_event(), Event("key", "left"))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_m_LEFT_ALT_PRESSED_and_LEFT_CTRL_PRESSED(self):
|
|
# For the shift keys, Windows does not send anything when
|
|
# ALT and CTRL are both pressed, so let's test with VK_M.
|
|
# get_event() receives this input, but does not
|
|
# generate an event.
|
|
# This is for e.g. an English keyboard layout, for a
|
|
# German layout this returns `µ`, see test_AltGr_m.
|
|
ir = self.get_input_record(
|
|
"\x00", self.VK_M, self.LEFT_ALT_PRESSED | self.LEFT_CTRL_PRESSED)
|
|
self.assertEqual(self.get_event([ir]), None)
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_m_LEFT_ALT_PRESSED(self):
|
|
ir = self.get_input_record(
|
|
"m", vcode=self.VK_M, control=self.LEFT_ALT_PRESSED)
|
|
self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
|
|
self.assertEqual(self.console.get_event(), Event("key", "m"))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_m_RIGHT_ALT_PRESSED(self):
|
|
ir = self.get_input_record(
|
|
"m", vcode=self.VK_M, control=self.RIGHT_ALT_PRESSED)
|
|
self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
|
|
self.assertEqual(self.console.get_event(), Event("key", "m"))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_AltGr_7(self):
|
|
# E.g. on a German keyboard layout, '{' is entered via
|
|
# AltGr + 7, where AltGr is the right Alt key on the keyboard.
|
|
# In this case, Windows automatically sets
|
|
# RIGHT_ALT_PRESSED = 0x0001 + LEFT_CTRL_PRESSED = 0x0008
|
|
# This can also be entered like
|
|
# LeftAlt + LeftCtrl + 7 or
|
|
# LeftAlt + RightCtrl + 7
|
|
# See https://learn.microsoft.com/en-us/windows/console/key-event-record-str
|
|
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-vkkeyscanw
|
|
ir = self.get_input_record(
|
|
"{", vcode=self.VK_7,
|
|
control=self.RIGHT_ALT_PRESSED | self.LEFT_CTRL_PRESSED)
|
|
self.assertEqual(self.get_event([ir]), Event("key", "{"))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_AltGr_m(self):
|
|
# E.g. on a German keyboard layout, this yields 'µ'
|
|
# Let's use LEFT_ALT_PRESSED and RIGHT_CTRL_PRESSED this
|
|
# time, to cover that, too. See above in test_AltGr_7.
|
|
ir = self.get_input_record(
|
|
"µ", vcode=self.VK_M, control=self.LEFT_ALT_PRESSED | self.RIGHT_CTRL_PRESSED)
|
|
self.assertEqual(self.get_event([ir]), Event("key", "µ"))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_umlaut_a_german(self):
|
|
ir = self.get_input_record("ä", self.VK_OEM_7)
|
|
self.assertEqual(self.get_event([ir]), Event("key", "ä"))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
# virtual terminal tests
|
|
# Note: wVirtualKeyCode, wVirtualScanCode and dwControlKeyState
|
|
# are always zero in this case.
|
|
# "\r" and backspace are handled specially, everything else
|
|
# is handled in "elif self.__vt_support:" in WindowsConsole.get_event().
|
|
# Hence, only one regular key ("m") and a terminal sequence
|
|
# are sufficient to test here, the real tests happen in test_eventqueue
|
|
# and test_keymap.
|
|
|
|
def test_enter_vt(self):
|
|
ir = self.get_input_record("\r")
|
|
self.assertEqual(self.get_event([ir], vt_support=True),
|
|
Event("key", "\n"))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_backspace_vt(self):
|
|
ir = self.get_input_record("\x7f")
|
|
self.assertEqual(self.get_event([ir], vt_support=True),
|
|
Event("key", "backspace", b"\x7f"))
|
|
self.assertEqual(self.mock.call_count, 1)
|
|
|
|
def test_up_vt(self):
|
|
irs = [self.get_input_record(x) for x in "\x1b[A"]
|
|
self.assertEqual(self.get_event(irs, vt_support=True),
|
|
Event(evt='key', data='up', raw=bytearray(b'\x1b[A')))
|
|
self.assertEqual(self.mock.call_count, 3)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|