diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 8f91bc9bf919b7..5e2a9d7ee18677 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -12,6 +12,7 @@ import platform import random import re +import subprocess import sys import traceback import types @@ -1665,18 +1666,7 @@ def run_child(self, child, terminal_input): # Check the result was got and corresponds to the user's terminal input if len(lines) != 2: # Something went wrong, try to get at stderr - # Beware of Linux raising EIO when the slave is closed - child_output = bytearray() - while True: - try: - chunk = os.read(fd, 3000) - except OSError: # Assume EIO - break - if not chunk: - break - child_output.extend(chunk) - os.close(fd) - child_output = child_output.decode("ascii", "ignore") + child_output = final_output(fd) self.fail("got %d lines in pipe but expected 2, child output was:\n%s" % (len(lines), child_output)) os.close(fd) @@ -1686,31 +1676,80 @@ def run_child(self, child, terminal_input): return lines + def final_output(self, fd): + # Beware of Linux raising EIO when the slave is closed + child_output = bytearray() + while True: + try: + chunk = os.read(fd, 3000) + except OSError: # Assume EIO + break + if not chunk: + break + child_output.extend(chunk) + os.close(fd) + return child_output.decode("ascii", "backslashreplace") + def check_input_tty(self, prompt, terminal_input, stdio_encoding=None): - if not sys.stdin.isatty() or not sys.stdout.isatty(): - self.skipTest("stdin and stdout must be ttys") - def child(wpipe): - # Check the error handlers are accounted for - if stdio_encoding: - sys.stdin = io.TextIOWrapper(sys.stdin.detach(), - encoding=stdio_encoding, - errors='surrogateescape') - sys.stdout = io.TextIOWrapper(sys.stdout.detach(), - encoding=stdio_encoding, - errors='replace') - print("tty =", sys.stdin.isatty() and sys.stdout.isatty(), file=wpipe) - print(ascii(input(prompt)), file=wpipe) - lines = self.run_child(child, terminal_input + b"\r\n") - # Check we did exercise the GNU readline path - self.assertIn(lines[0], {'tty = True', 'tty = False'}) - if lines[0] != 'tty = True': - self.skipTest("standard IO in should have been a tty") - input_result = eval(lines[1]) # ascii() -> eval() roundtrip + template = ( + 'import sys, io\n' + '# Check the error handlers are accounted for\n' + 'stdio_encoding = {stdio_encoding!a}\n' + 'if stdio_encoding:\n' + ' sys.stdin = io.TextIOWrapper(sys.stdin.detach(),\n' + ' encoding=stdio_encoding,\n' + " errors='surrogateescape')\n" + ' sys.stdout = io.TextIOWrapper(sys.stdout.detach(),\n' + ' encoding=stdio_encoding,\n' + " errors='replace')\n" + '# Check we exercise the PyOS_Readline() path\n' + 'if not sys.stdin.isatty() or not sys.stdout.isatty():\n' + ' raise AssertionError("standard IO should be a tty")\n' + 'print("{sentinel}", end="")\n' + 'result = input({prompt!a})\n' + 'if result != {expected!a}:\n' + ' raise AssertionError("unexpected input " + ascii(result))\n' + ) + sentinel = '123' if stdio_encoding: expected = terminal_input.decode(stdio_encoding, 'surrogateescape') + exp_sentinel = sentinel.encode(stdio_encoding, errors='replace') else: expected = terminal_input.decode(sys.stdin.encoding) # what else? - self.assertEqual(input_result, expected) + exp_sentinel = sentinel.encode(sys.stdout.encoding) + code = template.format( + stdio_encoding=stdio_encoding, prompt=prompt, expected=expected, + sentinel=sentinel) + + # Without Readline module + self.assert_script(code, terminal_input, exp_sentinel) + + readline_encoding = locale.getpreferredencoding() + try: + terminal_input.decode(readline_encoding) + except UnicodeDecodeError: + # Readline may handle undecodable bytes differently to Python + # (e.g. by its convert-meta setting) + pass + else: + self.assert_script('import readline\n' + code, terminal_input, + exp_sentinel) + + def assert_script(self, code, terminal_input, expected): + cmd = (sys.executable, '-c', code) + [master, slave] = pty.openpty() + proc = subprocess.Popen(cmd, stdin=slave, stdout=slave, stderr=slave) + os.close(slave) + with proc, os.fdopen(master, "wb", closefd=False) as writer: + # Wait for the process to be fully started. + sentinel = os.read(master, len(expected)) + writer.write(terminal_input + b"\r\n") + writer.flush() + # Assert after writing to the process to avoid a deadlock if the + # assertion fails. + self.assertEqual(sentinel, expected) + output = self.final_output(master) + self.assertEqual(proc.returncode, 0, output) def test_input_tty(self): # Test input() functionality when wired to a tty (the code path @@ -1718,12 +1757,12 @@ def test_input_tty(self): self.check_input_tty("prompt", b"quux") def test_input_tty_non_ascii(self): - # Check stdin/stdout encoding is used when invoking GNU readline - self.check_input_tty("prompté", b"quux\xe9", "utf-8") + # Check stdin/stdout encoding is used when invoking PyOS_Readline() + self.check_input_tty("prompté", "quuxé".encode("utf-8"), "utf-8") def test_input_tty_non_ascii_unicode_errors(self): - # Check stdin/stdout error handler is used when invoking GNU readline - self.check_input_tty("prompté", b"quux\xe9", "ascii") + # Check stdin/stdout error handler used when invoking PyOS_Readline() + self.check_input_tty("prompté", "quuxé".encode("utf-8"), "ascii") def test_input_no_stdout_fileno(self): # Issue #24402: If stdin is the original terminal but stdout.fileno() diff --git a/Misc/NEWS.d/next/Tests/2018-05-26-18-09-39.bpo-13886.QdOme0.rst b/Misc/NEWS.d/next/Tests/2018-05-26-18-09-39.bpo-13886.QdOme0.rst new file mode 100644 index 00000000000000..df35c013f752a5 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2018-05-26-18-09-39.bpo-13886.QdOme0.rst @@ -0,0 +1 @@ +Fix test_builtin.PtyTests tests when readline is loaded.