diff -r b758351d191f -r cc9b7e14412b web/lib/django/test/simple.py --- a/web/lib/django/test/simple.py Wed May 19 17:43:59 2010 +0200 +++ b/web/lib/django/test/simple.py Tue May 25 02:43:45 2010 +0200 @@ -1,4 +1,7 @@ +import sys +import signal import unittest + from django.conf import settings from django.db.models import get_app, get_apps from django.test import _doctest as doctest @@ -10,6 +13,54 @@ doctestOutputChecker = OutputChecker() +class DjangoTestRunner(unittest.TextTestRunner): + + def __init__(self, verbosity=0, failfast=False, **kwargs): + super(DjangoTestRunner, self).__init__(verbosity=verbosity, **kwargs) + self.failfast = failfast + self._keyboard_interrupt_intercepted = False + + def run(self, *args, **kwargs): + """ + Runs the test suite after registering a custom signal handler + that triggers a graceful exit when Ctrl-C is pressed. + """ + self._default_keyboard_interrupt_handler = signal.signal(signal.SIGINT, + self._keyboard_interrupt_handler) + try: + result = super(DjangoTestRunner, self).run(*args, **kwargs) + finally: + signal.signal(signal.SIGINT, self._default_keyboard_interrupt_handler) + return result + + def _keyboard_interrupt_handler(self, signal_number, stack_frame): + """ + Handles Ctrl-C by setting a flag that will stop the test run when + the currently running test completes. + """ + self._keyboard_interrupt_intercepted = True + sys.stderr.write(" ") + # Set the interrupt handler back to the default handler, so that + # another Ctrl-C press will trigger immediate exit. + signal.signal(signal.SIGINT, self._default_keyboard_interrupt_handler) + + def _makeResult(self): + result = super(DjangoTestRunner, self)._makeResult() + failfast = self.failfast + + def stoptest_override(func): + def stoptest(test): + # If we were set to failfast and the unit test failed, + # or if the user has typed Ctrl-C, report and quit + if (failfast and not result.wasSuccessful()) or \ + self._keyboard_interrupt_intercepted: + result.stop() + func(test) + return stoptest + + setattr(result, 'stopTest', stoptest_override(result.stopTest)) + return result + def get_tests(app_module): try: app_path = app_module.__name__.split('.')[:-1] @@ -73,41 +124,66 @@ return suite def build_test(label): - """Construct a test case a test with the specified label. Label should - be of the form model.TestClass or model.TestClass.test_method. Returns - an instantiated test or test suite corresponding to the label provided. + """Construct a test case with the specified label. Label should be of the + form model.TestClass or model.TestClass.test_method. Returns an + instantiated test or test suite corresponding to the label provided. """ parts = label.split('.') if len(parts) < 2 or len(parts) > 3: raise ValueError("Test label '%s' should be of the form app.TestCase or app.TestCase.test_method" % label) + # + # First, look for TestCase instances with a name that matches + # app_module = get_app(parts[0]) + test_module = get_tests(app_module) TestClass = getattr(app_module, parts[1], None) # Couldn't find the test class in models.py; look in tests.py if TestClass is None: - test_module = get_tests(app_module) if test_module: TestClass = getattr(test_module, parts[1], None) - if len(parts) == 2: # label is app.TestClass + try: + if issubclass(TestClass, unittest.TestCase): + if len(parts) == 2: # label is app.TestClass + try: + return unittest.TestLoader().loadTestsFromTestCase(TestClass) + except TypeError: + raise ValueError("Test label '%s' does not refer to a test class" % label) + else: # label is app.TestClass.test_method + return TestClass(parts[2]) + except TypeError: + # TestClass isn't a TestClass - it must be a method or normal class + pass + + # + # If there isn't a TestCase, look for a doctest that matches + # + tests = [] + for module in app_module, test_module: try: - return unittest.TestLoader().loadTestsFromTestCase(TestClass) - except TypeError: - raise ValueError("Test label '%s' does not refer to a test class" % label) - else: # label is app.TestClass.test_method - if not TestClass: - raise ValueError("Test label '%s' does not refer to a test class" % label) - return TestClass(parts[2]) + doctests = doctest.DocTestSuite(module, + checker=doctestOutputChecker, + runner=DocTestRunner) + # Now iterate over the suite, looking for doctests whose name + # matches the pattern that was given + for test in doctests: + if test._dt_test.name in ( + '%s.%s' % (module.__name__, '.'.join(parts[1:])), + '%s.__test__.%s' % (module.__name__, '.'.join(parts[1:]))): + tests.append(test) + except ValueError: + # No doctests found. + pass -# Python 2.3 compatibility: TestSuites were made iterable in 2.4. -# We need to iterate over them, so we add the missing method when -# necessary. -try: - getattr(unittest.TestSuite, '__iter__') -except AttributeError: - setattr(unittest.TestSuite, '__iter__', lambda s: iter(s._tests)) + # If no tests were found, then we were given a bad test label. + if not tests: + raise ValueError("Test label '%s' does not refer to a test" % label) + + # Construct a suite out of the tests that matched. + return unittest.TestSuite(tests) def partition_suite(suite, classes, bins): """ @@ -146,52 +222,105 @@ bins[0].addTests(bins[i+1]) return bins[0] -def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[]): - """ - Run the unit tests for all the test labels in the provided list. - Labels must be of the form: - - app.TestClass.test_method - Run a single specific test method - - app.TestClass - Run all the test methods in a given class - - app - Search for doctests and unittests in the named application. + +class DjangoTestSuiteRunner(object): + def __init__(self, verbosity=1, interactive=True, failfast=True, **kwargs): + self.verbosity = verbosity + self.interactive = interactive + self.failfast = failfast + + def setup_test_environment(self, **kwargs): + setup_test_environment() + settings.DEBUG = False + + def build_suite(self, test_labels, extra_tests=None, **kwargs): + suite = unittest.TestSuite() + + if test_labels: + for label in test_labels: + if '.' in label: + suite.addTest(build_test(label)) + else: + app = get_app(label) + suite.addTest(build_suite(app)) + else: + for app in get_apps(): + suite.addTest(build_suite(app)) - When looking for tests, the test runner will look in the models and - tests modules for the application. + if extra_tests: + for test in extra_tests: + suite.addTest(test) - A list of 'extra' tests may also be provided; these tests - will be added to the test suite. + return reorder_suite(suite, (TestCase,)) - Returns the number of tests that failed. - """ - setup_test_environment() + def setup_databases(self, **kwargs): + from django.db import connections + old_names = [] + mirrors = [] + for alias in connections: + connection = connections[alias] + # If the database is a test mirror, redirect it's connection + # instead of creating a test database. + if connection.settings_dict['TEST_MIRROR']: + mirrors.append((alias, connection)) + mirror_alias = connection.settings_dict['TEST_MIRROR'] + connections._connections[alias] = connections[mirror_alias] + else: + old_names.append((connection, connection.settings_dict['NAME'])) + connection.creation.create_test_db(self.verbosity, autoclobber=not self.interactive) + return old_names, mirrors - settings.DEBUG = False - suite = unittest.TestSuite() + def run_suite(self, suite, **kwargs): + return DjangoTestRunner(verbosity=self.verbosity, failfast=self.failfast).run(suite) - if test_labels: - for label in test_labels: - if '.' in label: - suite.addTest(build_test(label)) - else: - app = get_app(label) - suite.addTest(build_suite(app)) - else: - for app in get_apps(): - suite.addTest(build_suite(app)) + def teardown_databases(self, old_config, **kwargs): + from django.db import connections + old_names, mirrors = old_config + # Point all the mirrors back to the originals + for alias, connection in mirrors: + connections._connections[alias] = connection + # Destroy all the non-mirror databases + for connection, old_name in old_names: + connection.creation.destroy_test_db(old_name, self.verbosity) + + def teardown_test_environment(self, **kwargs): + teardown_test_environment() + + def suite_result(self, suite, result, **kwargs): + return len(result.failures) + len(result.errors) - for test in extra_tests: - suite.addTest(test) + def run_tests(self, test_labels, extra_tests=None, **kwargs): + """ + Run the unit tests for all the test labels in the provided list. + Labels must be of the form: + - app.TestClass.test_method + Run a single specific test method + - app.TestClass + Run all the test methods in a given class + - app + Search for doctests and unittests in the named application. - suite = reorder_suite(suite, (TestCase,)) + When looking for tests, the test runner will look in the models and + tests modules for the application. + + A list of 'extra' tests may also be provided; these tests + will be added to the test suite. - old_name = settings.DATABASE_NAME - from django.db import connection - connection.creation.create_test_db(verbosity, autoclobber=not interactive) - result = unittest.TextTestRunner(verbosity=verbosity).run(suite) - connection.creation.destroy_test_db(old_name, verbosity) + Returns the number of tests that failed. + """ + self.setup_test_environment() + suite = self.build_suite(test_labels, extra_tests) + old_config = self.setup_databases() + result = self.run_suite(suite) + self.teardown_databases(old_config) + self.teardown_test_environment() + return self.suite_result(suite, result) - teardown_test_environment() - - return len(result.failures) + len(result.errors) +def run_tests(test_labels, verbosity=1, interactive=True, failfast=False, extra_tests=None): + import warnings + warnings.warn( + 'The run_tests() test runner has been deprecated in favor of DjangoTestSuiteRunner.', + PendingDeprecationWarning + ) + test_runner = DjangoTestSuiteRunner(verbosity=verbosity, interactive=interactive, failfast=failfast) + return test_runner.run_tests(test_labels, extra_tests=extra_tests)