00001
00002 """Support classes for automated testing.
00003
00004 This module contains three parts:
00005
00006 * `AsyncTestCase`/`AsyncHTTPTestCase`: Subclasses of unittest.TestCase
00007 with additional support for testing asynchronous (IOLoop-based) code.
00008
00009 * `LogTrapTestCase`: Subclass of unittest.TestCase that discards log output
00010 from tests that pass and only produces output for failing tests.
00011
00012 * `main()`: A simple test runner (wrapper around unittest.main()) with support
00013 for the tornado.autoreload module to rerun the tests when code changes.
00014
00015 These components may be used together or independently. In particular,
00016 it is safe to combine AsyncTestCase and LogTrapTestCase via multiple
00017 inheritance. See the docstrings for each class/function below for more
00018 information.
00019 """
00020
00021 from __future__ import absolute_import, division, with_statement
00022
00023 from cStringIO import StringIO
00024 try:
00025 from tornado.httpclient import AsyncHTTPClient
00026 from tornado.httpserver import HTTPServer
00027 from tornado.ioloop import IOLoop
00028 except ImportError:
00029
00030
00031 AsyncHTTPClient = None
00032 HTTPServer = None
00033 IOLoop = None
00034 from tornado.stack_context import StackContext, NullContext
00035 from tornado.util import raise_exc_info
00036 import contextlib
00037 import logging
00038 import signal
00039 import sys
00040 import time
00041 import unittest
00042
00043 _next_port = 10000
00044
00045
00046 def get_unused_port():
00047 """Returns a (hopefully) unused port number."""
00048 global _next_port
00049 port = _next_port
00050 _next_port = _next_port + 1
00051 return port
00052
00053
00054 class AsyncTestCase(unittest.TestCase):
00055 """TestCase subclass for testing IOLoop-based asynchronous code.
00056
00057 The unittest framework is synchronous, so the test must be complete
00058 by the time the test method returns. This method provides the stop()
00059 and wait() methods for this purpose. The test method itself must call
00060 self.wait(), and asynchronous callbacks should call self.stop() to signal
00061 completion.
00062
00063 By default, a new IOLoop is constructed for each test and is available
00064 as self.io_loop. This IOLoop should be used in the construction of
00065 HTTP clients/servers, etc. If the code being tested requires a
00066 global IOLoop, subclasses should override get_new_ioloop to return it.
00067
00068 The IOLoop's start and stop methods should not be called directly.
00069 Instead, use self.stop self.wait. Arguments passed to self.stop are
00070 returned from self.wait. It is possible to have multiple
00071 wait/stop cycles in the same test.
00072
00073 Example::
00074
00075 # This test uses an asynchronous style similar to most async
00076 # application code.
00077 class MyTestCase(AsyncTestCase):
00078 def test_http_fetch(self):
00079 client = AsyncHTTPClient(self.io_loop)
00080 client.fetch("http://www.tornadoweb.org/", self.handle_fetch)
00081 self.wait()
00082
00083 def handle_fetch(self, response):
00084 # Test contents of response (failures and exceptions here
00085 # will cause self.wait() to throw an exception and end the
00086 # test).
00087 # Exceptions thrown here are magically propagated to
00088 # self.wait() in test_http_fetch() via stack_context.
00089 self.assertIn("FriendFeed", response.body)
00090 self.stop()
00091
00092 # This test uses the argument passing between self.stop and self.wait
00093 # for a simpler, more synchronous style.
00094 # This style is recommended over the preceding example because it
00095 # keeps the assertions in the test method itself, and is therefore
00096 # less sensitive to the subtleties of stack_context.
00097 class MyTestCase2(AsyncTestCase):
00098 def test_http_fetch(self):
00099 client = AsyncHTTPClient(self.io_loop)
00100 client.fetch("http://www.tornadoweb.org/", self.stop)
00101 response = self.wait()
00102 # Test contents of response
00103 self.assertIn("FriendFeed", response.body)
00104 """
00105 def __init__(self, *args, **kwargs):
00106 super(AsyncTestCase, self).__init__(*args, **kwargs)
00107 self.__stopped = False
00108 self.__running = False
00109 self.__failure = None
00110 self.__stop_args = None
00111 self.__timeout = None
00112
00113 def setUp(self):
00114 super(AsyncTestCase, self).setUp()
00115 self.io_loop = self.get_new_ioloop()
00116
00117 def tearDown(self):
00118 if (not IOLoop.initialized() or
00119 self.io_loop is not IOLoop.instance()):
00120
00121
00122
00123
00124 self.io_loop.close(all_fds=True)
00125 super(AsyncTestCase, self).tearDown()
00126
00127 def get_new_ioloop(self):
00128 '''Creates a new IOLoop for this test. May be overridden in
00129 subclasses for tests that require a specific IOLoop (usually
00130 the singleton).
00131 '''
00132 return IOLoop()
00133
00134 @contextlib.contextmanager
00135 def _stack_context(self):
00136 try:
00137 yield
00138 except Exception:
00139 self.__failure = sys.exc_info()
00140 self.stop()
00141
00142 def __rethrow(self):
00143 if self.__failure is not None:
00144 failure = self.__failure
00145 self.__failure = None
00146 raise_exc_info(failure)
00147
00148 def run(self, result=None):
00149 with StackContext(self._stack_context):
00150 super(AsyncTestCase, self).run(result)
00151
00152
00153 self.__rethrow()
00154
00155 def stop(self, _arg=None, **kwargs):
00156 '''Stops the ioloop, causing one pending (or future) call to wait()
00157 to return.
00158
00159 Keyword arguments or a single positional argument passed to stop() are
00160 saved and will be returned by wait().
00161 '''
00162 assert _arg is None or not kwargs
00163 self.__stop_args = kwargs or _arg
00164 if self.__running:
00165 self.io_loop.stop()
00166 self.__running = False
00167 self.__stopped = True
00168
00169 def wait(self, condition=None, timeout=5):
00170 """Runs the IOLoop until stop is called or timeout has passed.
00171
00172 In the event of a timeout, an exception will be thrown.
00173
00174 If condition is not None, the IOLoop will be restarted after stop()
00175 until condition() returns true.
00176 """
00177 if not self.__stopped:
00178 if timeout:
00179 def timeout_func():
00180 try:
00181 raise self.failureException(
00182 'Async operation timed out after %s seconds' %
00183 timeout)
00184 except Exception:
00185 self.__failure = sys.exc_info()
00186 self.stop()
00187 if self.__timeout is not None:
00188 self.io_loop.remove_timeout(self.__timeout)
00189 self.__timeout = self.io_loop.add_timeout(time.time() + timeout, timeout_func)
00190 while True:
00191 self.__running = True
00192 with NullContext():
00193
00194
00195
00196 self.io_loop.start()
00197 if (self.__failure is not None or
00198 condition is None or condition()):
00199 break
00200 assert self.__stopped
00201 self.__stopped = False
00202 self.__rethrow()
00203 result = self.__stop_args
00204 self.__stop_args = None
00205 return result
00206
00207
00208 class AsyncHTTPTestCase(AsyncTestCase):
00209 '''A test case that starts up an HTTP server.
00210
00211 Subclasses must override get_app(), which returns the
00212 tornado.web.Application (or other HTTPServer callback) to be tested.
00213 Tests will typically use the provided self.http_client to fetch
00214 URLs from this server.
00215
00216 Example::
00217
00218 class MyHTTPTest(AsyncHTTPTestCase):
00219 def get_app(self):
00220 return Application([('/', MyHandler)...])
00221
00222 def test_homepage(self):
00223 # The following two lines are equivalent to
00224 # response = self.fetch('/')
00225 # but are shown in full here to demonstrate explicit use
00226 # of self.stop and self.wait.
00227 self.http_client.fetch(self.get_url('/'), self.stop)
00228 response = self.wait()
00229 # test contents of response
00230 '''
00231 def setUp(self):
00232 super(AsyncHTTPTestCase, self).setUp()
00233 self.__port = None
00234
00235 self.http_client = AsyncHTTPClient(io_loop=self.io_loop)
00236 self._app = self.get_app()
00237 self.http_server = HTTPServer(self._app, io_loop=self.io_loop,
00238 **self.get_httpserver_options())
00239 self.http_server.listen(self.get_http_port(), address="127.0.0.1")
00240
00241 def get_app(self):
00242 """Should be overridden by subclasses to return a
00243 tornado.web.Application or other HTTPServer callback.
00244 """
00245 raise NotImplementedError()
00246
00247 def fetch(self, path, **kwargs):
00248 """Convenience method to synchronously fetch a url.
00249
00250 The given path will be appended to the local server's host and port.
00251 Any additional kwargs will be passed directly to
00252 AsyncHTTPClient.fetch (and so could be used to pass method="POST",
00253 body="...", etc).
00254 """
00255 self.http_client.fetch(self.get_url(path), self.stop, **kwargs)
00256 return self.wait()
00257
00258 def get_httpserver_options(self):
00259 """May be overridden by subclasses to return additional
00260 keyword arguments for HTTPServer.
00261 """
00262 return {}
00263
00264 def get_http_port(self):
00265 """Returns the port used by the HTTPServer.
00266
00267 A new port is chosen for each test.
00268 """
00269 if self.__port is None:
00270 self.__port = get_unused_port()
00271 return self.__port
00272
00273 def get_url(self, path):
00274 """Returns an absolute url for the given path on the test server."""
00275 return 'http://localhost:%s%s' % (self.get_http_port(), path)
00276
00277 def tearDown(self):
00278 self.http_server.stop()
00279 self.http_client.close()
00280 super(AsyncHTTPTestCase, self).tearDown()
00281
00282
00283 class LogTrapTestCase(unittest.TestCase):
00284 """A test case that captures and discards all logging output
00285 if the test passes.
00286
00287 Some libraries can produce a lot of logging output even when
00288 the test succeeds, so this class can be useful to minimize the noise.
00289 Simply use it as a base class for your test case. It is safe to combine
00290 with AsyncTestCase via multiple inheritance
00291 ("class MyTestCase(AsyncHTTPTestCase, LogTrapTestCase):")
00292
00293 This class assumes that only one log handler is configured and that
00294 it is a StreamHandler. This is true for both logging.basicConfig
00295 and the "pretty logging" configured by tornado.options.
00296 """
00297 def run(self, result=None):
00298 logger = logging.getLogger()
00299 if len(logger.handlers) > 1:
00300
00301
00302
00303 super(LogTrapTestCase, self).run(result)
00304 return
00305 if not logger.handlers:
00306 logging.basicConfig()
00307 self.assertEqual(len(logger.handlers), 1)
00308 handler = logger.handlers[0]
00309 assert isinstance(handler, logging.StreamHandler)
00310 old_stream = handler.stream
00311 try:
00312 handler.stream = StringIO()
00313 logging.info("RUNNING TEST: " + str(self))
00314 old_error_count = len(result.failures) + len(result.errors)
00315 super(LogTrapTestCase, self).run(result)
00316 new_error_count = len(result.failures) + len(result.errors)
00317 if new_error_count != old_error_count:
00318 old_stream.write(handler.stream.getvalue())
00319 finally:
00320 handler.stream = old_stream
00321
00322
00323 def main():
00324 """A simple test runner.
00325
00326 This test runner is essentially equivalent to `unittest.main` from
00327 the standard library, but adds support for tornado-style option
00328 parsing and log formatting.
00329
00330 The easiest way to run a test is via the command line::
00331
00332 python -m tornado.testing tornado.test.stack_context_test
00333
00334 See the standard library unittest module for ways in which tests can
00335 be specified.
00336
00337 Projects with many tests may wish to define a test script like
00338 tornado/test/runtests.py. This script should define a method all()
00339 which returns a test suite and then call tornado.testing.main().
00340 Note that even when a test script is used, the all() test suite may
00341 be overridden by naming a single test on the command line::
00342
00343 # Runs all tests
00344 tornado/test/runtests.py
00345 # Runs one test
00346 tornado/test/runtests.py tornado.test.stack_context_test
00347
00348 """
00349 from tornado.options import define, options, parse_command_line
00350
00351 define('autoreload', type=bool, default=False,
00352 help="DEPRECATED: use tornado.autoreload.main instead")
00353 define('httpclient', type=str, default=None)
00354 define('exception_on_interrupt', type=bool, default=True,
00355 help=("If true (default), ctrl-c raises a KeyboardInterrupt "
00356 "exception. This prints a stack trace but cannot interrupt "
00357 "certain operations. If false, the process is more reliably "
00358 "killed, but does not print a stack trace."))
00359 argv = [sys.argv[0]] + parse_command_line(sys.argv)
00360
00361 if options.httpclient:
00362 from tornado.httpclient import AsyncHTTPClient
00363 AsyncHTTPClient.configure(options.httpclient)
00364
00365 if not options.exception_on_interrupt:
00366 signal.signal(signal.SIGINT, signal.SIG_DFL)
00367
00368 if __name__ == '__main__' and len(argv) == 1:
00369 print >> sys.stderr, "No tests specified"
00370 sys.exit(1)
00371 try:
00372
00373
00374
00375
00376
00377
00378 if len(argv) > 1:
00379 unittest.main(module=None, argv=argv)
00380 else:
00381 unittest.main(defaultTest="all", argv=argv)
00382 except SystemExit, e:
00383 if e.code == 0:
00384 logging.info('PASS')
00385 else:
00386 logging.error('FAIL')
00387 if not options.autoreload:
00388 raise
00389 if options.autoreload:
00390 import tornado.autoreload
00391 tornado.autoreload.wait()
00392
00393 if __name__ == '__main__':
00394 main()