mitogen/tests/poller_test.py

422 lines
13 KiB
Python

import errno
import os
import select
import socket
import sys
import unittest
import mitogen.core
import mitogen.parent
from mitogen.core import next
import testlib
class SockMixin(object):
def tearDown(self):
self.close_socks()
super(SockMixin, self).tearDown()
def setUp(self):
super(SockMixin, self).setUp()
self._setup_socks()
def _setup_socks(self):
# "left" and "right" side of two socket pairs. We use sockets instead
# of pipes since the same process can manipulate transmit/receive
# buffers on both sides (bidirectional IO), making it easier to test
# combinations of readability/writeability on the one side of a single
# file object.
self.l1_sock, self.r1_sock = socket.socketpair()
self.l1 = self.l1_sock.fileno()
self.r1 = self.r1_sock.fileno()
self.l2_sock, self.r2_sock = socket.socketpair()
self.l2 = self.l2_sock.fileno()
self.r2 = self.r2_sock.fileno()
for fp in self.l1, self.r1, self.l2, self.r2:
mitogen.core.set_nonblock(fp)
def fill(self, fd):
"""Make `fd` unwriteable."""
while True:
try:
os.write(fd, mitogen.core.b('x')*4096)
except OSError:
e = sys.exc_info()[1]
if e.args[0] == errno.EAGAIN:
return
raise
def drain(self, fd):
"""Make `fd` unreadable."""
while True:
try:
if not os.read(fd, 4096):
return
except OSError:
e = sys.exc_info()[1]
if e.args[0] == errno.EAGAIN:
return
raise
def close_socks(self):
for sock in self.l1_sock, self.r1_sock, self.l2_sock, self.r2_sock:
sock.close()
class PollerMixin(object):
klass = None
def setUp(self):
super(PollerMixin, self).setUp()
self.p = self.klass()
def tearDown(self):
self.p.close()
super(PollerMixin, self).tearDown()
class ReceiveStateMixin(PollerMixin, SockMixin):
def test_start_receive_adds_reader(self):
self.p.start_receive(self.l1)
self.assertEqual([(self.l1, self.l1)], self.p.readers)
self.assertEqual([], self.p.writers)
def test_start_receive_adds_reader_data(self):
data = object()
self.p.start_receive(self.l1, data=data)
self.assertEqual([(self.l1, data)], self.p.readers)
self.assertEqual([], self.p.writers)
def test_stop_receive(self):
self.p.start_receive(self.l1)
self.p.stop_receive(self.l1)
self.assertEqual([], self.p.readers)
self.assertEqual([], self.p.writers)
def test_stop_receive_dup(self):
self.p.start_receive(self.l1)
self.p.stop_receive(self.l1)
self.assertEqual([], self.p.readers)
self.assertEqual([], self.p.writers)
self.p.stop_receive(self.l1)
self.assertEqual([], self.p.readers)
self.assertEqual([], self.p.writers)
def test_stop_receive_noexist(self):
p = self.klass()
p.stop_receive(123) # should not fail
self.assertEqual([], p.readers)
self.assertEqual([], self.p.writers)
class TransmitStateMixin(PollerMixin, SockMixin):
def test_start_transmit_adds_writer(self):
self.p.start_transmit(self.r1)
self.assertEqual([], self.p.readers)
self.assertEqual([(self.r1, self.r1)], self.p.writers)
def test_start_transmit_adds_writer_data(self):
data = object()
self.p.start_transmit(self.r1, data=data)
self.assertEqual([], self.p.readers)
self.assertEqual([(self.r1, data)], self.p.writers)
def test_stop_transmit(self):
self.p.start_transmit(self.r1)
self.p.stop_transmit(self.r1)
self.assertEqual([], self.p.readers)
self.assertEqual([], self.p.writers)
def test_stop_transmit_dup(self):
self.p.start_transmit(self.r1)
self.p.stop_transmit(self.r1)
self.assertEqual([], self.p.readers)
self.assertEqual([], self.p.writers)
self.p.stop_transmit(self.r1)
self.assertEqual([], self.p.readers)
self.assertEqual([], self.p.writers)
def test_stop_transmit_noexist(self):
p = self.klass()
p.stop_receive(123) # should not fail
self.assertEqual([], p.readers)
self.assertEqual([], self.p.writers)
class CloseMixin(PollerMixin):
def test_single_close(self):
self.p.close()
def test_double_close(self):
self.p.close()
self.p.close()
class PollMixin(PollerMixin):
def test_empty_zero_timeout(self):
t0 = mitogen.core.now()
self.assertEqual([], list(self.p.poll(0)))
self.assertLess((mitogen.core.now() - t0), .1) # vaguely reasonable
def test_empty_small_timeout(self):
t0 = mitogen.core.now()
self.assertEqual([], list(self.p.poll(.2)))
self.assertGreaterEqual((mitogen.core.now() - t0), .2)
class ReadableMixin(PollerMixin, SockMixin):
def test_unreadable(self):
self.p.start_receive(self.l1)
self.assertEqual([], list(self.p.poll(0)))
def test_readable_before_add(self):
self.fill(self.r1)
self.p.start_receive(self.l1)
self.assertEqual([self.l1], list(self.p.poll(0)))
def test_readable_after_add(self):
self.p.start_receive(self.l1)
self.fill(self.r1)
self.assertEqual([self.l1], list(self.p.poll(0)))
def test_readable_then_unreadable(self):
self.fill(self.r1)
self.p.start_receive(self.l1)
self.assertEqual([self.l1], list(self.p.poll(0)))
self.drain(self.l1)
self.assertEqual([], list(self.p.poll(0)))
def test_readable_data(self):
data = object()
self.fill(self.r1)
self.p.start_receive(self.l1, data=data)
self.assertEqual([data], list(self.p.poll(0)))
def test_double_readable_data(self):
data1 = object()
data2 = object()
self.fill(self.r1)
self.p.start_receive(self.l1, data=data1)
self.fill(self.r2)
self.p.start_receive(self.l2, data=data2)
self.assertEqual(set([data1, data2]), set(self.p.poll(0)))
class WriteableMixin(PollerMixin, SockMixin):
def test_writeable(self):
self.p.start_transmit(self.r1)
self.assertEqual([self.r1], list(self.p.poll(0)))
def test_writeable_data(self):
data = object()
self.p.start_transmit(self.r1, data=data)
self.assertEqual([data], list(self.p.poll(0)))
def test_unwriteable_before_add(self):
self.fill(self.r1)
self.p.start_transmit(self.r1)
self.assertEqual([], list(self.p.poll(0)))
def test_unwriteable_after_add(self):
self.p.start_transmit(self.r1)
self.fill(self.r1)
self.assertEqual([], list(self.p.poll(0)))
def test_unwriteable_then_writeable(self):
self.fill(self.r1)
self.p.start_transmit(self.r1)
self.assertEqual([], list(self.p.poll(0)))
self.drain(self.l1)
self.assertEqual([self.r1], list(self.p.poll(0)))
def test_double_unwriteable_then_Writeable(self):
self.fill(self.r1)
self.p.start_transmit(self.r1)
self.fill(self.r2)
self.p.start_transmit(self.r2)
self.assertEqual([], list(self.p.poll(0)))
self.drain(self.l1)
self.assertEqual([self.r1], list(self.p.poll(0)))
self.drain(self.l2)
self.assertEqual(set([self.r1, self.r2]), set(self.p.poll(0)))
class MutateDuringYieldMixin(PollerMixin, SockMixin):
# verify behaviour when poller contents is modified in the middle of
# poll() output generation.
def test_one_readable_removed_before_yield(self):
self.fill(self.l1)
self.p.start_receive(self.r1)
p = self.p.poll(0)
self.p.stop_receive(self.r1)
self.assertEqual([], list(p))
def test_one_writeable_removed_before_yield(self):
self.p.start_transmit(self.r1)
p = self.p.poll(0)
self.p.stop_transmit(self.r1)
self.assertEqual([], list(p))
def test_one_readable_readded_before_yield(self):
# fd removed, closed, another fd opened, gets same fd number, re-added.
# event fires for wrong underlying object.
self.fill(self.l1)
self.p.start_receive(self.r1)
p = self.p.poll(0)
self.p.stop_receive(self.r1)
self.p.start_receive(self.r1)
self.assertEqual([], list(p))
def test_one_readable_readded_during_yield(self):
self.fill(self.l1)
self.p.start_receive(self.r1)
self.fill(self.l2)
self.p.start_receive(self.r2)
p = self.p.poll(0)
# figure out which one is consumed and which is still to-read.
consumed = next(p)
ready = (self.r1, self.r2)[consumed == self.r1]
# now remove and re-add the one that hasn't been read yet.
self.p.stop_receive(ready)
self.p.start_receive(ready)
# the start_receive() may be for a totally new underlying file object,
# the live loop iteration must not yield any buffered readiness event.
self.assertEqual([], list(p))
class FileClosedMixin(PollerMixin, SockMixin):
# Verify behaviour when a registered file object is closed in various
# scenarios, without first calling stop_receive()/stop_transmit().
def test_writeable_then_closed(self):
self.p.start_transmit(self.r1)
self.assertEqual([self.r1], list(self.p.poll(0)))
self.close_socks()
try:
self.assertEqual([], list(self.p.poll(0)))
except select.error:
# a crash is also reasonable here.
pass
def test_writeable_closed_before_yield(self):
self.p.start_transmit(self.r1)
p = self.p.poll(0)
self.close_socks()
try:
self.assertEqual([], list(p))
except select.error:
# a crash is also reasonable here.
pass
def test_readable_then_closed(self):
self.fill(self.l1)
self.p.start_receive(self.r1)
self.assertEqual([self.r1], list(self.p.poll(0)))
self.close_socks()
try:
self.assertEqual([], list(self.p.poll(0)))
except select.error:
# a crash is also reasonable here.
pass
def test_readable_closed_before_yield(self):
self.fill(self.l1)
self.p.start_receive(self.r1)
p = self.p.poll(0)
self.close_socks()
try:
self.assertEqual([], list(p))
except select.error:
# a crash is also reasonable here.
pass
class TtyHangupMixin(PollerMixin):
def test_tty_hangup_detected(self):
# bug in initial select.poll() implementation failed to detect POLLHUP.
master_fp, slave_fp = mitogen.parent.openpty()
try:
self.p.start_receive(master_fp.fileno())
self.assertEqual([], list(self.p.poll(0)))
slave_fp.close()
slave_fp = None
self.assertEqual([master_fp.fileno()], list(self.p.poll(0)))
finally:
if slave_fp is not None:
slave_fp.close()
master_fp.close()
class DistinctDataMixin(PollerMixin, SockMixin):
# Verify different data is yielded for the same FD according to the event
# being raised.
def test_one_distinct(self):
rdata = object()
wdata = object()
self.p.start_receive(self.r1, data=rdata)
self.p.start_transmit(self.r1, data=wdata)
self.assertEqual([wdata], list(self.p.poll(0)))
self.fill(self.l1) # r1 is now readable and writeable.
self.assertEqual(set([rdata, wdata]), set(self.p.poll(0)))
class AllMixin(ReceiveStateMixin,
TransmitStateMixin,
ReadableMixin,
WriteableMixin,
MutateDuringYieldMixin,
FileClosedMixin,
DistinctDataMixin,
PollMixin,
TtyHangupMixin,
CloseMixin):
"""
Helper to avoid cutpasting mixin names below.
"""
class CorePollerTest(AllMixin, testlib.TestCase):
klass = mitogen.core.Poller
class PollTest(AllMixin, testlib.TestCase):
klass = mitogen.parent.PollPoller
PollTest = unittest.skipIf(
condition=(not PollTest.klass.SUPPORTED),
reason='select.poll() not available',
)(PollTest)
class KqueueTest(AllMixin, testlib.TestCase):
klass = mitogen.parent.KqueuePoller
KqueueTest = unittest.skipIf(
condition=(not KqueueTest.klass.SUPPORTED),
reason='select.kqueue() not available',
)(KqueueTest)
class EpollTest(AllMixin, testlib.TestCase):
klass = mitogen.parent.EpollPoller
EpollTest = unittest.skipIf(
condition=(not EpollTest.klass.SUPPORTED),
reason='select.epoll() not available',
)(EpollTest)