Permalink
Browse files

Merge pull request #723 from ajdavis/gen-test-timeout

Make gen_test's timeout configurable
  • Loading branch information...
2 parents d80fa56 + 62dde35 commit 0d44009cdc165fc3810945c59db5743bc5e809d3 @bdarnell bdarnell committed Apr 18, 2013
Showing with 133 additions and 10 deletions.
  1. +82 −1 tornado/test/testing_test.py
  2. +51 −9 tornado/testing.py
@@ -2,10 +2,27 @@
from __future__ import absolute_import, division, print_function, with_statement
-from tornado import gen
+from tornado import gen, ioloop
from tornado.testing import AsyncTestCase, gen_test
from tornado.test.util import unittest
+import contextlib
+import os
+
+
+@contextlib.contextmanager
+def set_environ(name, value):
+ old_value = os.environ.get('name')
+ os.environ[name] = value
+
+ try:
+ yield
+ finally:
+ if old_value is None:
+ del os.environ[name]
+ else:
+ os.environ[name] = old_value
+
class AsyncTestCaseTest(AsyncTestCase):
def test_exception_in_callback(self):
@@ -16,6 +33,24 @@ def test_exception_in_callback(self):
except ZeroDivisionError:
pass
+ def test_wait_timeout(self):
+ time = self.io_loop.time
+
+ # Accept default 5-second timeout, no error
+ self.io_loop.add_timeout(time() + 0.01, self.stop)
+ self.wait()
+
+ # Timeout passed to wait()
+ self.io_loop.add_timeout(time() + 1, self.stop)
+ with self.assertRaises(self.failureException):
+ self.wait(timeout=0.01)
+
+ # Timeout set with environment variable
+ self.io_loop.add_timeout(time() + 1, self.stop)
+ with set_environ('ASYNC_TEST_TIMEOUT', '0.01'):
+ with self.assertRaises(self.failureException):
+ self.wait()
+
def test_subsequent_wait_calls(self):
"""
This test makes sure that a second call to wait()
@@ -74,5 +109,51 @@ def test_async(self):
yield gen.Task(self.io_loop.add_callback)
self.finished = True
+ def test_timeout(self):
+ # Set a short timeout and exceed it.
+ @gen_test(timeout=0.1)
+ def test(self):
+ yield gen.Task(self.io_loop.add_timeout, self.io_loop.time() + 1)
+
+ with self.assertRaises(ioloop.TimeoutError):
+ test(self)
+
+ self.finished = True
+
+ def test_no_timeout(self):
+ # A test that does not exceed its timeout should succeed.
+ @gen_test(timeout=1)
+ def test(self):
+ time = self.io_loop.time
+ yield gen.Task(self.io_loop.add_timeout, time() + 0.1)
+
+ test(self)
+ self.finished = True
+
+ def test_timeout_environment_variable(self):
+ @gen_test(timeout=0.5)
+ def test_long_timeout(self):
+ time = self.io_loop.time
+ yield gen.Task(self.io_loop.add_timeout, time() + 0.25)
+
+ # Uses provided timeout of 0.5 seconds, doesn't time out.
+ with set_environ('ASYNC_TEST_TIMEOUT', '0.1'):
+ test_long_timeout(self)
+
+ self.finished = True
+
+ def test_no_timeout_environment_variable(self):
+ @gen_test(timeout=0.01)
+ def test_short_timeout(self):
+ time = self.io_loop.time
+ yield gen.Task(self.io_loop.add_timeout, time() + 1)
+
+ # Uses environment-variable timeout of 0.1, times out.
+ with set_environ('ASYNC_TEST_TIMEOUT', '0.1'):
+ with self.assertRaises(ioloop.TimeoutError):
+ test_short_timeout(self)
+
+ self.finished = True
+
if __name__ == '__main__':
unittest.main()
View
@@ -82,6 +82,17 @@ def bind_unused_port():
return sock, port
+def get_async_test_timeout():
+ """Get the global timeout setting for async tests.
+
+ Returns a float, the timeout in seconds.
+ """
+ try:
+ return float(os.environ.get('ASYNC_TEST_TIMEOUT'))
+ except (ValueError, TypeError):
+ return 5
+
+
class AsyncTestCase(unittest.TestCase):
"""`~unittest.TestCase` subclass for testing `.IOLoop`-based
asynchronous code.
@@ -202,14 +213,19 @@ def stop(self, _arg=None, **kwargs):
self.__running = False
self.__stopped = True
- def wait(self, condition=None, timeout=5):
+ def wait(self, condition=None, timeout=None):
"""Runs the `.IOLoop` until stop is called or timeout has passed.
- In the event of a timeout, an exception will be thrown.
+ In the event of a timeout, an exception will be thrown. The default
+ timeout is 5 seconds; it may be overridden with a ``timeout`` keyword
+ argument or globally with the ASYNC_TEST_TIMEOUT environment variable.
If ``condition`` is not None, the `.IOLoop` will be restarted
after `stop()` until ``condition()`` returns true.
"""
+ if timeout is None:
+ timeout = get_async_test_timeout()
+
if not self.__stopped:
if timeout:
def timeout_func():
@@ -354,7 +370,7 @@ def get_protocol(self):
return 'https'
-def gen_test(f):
+def gen_test(func=None, timeout=None):
"""Testing equivalent of ``@gen.coroutine``, to be applied to test methods.
``@gen.coroutine`` cannot be used on tests because the `.IOLoop` is not
@@ -368,13 +384,39 @@ class MyTest(AsyncHTTPTestCase):
def test_something(self):
response = yield gen.Task(self.fetch('/'))
- """
- f = gen.coroutine(f)
+ By default, ``@gen_test`` times out after 5 seconds. The timeout may be
+ overridden globally with the ASYNC_TEST_TIMEOUT environment variable,
+ or for each test with the ``timeout`` keyword argument:
- @functools.wraps(f)
- def wrapper(self):
- return self.io_loop.run_sync(functools.partial(f, self), timeout=5)
- return wrapper
+ class MyTest(AsyncHTTPTestCase):
+ @gen_test(timeout=10)
+ def test_something_slow(self):
+ response = yield gen.Task(self.fetch('/'))
+
+ If both the environment variable and the parameter are set, ``gen_test``
+ uses the maximum of the two.
+ """
+ if timeout is None:
+ timeout = get_async_test_timeout()
+
+ def wrap(f):
+ f = gen.coroutine(f)
+
+ @functools.wraps(f)
+ def wrapper(self):
+ return self.io_loop.run_sync(
+ functools.partial(f, self), timeout=timeout)
+ return wrapper
+
+ if func is not None:
+ # Used like:
+ # @gen_test
+ # def f(self):
+ # pass
+ return wrap(func)
+ else:
+ # Used like @gen_test(timeout=10)
+ return wrap
# Without this attribute, nosetests will try to run gen_test as a test

0 comments on commit 0d44009

Please sign in to comment.