From bce933adf74936b9fe13b403ec9609a17f654bee Mon Sep 17 00:00:00 2001 From: Daniel Miller Date: Fri, 31 Jul 2015 13:50:48 -0400 Subject: [PATCH 01/10] Run non-db tests first --- django_nose/plugin.py | 47 +++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/django_nose/plugin.py b/django_nose/plugin.py index 2e5e409..886de30 100644 --- a/django_nose/plugin.py +++ b/django_nose/plugin.py @@ -98,6 +98,9 @@ class Bucketer(object): def __init__(self): """Initialize the test buckets.""" + # All non-FastFixtureTestCase we saw before hitting the first FFTC + self.preamble = [] + # { (frozenset(['users.json']), True): # [ContextSuite(...), ContextSuite(...)] } self.buckets = {} @@ -120,6 +123,8 @@ def add(self, test): 'exempt_from_fixture_bundling', False)) self.buckets.setdefault(key, []).append(test) + elif not self.buckets: + self.preamble.append(test) else: self.remainder.append(test) @@ -173,26 +178,34 @@ def filthiness(test): Thus, things will get these comparands (and run in this order): - * 1: TestCase subclasses. These clean up after themselves. - * 1: TransactionTestCase subclasses with - cleans_up_after_itself=True. These include - FastFixtureTestCases. If you're using the - FixtureBundlingPlugin, it will pull the FFTCs out, reorder - them, and run them first of all. - * 2: TransactionTestCase subclasses. These leave a mess. - * 2: Anything else (including doctests, I hope). These don't care - about the mess you left, because they don't hit the DB or, if - they do, are responsible for ensuring that it's clean (as per + * 0: Not a subclass of TransactionTestCase. These should not hit + the DB or, if they do, are responsible for ensuring that it's + clean (as per https://docs.djangoproject.com/en/dev/topics/testing/?from= - olddocs#writing-doctests) + olddocs#writing-doctests). Note that this could include a + group of tests (including TransactionTestCase tests) with + setup and/or teardown routines in a "context" such as a + module or package defining fixture functions. To avoid that + scenario, don't use TestCase or TransactionTestCase in + modules or packages with setup or teardown functions. + * 1: FastFixtureTestCase subclasses. If you're using the + FixtureBundlingPlugin, it will pull the FFTCs out, reorder + them, and run them before the following groups. + * 2: TestCase subclasses. These clean up after themselves. + * 2: TransactionTestCase subclasses with + cleans_up_after_itself=True. + * 3: TransactionTestCase subclasses. These leave a mess. """ test_class = test.context - if (is_subclass_at_all(test_class, TestCase) or - (is_subclass_at_all(test_class, TransactionTestCase) and - getattr(test_class, 'cleans_up_after_itself', False))): - return 1 - return 2 + if is_subclass_at_all(test_class, TransactionTestCase): + if is_subclass_at_all(test_class, FastFixtureTestCase): + return 1 + if (is_subclass_at_all(test_class, TestCase) or + getattr(test_class, 'cleans_up_after_itself', False)): + return 2 + return 3 + return 0 flattened = [] process_tests(test, flattened.append) @@ -228,7 +241,7 @@ def suite_sorted_by_fixtures(suite): # Lay the bundles of common-fixture-having test classes end to end # in a single list so we can make a test suite out of them: - flattened = [] + flattened = list(bucketer.preamble) for (key, fixture_bundle) in bucketer.buckets.items(): fixtures, is_exempt = key # Advise first and last test classes in each bundle to set up From b9b4ef639dab63661b26c23b3e795c2e5097a7e8 Mon Sep 17 00:00:00 2001 From: Daniel Miller Date: Fri, 31 Jul 2015 14:56:46 -0400 Subject: [PATCH 02/10] Allow custom non-db test context --- django_nose/plugin.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/django_nose/plugin.py b/django_nose/plugin.py index 886de30..12bbc76 100644 --- a/django_nose/plugin.py +++ b/django_nose/plugin.py @@ -145,11 +145,22 @@ def options(self, parser, env): help='Load a unique set of fixtures only once, even ' 'across test classes. ' '[NOSE_WITH_FIXTURE_BUNDLING]') + parser.add_option('--non-db-test-context', + dest='non_db_test_context', + default=env.get('NOSE_NON_DB_TEST_CONTEXT'), + help='A module path to a callable taking a list of ' + 'tests and returning a context object (with ' + '`setup` and `teardown` methods) to be used ' + 'while running non-DB tests. This is useful, ' + 'for example, to enforce that non-DB tests do ' + 'not try to access a database. ' + '[NOSE_NON_DB_TEST_CONTEXT]') def configure(self, options, conf): """Configure plugin, reading the with_fixture_bundling option.""" super(TestReorderer, self).configure(options, conf) self.should_bundle = options.with_fixture_bundling + self.non_db_test_context = options.non_db_test_context def _put_transaction_test_cases_last(self, test): """Reorder test suite so TransactionTestCase-based tests come last. @@ -241,7 +252,12 @@ def suite_sorted_by_fixtures(suite): # Lay the bundles of common-fixture-having test classes end to end # in a single list so we can make a test suite out of them: - flattened = list(bucketer.preamble) + if self.non_db_test_context: + context = get_non_db_test_context( + self.non_db_test_context, bucketer.preamble) + flattened = [ContextSuite(bucketer.preamble, context)] + else: + flattened = list(bucketer.preamble) for (key, fixture_bundle) in bucketer.buckets.items(): fixtures, is_exempt = key # Advise first and last test classes in each bundle to set up @@ -279,3 +295,10 @@ def prepareTest(self, test): if self.should_bundle: test = self._bundle_fixtures(test) return test + + +def get_non_db_test_context(context_path, tests): + """Make a test context for non-db tests""" + module_path, context_name = context_path.rsplit(".", 1) + module = __import__(module_path, globals(), locals(), [context_name]) + return getattr(module, context_name)(tests) From 50b1cfea66f5fc5e45ba1d73e415ce6ce3c0523f Mon Sep 17 00:00:00 2001 From: Daniel Miller Date: Tue, 4 Aug 2015 11:52:16 -0400 Subject: [PATCH 03/10] Improve test grouping --- django_nose/plugin.py | 154 +++++++++++++++++++++++------------------- 1 file changed, 85 insertions(+), 69 deletions(-) diff --git a/django_nose/plugin.py b/django_nose/plugin.py index 12bbc76..508da4c 100644 --- a/django_nose/plugin.py +++ b/django_nose/plugin.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import sys +from itertools import groupby from nose.plugins.base import Plugin from nose.suite import ContextSuite @@ -98,9 +99,6 @@ class Bucketer(object): def __init__(self): """Initialize the test buckets.""" - # All non-FastFixtureTestCase we saw before hitting the first FFTC - self.preamble = [] - # { (frozenset(['users.json']), True): # [ContextSuite(...), ContextSuite(...)] } self.buckets = {} @@ -123,8 +121,6 @@ def add(self, test): 'exempt_from_fixture_bundling', False)) self.buckets.setdefault(key, []).append(test) - elif not self.buckets: - self.preamble.append(test) else: self.remainder.append(test) @@ -135,6 +131,11 @@ class TestReorderer(AlwaysOnPlugin): name = 'django-nose-test-reorderer' + NON_DB_TESTS = 0 + FFTC_TESTS = 1 + CLEAN_TESTS = 2 + DIRTY_TESTS = 3 + def options(self, parser, env): """Add --with-fixture-bundling to options.""" super(TestReorderer, self).options(parser, env) # pointless @@ -160,10 +161,20 @@ def configure(self, options, conf): """Configure plugin, reading the with_fixture_bundling option.""" super(TestReorderer, self).configure(options, conf) self.should_bundle = options.with_fixture_bundling - self.non_db_test_context = options.non_db_test_context + self.non_db_test_context_path = options.non_db_test_context + + def _group_test_cases_by_type(self, test): + """Group test suite by test type + + Test types: - def _put_transaction_test_cases_last(self, test): - """Reorder test suite so TransactionTestCase-based tests come last. + - Non-database tests (or ones that manage their own database connection + and cleanup) In simplest terms, this is any test that is not a + subclass of ``TransactionTestCase``. + - ``FastFixtureTestCase`` tests. + - Subclasses of ``django.test.TestCase`` and other database tests that + clean up after themselves. + - ``TransactionTestCase``-based tests, which often leave a mess. Django has a weird design decision wherein TransactionTestCase doesn't clean up after itself. Instead, it resets the DB to a clean state only @@ -210,20 +221,22 @@ def filthiness(test): """ test_class = test.context if is_subclass_at_all(test_class, TransactionTestCase): - if is_subclass_at_all(test_class, FastFixtureTestCase): - return 1 + if (is_subclass_at_all(test_class, FastFixtureTestCase) and + self.should_bundle): + return self.FFTC_TESTS if (is_subclass_at_all(test_class, TestCase) or getattr(test_class, 'cleans_up_after_itself', False)): - return 2 - return 3 - return 0 + return self.CLEAN_TESTS + return self.DIRTY_TESTS + return self.NON_DB_TESTS flattened = [] process_tests(test, flattened.append) flattened.sort(key=filthiness) - return ContextSuite(flattened) + return {key: list(group) + for key, group in groupby(flattened, filthiness)} - def _bundle_fixtures(self, test): + def _bundle_fixtures(self, fftc_tests): """Reorder tests to minimize fixture loading. I reorder FastFixtureTestCases so ones using identical sets @@ -236,65 +249,68 @@ def _bundle_fixtures(self, test): nobody else, in practice, pays attention to the ``_fb`` advisory bits. We return those first, then any remaining tests in the order they were received. - """ - def suite_sorted_by_fixtures(suite): - """Flatten and sort a tree of Suites by fixture. - - Add ``_fb_should_setup_fixtures`` and - ``_fb_should_teardown_fixtures`` attrs to each test class to advise - it whether to set up or tear down (respectively) the fixtures. - Return a Suite. + Add ``_fb_should_setup_fixtures`` and + ``_fb_should_teardown_fixtures`` attrs to each test class to advise + it whether to set up or tear down (respectively) the fixtures. - """ - bucketer = Bucketer() - process_tests(suite, bucketer.add) - - # Lay the bundles of common-fixture-having test classes end to end - # in a single list so we can make a test suite out of them: - if self.non_db_test_context: - context = get_non_db_test_context( - self.non_db_test_context, bucketer.preamble) - flattened = [ContextSuite(bucketer.preamble, context)] - else: - flattened = list(bucketer.preamble) - for (key, fixture_bundle) in bucketer.buckets.items(): - fixtures, is_exempt = key - # Advise first and last test classes in each bundle to set up - # and tear down fixtures and the rest not to: - if fixtures and not is_exempt: - # Ones with fixtures are sure to be classes, which means - # they're sure to be ContextSuites with contexts. - - # First class with this set of fixtures sets up: - first = fixture_bundle[0].context - first._fb_should_setup_fixtures = True - - # Set all classes' 1..n should_setup to False: - for cls in fixture_bundle[1:]: - cls.context._fb_should_setup_fixtures = False - - # Last class tears down: - last = fixture_bundle[-1].context - last._fb_should_teardown_fixtures = True - - # Set all classes' 0..(n-1) should_teardown to False: - for cls in fixture_bundle[:-1]: - cls.context._fb_should_teardown_fixtures = False - - flattened.extend(fixture_bundle) - flattened.extend(bucketer.remainder) - - return ContextSuite(flattened) - - return suite_sorted_by_fixtures(test) + Return a list of tests. + """ + bucketer = Bucketer() + for test in fftc_tests: + bucketer.add(test) + + # Lay the bundles of common-fixture-having test classes end to end + # in a single list so we can make a test suite out of them: + tests = [] + for (key, fixture_bundle) in bucketer.buckets.items(): + fixtures, is_exempt = key + # Advise first and last test classes in each bundle to set up + # and tear down fixtures and the rest not to: + if fixtures and not is_exempt: + # Ones with fixtures are sure to be classes, which means + # they're sure to be ContextSuites with contexts. + + # First class with this set of fixtures sets up: + first = fixture_bundle[0].context + first._fb_should_setup_fixtures = True + + # Set all classes' 1..n should_setup to False: + for cls in fixture_bundle[1:]: + cls.context._fb_should_setup_fixtures = False + + # Last class tears down: + last = fixture_bundle[-1].context + last._fb_should_teardown_fixtures = True + + # Set all classes' 0..(n-1) should_teardown to False: + for cls in fixture_bundle[:-1]: + cls.context._fb_should_teardown_fixtures = False + + tests.extend(fixture_bundle) + tests.extend(bucketer.remainder) + + return tests def prepareTest(self, test): """Reorder the tests.""" - test = self._put_transaction_test_cases_last(test) - if self.should_bundle: - test = self._bundle_fixtures(test) - return test + test_groups = self._group_test_cases_by_type(test) + + if self.non_db_test_context_path and self.NON_DB_TESTS in test_groups: + non_db_tests = test_groups[self.NON_DB_TESTS] + context = get_non_db_test_context( + self.non_db_test_context_path, non_db_tests) + test_groups[self.NON_DB_TESTS] = [ + ContextSuite(non_db_tests, context) + ] + + if self.should_bundle and self.FFTC_TESTS in test_groups: + fftc_tests = test_groups[self.FFTC_TESTS] + test_groups[self.FFTC_TESTS] = self._bundle_fixtures(fftc_tests) + + return ContextSuite([test + for key, group in sorted(test_groups.items()) + for test in group]) def get_non_db_test_context(context_path, tests): From 8ca2e5a86221a2c0cead1c2f78f84fdcfdaa7c94 Mon Sep 17 00:00:00 2001 From: Daniel Miller Date: Tue, 4 Aug 2015 17:04:04 -0400 Subject: [PATCH 04/10] Allow database setup/teardown to be customized Custom database setup/teardown can be done by specifying --db-test-context with a custom context class path. Database setup/teardown can be deferred until needed (or completely omitted if no tests require it) with --non-db-test-context and an appropriate class path such as `django_nose.plugin.NullContext`. --- django_nose/plugin.py | 109 ++++++++++++++++++++++++++++++++---------- django_nose/runner.py | 11 ++--- 2 files changed, 89 insertions(+), 31 deletions(-) diff --git a/django_nose/plugin.py b/django_nose/plugin.py index 508da4c..79f0517 100644 --- a/django_nose/plugin.py +++ b/django_nose/plugin.py @@ -83,13 +83,11 @@ def prepareTest(self, test): sys.stdout = self.sys_stdout self.runner.setup_test_environment() - self.old_names = self.runner.setup_databases() sys.stdout = sys_stdout def finalize(self, result): """Finalize test run by cleaning up databases and environment.""" - self.runner.teardown_databases(self.old_names) self.runner.teardown_test_environment() @@ -125,7 +123,7 @@ def add(self, test): self.remainder.append(test) -class TestReorderer(AlwaysOnPlugin): +class DatabaseSetUpPlugin(AlwaysOnPlugin): """Reorder tests for various reasons.""" @@ -136,9 +134,14 @@ class TestReorderer(AlwaysOnPlugin): CLEAN_TESTS = 2 DIRTY_TESTS = 3 + def __init__(self, runner): + """Initialize the plugin with the test runner.""" + super(DatabaseSetUpPlugin, self).__init__() + self.runner = runner + def options(self, parser, env): - """Add --with-fixture-bundling to options.""" - super(TestReorderer, self).options(parser, env) # pointless + """Add options.""" + super(DatabaseSetUpPlugin, self).options(parser, env) # pointless parser.add_option('--with-fixture-bundling', action='store_true', dest='with_fixture_bundling', @@ -146,21 +149,36 @@ def options(self, parser, env): help='Load a unique set of fixtures only once, even ' 'across test classes. ' '[NOSE_WITH_FIXTURE_BUNDLING]') + parser.add_option('--db-test-context', + dest='db_test_context', + default=env.get('NOSE_DB_TEST_CONTEXT', + 'django_nose.plugin.DatabaseContext'), + help='A module path to a callable accepting two ' + 'arguments (tests, runner) and returning a ' + 'context object (with `setup` and `teardown` ' + 'methods) to be used while running database ' + 'tests. This is useful, for example, to perform ' + 'custom database setup/teardown. Defaults to ' + 'django_nose.plugin.DatabaseContext. ' + '[NOSE_DB_TEST_CONTEXT]') parser.add_option('--non-db-test-context', dest='non_db_test_context', default=env.get('NOSE_NON_DB_TEST_CONTEXT'), - help='A module path to a callable taking a list of ' - 'tests and returning a context object (with ' - '`setup` and `teardown` methods) to be used ' - 'while running non-DB tests. This is useful, ' - 'for example, to enforce that non-DB tests do ' - 'not try to access a database. ' + help='A module path to a callable accepting two ' + 'arguments (tests, runner) and returning a ' + 'context object (with `setup` and `teardown` ' + 'methods) to be used while running non-database ' + 'tests. This is useful, for example, to enforce ' + 'that non-database tests do not try to access a ' + 'database. The db-test-context will be used for ' + 'all tests if this option is not specified. ' '[NOSE_NON_DB_TEST_CONTEXT]') def configure(self, options, conf): """Configure plugin, reading the with_fixture_bundling option.""" - super(TestReorderer, self).configure(options, conf) + super(DatabaseSetUpPlugin, self).configure(options, conf) self.should_bundle = options.with_fixture_bundling + self.db_test_context_path = options.db_test_context self.non_db_test_context_path = options.non_db_test_context def _group_test_cases_by_type(self, test): @@ -295,26 +313,67 @@ def _bundle_fixtures(self, fftc_tests): def prepareTest(self, test): """Reorder the tests.""" test_groups = self._group_test_cases_by_type(test) + suites = [] if self.non_db_test_context_path and self.NON_DB_TESTS in test_groups: - non_db_tests = test_groups[self.NON_DB_TESTS] - context = get_non_db_test_context( - self.non_db_test_context_path, non_db_tests) - test_groups[self.NON_DB_TESTS] = [ - ContextSuite(non_db_tests, context) - ] + # setup context for non-database tests + non_db_tests = test_groups.pop(self.NON_DB_TESTS) + context = get_test_context( + self.non_db_test_context_path, non_db_tests, self.runner) + suites.append(ContextSuite(non_db_tests, context)) if self.should_bundle and self.FFTC_TESTS in test_groups: fftc_tests = test_groups[self.FFTC_TESTS] test_groups[self.FFTC_TESTS] = self._bundle_fixtures(fftc_tests) - return ContextSuite([test - for key, group in sorted(test_groups.items()) - for test in group]) + if test_groups: + db_tests = [test + for key, group in sorted(test_groups.items()) + for test in group] + context = get_test_context( + self.db_test_context_path, db_tests, self.runner) + suites.append(ContextSuite(db_tests, context)) + return suites[0] if len(suites) == 1 else ContextSuite(suites) -def get_non_db_test_context(context_path, tests): - """Make a test context for non-db tests""" + +def get_test_context(context_path, tests, runner): + """Make a test context + + Lookup context constructor and call it with the given list of + tests and runner. + + Returns the context + """ + from django_nose.runner import import_module module_path, context_name = context_path.rsplit(".", 1) - module = __import__(module_path, globals(), locals(), [context_name]) - return getattr(module, context_name)(tests) + module = import_module(module_path) + return getattr(module, context_name)(tests, runner) + + +class DatabaseContext(object): + """A context that performs standard Django database setup/teardown""" + + def __init__(self, tests, runner): + self.runner = runner + + def setup(self): + """Setup database.""" + self.old_names = self.runner.setup_databases() + + def teardown(self): + """Tear down database.""" + self.runner.teardown_databases(self.old_names) + + +class NullContext(object): + """A context that does nothing""" + + def __init__(self, tests, runner): + pass + + def setup(self): + pass + + def teardown(self): + pass diff --git a/django_nose/runner.py b/django_nose/runner.py index e624db2..df547e3 100644 --- a/django_nose/runner.py +++ b/django_nose/runner.py @@ -42,7 +42,7 @@ import nose.core -from django_nose.plugin import DjangoSetUpPlugin, ResultPlugin, TestReorderer +from django_nose.plugin import DjangoSetUpPlugin, ResultPlugin, DatabaseSetUpPlugin from django_nose.utils import uses_mysql try: @@ -90,8 +90,7 @@ def _get_test_db_name(self): def _get_plugins_from_settings(): - plugins = (list(getattr(settings, 'NOSE_PLUGINS', [])) + - ['django_nose.plugin.TestReorderer']) + plugins = list(getattr(settings, 'NOSE_PLUGINS', [])) for plug_path in plugins: try: dot = plug_path.rindex('.') @@ -285,13 +284,13 @@ class BasicNoseRunner(BaseRunner): def run_suite(self, nose_argv): """Run the test suite.""" result_plugin = ResultPlugin() - plugins_to_add = [DjangoSetUpPlugin(self), - result_plugin, - TestReorderer()] + plugins_to_add = [DjangoSetUpPlugin(self), result_plugin] for plugin in _get_plugins_from_settings(): plugins_to_add.append(plugin) + plugins_to_add.append(DatabaseSetUpPlugin(self)) + try: django.setup() except AttributeError: From 3d5aa62e44e6f101db3fbf4a0941e35ccc075f8d Mon Sep 17 00:00:00 2001 From: Daniel Miller Date: Tue, 4 Aug 2015 21:52:21 -0400 Subject: [PATCH 05/10] Python 2.6 and flake8 --- django_nose/plugin.py | 42 +++++++++++++++++++++++++----------------- django_nose/runner.py | 3 ++- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/django_nose/plugin.py b/django_nose/plugin.py index 79f0517..a7aa5c1 100644 --- a/django_nose/plugin.py +++ b/django_nose/plugin.py @@ -152,13 +152,15 @@ def options(self, parser, env): parser.add_option('--db-test-context', dest='db_test_context', default=env.get('NOSE_DB_TEST_CONTEXT', - 'django_nose.plugin.DatabaseContext'), + 'django_nose.plugin.DatabaseContext' + ), help='A module path to a callable accepting two ' 'arguments (tests, runner) and returning a ' 'context object (with `setup` and `teardown` ' 'methods) to be used while running database ' - 'tests. This is useful, for example, to perform ' - 'custom database setup/teardown. Defaults to ' + 'tests. This is useful, for example, to ' + 'perform custom database setup/teardown. ' + 'Defaults to ' 'django_nose.plugin.DatabaseContext. ' '[NOSE_DB_TEST_CONTEXT]') parser.add_option('--non-db-test-context', @@ -167,12 +169,12 @@ def options(self, parser, env): help='A module path to a callable accepting two ' 'arguments (tests, runner) and returning a ' 'context object (with `setup` and `teardown` ' - 'methods) to be used while running non-database ' - 'tests. This is useful, for example, to enforce ' - 'that non-database tests do not try to access a ' - 'database. The db-test-context will be used for ' - 'all tests if this option is not specified. ' - '[NOSE_NON_DB_TEST_CONTEXT]') + 'methods) to be used while running non-' + 'database tests. This is useful, for example, ' + 'to enforce that non-database tests do not try ' + 'to access a database. The db-test-context ' + 'will be used for all tests if this option is ' + 'not specified. [NOSE_NON_DB_TEST_CONTEXT]') def configure(self, options, conf): """Configure plugin, reading the with_fixture_bundling option.""" @@ -182,7 +184,7 @@ def configure(self, options, conf): self.non_db_test_context_path = options.non_db_test_context def _group_test_cases_by_type(self, test): - """Group test suite by test type + """Group test suite by test type. Test types: @@ -251,8 +253,8 @@ def filthiness(test): flattened = [] process_tests(test, flattened.append) flattened.sort(key=filthiness) - return {key: list(group) - for key, group in groupby(flattened, filthiness)} + return dict((key, list(group)) + for key, group in groupby(flattened, filthiness)) def _bundle_fixtures(self, fftc_tests): """Reorder tests to minimize fixture loading. @@ -327,9 +329,9 @@ def prepareTest(self, test): test_groups[self.FFTC_TESTS] = self._bundle_fixtures(fftc_tests) if test_groups: - db_tests = [test + db_tests = [test_ for key, group in sorted(test_groups.items()) - for test in group] + for test_ in group] context = get_test_context( self.db_test_context_path, db_tests, self.runner) suites.append(ContextSuite(db_tests, context)) @@ -338,7 +340,7 @@ def prepareTest(self, test): def get_test_context(context_path, tests, runner): - """Make a test context + """Make a test context. Lookup context constructor and call it with the given list of tests and runner. @@ -352,9 +354,11 @@ def get_test_context(context_path, tests, runner): class DatabaseContext(object): - """A context that performs standard Django database setup/teardown""" + + """A context that performs standard Django database setup/teardown.""" def __init__(self, tests, runner): + """Initialize database context.""" self.runner = runner def setup(self): @@ -367,13 +371,17 @@ def teardown(self): class NullContext(object): - """A context that does nothing""" + + """A context that does nothing.""" def __init__(self, tests, runner): + """Initialize the context.""" pass def setup(self): + """Do setup.""" pass def teardown(self): + """Do teardown.""" pass diff --git a/django_nose/runner.py b/django_nose/runner.py index df547e3..e316c6b 100644 --- a/django_nose/runner.py +++ b/django_nose/runner.py @@ -42,7 +42,8 @@ import nose.core -from django_nose.plugin import DjangoSetUpPlugin, ResultPlugin, DatabaseSetUpPlugin +from django_nose.plugin import DjangoSetUpPlugin, ResultPlugin +from django_nose.plugin import DatabaseSetUpPlugin from django_nose.utils import uses_mysql try: From a4034f8df34dfe53a860fa0f3d500563053207ab Mon Sep 17 00:00:00 2001 From: Daniel Miller Date: Tue, 4 Aug 2015 22:51:01 -0400 Subject: [PATCH 06/10] Document custom database contexts --- AUTHORS.rst | 1 + changelog.rst | 4 ++++ docs/usage.rst | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 4b0e627..45cc98a 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -29,6 +29,7 @@ These non-maintainers have contributed code to a django-nose release: * Camilo Nova (`camilonova `_) * Carl Meyer (`carljm `_) * Conrado Buhrer (`conrado `_) +* Daniel Miller (`millerdev `_) * David Baumgold (`singingwolfboy `_) * David Cramer (`dcramer `_) * Dmitry Gladkov (`dgladkov `_) diff --git a/changelog.rst b/changelog.rst index b9fea48..1f3dfde 100644 --- a/changelog.rst +++ b/changelog.rst @@ -1,6 +1,10 @@ Changelog --------- +*Unreleased* +~~~~~~~~~~~~ +* Add support for custom database contexts. + 1.4.2 (2015-10-07) ~~~~~~~~~~~~~~~~~~ * Warn against using REUSE_DB=1 and FastFixtureTestCase in docs diff --git a/docs/usage.rst b/docs/usage.rst index ffbc1ca..a5a32db 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -150,6 +150,54 @@ course of loading tests. For example, if the tests that need it are in sure its DB table gets created. +Custom Database Contexts +------------------------ + +Django-nose can be instructed to use custom contexts for database and non- +database tests. What this means is that you can write a "context" class to do +custom setup and/or teardown for database tests (i.e., tests that extend +``TransactionTestCase`` or one of its subclasses) and/or non-database tests +(i.e., tests that do not extend ``TransactionTestCase``). This might mean +creating a new database that Django does not support or patching Django's +database machinery to assert that tests that should not access a database fail +if they try to do so. + +Custom contexts can be specified with the following options: + + NOSE_PLUGINS = [ + '--db-test-context=django_nose.plugin.DatabaseContext', + '--non-db-test-context=django_nose.plugin.NullContext', + ] + +While this example uses contexts that come with django-nose, you can of +course specify paths for your own custom context classes. A custom context +class might look something like this: + + class CustomContext(django_nose.plugin.DatabaseContext): + + """Setup/teardown custom database and standard Django databases.""" + + def setup(self): + """Setup database.""" + + # do custom database setup here + + super(CustomContext, self).setup() + + def teardown(self): + """Tear down database.""" + + # do custom database teardown here + + super(CustomContext, self).teardown() + +For each type of context, ``context.setup()`` is called once before the first +test of its type is run, and ``context.teardown()`` is called once after the +last test of its type is run. The database context will be used for all tests, +including "non-database" tests, unless ``--non-db-test-context=...`` is +specified. + + Assertions ---------- From b8568edc3b467c880044ff760b55a5a720b38089 Mon Sep 17 00:00:00 2001 From: Daniel Miller Date: Wed, 5 Aug 2015 11:00:43 -0400 Subject: [PATCH 07/10] Restore sys.stdout for db delete prompt --- django_nose/plugin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/django_nose/plugin.py b/django_nose/plugin.py index a7aa5c1..4505ef2 100644 --- a/django_nose/plugin.py +++ b/django_nose/plugin.py @@ -363,7 +363,13 @@ def __init__(self, tests, runner): def setup(self): """Setup database.""" - self.old_names = self.runner.setup_databases() + # temporarily restore sys.stdout in case of propmt to delete database + test_stdout = sys.stdout + sys.stdout = sys.__stdout__ + try: + self.old_names = self.runner.setup_databases() + finally: + sys.stdout = test_stdout def teardown(self): """Tear down database.""" From be176bafde4fa83031d1e7faccf7b385aa5b3ade Mon Sep 17 00:00:00 2001 From: Daniel Miller Date: Wed, 7 Oct 2015 11:08:52 -0400 Subject: [PATCH 08/10] Make DatabaseSetupPlugin work with other plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes “with profile plugin” tests --- django_nose/plugin.py | 24 +++++++++++++++++++++++- django_nose/runner.py | 10 ++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/django_nose/plugin.py b/django_nose/plugin.py index 4505ef2..33088c5 100644 --- a/django_nose/plugin.py +++ b/django_nose/plugin.py @@ -312,7 +312,11 @@ def _bundle_fixtures(self, fftc_tests): return tests - def prepareTest(self, test): + def prepareTestRunner(self, runner): + """Get a runner that reorders tests before running them.""" + return _DatabaseSetupTestRunner(self, runner) + + def group_by_database_setup(self, test): """Reorder the tests.""" test_groups = self._group_test_cases_by_type(test) suites = [] @@ -339,6 +343,24 @@ def prepareTest(self, test): return suites[0] if len(suites) == 1 else ContextSuite(suites) +class _DatabaseSetupTestRunner(object): + + """A test runner that groups tests by database setup. + + This is a helper class that reorders tests for efficient database + setup. It modifies the test suite before any other plugins have a + chance to wrap it in the `prepareTest` hook. + """ + + def __init__(self, plugin, real_runner): + self.plugin = plugin + self.runner = real_runner + + def run(self, test): + test = self.plugin.group_by_database_setup(test) + return self.runner.run(test) + + def get_test_context(context_path, tests, runner): """Make a test context. diff --git a/django_nose/runner.py b/django_nose/runner.py index e316c6b..e6adee2 100644 --- a/django_nose/runner.py +++ b/django_nose/runner.py @@ -42,8 +42,8 @@ import nose.core -from django_nose.plugin import DjangoSetUpPlugin, ResultPlugin from django_nose.plugin import DatabaseSetUpPlugin +from django_nose.plugin import DjangoSetUpPlugin, ResultPlugin from django_nose.utils import uses_mysql try: @@ -285,13 +285,15 @@ class BasicNoseRunner(BaseRunner): def run_suite(self, nose_argv): """Run the test suite.""" result_plugin = ResultPlugin() - plugins_to_add = [DjangoSetUpPlugin(self), result_plugin] + plugins_to_add = [ + DjangoSetUpPlugin(self), + DatabaseSetUpPlugin(self), + result_plugin, + ] for plugin in _get_plugins_from_settings(): plugins_to_add.append(plugin) - plugins_to_add.append(DatabaseSetUpPlugin(self)) - try: django.setup() except AttributeError: From 472adb3a91626f888066700cfa4741fedb2aee3e Mon Sep 17 00:00:00 2001 From: Daniel Miller Date: Tue, 20 Oct 2015 22:04:40 -0400 Subject: [PATCH 09/10] Add --db=(skip|only) for easy test selection Also fix missing django-nose options. --- django_nose/plugin.py | 22 ++++++++++++++++++++-- django_nose/runner.py | 2 ++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/django_nose/plugin.py b/django_nose/plugin.py index 33088c5..f5a08ae 100644 --- a/django_nose/plugin.py +++ b/django_nose/plugin.py @@ -6,6 +6,7 @@ from itertools import groupby from nose.plugins.base import Plugin +from nose.plugins.skip import SkipTest from nose.suite import ContextSuite from django.test.testcases import TransactionTestCase, TestCase @@ -149,6 +150,16 @@ def options(self, parser, env): help='Load a unique set of fixtures only once, even ' 'across test classes. ' '[NOSE_WITH_FIXTURE_BUNDLING]') + parser.add_option('--db', + dest='db_tests', + metavar='(skip|only)', + choices=['skip', 'only'], + default=env.get('NOSE_DB_TESTS'), + help='Skip or run only database tests. This option ' + 'accepts two values: "skip" database tests or ' + 'run "only" database tests. Both non-database ' + 'and database tests are run by default. ' + '[NOSE_DB_TESTS]') parser.add_option('--db-test-context', dest='db_test_context', default=env.get('NOSE_DB_TEST_CONTEXT', @@ -182,6 +193,8 @@ def configure(self, options, conf): self.should_bundle = options.with_fixture_bundling self.db_test_context_path = options.db_test_context self.non_db_test_context_path = options.non_db_test_context + self.skip_non_db_tests = options.db_tests == 'only' + self.skip_db_tests = options.db_tests == 'skip' def _group_test_cases_by_type(self, test): """Group test suite by test type. @@ -321,7 +334,10 @@ def group_by_database_setup(self, test): test_groups = self._group_test_cases_by_type(test) suites = [] - if self.non_db_test_context_path and self.NON_DB_TESTS in test_groups: + if self.skip_non_db_tests: + test_groups.pop(self.NON_DB_TESTS, None) + sys.__stdout__.write('skipped non-database tests\n') + elif self.non_db_test_context_path and self.NON_DB_TESTS in test_groups: # setup context for non-database tests non_db_tests = test_groups.pop(self.NON_DB_TESTS) context = get_test_context( @@ -332,7 +348,9 @@ def group_by_database_setup(self, test): fftc_tests = test_groups[self.FFTC_TESTS] test_groups[self.FFTC_TESTS] = self._bundle_fixtures(fftc_tests) - if test_groups: + if self.skip_db_tests: + sys.__stdout__.write('skipped database tests (and setup)\n') + elif test_groups: db_tests = [test_ for key, group in sorted(test_groups.items()) for test_ in group] diff --git a/django_nose/runner.py b/django_nose/runner.py index e6adee2..38745e4 100644 --- a/django_nose/runner.py +++ b/django_nose/runner.py @@ -121,6 +121,7 @@ def _get_options(): cfg_files = nose.core.all_config_files() manager = nose.core.DefaultPluginManager() config = nose.core.Config(env=os.environ, files=cfg_files, plugins=manager) + config.plugins.addPlugin(DatabaseSetUpPlugin(None)) config.plugins.addPlugins(list(_get_plugins_from_settings())) options = config.getParser()._get_all_options() @@ -195,6 +196,7 @@ def add_arguments(cls, parser): manager = nose.core.DefaultPluginManager() config = nose.core.Config( env=os.environ, files=cfg_files, plugins=manager) + config.plugins.addPlugin(DatabaseSetUpPlugin(None)) config.plugins.addPlugins(list(_get_plugins_from_settings())) options = config.getParser()._get_all_options() From e581fd72bf55f9c0a42b871f570d7567fa4894a9 Mon Sep 17 00:00:00 2001 From: Daniel Miller Date: Mon, 22 Feb 2016 10:45:32 -0500 Subject: [PATCH 10/10] Add support for database tests in context suites Make it possible to use module-level setup/teardown functions in modules with database tests. --- django_nose/plugin.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/django_nose/plugin.py b/django_nose/plugin.py index f5a08ae..a0d3106 100644 --- a/django_nose/plugin.py +++ b/django_nose/plugin.py @@ -4,6 +4,7 @@ import sys from itertools import groupby +from types import ModuleType from nose.plugins.base import Plugin from nose.plugins.skip import SkipTest @@ -231,18 +232,18 @@ def filthiness(test): because the odd behavior of TransactionTestCase is documented, so subclasses should by default be assumed to preserve it. + If the given test is a suite of tests with setup and/or teardown + routines in a "context" such as a module or package defining + fixture functions, then the entire suite will be assigned the value + of the filthiest test in the suite. + Thus, things will get these comparands (and run in this order): * 0: Not a subclass of TransactionTestCase. These should not hit the DB or, if they do, are responsible for ensuring that it's clean (as per https://docs.djangoproject.com/en/dev/topics/testing/?from= - olddocs#writing-doctests). Note that this could include a - group of tests (including TransactionTestCase tests) with - setup and/or teardown routines in a "context" such as a - module or package defining fixture functions. To avoid that - scenario, don't use TestCase or TransactionTestCase in - modules or packages with setup or teardown functions. + olddocs#writing-doctests). * 1: FastFixtureTestCase subclasses. If you're using the FixtureBundlingPlugin, it will pull the FFTCs out, reorder them, and run them before the following groups. @@ -261,6 +262,23 @@ def filthiness(test): getattr(test_class, 'cleans_up_after_itself', False)): return self.CLEAN_TESTS return self.DIRTY_TESTS + if isinstance(test, ContextSuite) and \ + isinstance(test_class, ModuleType): + # Get value of filthiest test in module with setup/teardown. + # It should be safe to iterate over tests in a module because + # modules should not change state as tests are generated. + # However, other test types such as test generator functions + # may not be safe to iterate. + filth = set(filthiness(t) for t in test) + if len(filth) == 1: + return filth.pop() + for const in [ + self.DIRTY_TESTS, + self.CLEAN_TESTS, + self.FFTC_TESTS, # will be bundled in remainder bucket + ]: + if const in filth: + return const return self.NON_DB_TESTS flattened = []