Skip to content

Commit 312cef7

Browse files
committed
Issue #28217: Adds _testconsole module to test console input. Fixes some issues found by the tests.
1 parent 7fe091d commit 312cef7

File tree

11 files changed

+460
-21
lines changed

11 files changed

+460
-21
lines changed

Lib/test/test_winconsoleio.py

+62-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
11
'''Tests for WindowsConsoleIO
2-
3-
Unfortunately, most testing requires interactive use, since we have no
4-
API to read back from a real console, and this class is only for use
5-
with real consoles.
6-
7-
Instead, we validate that basic functionality such as opening, closing
8-
and in particular fileno() work, but are forced to leave real testing
9-
to real people with real keyborads.
102
'''
113

124
import io
@@ -16,6 +8,8 @@
168
if sys.platform != 'win32':
179
raise unittest.SkipTest("test only relevant on win32")
1810

11+
from _testconsole import write_input
12+
1913
ConIO = io._WindowsConsoleIO
2014

2115
class WindowsConsoleIOTests(unittest.TestCase):
@@ -83,5 +77,65 @@ def test_open_name(self):
8377
f.close()
8478
f.close()
8579

80+
def assertStdinRoundTrip(self, text):
81+
stdin = open('CONIN$', 'r')
82+
old_stdin = sys.stdin
83+
try:
84+
sys.stdin = stdin
85+
write_input(
86+
stdin.buffer.raw,
87+
(text + '\r\n').encode('utf-16-le', 'surrogatepass')
88+
)
89+
actual = input()
90+
finally:
91+
sys.stdin = old_stdin
92+
self.assertEqual(actual, text)
93+
94+
def test_input(self):
95+
# ASCII
96+
self.assertStdinRoundTrip('abc123')
97+
# Non-ASCII
98+
self.assertStdinRoundTrip('ϼўТλФЙ')
99+
# Combining characters
100+
self.assertStdinRoundTrip('A͏B ﬖ̳AA̝')
101+
# Non-BMP
102+
self.assertStdinRoundTrip('\U00100000\U0010ffff\U0010fffd')
103+
104+
def test_partial_reads(self):
105+
# Test that reading less than 1 full character works when stdin
106+
# contains multibyte UTF-8 sequences
107+
source = 'ϼўТλФЙ\r\n'.encode('utf-16-le')
108+
expected = 'ϼўТλФЙ\r\n'.encode('utf-8')
109+
for read_count in range(1, 16):
110+
stdin = open('CONIN$', 'rb', buffering=0)
111+
write_input(stdin, source)
112+
113+
actual = b''
114+
while not actual.endswith(b'\n'):
115+
b = stdin.read(read_count)
116+
actual += b
117+
118+
self.assertEqual(actual, expected, 'stdin.read({})'.format(read_count))
119+
stdin.close()
120+
121+
def test_partial_surrogate_reads(self):
122+
# Test that reading less than 1 full character works when stdin
123+
# contains surrogate pairs that cannot be decoded to UTF-8 without
124+
# reading an extra character.
125+
source = '\U00101FFF\U00101001\r\n'.encode('utf-16-le')
126+
expected = '\U00101FFF\U00101001\r\n'.encode('utf-8')
127+
for read_count in range(1, 16):
128+
stdin = open('CONIN$', 'rb', buffering=0)
129+
write_input(stdin, source)
130+
131+
actual = b''
132+
while not actual.endswith(b'\n'):
133+
b = stdin.read(read_count)
134+
actual += b
135+
136+
self.assertEqual(actual, expected, 'stdin.read({})'.format(read_count))
137+
stdin.close()
138+
139+
86140
if __name__ == "__main__":
87141
unittest.main()

Misc/NEWS

+4
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ Build
186186
- Issue #15819: Remove redundant include search directory option for building
187187
outside the source tree.
188188

189+
Tests
190+
-----
191+
192+
- Issue #28217: Adds _testconsole module to test console input.
189193

190194
What's New in Python 3.6.0 beta 1
191195
=================================

Modules/_io/_iomodule.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ extern PyTypeObject PyIncrementalNewlineDecoder_Type;
2222
#ifndef Py_LIMITED_API
2323
#ifdef MS_WINDOWS
2424
extern PyTypeObject PyWindowsConsoleIO_Type;
25-
#define PyWindowsConsoleIO_Check(op) (PyObject_TypeCheck((op), &PyWindowsConsoleIO_Type))
25+
PyAPI_DATA(PyObject *) _PyWindowsConsoleIO_Type;
26+
#define PyWindowsConsoleIO_Check(op) (PyObject_TypeCheck((op), (PyTypeObject*)_PyWindowsConsoleIO_Type))
2627
#endif /* MS_WINDOWS */
2728
#endif /* Py_LIMITED_API */
2829

Modules/_io/winconsoleio.c

+43-11
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
/* BUFMAX determines how many bytes can be read in one go. */
4040
#define BUFMAX (32*1024*1024)
4141

42+
/* SMALLBUF determines how many utf-8 characters will be
43+
buffered within the stream, in order to support reads
44+
of less than one character */
45+
#define SMALLBUF 4
46+
4247
char _get_console_type(HANDLE handle) {
4348
DWORD mode, peek_count;
4449

@@ -125,7 +130,8 @@ typedef struct {
125130
unsigned int blksize;
126131
PyObject *weakreflist;
127132
PyObject *dict;
128-
char buf[4];
133+
char buf[SMALLBUF];
134+
wchar_t wbuf;
129135
} winconsoleio;
130136

131137
PyTypeObject PyWindowsConsoleIO_Type;
@@ -500,11 +506,11 @@ _io__WindowsConsoleIO_writable_impl(winconsoleio *self)
500506
static DWORD
501507
_buflen(winconsoleio *self)
502508
{
503-
for (DWORD i = 0; i < 4; ++i) {
509+
for (DWORD i = 0; i < SMALLBUF; ++i) {
504510
if (!self->buf[i])
505511
return i;
506512
}
507-
return 4;
513+
return SMALLBUF;
508514
}
509515

510516
static DWORD
@@ -513,12 +519,10 @@ _copyfrombuf(winconsoleio *self, char *buf, DWORD len)
513519
DWORD n = 0;
514520

515521
while (self->buf[0] && len--) {
516-
n += 1;
517-
buf[0] = self->buf[0];
518-
self->buf[0] = self->buf[1];
519-
self->buf[1] = self->buf[2];
520-
self->buf[2] = self->buf[3];
521-
self->buf[3] = 0;
522+
buf[n++] = self->buf[0];
523+
for (int i = 1; i < SMALLBUF; ++i)
524+
self->buf[i - 1] = self->buf[i];
525+
self->buf[SMALLBUF - 1] = 0;
522526
}
523527

524528
return n;
@@ -531,10 +535,13 @@ read_console_w(HANDLE handle, DWORD maxlen, DWORD *readlen) {
531535
wchar_t *buf = (wchar_t*)PyMem_Malloc(maxlen * sizeof(wchar_t));
532536
if (!buf)
533537
goto error;
538+
534539
*readlen = 0;
535540

541+
//DebugBreak();
536542
Py_BEGIN_ALLOW_THREADS
537-
for (DWORD off = 0; off < maxlen; off += BUFSIZ) {
543+
DWORD off = 0;
544+
while (off < maxlen) {
538545
DWORD n, len = min(maxlen - off, BUFSIZ);
539546
SetLastError(0);
540547
BOOL res = ReadConsoleW(handle, &buf[off], len, &n, NULL);
@@ -550,7 +557,7 @@ read_console_w(HANDLE handle, DWORD maxlen, DWORD *readlen) {
550557
err = 0;
551558
HANDLE hInterruptEvent = _PyOS_SigintEvent();
552559
if (WaitForSingleObjectEx(hInterruptEvent, 100, FALSE)
553-
== WAIT_OBJECT_0) {
560+
== WAIT_OBJECT_0) {
554561
ResetEvent(hInterruptEvent);
555562
Py_BLOCK_THREADS
556563
sig = PyErr_CheckSignals();
@@ -568,7 +575,30 @@ read_console_w(HANDLE handle, DWORD maxlen, DWORD *readlen) {
568575
/* If the buffer ended with a newline, break out */
569576
if (buf[*readlen - 1] == '\n')
570577
break;
578+
/* If the buffer ends with a high surrogate, expand the
579+
buffer and read an extra character. */
580+
WORD char_type;
581+
if (off + BUFSIZ >= maxlen &&
582+
GetStringTypeW(CT_CTYPE3, &buf[*readlen - 1], 1, &char_type) &&
583+
char_type == C3_HIGHSURROGATE) {
584+
wchar_t *newbuf;
585+
maxlen += 1;
586+
Py_BLOCK_THREADS
587+
newbuf = (wchar_t*)PyMem_Realloc(buf, maxlen * sizeof(wchar_t));
588+
Py_UNBLOCK_THREADS
589+
if (!newbuf) {
590+
sig = -1;
591+
break;
592+
}
593+
buf = newbuf;
594+
/* Only advance by n and not BUFSIZ in this case */
595+
off += n;
596+
continue;
597+
}
598+
599+
off += BUFSIZ;
571600
}
601+
572602
Py_END_ALLOW_THREADS
573603

574604
if (sig)
@@ -1110,4 +1140,6 @@ PyTypeObject PyWindowsConsoleIO_Type = {
11101140
0, /* tp_finalize */
11111141
};
11121142

1143+
PyAPI_DATA(PyObject *) _PyWindowsConsoleIO_Type = (PyObject*)&PyWindowsConsoleIO_Type;
1144+
11131145
#endif /* MS_WINDOWS */

PC/_testconsole.c

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
2+
/* Testing module for multi-phase initialization of extension modules (PEP 489)
3+
*/
4+
5+
#include "Python.h"
6+
7+
#ifdef MS_WINDOWS
8+
9+
#include "..\modules\_io\_iomodule.h"
10+
11+
#define WIN32_LEAN_AND_MEAN
12+
#include <windows.h>
13+
#include <fcntl.h>
14+
15+
/* The full definition is in iomodule. We reproduce
16+
enough here to get the handle, which is all we want. */
17+
typedef struct {
18+
PyObject_HEAD
19+
HANDLE handle;
20+
} winconsoleio;
21+
22+
23+
static int execfunc(PyObject *m)
24+
{
25+
return 0;
26+
}
27+
28+
PyModuleDef_Slot testconsole_slots[] = {
29+
{Py_mod_exec, execfunc},
30+
{0, NULL},
31+
};
32+
33+
/*[clinic input]
34+
module _testconsole
35+
36+
_testconsole.write_input
37+
file: object
38+
s: PyBytesObject
39+
40+
Writes UTF-16-LE encoded bytes to the console as if typed by a user.
41+
[clinic start generated code]*/
42+
43+
static PyObject *
44+
_testconsole_write_input_impl(PyObject *module, PyObject *file,
45+
PyBytesObject *s)
46+
/*[clinic end generated code: output=48f9563db34aedb3 input=4c774f2d05770bc6]*/
47+
{
48+
INPUT_RECORD *rec = NULL;
49+
50+
if (!PyWindowsConsoleIO_Check(file)) {
51+
PyErr_SetString(PyExc_TypeError, "expected raw console object");
52+
return NULL;
53+
}
54+
55+
const wchar_t *p = (const wchar_t *)PyBytes_AS_STRING(s);
56+
DWORD size = (DWORD)PyBytes_GET_SIZE(s) / sizeof(wchar_t);
57+
58+
rec = (INPUT_RECORD*)PyMem_Malloc(sizeof(INPUT_RECORD) * size);
59+
if (!rec)
60+
goto error;
61+
memset(rec, 0, sizeof(INPUT_RECORD) * size);
62+
63+
INPUT_RECORD *prec = rec;
64+
for (DWORD i = 0; i < size; ++i, ++p, ++prec) {
65+
prec->EventType = KEY_EVENT;
66+
prec->Event.KeyEvent.bKeyDown = TRUE;
67+
prec->Event.KeyEvent.wRepeatCount = 10;
68+
prec->Event.KeyEvent.uChar.UnicodeChar = *p;
69+
}
70+
71+
HANDLE hInput = ((winconsoleio*)file)->handle;
72+
DWORD total = 0;
73+
while (total < size) {
74+
DWORD wrote;
75+
if (!WriteConsoleInputW(hInput, &rec[total], (size - total), &wrote)) {
76+
PyErr_SetFromWindowsErr(0);
77+
goto error;
78+
}
79+
total += wrote;
80+
}
81+
82+
PyMem_Free((void*)rec);
83+
84+
Py_RETURN_NONE;
85+
error:
86+
if (rec)
87+
PyMem_Free((void*)rec);
88+
return NULL;
89+
}
90+
91+
/*[clinic input]
92+
_testconsole.read_output
93+
file: object
94+
95+
Reads a str from the console as written to stdout.
96+
[clinic start generated code]*/
97+
98+
static PyObject *
99+
_testconsole_read_output_impl(PyObject *module, PyObject *file)
100+
/*[clinic end generated code: output=876310d81a73e6d2 input=b3521f64b1b558e3]*/
101+
{
102+
Py_RETURN_NONE;
103+
}
104+
105+
#include "clinic\_testconsole.c.h"
106+
107+
PyMethodDef testconsole_methods[] = {
108+
_TESTCONSOLE_WRITE_INPUT_METHODDEF
109+
_TESTCONSOLE_READ_OUTPUT_METHODDEF
110+
{NULL, NULL}
111+
};
112+
113+
static PyModuleDef testconsole_def = {
114+
PyModuleDef_HEAD_INIT, /* m_base */
115+
"_testconsole", /* m_name */
116+
PyDoc_STR("Test module for the Windows console"), /* m_doc */
117+
0, /* m_size */
118+
testconsole_methods, /* m_methods */
119+
testconsole_slots, /* m_slots */
120+
NULL, /* m_traverse */
121+
NULL, /* m_clear */
122+
NULL, /* m_free */
123+
};
124+
125+
PyMODINIT_FUNC
126+
PyInit__testconsole(PyObject *spec)
127+
{
128+
return PyModuleDef_Init(&testconsole_def);
129+
}
130+
131+
#endif /* MS_WINDOWS */

0 commit comments

Comments
 (0)