Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 400 lines (335 sloc) 15.371 kB
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
1 #!/usr/bin/env python
2 """Support classes for automated testing.
3
4 This module contains three parts:
96c2f2f @bdarnell Finish automatic documentation
bdarnell authored
5
300d231 @bdarnell Finish this round of doc updates
bdarnell authored
6 * `AsyncTestCase`/`AsyncHTTPTestCase`: Subclasses of unittest.TestCase
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
7 with additional support for testing asynchronous (IOLoop-based) code.
96c2f2f @bdarnell Finish automatic documentation
bdarnell authored
8
300d231 @bdarnell Finish this round of doc updates
bdarnell authored
9 * `LogTrapTestCase`: Subclass of unittest.TestCase that discards log output
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
10 from tests that pass and only produces output for failing tests.
96c2f2f @bdarnell Finish automatic documentation
bdarnell authored
11
300d231 @bdarnell Finish this round of doc updates
bdarnell authored
12 * `main()`: A simple test runner (wrapper around unittest.main()) with support
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
13 for the tornado.autoreload module to rerun the tests when code changes.
14
15 These components may be used together or independently. In particular,
16 it is safe to combine AsyncTestCase and LogTrapTestCase via multiple
17 inheritance. See the docstrings for each class/function below for more
18 information.
19 """
20
58a7ff1 @bdarnell Turn on __future__ division too.
bdarnell authored
21 from __future__ import absolute_import, division, with_statement
b260c92 Add __future__ import for python2.5 compatibility
Ben Darnell authored
22
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
23 from cStringIO import StringIO
7a48715 @bdarnell Add very basic testing for app engine
bdarnell authored
24 try:
25 from tornado.httpclient import AsyncHTTPClient
26 from tornado.httpserver import HTTPServer
27 from tornado.ioloop import IOLoop
28 except ImportError:
29 # These modules are not importable on app engine. Parts of this module
30 # won't work, but e.g. LogTrapTestCase and main() will.
31 AsyncHTTPClient = None
32 HTTPServer = None
33 IOLoop = None
6df410a Fix a StackContext-related bug that was causing exceptions in callbacks
Ben Darnell authored
34 from tornado.stack_context import StackContext, NullContext
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
35 import contextlib
36 import logging
89f35bc @bdarnell Add an option to the test runner to kill the process on SIGINT
bdarnell authored
37 import signal
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
38 import sys
39 import time
40 import unittest
41
b02a31b @bdarnell Factor port-selection logic out of AsyncHTTPTestCase so it can be used
bdarnell authored
42 _next_port = 10000
c152b78 @bdarnell While I'm touching every file, run autopep8 too.
bdarnell authored
43
44
b02a31b @bdarnell Factor port-selection logic out of AsyncHTTPTestCase so it can be used
bdarnell authored
45 def get_unused_port():
46 """Returns a (hopefully) unused port number."""
47 global _next_port
48 port = _next_port
49 _next_port = _next_port + 1
50 return port
51
c152b78 @bdarnell While I'm touching every file, run autopep8 too.
bdarnell authored
52
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
53 class AsyncTestCase(unittest.TestCase):
54 """TestCase subclass for testing IOLoop-based asynchronous code.
55
56 The unittest framework is synchronous, so the test must be complete
57 by the time the test method returns. This method provides the stop()
58 and wait() methods for this purpose. The test method itself must call
59 self.wait(), and asynchronous callbacks should call self.stop() to signal
60 completion.
61
62 By default, a new IOLoop is constructed for each test and is available
63 as self.io_loop. This IOLoop should be used in the construction of
64 HTTP clients/servers, etc. If the code being tested requires a
65 global IOLoop, subclasses should override get_new_ioloop to return it.
66
67 The IOLoop's start and stop methods should not be called directly.
68 Instead, use self.stop self.wait. Arguments passed to self.stop are
69 returned from self.wait. It is possible to have multiple
70 wait/stop cycles in the same test.
71
96c2f2f @bdarnell Finish automatic documentation
bdarnell authored
72 Example::
73
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
74 # This test uses an asynchronous style similar to most async
75 # application code.
76 class MyTestCase(AsyncTestCase):
77 def test_http_fetch(self):
78 client = AsyncHTTPClient(self.io_loop)
79 client.fetch("http://www.tornadoweb.org/", self.handle_fetch)
80 self.wait()
81
c7c0caf @jlfwong Documentation fix for AsyncTestCase
jlfwong authored
82 def handle_fetch(self, response):
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
83 # Test contents of response (failures and exceptions here
84 # will cause self.wait() to throw an exception and end the
85 # test).
f27b49a @bdarnell Misc doc updates.
bdarnell authored
86 # Exceptions thrown here are magically propagated to
87 # self.wait() in test_http_fetch() via stack_context.
88 self.assertIn("FriendFeed", response.body)
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
89 self.stop()
90
91 # This test uses the argument passing between self.stop and self.wait
f27b49a @bdarnell Misc doc updates.
bdarnell authored
92 # for a simpler, more synchronous style.
93 # This style is recommended over the preceding example because it
94 # keeps the assertions in the test method itself, and is therefore
95 # less sensitive to the subtleties of stack_context.
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
96 class MyTestCase2(AsyncTestCase):
97 def test_http_fetch(self):
98 client = AsyncHTTPClient(self.io_loop)
99 client.fetch("http://www.tornadoweb.org/", self.stop)
100 response = self.wait()
101 # Test contents of response
f27b49a @bdarnell Misc doc updates.
bdarnell authored
102 self.assertIn("FriendFeed", response.body)
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
103 """
551ea94 @bdarnell Initialize member variables in AsyncTestCase.__init__ instead of setUp
bdarnell authored
104 def __init__(self, *args, **kwargs):
105 super(AsyncTestCase, self).__init__(*args, **kwargs)
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
106 self.__stopped = False
107 self.__running = False
108 self.__failure = None
109 self.__stop_args = None
6ed1eaf @bergundy Fixed: Subsequent calls to AsyncTestCase.wait() now clear previously …
bergundy authored
110 self.__timeout = None
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
111
551ea94 @bdarnell Initialize member variables in AsyncTestCase.__init__ instead of setUp
bdarnell authored
112 def setUp(self):
113 super(AsyncTestCase, self).setUp()
114 self.io_loop = self.get_new_ioloop()
115
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
116 def tearDown(self):
93e22bb @bdarnell Don't initialize the singleton IOLoop from tearDown
bdarnell authored
117 if (not IOLoop.initialized() or
118 self.io_loop is not IOLoop.instance()):
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
119 # Try to clean up any file descriptors left open in the ioloop.
120 # This avoids leaks, especially when tests are run repeatedly
121 # in the same process with autoreload (because curl does not
122 # set FD_CLOEXEC on its file descriptors)
9d1af05 @bdarnell Clean up shutdown process for IOLoop and HTTPClient.
bdarnell authored
123 self.io_loop.close(all_fds=True)
6f0a8a7 @bwbeach Make AsyncTestCase call tearDown in super class.
bwbeach authored
124 super(AsyncTestCase, self).tearDown()
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
125
126 def get_new_ioloop(self):
127 '''Creates a new IOLoop for this test. May be overridden in
128 subclasses for tests that require a specific IOLoop (usually
129 the singleton).
130 '''
93e22bb @bdarnell Don't initialize the singleton IOLoop from tearDown
bdarnell authored
131 return IOLoop()
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
132
133 @contextlib.contextmanager
134 def _stack_context(self):
135 try:
136 yield
17eed4f @bdarnell Replace all bare "except:" blocks with "except Exception:" so we don't
bdarnell authored
137 except Exception:
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
138 self.__failure = sys.exc_info()
139 self.stop()
140
94dc4c7 @bdarnell Don't swallow exceptions that escape from TestCase.run().
bdarnell authored
141 def __rethrow(self):
142 if self.__failure is not None:
143 failure = self.__failure
144 self.__failure = None
145 # 2to3 isn't smart enough to convert three-argument raise
146 # statements correctly in some cases.
147 if isinstance(failure[1], failure[0]):
148 raise failure[1], None, failure[2]
149 else:
150 raise failure[0], failure[1], failure[2]
151
152
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
153 def run(self, result=None):
154 with StackContext(self._stack_context):
155 super(AsyncTestCase, self).run(result)
94dc4c7 @bdarnell Don't swallow exceptions that escape from TestCase.run().
bdarnell authored
156 # In case an exception escaped super.run or the StackContext caught
157 # an exception when there wasn't a wait() to re-raise it, do so here.
158 self.__rethrow()
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
159
160 def stop(self, _arg=None, **kwargs):
161 '''Stops the ioloop, causing one pending (or future) call to wait()
162 to return.
163
164 Keyword arguments or a single positional argument passed to stop() are
165 saved and will be returned by wait().
166 '''
e210a64 @bdarnell Make AsyncTestCase.stop() prefer _arg over **kwargs, even when the fo…
bdarnell authored
167 assert _arg is None or not kwargs
168 self.__stop_args = kwargs or _arg
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
169 if self.__running:
170 self.io_loop.stop()
171 self.__running = False
172 self.__stopped = True
173
174 def wait(self, condition=None, timeout=5):
252cb1c Docstring updates
Ben Darnell authored
175 """Runs the IOLoop until stop is called or timeout has passed.
176
177 In the event of a timeout, an exception will be thrown.
178
179 If condition is not None, the IOLoop will be restarted after stop()
180 until condition() returns true.
181 """
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
182 if not self.__stopped:
183 if timeout:
184 def timeout_func():
185 try:
186 raise self.failureException(
187 'Async operation timed out after %d seconds' %
188 timeout)
17eed4f @bdarnell Replace all bare "except:" blocks with "except Exception:" so we don't
bdarnell authored
189 except Exception:
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
190 self.__failure = sys.exc_info()
191 self.stop()
6ed1eaf @bergundy Fixed: Subsequent calls to AsyncTestCase.wait() now clear previously …
bergundy authored
192 if self.__timeout is not None:
193 self.io_loop.remove_timeout(self.__timeout)
194 self.__timeout = self.io_loop.add_timeout(time.time() + timeout, timeout_func)
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
195 while True:
196 self.__running = True
6df410a Fix a StackContext-related bug that was causing exceptions in callbacks
Ben Darnell authored
197 with NullContext():
198 # Wipe out the StackContext that was established in
199 # self.run() so that all callbacks executed inside the
200 # IOLoop will re-run it.
201 self.io_loop.start()
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
202 if (self.__failure is not None or
203 condition is None or condition()):
204 break
205 assert self.__stopped
206 self.__stopped = False
94dc4c7 @bdarnell Don't swallow exceptions that escape from TestCase.run().
bdarnell authored
207 self.__rethrow()
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
208 result = self.__stop_args
209 self.__stop_args = None
210 return result
211
212
213 class AsyncHTTPTestCase(AsyncTestCase):
214 '''A test case that starts up an HTTP server.
215
216 Subclasses must override get_app(), which returns the
217 tornado.web.Application (or other HTTPServer callback) to be tested.
218 Tests will typically use the provided self.http_client to fetch
219 URLs from this server.
220
96c2f2f @bdarnell Finish automatic documentation
bdarnell authored
221 Example::
222
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
223 class MyHTTPTest(AsyncHTTPTestCase):
224 def get_app(self):
225 return Application([('/', MyHandler)...])
226
227 def test_homepage(self):
11fdd47 @bdarnell Add a convenience method for synchronous fetches in AsyncHTTPTestCase
bdarnell authored
228 # The following two lines are equivalent to
229 # response = self.fetch('/')
230 # but are shown in full here to demonstrate explicit use
231 # of self.stop and self.wait.
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
232 self.http_client.fetch(self.get_url('/'), self.stop)
233 response = self.wait()
234 # test contents of response
235 '''
236 def setUp(self):
237 super(AsyncHTTPTestCase, self).setUp()
238 self.__port = None
239
240 self.http_client = AsyncHTTPClient(io_loop=self.io_loop)
241 self._app = self.get_app()
b398a81 @bdarnell Add a test for HTTPServer's SSL support
bdarnell authored
242 self.http_server = HTTPServer(self._app, io_loop=self.io_loop,
243 **self.get_httpserver_options())
6268052 @bdarnell Only listen on localhost so unittests don't cause firewall warnings
bdarnell authored
244 self.http_server.listen(self.get_http_port(), address="127.0.0.1")
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
245
246 def get_app(self):
252cb1c Docstring updates
Ben Darnell authored
247 """Should be overridden by subclasses to return a
248 tornado.web.Application or other HTTPServer callback.
249 """
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
250 raise NotImplementedError()
251
11fdd47 @bdarnell Add a convenience method for synchronous fetches in AsyncHTTPTestCase
bdarnell authored
252 def fetch(self, path, **kwargs):
253 """Convenience method to synchronously fetch a url.
254
255 The given path will be appended to the local server's host and port.
256 Any additional kwargs will be passed directly to
257 AsyncHTTPClient.fetch (and so could be used to pass method="POST",
258 body="...", etc).
259 """
260 self.http_client.fetch(self.get_url(path), self.stop, **kwargs)
261 return self.wait()
262
b398a81 @bdarnell Add a test for HTTPServer's SSL support
bdarnell authored
263 def get_httpserver_options(self):
264 """May be overridden by subclasses to return additional
265 keyword arguments for HTTPServer.
266 """
267 return {}
268
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
269 def get_http_port(self):
252cb1c Docstring updates
Ben Darnell authored
270 """Returns the port used by the HTTPServer.
271
272 A new port is chosen for each test.
273 """
b02a31b @bdarnell Factor port-selection logic out of AsyncHTTPTestCase so it can be used
bdarnell authored
274 if self.__port is None:
275 self.__port = get_unused_port()
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
276 return self.__port
277
278 def get_url(self, path):
252cb1c Docstring updates
Ben Darnell authored
279 """Returns an absolute url for the given path on the test server."""
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
280 return 'http://localhost:%s%s' % (self.get_http_port(), path)
281
282 def tearDown(self):
283 self.http_server.stop()
284 self.http_client.close()
285 super(AsyncHTTPTestCase, self).tearDown()
286
c152b78 @bdarnell While I'm touching every file, run autopep8 too.
bdarnell authored
287
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
288 class LogTrapTestCase(unittest.TestCase):
289 """A test case that captures and discards all logging output
290 if the test passes.
291
292 Some libraries can produce a lot of logging output even when
293 the test succeeds, so this class can be useful to minimize the noise.
294 Simply use it as a base class for your test case. It is safe to combine
295 with AsyncTestCase via multiple inheritance
296 ("class MyTestCase(AsyncHTTPTestCase, LogTrapTestCase):")
297
298 This class assumes that only one log handler is configured and that
299 it is a StreamHandler. This is true for both logging.basicConfig
300 and the "pretty logging" configured by tornado.options.
301 """
302 def run(self, result=None):
303 logger = logging.getLogger()
221058e @bdarnell In LogTrapTestCase, leave everything alone rather than throwing an ex…
bdarnell authored
304 if len(logger.handlers) > 1:
305 # Multiple handlers have been defined. It gets messy to handle
306 # this, especially since the handlers may have different
307 # formatters. Just leave the logging alone in this case.
308 super(LogTrapTestCase, self).run(result)
309 return
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
310 if not logger.handlers:
311 logging.basicConfig()
312 self.assertEqual(len(logger.handlers), 1)
313 handler = logger.handlers[0]
314 assert isinstance(handler, logging.StreamHandler)
315 old_stream = handler.stream
316 try:
317 handler.stream = StringIO()
318 logging.info("RUNNING TEST: " + str(self))
319 old_error_count = len(result.failures) + len(result.errors)
320 super(LogTrapTestCase, self).run(result)
321 new_error_count = len(result.failures) + len(result.errors)
322 if new_error_count != old_error_count:
323 old_stream.write(handler.stream.getvalue())
324 finally:
325 handler.stream = old_stream
326
c152b78 @bdarnell While I'm touching every file, run autopep8 too.
bdarnell authored
327
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
328 def main():
815a1f0 @bdarnell Add command-line interface to tornado.autoreload
bdarnell authored
329 """A simple test runner.
330
331 This test runner is essentially equivalent to `unittest.main` from
332 the standard library, but adds support for tornado-style option
333 parsing and log formatting.
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
334
96c2f2f @bdarnell Finish automatic documentation
bdarnell authored
335 The easiest way to run a test is via the command line::
336
815a1f0 @bdarnell Add command-line interface to tornado.autoreload
bdarnell authored
337 python -m tornado.testing tornado.test.stack_context_test
96c2f2f @bdarnell Finish automatic documentation
bdarnell authored
338
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
339 See the standard library unittest module for ways in which tests can
340 be specified.
341
342 Projects with many tests may wish to define a test script like
343 tornado/test/runtests.py. This script should define a method all()
344 which returns a test suite and then call tornado.testing.main().
345 Note that even when a test script is used, the all() test suite may
96c2f2f @bdarnell Finish automatic documentation
bdarnell authored
346 be overridden by naming a single test on the command line::
347
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
348 # Runs all tests
815a1f0 @bdarnell Add command-line interface to tornado.autoreload
bdarnell authored
349 tornado/test/runtests.py
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
350 # Runs one test
815a1f0 @bdarnell Add command-line interface to tornado.autoreload
bdarnell authored
351 tornado/test/runtests.py tornado.test.stack_context_test
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
352
353 """
354 from tornado.options import define, options, parse_command_line
355
815a1f0 @bdarnell Add command-line interface to tornado.autoreload
bdarnell authored
356 define('autoreload', type=bool, default=False,
357 help="DEPRECATED: use tornado.autoreload.main instead")
9fc882a @bdarnell Add a new method to configure AsyncHTTPClient.
bdarnell authored
358 define('httpclient', type=str, default=None)
89f35bc @bdarnell Add an option to the test runner to kill the process on SIGINT
bdarnell authored
359 define('exception_on_interrupt', type=bool, default=True,
360 help=("If true (default), ctrl-c raises a KeyboardInterrupt "
361 "exception. This prints a stack trace but cannot interrupt "
362 "certain operations. If false, the process is more reliably "
363 "killed, but does not print a stack trace."))
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
364 argv = [sys.argv[0]] + parse_command_line(sys.argv)
9fc882a @bdarnell Add a new method to configure AsyncHTTPClient.
bdarnell authored
365
366 if options.httpclient:
367 from tornado.httpclient import AsyncHTTPClient
368 AsyncHTTPClient.configure(options.httpclient)
369
89f35bc @bdarnell Add an option to the test runner to kill the process on SIGINT
bdarnell authored
370 if not options.exception_on_interrupt:
371 signal.signal(signal.SIGINT, signal.SIG_DFL)
372
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
373 if __name__ == '__main__' and len(argv) == 1:
374 print >> sys.stderr, "No tests specified"
375 sys.exit(1)
376 try:
377 # In order to be able to run tests by their fully-qualified name
378 # on the command line without importing all tests here,
05fd0fc @bdarnell Make tornado.testing.main with no arguments work in python 3.2
bdarnell authored
379 # module must be set to None. Python 3.2's unittest.main ignores
380 # defaultTest if no module is given (it tries to do its own
381 # test discovery, which is incompatible with auto2to3), so don't
382 # set module if we're not asking for a specific test.
383 if len(argv) > 1:
384 unittest.main(module=None, argv=argv)
385 else:
386 unittest.main(defaultTest="all", argv=argv)
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
387 except SystemExit, e:
388 if e.code == 0:
389 logging.info('PASS')
390 else:
391 logging.error('FAIL')
392 if not options.autoreload:
393 raise
394 if options.autoreload:
395 import tornado.autoreload
815a1f0 @bdarnell Add command-line interface to tornado.autoreload
bdarnell authored
396 tornado.autoreload.wait()
15e5bf0 Add a test framework for IOLoop-based async code
Ben Darnell authored
397
398 if __name__ == '__main__':
399 main()
Something went wrong with that request. Please try again.