From: Ben Darnell Date: Sat, 25 Jan 2014 18:43:27 +0000 (-0500) Subject: Add support in AsyncTestCase for detecting undecorated generators. X-Git-Tag: v4.0.0b1~135 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=806942893499ecf14de28162f26f3595e4e5eb12;p=thirdparty%2Ftornado.git Add support in AsyncTestCase for detecting undecorated generators. Fix a test that was not being run because of this issue. --- diff --git a/tornado/test/testing_test.py b/tornado/test/testing_test.py index 64e5683e4..569f32fc4 100644 --- a/tornado/test/testing_test.py +++ b/tornado/test/testing_test.py @@ -62,6 +62,39 @@ class AsyncTestCaseTest(AsyncTestCase): self.wait(timeout=0.15) +class AsyncTestCaseWrapperTest(unittest.TestCase): + def test_undecorated_generator(self): + class Test(AsyncTestCase): + def test_gen(self): + yield + test = Test('test_gen') + result = unittest.TestResult() + test.run(result) + self.assertEqual(len(result.errors), 1) + self.assertIn("should be decorated", result.errors[0][1]) + + def test_undecorated_generator_with_skip(self): + class Test(AsyncTestCase): + @unittest.skip("don't run this") + def test_gen(self): + yield + test = Test('test_gen') + result = unittest.TestResult() + test.run(result) + self.assertEqual(len(result.errors), 0) + self.assertEqual(len(result.skipped), 1) + + def test_other_return(self): + class Test(AsyncTestCase): + def test_other_return(self): + return 42 + test = Test('test_other_return') + result = unittest.TestResult() + test.run(result) + self.assertEqual(len(result.errors), 1) + self.assertIn("Return value from test method ignored", result.errors[0][1]) + + class SetUpTearDownTest(unittest.TestCase): def test_set_up_tear_down(self): """ diff --git a/tornado/test/websocket_test.py b/tornado/test/websocket_test.py index 01fee72b2..e4462b2ed 100644 --- a/tornado/test/websocket_test.py +++ b/tornado/test/websocket_test.py @@ -83,8 +83,9 @@ class WebSocketTest(AsyncHTTPTestCase): ws.read_message(self.stop) response = self.wait().result() self.assertEqual(response, 'hello') + self.close_future.add_done_callback(lambda f: self.stop()) ws.close() - yield self.close_future + self.wait() @gen_test def test_websocket_http_fail(self): diff --git a/tornado/testing.py b/tornado/testing.py index 8355dcfc5..c563bd711 100644 --- a/tornado/testing.py +++ b/tornado/testing.py @@ -38,6 +38,7 @@ import re import signal import socket import sys +import types try: from cStringIO import StringIO # py2 @@ -95,6 +96,35 @@ def get_async_test_timeout(): return 5 +class _TestMethodWrapper(object): + """Wraps a test method to raise an error if it returns a value. + + This is mainly used to detect undecorated generators (if a test + method yields it must use a decorator to consume the generator), + but will also detect other kinds of return values (these are not + necessarily errors, but we alert anyway since there is no good + reason to return a value from a test. + """ + def __init__(self, orig_method): + self.orig_method = orig_method + + def __call__(self): + result = self.orig_method() + if isinstance(result, types.GeneratorType): + raise TypeError("Generator test methods should be decorated with " + "tornado.testing.gen_test") + elif result is not None: + raise ValueError("Return value from test method ignored: %r" % + result) + + def __getattr__(self, name): + """Proxy all unknown attributes to the original method. + + This is important for some of the decorators in the `unittest` + module, such as `unittest.skipIf`. + """ + return getattr(self.orig_method, name) + class AsyncTestCase(unittest.TestCase): """`~unittest.TestCase` subclass for testing `.IOLoop`-based asynchronous code. @@ -157,14 +187,20 @@ class AsyncTestCase(unittest.TestCase): self.assertIn("FriendFeed", response.body) self.stop() """ - def __init__(self, *args, **kwargs): - super(AsyncTestCase, self).__init__(*args, **kwargs) + def __init__(self, methodName='runTest', **kwargs): + super(AsyncTestCase, self).__init__(methodName, **kwargs) self.__stopped = False self.__running = False self.__failure = None self.__stop_args = None self.__timeout = None + # It's easy to forget the @gen_test decorator, but if you do + # the test will silently be ignored because nothing will consume + # the generator. Replace the test method with a wrapper that will + # make sure it's not an undecorated generator. + setattr(self, methodName, _TestMethodWrapper(getattr(self, methodName))) + def setUp(self): super(AsyncTestCase, self).setUp() self.io_loop = self.get_new_ioloop()