From 7e35c2950ab16c29ec5ab5a1c17dd3b08639ceaf Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Fri, 3 Nov 2023 13:57:39 -0700 Subject: [PATCH 1/5] Add readline tests for pdb completion --- Lib/test/support/pty_helper.py | 60 ++++++++++++++++++++++++++++++++++ Lib/test/test_pdb.py | 41 +++++++++++++++++++++++ Lib/test/test_readline.py | 55 +------------------------------ 3 files changed, 102 insertions(+), 54 deletions(-) create mode 100644 Lib/test/support/pty_helper.py diff --git a/Lib/test/support/pty_helper.py b/Lib/test/support/pty_helper.py new file mode 100644 index 00000000000000..11037d22516448 --- /dev/null +++ b/Lib/test/support/pty_helper.py @@ -0,0 +1,60 @@ +""" +Helper to run a script in a pseudo-terminal. +""" +import os +import selectors +import subprocess +import sys +from contextlib import ExitStack +from errno import EIO + +from test.support.import_helper import import_module + +def run_pty(script, input=b"dummy input\r", env=None): + pty = import_module('pty') + output = bytearray() + [master, slave] = pty.openpty() + args = (sys.executable, '-c', script) + proc = subprocess.Popen(args, stdin=slave, stdout=slave, stderr=slave, env=env) + os.close(slave) + with ExitStack() as cleanup: + cleanup.enter_context(proc) + def terminate(proc): + try: + proc.terminate() + except ProcessLookupError: + # Workaround for Open/Net BSD bug (Issue 16762) + pass + cleanup.callback(terminate, proc) + cleanup.callback(os.close, master) + # Avoid using DefaultSelector and PollSelector. Kqueue() does not + # work with pseudo-terminals on OS X < 10.9 (Issue 20365) and Open + # BSD (Issue 20667). Poll() does not work with OS X 10.6 or 10.4 + # either (Issue 20472). Hopefully the file descriptor is low enough + # to use with select(). + sel = cleanup.enter_context(selectors.SelectSelector()) + sel.register(master, selectors.EVENT_READ | selectors.EVENT_WRITE) + os.set_blocking(master, False) + while True: + for [_, events] in sel.select(): + if events & selectors.EVENT_READ: + try: + chunk = os.read(master, 0x10000) + except OSError as err: + # Linux raises EIO when slave is closed (Issue 5380) + if err.errno != EIO: + raise + chunk = b"" + if not chunk: + return output + output.extend(chunk) + if events & selectors.EVENT_WRITE: + try: + input = input[os.write(master, input):] + except OSError as err: + # Apparently EIO means the slave was closed + if err.errno != EIO: + raise + input = b"" # Stop writing + if not input: + sel.modify(master, selectors.EVENT_READ) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 5508f7bff37994..26fef35b8b8556 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -15,6 +15,8 @@ from io import StringIO from test import support from test.support import os_helper +from test.support.import_helper import import_module +from test.support.pty_helper import run_pty # This little helper class is essential for testing pdb under doctest. from test.test_doctest import _FakeInput from unittest.mock import patch @@ -3235,6 +3237,45 @@ def test_checkline_is_not_executable(self): self.assertFalse(db.checkline(os_helper.TESTFN, lineno)) +@support.requires_subprocess() +class PdbTestReadline(unittest.TestCase): + def setUpClass(): + # Ensure that the readline module is loaded + # If this fails, the test is skipped because SkipTest will be raised + import_module('readline') + + def test_basic_completion(self): + script = textwrap.dedent(""" + import pdb; pdb.Pdb().set_trace() + print('hello') + """) + + input = b"co\t\tntin\t\n" + + output = run_pty(script, input) + + self.assertIn(b'cont', output) + self.assertIn(b'condition', output) + self.assertIn(b'continue', output) + + def test_expression_completion(self): + script = textwrap.dedent(""" + value = "speci" + import pdb; pdb.Pdb().set_trace() + """) + + input = b"val\t + 'al'\n" + input += b"p val\t + 'es'\n" + input += b"$_fra\t\n" + input += b"c\n" + + output = run_pty(script, input) + + self.assertIn(b'special', output) + self.assertIn(b'species', output) + self.assertIn(b'$_frame', output) + + def load_tests(loader, tests, pattern): from test import test_pdb tests.addTest(doctest.DocTestSuite(test_pdb)) diff --git a/Lib/test/test_readline.py b/Lib/test/test_readline.py index 59dbef90380053..835280f2281cde 100644 --- a/Lib/test/test_readline.py +++ b/Lib/test/test_readline.py @@ -1,18 +1,15 @@ """ Very minimal unittests for parts of the readline module. """ -from contextlib import ExitStack -from errno import EIO import locale import os -import selectors -import subprocess import sys import tempfile import unittest from test.support import verbose from test.support.import_helper import import_module from test.support.os_helper import unlink, temp_dir, TESTFN +from test.support.pty_helper import run_pty from test.support.script_helper import assert_python_ok # Skip tests if there is no readline module @@ -304,55 +301,5 @@ def test_history_size(self): self.assertEqual(lines[-1].strip(), b"last input") -def run_pty(script, input=b"dummy input\r", env=None): - pty = import_module('pty') - output = bytearray() - [master, slave] = pty.openpty() - args = (sys.executable, '-c', script) - proc = subprocess.Popen(args, stdin=slave, stdout=slave, stderr=slave, env=env) - os.close(slave) - with ExitStack() as cleanup: - cleanup.enter_context(proc) - def terminate(proc): - try: - proc.terminate() - except ProcessLookupError: - # Workaround for Open/Net BSD bug (Issue 16762) - pass - cleanup.callback(terminate, proc) - cleanup.callback(os.close, master) - # Avoid using DefaultSelector and PollSelector. Kqueue() does not - # work with pseudo-terminals on OS X < 10.9 (Issue 20365) and Open - # BSD (Issue 20667). Poll() does not work with OS X 10.6 or 10.4 - # either (Issue 20472). Hopefully the file descriptor is low enough - # to use with select(). - sel = cleanup.enter_context(selectors.SelectSelector()) - sel.register(master, selectors.EVENT_READ | selectors.EVENT_WRITE) - os.set_blocking(master, False) - while True: - for [_, events] in sel.select(): - if events & selectors.EVENT_READ: - try: - chunk = os.read(master, 0x10000) - except OSError as err: - # Linux raises EIO when slave is closed (Issue 5380) - if err.errno != EIO: - raise - chunk = b"" - if not chunk: - return output - output.extend(chunk) - if events & selectors.EVENT_WRITE: - try: - input = input[os.write(master, input):] - except OSError as err: - # Apparently EIO means the slave was closed - if err.errno != EIO: - raise - input = b"" # Stop writing - if not input: - sel.modify(master, selectors.EVENT_READ) - - if __name__ == "__main__": unittest.main() From ae643184ede6dfbcba283b881a1baf93a453e373 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Fri, 3 Nov 2023 16:10:32 -0700 Subject: [PATCH 2/5] Disable test with libedit --- Lib/test/test_pdb.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 26fef35b8b8556..e80e238cf5ca1d 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -3242,7 +3242,9 @@ class PdbTestReadline(unittest.TestCase): def setUpClass(): # Ensure that the readline module is loaded # If this fails, the test is skipped because SkipTest will be raised - import_module('readline') + readline = import_module('readline') + if readline.__doc__ and "libedit" in readline.__doc__: + raise unittest.SkipTest("libedit readline is not supported for pdb") def test_basic_completion(self): script = textwrap.dedent(""" From ddd67d58735d91d26c1a1f96043cee3ba530915f Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Tue, 7 Nov 2023 10:27:51 -0800 Subject: [PATCH 3/5] Improve comments --- Lib/test/test_pdb.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index e80e238cf5ca1d..5ff7e06ac0612e 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -3252,6 +3252,8 @@ def test_basic_completion(self): print('hello') """) + # List everything starting with 'co', there should be multiple matches + # then add ntin and complete 'contin' to 'continue' input = b"co\t\tntin\t\n" output = run_pty(script, input) @@ -3260,23 +3262,6 @@ def test_basic_completion(self): self.assertIn(b'condition', output) self.assertIn(b'continue', output) - def test_expression_completion(self): - script = textwrap.dedent(""" - value = "speci" - import pdb; pdb.Pdb().set_trace() - """) - - input = b"val\t + 'al'\n" - input += b"p val\t + 'es'\n" - input += b"$_fra\t\n" - input += b"c\n" - - output = run_pty(script, input) - - self.assertIn(b'special', output) - self.assertIn(b'species', output) - self.assertIn(b'$_frame', output) - def load_tests(loader, tests, pattern): from test import test_pdb From 2bc94b7057687c3d0ad1468899c756917edca7a6 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Thu, 9 Nov 2023 10:45:03 -0800 Subject: [PATCH 4/5] Update Lib/test/test_pdb.py Co-authored-by: Petr Viktorin --- Lib/test/test_pdb.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 5ff7e06ac0612e..159a26a24a834f 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -3249,7 +3249,8 @@ def setUpClass(): def test_basic_completion(self): script = textwrap.dedent(""" import pdb; pdb.Pdb().set_trace() - print('hello') + # Concatenate strings so that the output doesn't appear in the source + print('hello' + '!') """) # List everything starting with 'co', there should be multiple matches From fe6c8ab3b39437b04059249f4bea13ec958ebe0e Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Thu, 9 Nov 2023 10:47:05 -0800 Subject: [PATCH 5/5] Use commands instead of cont --- Lib/test/test_pdb.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 159a26a24a834f..e08c32196a6581 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -3259,9 +3259,10 @@ def test_basic_completion(self): output = run_pty(script, input) - self.assertIn(b'cont', output) + self.assertIn(b'commands', output) self.assertIn(b'condition', output) self.assertIn(b'continue', output) + self.assertIn(b'hello!', output) def load_tests(loader, tests, pattern):