334 lines
12 KiB
Python
334 lines
12 KiB
Python
#!/usr/bin/env python
|
|
|
|
|
|
from __future__ import absolute_import, division, print_function, with_statement
|
|
import contextlib
|
|
import datetime
|
|
import functools
|
|
import socket
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
from tornado import gen
|
|
from tornado.ioloop import IOLoop, TimeoutError
|
|
from tornado.stack_context import ExceptionStackContext, StackContext, wrap, NullContext
|
|
from tornado.testing import AsyncTestCase, bind_unused_port
|
|
from tornado.test.util import unittest, skipIfNonUnix, skipOnTravis
|
|
|
|
try:
|
|
from concurrent import futures
|
|
except ImportError:
|
|
futures = None
|
|
|
|
|
|
class TestIOLoop(AsyncTestCase):
|
|
@skipOnTravis
|
|
def test_add_callback_wakeup(self):
|
|
# Make sure that add_callback from inside a running IOLoop
|
|
# wakes up the IOLoop immediately instead of waiting for a timeout.
|
|
def callback():
|
|
self.called = True
|
|
self.stop()
|
|
|
|
def schedule_callback():
|
|
self.called = False
|
|
self.io_loop.add_callback(callback)
|
|
# Store away the time so we can check if we woke up immediately
|
|
self.start_time = time.time()
|
|
self.io_loop.add_timeout(self.io_loop.time(), schedule_callback)
|
|
self.wait()
|
|
self.assertAlmostEqual(time.time(), self.start_time, places=2)
|
|
self.assertTrue(self.called)
|
|
|
|
@skipOnTravis
|
|
def test_add_callback_wakeup_other_thread(self):
|
|
def target():
|
|
# sleep a bit to let the ioloop go into its poll loop
|
|
time.sleep(0.01)
|
|
self.stop_time = time.time()
|
|
self.io_loop.add_callback(self.stop)
|
|
thread = threading.Thread(target=target)
|
|
self.io_loop.add_callback(thread.start)
|
|
self.wait()
|
|
self.assertAlmostEqual(time.time(), self.stop_time, places=2)
|
|
thread.join()
|
|
|
|
def test_add_timeout_timedelta(self):
|
|
self.io_loop.add_timeout(datetime.timedelta(microseconds=1), self.stop)
|
|
self.wait()
|
|
|
|
def test_multiple_add(self):
|
|
sock, port = bind_unused_port()
|
|
try:
|
|
self.io_loop.add_handler(sock.fileno(), lambda fd, events: None,
|
|
IOLoop.READ)
|
|
# Attempting to add the same handler twice fails
|
|
# (with a platform-dependent exception)
|
|
self.assertRaises(Exception, self.io_loop.add_handler,
|
|
sock.fileno(), lambda fd, events: None,
|
|
IOLoop.READ)
|
|
finally:
|
|
self.io_loop.remove_handler(sock.fileno())
|
|
sock.close()
|
|
|
|
def test_remove_without_add(self):
|
|
# remove_handler should not throw an exception if called on an fd
|
|
# was never added.
|
|
sock, port = bind_unused_port()
|
|
try:
|
|
self.io_loop.remove_handler(sock.fileno())
|
|
finally:
|
|
sock.close()
|
|
|
|
def test_add_callback_from_signal(self):
|
|
# cheat a little bit and just run this normally, since we can't
|
|
# easily simulate the races that happen with real signal handlers
|
|
self.io_loop.add_callback_from_signal(self.stop)
|
|
self.wait()
|
|
|
|
def test_add_callback_from_signal_other_thread(self):
|
|
# Very crude test, just to make sure that we cover this case.
|
|
# This also happens to be the first test where we run an IOLoop in
|
|
# a non-main thread.
|
|
other_ioloop = IOLoop()
|
|
thread = threading.Thread(target=other_ioloop.start)
|
|
thread.start()
|
|
other_ioloop.add_callback_from_signal(other_ioloop.stop)
|
|
thread.join()
|
|
other_ioloop.close()
|
|
|
|
def test_add_callback_while_closing(self):
|
|
# Issue #635: add_callback() should raise a clean exception
|
|
# if called while another thread is closing the IOLoop.
|
|
closing = threading.Event()
|
|
|
|
def target():
|
|
other_ioloop.add_callback(other_ioloop.stop)
|
|
other_ioloop.start()
|
|
closing.set()
|
|
other_ioloop.close(all_fds=True)
|
|
other_ioloop = IOLoop()
|
|
thread = threading.Thread(target=target)
|
|
thread.start()
|
|
closing.wait()
|
|
for i in range(1000):
|
|
try:
|
|
other_ioloop.add_callback(lambda: None)
|
|
except RuntimeError as e:
|
|
self.assertEqual("IOLoop is closing", str(e))
|
|
break
|
|
|
|
def test_handle_callback_exception(self):
|
|
# IOLoop.handle_callback_exception can be overridden to catch
|
|
# exceptions in callbacks.
|
|
def handle_callback_exception(callback):
|
|
self.assertIs(sys.exc_info()[0], ZeroDivisionError)
|
|
self.stop()
|
|
self.io_loop.handle_callback_exception = handle_callback_exception
|
|
with NullContext():
|
|
# remove the test StackContext that would see this uncaught
|
|
# exception as a test failure.
|
|
self.io_loop.add_callback(lambda: 1 / 0)
|
|
self.wait()
|
|
|
|
@skipIfNonUnix # just because socketpair is so convenient
|
|
def test_read_while_writeable(self):
|
|
# Ensure that write events don't come in while we're waiting for
|
|
# a read and haven't asked for writeability. (the reverse is
|
|
# difficult to test for)
|
|
client, server = socket.socketpair()
|
|
try:
|
|
def handler(fd, events):
|
|
self.assertEqual(events, IOLoop.READ)
|
|
self.stop()
|
|
self.io_loop.add_handler(client.fileno(), handler, IOLoop.READ)
|
|
self.io_loop.add_timeout(self.io_loop.time() + 0.01,
|
|
functools.partial(server.send, b'asdf'))
|
|
self.wait()
|
|
self.io_loop.remove_handler(client.fileno())
|
|
finally:
|
|
client.close()
|
|
server.close()
|
|
|
|
def test_remove_timeout_after_fire(self):
|
|
# It is not an error to call remove_timeout after it has run.
|
|
handle = self.io_loop.add_timeout(self.io_loop.time(), self.stop())
|
|
self.wait()
|
|
self.io_loop.remove_timeout(handle)
|
|
|
|
def test_remove_timeout_cleanup(self):
|
|
# Add and remove enough callbacks to trigger cleanup.
|
|
# Not a very thorough test, but it ensures that the cleanup code
|
|
# gets executed and doesn't blow up. This test is only really useful
|
|
# on PollIOLoop subclasses, but it should run silently on any
|
|
# implementation.
|
|
for i in range(2000):
|
|
timeout = self.io_loop.add_timeout(self.io_loop.time() + 3600,
|
|
lambda: None)
|
|
self.io_loop.remove_timeout(timeout)
|
|
# HACK: wait two IOLoop iterations for the GC to happen.
|
|
self.io_loop.add_callback(lambda: self.io_loop.add_callback(self.stop))
|
|
self.wait()
|
|
|
|
|
|
# Deliberately not a subclass of AsyncTestCase so the IOLoop isn't
|
|
# automatically set as current.
|
|
class TestIOLoopCurrent(unittest.TestCase):
|
|
def setUp(self):
|
|
self.io_loop = IOLoop()
|
|
|
|
def tearDown(self):
|
|
self.io_loop.close()
|
|
|
|
def test_current(self):
|
|
def f():
|
|
self.current_io_loop = IOLoop.current()
|
|
self.io_loop.stop()
|
|
self.io_loop.add_callback(f)
|
|
self.io_loop.start()
|
|
self.assertIs(self.current_io_loop, self.io_loop)
|
|
|
|
|
|
class TestIOLoopAddCallback(AsyncTestCase):
|
|
def setUp(self):
|
|
super(TestIOLoopAddCallback, self).setUp()
|
|
self.active_contexts = []
|
|
|
|
def add_callback(self, callback, *args, **kwargs):
|
|
self.io_loop.add_callback(callback, *args, **kwargs)
|
|
|
|
@contextlib.contextmanager
|
|
def context(self, name):
|
|
self.active_contexts.append(name)
|
|
yield
|
|
self.assertEqual(self.active_contexts.pop(), name)
|
|
|
|
def test_pre_wrap(self):
|
|
# A pre-wrapped callback is run in the context in which it was
|
|
# wrapped, not when it was added to the IOLoop.
|
|
def f1():
|
|
self.assertIn('c1', self.active_contexts)
|
|
self.assertNotIn('c2', self.active_contexts)
|
|
self.stop()
|
|
|
|
with StackContext(functools.partial(self.context, 'c1')):
|
|
wrapped = wrap(f1)
|
|
|
|
with StackContext(functools.partial(self.context, 'c2')):
|
|
self.add_callback(wrapped)
|
|
|
|
self.wait()
|
|
|
|
def test_pre_wrap_with_args(self):
|
|
# Same as test_pre_wrap, but the function takes arguments.
|
|
# Implementation note: The function must not be wrapped in a
|
|
# functools.partial until after it has been passed through
|
|
# stack_context.wrap
|
|
def f1(foo, bar):
|
|
self.assertIn('c1', self.active_contexts)
|
|
self.assertNotIn('c2', self.active_contexts)
|
|
self.stop((foo, bar))
|
|
|
|
with StackContext(functools.partial(self.context, 'c1')):
|
|
wrapped = wrap(f1)
|
|
|
|
with StackContext(functools.partial(self.context, 'c2')):
|
|
self.add_callback(wrapped, 1, bar=2)
|
|
|
|
result = self.wait()
|
|
self.assertEqual(result, (1, 2))
|
|
|
|
|
|
class TestIOLoopAddCallbackFromSignal(TestIOLoopAddCallback):
|
|
# Repeat the add_callback tests using add_callback_from_signal
|
|
def add_callback(self, callback, *args, **kwargs):
|
|
self.io_loop.add_callback_from_signal(callback, *args, **kwargs)
|
|
|
|
|
|
@unittest.skipIf(futures is None, "futures module not present")
|
|
class TestIOLoopFutures(AsyncTestCase):
|
|
def test_add_future_threads(self):
|
|
with futures.ThreadPoolExecutor(1) as pool:
|
|
self.io_loop.add_future(pool.submit(lambda: None),
|
|
lambda future: self.stop(future))
|
|
future = self.wait()
|
|
self.assertTrue(future.done())
|
|
self.assertTrue(future.result() is None)
|
|
|
|
def test_add_future_stack_context(self):
|
|
ready = threading.Event()
|
|
|
|
def task():
|
|
# we must wait for the ioloop callback to be scheduled before
|
|
# the task completes to ensure that add_future adds the callback
|
|
# asynchronously (which is the scenario in which capturing
|
|
# the stack_context matters)
|
|
ready.wait(1)
|
|
assert ready.isSet(), "timed out"
|
|
raise Exception("worker")
|
|
|
|
def callback(future):
|
|
self.future = future
|
|
raise Exception("callback")
|
|
|
|
def handle_exception(typ, value, traceback):
|
|
self.exception = value
|
|
self.stop()
|
|
return True
|
|
|
|
# stack_context propagates to the ioloop callback, but the worker
|
|
# task just has its exceptions caught and saved in the Future.
|
|
with futures.ThreadPoolExecutor(1) as pool:
|
|
with ExceptionStackContext(handle_exception):
|
|
self.io_loop.add_future(pool.submit(task), callback)
|
|
ready.set()
|
|
self.wait()
|
|
|
|
self.assertEqual(self.exception.args[0], "callback")
|
|
self.assertEqual(self.future.exception().args[0], "worker")
|
|
|
|
|
|
class TestIOLoopRunSync(unittest.TestCase):
|
|
def setUp(self):
|
|
self.io_loop = IOLoop()
|
|
|
|
def tearDown(self):
|
|
self.io_loop.close()
|
|
|
|
def test_sync_result(self):
|
|
self.assertEqual(self.io_loop.run_sync(lambda: 42), 42)
|
|
|
|
def test_sync_exception(self):
|
|
with self.assertRaises(ZeroDivisionError):
|
|
self.io_loop.run_sync(lambda: 1 / 0)
|
|
|
|
def test_async_result(self):
|
|
@gen.coroutine
|
|
def f():
|
|
yield gen.Task(self.io_loop.add_callback)
|
|
raise gen.Return(42)
|
|
self.assertEqual(self.io_loop.run_sync(f), 42)
|
|
|
|
def test_async_exception(self):
|
|
@gen.coroutine
|
|
def f():
|
|
yield gen.Task(self.io_loop.add_callback)
|
|
1 / 0
|
|
with self.assertRaises(ZeroDivisionError):
|
|
self.io_loop.run_sync(f)
|
|
|
|
def test_current(self):
|
|
def f():
|
|
self.assertIs(IOLoop.current(), self.io_loop)
|
|
self.io_loop.run_sync(f)
|
|
|
|
def test_timeout(self):
|
|
@gen.coroutine
|
|
def f():
|
|
yield gen.Task(self.io_loop.add_timeout, self.io_loop.time() + 1)
|
|
self.assertRaises(TimeoutError, self.io_loop.run_sync, f, timeout=0.01)
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|