Skip to content

Commit 514b46e

Browse files
add tests
1 parent 0df6a92 commit 514b46e

4 files changed

Lines changed: 199 additions & 38 deletions

File tree

Doc/library/asyncio-eventloop.rst

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,9 +1263,9 @@ Working with pipes
12631263
*protocol_factory* must be a callable returning an
12641264
:ref:`asyncio protocol <asyncio-protocol>` implementation.
12651265

1266-
*pipe* is a :term:`file-like object <file object>`. Not every file-like
1267-
object is accepted; see :ref:`Supported pipe objects
1268-
<asyncio-pipe-objects>` below.
1266+
*pipe* is a :term:`file-like object <file object>`. See
1267+
:ref:`Supported pipe objects <asyncio-pipe-objects>` for the objects
1268+
supported as *pipe*.
12691269

12701270
Return pair ``(transport, protocol)``, where *transport* supports
12711271
the :class:`ReadTransport` interface and *protocol* is an object
@@ -1282,9 +1282,9 @@ Working with pipes
12821282
*protocol_factory* must be a callable returning an
12831283
:ref:`asyncio protocol <asyncio-protocol>` implementation.
12841284

1285-
*pipe* is a :term:`file-like object <file object>`. Not every file-like
1286-
object is accepted; see :ref:`Supported pipe objects
1287-
<asyncio-pipe-objects>` below.
1285+
*pipe* is a :term:`file-like object <file object>`. See
1286+
:ref:`Supported pipe objects <asyncio-pipe-objects>` for the objects
1287+
supported as *pipe*.
12881288

12891289
Return pair ``(transport, protocol)``, where *transport* supports
12901290
:class:`WriteTransport` interface and *protocol* is an object
@@ -1297,13 +1297,10 @@ Working with pipes
12971297

12981298
.. rubric:: Supported pipe objects
12991299

1300-
Even though the *pipe* argument is a :term:`file-like object <file object>`,
1301-
these methods only work with objects the operating system can poll for
1300+
These methods only work with objects the operating system can poll for
13021301
readiness or perform overlapped I/O on. Regular files on disk are **not**
1303-
supported on any platform, and neither are :data:`sys.stdin`,
1304-
:data:`sys.stdout` and :data:`sys.stderr` when they have been redirected to
1305-
or from a regular file. There is no asynchronous file I/O in asyncio; use
1306-
:meth:`loop.run_in_executor` to read and write regular files without
1302+
supported on any platform. There is no asynchronous file I/O in asyncio;
1303+
use :meth:`loop.run_in_executor` to read and write regular files without
13071304
blocking the event loop.
13081305

13091306
On Unix, with :class:`SelectorEventLoop`, *pipe* must wrap one of the
@@ -1312,24 +1309,11 @@ following:
13121309
* a pipe, such as an end of an :func:`os.pipe` pair or a FIFO created with
13131310
:func:`os.mkfifo`;
13141311
* a socket;
1315-
* a character device, such as a terminal. This is why :data:`sys.stdin`
1316-
works when the program is run interactively but fails when its input is
1317-
redirected from a file.
1318-
1319-
Anything else, a regular file in particular, raises :exc:`ValueError`.
1312+
* a character device, such as a terminal.
13201313

13211314
On Windows, where only :class:`ProactorEventLoop` implements these methods,
13221315
*pipe* must wrap a handle opened for overlapped I/O, since the handle has to
1323-
be associated with an I/O completion port. In practice this means a named
1324-
pipe, such as the ones created by :func:`!asyncio.windows_utils.pipe` and
1325-
used for the standard streams of a subprocess started by
1326-
:meth:`loop.subprocess_exec`. Console handles and regular file handles
1327-
cannot be associated with a completion port, so :data:`sys.stdin` and files
1328-
opened with :func:`open` do not work. Unlike on Unix, this is not reported
1329-
by the method itself: it returns successfully and the resulting
1330-
:exc:`OSError` is later passed to the
1331-
:meth:`event loop exception handler <loop.call_exception_handler>` when the
1332-
transport first reads or writes.
1316+
be associated with an I/O completion port.
13331317

13341318
.. note::
13351319

Doc/library/asyncio-platforms.rst

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ All Platforms
2020
cannot be used to monitor file I/O.
2121

2222
* :meth:`loop.connect_read_pipe` and :meth:`loop.connect_write_pipe`
23-
cannot be used with regular files, nor with :data:`sys.stdin`,
24-
:data:`sys.stdout` or :data:`sys.stderr` when these are redirected to or
25-
from a regular file. See :ref:`Supported pipe objects
23+
cannot be used with regular files. See :ref:`Supported pipe objects
2624
<asyncio-pipe-objects>` for the objects that are accepted on each
2725
platform.
2826

@@ -71,14 +69,11 @@ All event loops on Windows do not support the following methods:
7169

7270
* :meth:`loop.connect_read_pipe` and :meth:`loop.connect_write_pipe` only
7371
accept a handle opened for overlapped I/O, which in practice means a named
74-
pipe such as those created by :func:`!asyncio.windows_utils.pipe`.
75-
Console handles and regular file handles cannot be associated with an I/O
76-
completion port, so :data:`sys.stdin`, :data:`sys.stdout`,
77-
:data:`sys.stderr` and files opened with :func:`open` are not supported.
78-
These methods do not fail immediately in that case: the resulting
79-
:exc:`OSError` is passed to the
80-
:meth:`event loop exception handler <loop.call_exception_handler>` when the
81-
transport first reads or writes.
72+
pipe such as those created by :func:`!asyncio.windows_utils.pipe`. A
73+
console handle cannot be associated with an I/O completion port, so
74+
:data:`sys.stdin`, :data:`sys.stdout` and :data:`sys.stderr` are not
75+
supported. See :ref:`Supported pipe objects <asyncio-pipe-objects>` for
76+
how such a handle is rejected, which differs between the two methods.
8277

8378
The resolution of the monotonic clock on Windows is usually around 15.6
8479
milliseconds. The best resolution is 0.5 milliseconds. The resolution depends on the

Lib/test/test_asyncio/test_events.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from multiprocessing.util import _cleanup_tests as multiprocessing_cleanup_tests
3434
from test.test_asyncio import utils as test_utils
3535
from test import support
36+
from test.support import os_helper
3637
from test.support import socket_helper
3738
from test.support import threading_helper
3839
from test.support import ALWAYS_EQ, LARGEST, SMALLEST
@@ -3134,5 +3135,100 @@ def test_get_loop(self):
31343135
events.AbstractServer().get_loop()
31353136

31363137

3138+
@unittest.skipIf(sys.platform == 'win32', 'Unix pipe transport semantics')
3139+
class UnixPipeObjectSupportTests(unittest.TestCase):
3140+
def setUp(self):
3141+
super().setUp()
3142+
self.loop = asyncio.SelectorEventLoop()
3143+
self.addCleanup(self.loop.close)
3144+
3145+
def check_accepted(self, connect, pipeobj):
3146+
lost = self.loop.create_future()
3147+
3148+
class Proto(asyncio.Protocol):
3149+
def connection_lost(self, exc):
3150+
if not lost.done():
3151+
lost.set_result(exc)
3152+
3153+
async def run():
3154+
transport, protocol = await connect(Proto, pipeobj)
3155+
self.assertIsInstance(protocol, Proto)
3156+
self.assertFalse(pipeobj.closed)
3157+
transport.close()
3158+
self.assertIsNone(await lost)
3159+
3160+
self.loop.run_until_complete(run())
3161+
self.assertTrue(pipeobj.closed)
3162+
3163+
def check_rejected(self, connect, pipeobj):
3164+
self.addCleanup(pipeobj.close)
3165+
with self.assertRaisesRegex(ValueError, 'Pipe transport is'):
3166+
self.loop.run_until_complete(connect(asyncio.Protocol, pipeobj))
3167+
3168+
def test_read_pipe(self):
3169+
rfd, wfd = os.pipe()
3170+
self.addCleanup(os.close, wfd)
3171+
self.check_accepted(self.loop.connect_read_pipe, open(rfd, 'rb', 0))
3172+
3173+
def test_write_pipe(self):
3174+
rfd, wfd = os.pipe()
3175+
self.addCleanup(os.close, rfd)
3176+
self.check_accepted(self.loop.connect_write_pipe, open(wfd, 'wb', 0))
3177+
3178+
def test_read_fifo(self):
3179+
path = os_helper.TESTFN
3180+
os.mkfifo(path)
3181+
self.addCleanup(os_helper.unlink, path)
3182+
rfd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
3183+
wfd = os.open(path, os.O_WRONLY)
3184+
self.addCleanup(os.close, wfd)
3185+
self.check_accepted(self.loop.connect_read_pipe, open(rfd, 'rb', 0))
3186+
3187+
def test_write_fifo(self):
3188+
path = os_helper.TESTFN
3189+
os.mkfifo(path)
3190+
self.addCleanup(os_helper.unlink, path)
3191+
rfd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
3192+
self.addCleanup(os.close, rfd)
3193+
wfd = os.open(path, os.O_WRONLY)
3194+
self.check_accepted(self.loop.connect_write_pipe, open(wfd, 'wb', 0))
3195+
3196+
def test_read_socket(self):
3197+
rsock, wsock = socket.socketpair()
3198+
self.addCleanup(wsock.close)
3199+
self.check_accepted(self.loop.connect_read_pipe,
3200+
open(rsock.detach(), 'rb', 0))
3201+
3202+
def test_write_socket(self):
3203+
rsock, wsock = socket.socketpair()
3204+
self.addCleanup(rsock.close)
3205+
self.check_accepted(self.loop.connect_write_pipe,
3206+
open(wsock.detach(), 'wb', 0))
3207+
3208+
@unittest.skipUnless(hasattr(os, 'openpty'), 'need os.openpty()')
3209+
def test_read_character_device(self):
3210+
master, slave = os.openpty()
3211+
self.addCleanup(os.close, slave)
3212+
self.check_accepted(self.loop.connect_read_pipe, open(master, 'rb', 0))
3213+
3214+
@unittest.skipUnless(hasattr(os, 'openpty'), 'need os.openpty()')
3215+
def test_write_character_device(self):
3216+
master, slave = os.openpty()
3217+
self.addCleanup(os.close, master)
3218+
self.check_accepted(self.loop.connect_write_pipe, open(slave, 'wb', 0))
3219+
3220+
def test_read_regular_file(self):
3221+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
3222+
with open(os_helper.TESTFN, 'wb') as f:
3223+
f.write(b'spam')
3224+
self.check_rejected(self.loop.connect_read_pipe,
3225+
open(os_helper.TESTFN, 'rb', 0))
3226+
3227+
def test_write_regular_file(self):
3228+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
3229+
self.check_rejected(self.loop.connect_write_pipe,
3230+
open(os_helper.TESTFN, 'wb', 0))
3231+
3232+
31373233
if __name__ == '__main__':
31383234
unittest.main()

Lib/test/test_asyncio/test_windows_events.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
import asyncio
1717
from asyncio import windows_events
18+
from asyncio import windows_utils
19+
from test.support import os_helper
1820
from test.test_asyncio import utils as test_utils
1921

2022

@@ -324,5 +326,89 @@ def threadMain():
324326
thr.join()
325327

326328

329+
class ProactorPipeObjectSupportTests(unittest.TestCase):
330+
331+
def setUp(self):
332+
super().setUp()
333+
self.loop = asyncio.ProactorEventLoop()
334+
self.addCleanup(self.loop.close)
335+
self.errors = []
336+
self.loop.set_exception_handler(
337+
lambda loop, context: self.errors.append(context))
338+
339+
def check_read_rejected(self, pipe):
340+
self.addCleanup(pipe.close)
341+
lost = self.loop.create_future()
342+
343+
class Proto(asyncio.Protocol):
344+
def connection_lost(self, exc):
345+
if not lost.done():
346+
lost.set_result(exc)
347+
348+
async def run():
349+
transport, _ = await self.loop.connect_read_pipe(Proto, pipe)
350+
await lost
351+
transport.close()
352+
353+
self.loop.run_until_complete(run())
354+
self.assertTrue(self.errors, 'no error reached the exception handler')
355+
self.assertIsInstance(self.errors[0]['exception'], OSError)
356+
357+
def check_write_rejected(self, pipe):
358+
self.addCleanup(pipe.close)
359+
with self.assertRaises(OSError):
360+
self.loop.run_until_complete(
361+
self.loop.connect_write_pipe(asyncio.BaseProtocol, pipe))
362+
363+
def test_overlapped_pipe(self):
364+
rhandle, whandle = windows_utils.pipe(overlapped=(True, True))
365+
rpipe = windows_utils.PipeHandle(rhandle)
366+
wpipe = windows_utils.PipeHandle(whandle)
367+
368+
chunks = []
369+
lost = self.loop.create_future()
370+
371+
class ReadProto(asyncio.Protocol):
372+
def data_received(self, data):
373+
chunks.append(data)
374+
375+
def connection_lost(self, exc):
376+
if not lost.done():
377+
lost.set_result(exc)
378+
379+
async def run():
380+
rtransport, _ = await self.loop.connect_read_pipe(ReadProto, rpipe)
381+
wtransport, _ = await self.loop.connect_write_pipe(
382+
asyncio.BaseProtocol, wpipe)
383+
wtransport.write(b'spam')
384+
wtransport.close()
385+
await lost
386+
rtransport.close()
387+
388+
self.loop.run_until_complete(run())
389+
self.assertEqual(b''.join(chunks), b'spam')
390+
self.assertFalse(self.errors)
391+
392+
def test_read_non_overlapped_pipe(self):
393+
rhandle, whandle = windows_utils.pipe(overlapped=(False, False))
394+
self.addCleanup(windows_utils.PipeHandle(whandle).close)
395+
self.check_read_rejected(windows_utils.PipeHandle(rhandle))
396+
397+
def test_write_non_overlapped_pipe(self):
398+
rhandle, whandle = windows_utils.pipe(overlapped=(False, False))
399+
self.addCleanup(windows_utils.PipeHandle(rhandle).close)
400+
self.check_write_rejected(windows_utils.PipeHandle(whandle))
401+
402+
def test_read_regular_file(self):
403+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
404+
with open(os_helper.TESTFN, 'wb') as f:
405+
f.write(b'spam')
406+
self.check_read_rejected(open(os_helper.TESTFN, 'rb', 0))
407+
408+
def test_write_regular_file(self):
409+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
410+
self.check_write_rejected(open(os_helper.TESTFN, 'wb', 0))
411+
412+
327413
if __name__ == '__main__':
328414
unittest.main()

0 commit comments

Comments
 (0)