Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 75 additions & 36 deletions Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import platform
import random
import re
import subprocess
import sys
import traceback
import types
Expand Down Expand Up @@ -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)
Expand All @@ -1686,44 +1676,93 @@ 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'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the end="" bit? Add a comment saying you are testing that input flushes stdout if that is your intention, because it’s not obvious.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is expected that input() flushes stdout, not sure why you think this would need to be tested here. Avoiding the \n makes it clear that the length of sentinel on input is not tied to platform new line conversions.

'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
# is different and invokes GNU readline if available).
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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix test_builtin.PtyTests tests when readline is loaded.