Issue #28217: Adds _testconsole module to test console input. Fixes some issues found by the tests.
diff --git a/Modules/_io/winconsoleio.c b/Modules/_io/winconsoleio.c
index 0bf4ddf..ee7a1b2 100644
--- a/Modules/_io/winconsoleio.c
+++ b/Modules/_io/winconsoleio.c
@@ -39,6 +39,11 @@
 /* BUFMAX determines how many bytes can be read in one go. */
 #define BUFMAX (32*1024*1024)
 
+/* SMALLBUF determines how many utf-8 characters will be
+   buffered within the stream, in order to support reads
+   of less than one character */
+#define SMALLBUF 4
+
 char _get_console_type(HANDLE handle) {
     DWORD mode, peek_count;
 
@@ -125,7 +130,8 @@
     unsigned int blksize;
     PyObject *weakreflist;
     PyObject *dict;
-    char buf[4];
+    char buf[SMALLBUF];
+    wchar_t wbuf;
 } winconsoleio;
 
 PyTypeObject PyWindowsConsoleIO_Type;
@@ -500,11 +506,11 @@
 static DWORD
 _buflen(winconsoleio *self)
 {
-    for (DWORD i = 0; i < 4; ++i) {
+    for (DWORD i = 0; i < SMALLBUF; ++i) {
         if (!self->buf[i])
             return i;
     }
-    return 4;
+    return SMALLBUF;
 }
 
 static DWORD
@@ -513,12 +519,10 @@
     DWORD n = 0;
 
     while (self->buf[0] && len--) {
-        n += 1;
-        buf[0] = self->buf[0];
-        self->buf[0] = self->buf[1];
-        self->buf[1] = self->buf[2];
-        self->buf[2] = self->buf[3];
-        self->buf[3] = 0;
+        buf[n++] = self->buf[0];
+        for (int i = 1; i < SMALLBUF; ++i)
+            self->buf[i - 1] = self->buf[i];
+        self->buf[SMALLBUF - 1] = 0;
     }
 
     return n;
@@ -531,10 +535,13 @@
     wchar_t *buf = (wchar_t*)PyMem_Malloc(maxlen * sizeof(wchar_t));
     if (!buf)
         goto error;
+
     *readlen = 0;
 
+    //DebugBreak();
     Py_BEGIN_ALLOW_THREADS
-    for (DWORD off = 0; off < maxlen; off += BUFSIZ) {
+    DWORD off = 0;
+    while (off < maxlen) {
         DWORD n, len = min(maxlen - off, BUFSIZ);
         SetLastError(0);
         BOOL res = ReadConsoleW(handle, &buf[off], len, &n, NULL);
@@ -550,7 +557,7 @@
             err = 0;
             HANDLE hInterruptEvent = _PyOS_SigintEvent();
             if (WaitForSingleObjectEx(hInterruptEvent, 100, FALSE)
-                == WAIT_OBJECT_0) {
+                    == WAIT_OBJECT_0) {
                 ResetEvent(hInterruptEvent);
                 Py_BLOCK_THREADS
                 sig = PyErr_CheckSignals();
@@ -568,7 +575,30 @@
         /* If the buffer ended with a newline, break out */
         if (buf[*readlen - 1] == '\n')
             break;
+        /* If the buffer ends with a high surrogate, expand the
+           buffer and read an extra character. */
+        WORD char_type;
+        if (off + BUFSIZ >= maxlen &&
+            GetStringTypeW(CT_CTYPE3, &buf[*readlen - 1], 1, &char_type) &&
+            char_type == C3_HIGHSURROGATE) {
+            wchar_t *newbuf;
+            maxlen += 1;
+            Py_BLOCK_THREADS
+            newbuf = (wchar_t*)PyMem_Realloc(buf, maxlen * sizeof(wchar_t));
+            Py_UNBLOCK_THREADS
+            if (!newbuf) {
+                sig = -1;
+                break;
+            }
+            buf = newbuf;
+            /* Only advance by n and not BUFSIZ in this case */
+            off += n;
+            continue;
+        }
+
+        off += BUFSIZ;
     }
+
     Py_END_ALLOW_THREADS
 
     if (sig)
@@ -1110,4 +1140,6 @@
     0,                                          /* tp_finalize */
 };
 
+PyAPI_DATA(PyObject *) _PyWindowsConsoleIO_Type = (PyObject*)&PyWindowsConsoleIO_Type;
+
 #endif /* MS_WINDOWS */