Skip to content

Commit

Permalink
[3.13] pythongh-119306: Break up _pyrepl tests (pythonGH-119307)
Browse files Browse the repository at this point in the history
(cherry picked from commit f49df4f)

Co-authored-by: Eugene Triguba <eugenetriguba@gmail.com>
  • Loading branch information
eugenetriguba authored and lysnikolaou committed May 21, 2024
1 parent 256b791 commit 47ac2b0
Show file tree
Hide file tree
Showing 10 changed files with 903 additions and 392 deletions.
14 changes: 14 additions & 0 deletions Lib/test/test_pyrepl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import os
from test.support import requires, load_package_tests
from test.support.import_helper import import_module

# Optionally test pyrepl. This currently requires that the
# 'curses' resource be given on the regrtest command line using the -u
# option. Additionally, we need to attempt to import curses and readline.
requires("curses")
curses = import_module("curses")
readline = import_module("readline")


def load_tests(*args):
return load_package_tests(os.path.dirname(__file__), *args)
4 changes: 4 additions & 0 deletions Lib/test/test_pyrepl/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import unittest
from test.test_pyrepl import load_tests

unittest.main()
141 changes: 141 additions & 0 deletions Lib/test/test_pyrepl/support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from code import InteractiveConsole
from functools import partial
from typing import Iterable
from unittest.mock import MagicMock

from _pyrepl.console import Console, Event
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
from _pyrepl.simple_interact import _strip_final_indent


def multiline_input(reader: ReadlineAlikeReader, namespace: dict | None = None):
saved = reader.more_lines
try:
reader.more_lines = partial(more_lines, namespace=namespace)
reader.ps1 = reader.ps2 = ">>>"
reader.ps3 = reader.ps4 = "..."
return reader.readline()
finally:
reader.more_lines = saved
reader.paste_mode = False


def more_lines(text: str, namespace: dict | None = None):
if namespace is None:
namespace = {}
src = _strip_final_indent(text)
console = InteractiveConsole(namespace, filename="<stdin>")
try:
code = console.compile(src, "<stdin>", "single")
except (OverflowError, SyntaxError, ValueError):
return False
else:
return code is None


def code_to_events(code: str):
for c in code:
yield Event(evt="key", data=c, raw=bytearray(c.encode("utf-8")))


def prepare_reader(console: Console, **kwargs):
config = ReadlineConfig(readline_completer=None)
reader = ReadlineAlikeReader(console=console, config=config)
reader.more_lines = partial(more_lines, namespace=None)
reader.paste_mode = True # Avoid extra indents

def get_prompt(lineno, cursor_on_line) -> str:
return ""

reader.get_prompt = get_prompt # Remove prompt for easier calculations of (x, y)

for key, val in kwargs.items():
setattr(reader, key, val)

return reader


def prepare_console(events: Iterable[Event], **kwargs):
console = MagicMock()
console.get_event.side_effect = events
console.height = 100
console.width = 80
for key, val in kwargs.items():
setattr(console, key, val)
return console


def handle_all_events(
events, prepare_console=prepare_console, prepare_reader=prepare_reader
):
console = prepare_console(events)
reader = prepare_reader(console)
try:
while True:
reader.handle1()
except StopIteration:
pass
return reader, console


handle_events_narrow_console = partial(
handle_all_events,
prepare_console=partial(prepare_console, width=10),
)


class FakeConsole(Console):
def __init__(self, events, encoding="utf-8"):
self.events = iter(events)
self.encoding = encoding
self.screen = []
self.height = 100
self.width = 80

def get_event(self, block: bool = True) -> Event | None:
return next(self.events)

def getpending(self) -> Event:
return self.get_event(block=False)

def getheightwidth(self) -> tuple[int, int]:
return self.height, self.width

def refresh(self, screen: list[str], xy: tuple[int, int]) -> None:
pass

def prepare(self) -> None:
pass

def restore(self) -> None:
pass

def move_cursor(self, x: int, y: int) -> None:
pass

def set_cursor_vis(self, visible: bool) -> None:
pass

def push_char(self, char: int | bytes) -> None:
pass

def beep(self) -> None:
pass

def clear(self) -> None:
pass

def finish(self) -> None:
pass

def flushoutput(self) -> None:
pass

def forgetinput(self) -> None:
pass

def wait(self) -> None:
pass

def repaint(self) -> None:
pass
102 changes: 102 additions & 0 deletions Lib/test/test_pyrepl/test_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import unittest

from _pyrepl.console import Event
from _pyrepl.input import KeymapTranslator


class KeymapTranslatorTests(unittest.TestCase):
def test_push_single_key(self):
keymap = [("a", "command_a")]
translator = KeymapTranslator(keymap)
evt = Event("key", "a")
translator.push(evt)
result = translator.get()
self.assertEqual(result, ("command_a", ["a"]))

def test_push_multiple_keys(self):
keymap = [("ab", "command_ab")]
translator = KeymapTranslator(keymap)
evt1 = Event("key", "a")
evt2 = Event("key", "b")
translator.push(evt1)
translator.push(evt2)
result = translator.get()
self.assertEqual(result, ("command_ab", ["a", "b"]))

def test_push_invalid_key(self):
keymap = [("a", "command_a")]
translator = KeymapTranslator(keymap)
evt = Event("key", "b")
translator.push(evt)
result = translator.get()
self.assertEqual(result, (None, ["b"]))

def test_push_invalid_key_with_stack(self):
keymap = [("ab", "command_ab")]
translator = KeymapTranslator(keymap)
evt1 = Event("key", "a")
evt2 = Event("key", "c")
translator.push(evt1)
translator.push(evt2)
result = translator.get()
self.assertEqual(result, (None, ["a", "c"]))

def test_push_character_key(self):
keymap = [("a", "command_a")]
translator = KeymapTranslator(keymap)
evt = Event("key", "a")
translator.push(evt)
result = translator.get()
self.assertEqual(result, ("command_a", ["a"]))

def test_push_character_key_with_stack(self):
keymap = [("ab", "command_ab")]
translator = KeymapTranslator(keymap)
evt1 = Event("key", "a")
evt2 = Event("key", "b")
evt3 = Event("key", "c")
translator.push(evt1)
translator.push(evt2)
translator.push(evt3)
result = translator.get()
self.assertEqual(result, ("command_ab", ["a", "b"]))

def test_push_transition_key(self):
keymap = [("a", {"b": "command_ab"})]
translator = KeymapTranslator(keymap)
evt1 = Event("key", "a")
evt2 = Event("key", "b")
translator.push(evt1)
translator.push(evt2)
result = translator.get()
self.assertEqual(result, ("command_ab", ["a", "b"]))

def test_push_transition_key_interrupted(self):
keymap = [("a", {"b": "command_ab"})]
translator = KeymapTranslator(keymap)
evt1 = Event("key", "a")
evt2 = Event("key", "c")
evt3 = Event("key", "b")
translator.push(evt1)
translator.push(evt2)
translator.push(evt3)
result = translator.get()
self.assertEqual(result, (None, ["a", "c"]))

def test_push_invalid_key_with_unicode_category(self):
keymap = [("a", "command_a")]
translator = KeymapTranslator(keymap)
evt = Event("key", "\u0003") # Control character
translator.push(evt)
result = translator.get()
self.assertEqual(result, (None, ["\u0003"]))

def test_empty(self):
keymap = [("a", "command_a")]
translator = KeymapTranslator(keymap)
self.assertTrue(translator.empty())
evt = Event("key", "a")
translator.push(evt)
self.assertFalse(translator.empty())
translator.get()
self.assertTrue(translator.empty())
74 changes: 74 additions & 0 deletions Lib/test/test_pyrepl/test_keymap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import unittest

from _pyrepl.keymap import parse_keys, compile_keymap


class TestParseKeys(unittest.TestCase):
def test_single_character(self):
self.assertEqual(parse_keys("a"), ["a"])
self.assertEqual(parse_keys("b"), ["b"])
self.assertEqual(parse_keys("1"), ["1"])

def test_escape_sequences(self):
self.assertEqual(parse_keys("\\n"), ["\n"])
self.assertEqual(parse_keys("\\t"), ["\t"])
self.assertEqual(parse_keys("\\\\"), ["\\"])
self.assertEqual(parse_keys("\\'"), ["'"])
self.assertEqual(parse_keys('\\"'), ['"'])

def test_control_sequences(self):
self.assertEqual(parse_keys("\\C-a"), ["\x01"])
self.assertEqual(parse_keys("\\C-b"), ["\x02"])
self.assertEqual(parse_keys("\\C-c"), ["\x03"])

def test_meta_sequences(self):
self.assertEqual(parse_keys("\\M-a"), ["\033", "a"])
self.assertEqual(parse_keys("\\M-b"), ["\033", "b"])
self.assertEqual(parse_keys("\\M-c"), ["\033", "c"])

def test_keynames(self):
self.assertEqual(parse_keys("\\<up>"), ["up"])
self.assertEqual(parse_keys("\\<down>"), ["down"])
self.assertEqual(parse_keys("\\<left>"), ["left"])
self.assertEqual(parse_keys("\\<right>"), ["right"])

def test_combinations(self):
self.assertEqual(parse_keys("\\C-a\\n\\<up>"), ["\x01", "\n", "up"])
self.assertEqual(parse_keys("\\M-a\\t\\<down>"), ["\033", "a", "\t", "down"])


class TestCompileKeymap(unittest.TestCase):
def test_empty_keymap(self):
keymap = {}
result = compile_keymap(keymap)
self.assertEqual(result, {})

def test_single_keymap(self):
keymap = {b"a": "action"}
result = compile_keymap(keymap)
self.assertEqual(result, {b"a": "action"})

def test_nested_keymap(self):
keymap = {b"a": {b"b": "action"}}
result = compile_keymap(keymap)
self.assertEqual(result, {b"a": {b"b": "action"}})

def test_empty_value(self):
keymap = {b"a": {b"": "action"}}
result = compile_keymap(keymap)
self.assertEqual(result, {b"a": {b"": "action"}})

def test_multiple_empty_values(self):
keymap = {b"a": {b"": "action1", b"b": "action2"}}
result = compile_keymap(keymap)
self.assertEqual(result, {b"a": {b"": "action1", b"b": "action2"}})

def test_multiple_keymaps(self):
keymap = {b"a": {b"b": "action1", b"c": "action2"}}
result = compile_keymap(keymap)
self.assertEqual(result, {b"a": {b"b": "action1", b"c": "action2"}})

def test_nested_multiple_keymaps(self):
keymap = {b"a": {b"b": {b"c": "action"}}}
result = compile_keymap(keymap)
self.assertEqual(result, {b"a": {b"b": {b"c": "action"}}})
Loading

0 comments on commit 47ac2b0

Please sign in to comment.