diff --git a/django/test/runner.py b/django/test/runner.py index 02a4c826ac..0610d303f8 100644 --- a/django/test/runner.py +++ b/django/test/runner.py @@ -77,6 +77,9 @@ class RemoteTestResult(object): """ def __init__(self): + if tblib is not None: + tblib.pickling_support.install() + self.events = [] self.failfast = False self.shouldStop = False @@ -86,6 +89,22 @@ class RemoteTestResult(object): def test_index(self): return self.testsRun - 1 + def _print_unpicklable_subtest(self, test, subtest, pickle_exc): + print(""" +Subtest failed: + + test: {} + subtest: {} + +Unfortunately, the subtest that failed cannot be pickled, so the parallel +test runner cannot handle it cleanly. Here is the pickling error: + +> {} + +You should re-run this test with --parallel=1 to reproduce the failure +with a cleaner failure message. +""".format(test, subtest, pickle_exc)) + def check_picklable(self, test, err): # Ensure that sys.exc_info() tuples are picklable. This displays a # clear multiprocessing.pool.RemoteTraceback generated in the child @@ -133,6 +152,13 @@ failure and get a correct traceback. """.format(test, original_exc_txt, pickle_exc_txt)) raise + def check_subtest_picklable(self, test, subtest): + try: + pickle.dumps(subtest) + except Exception as exc: + self._print_unpicklable_subtest(test, subtest, exc) + raise + def stop_if_failfast(self): if self.failfast: self.stop() @@ -164,7 +190,15 @@ failure and get a correct traceback. self.stop_if_failfast() def addSubTest(self, test, subtest, err): - raise NotImplementedError("subtests aren't supported at this time") + # Follow Python 3.5's implementation of unittest.TestResult.addSubTest() + # by not doing anything when a subtest is successful. + if err is not None: + # Call check_picklable() before check_subtest_picklable() since + # check_picklable() performs the tblib check. + self.check_picklable(test, err) + self.check_subtest_picklable(test, subtest) + self.events.append(('addSubTest', self.test_index, subtest, err)) + self.stop_if_failfast() def addSuccess(self, test): self.events.append(('addSuccess', self.test_index)) @@ -307,9 +341,6 @@ class ParallelTestSuite(unittest.TestSuite): Even with tblib, errors may still occur for dynamically created exception classes such Model.DoesNotExist which cannot be unpickled. """ - if tblib is not None: - tblib.pickling_support.install() - counter = multiprocessing.Value(ctypes.c_int, 0) pool = multiprocessing.Pool( processes=self.processes, diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index 498b3ee09f..c8282f70d6 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -313,6 +313,9 @@ Tests ``django.test.runner``) and :func:`~django.test.utils.teardown_databases` functions make it easier to build custom test runners. +* Added support for :meth:`python:unittest.TestCase.subTest`’s when using the + :option:`test --parallel` option. + URLs ~~~~ diff --git a/tests/test_runner/test_parallel.py b/tests/test_runner/test_parallel.py new file mode 100644 index 0000000000..529fe22092 --- /dev/null +++ b/tests/test_runner/test_parallel.py @@ -0,0 +1,67 @@ +import unittest + +from django.test import SimpleTestCase +from django.test.runner import RemoteTestResult +from django.utils import six + +try: + import tblib +except ImportError: + tblib = None + + +class ParallelTestRunnerTest(SimpleTestCase): + """ + End-to-end tests of the parallel test runner. + + These tests are only meaningful when running tests in parallel using + the --parallel option, though it doesn't hurt to run them not in + parallel. + """ + + @unittest.skipUnless(six.PY3, 'subtests were added in Python 3.4') + def test_subtest(self): + """ + Check that passing subtests work. + """ + for i in range(2): + with self.subTest(index=i): + self.assertEqual(i, i) + + +class SampleFailingSubtest(SimpleTestCase): + + # This method name doesn't begin with "test" to prevent test discovery + # from seeing it. + def dummy_test(self): + """ + A dummy test for testing subTest failures. + """ + for i in range(3): + with self.subTest(index=i): + self.assertEqual(i, 1) + + +class RemoteTestResultTest(SimpleTestCase): + + @unittest.skipUnless(six.PY3 and tblib is not None, 'requires tblib to be installed') + def test_add_failing_subtests(self): + """ + Failing subtests are added correctly using addSubTest(). + """ + # Manually run a test with failing subtests to prevent the failures + # from affecting the actual test run. + result = RemoteTestResult() + subtest_test = SampleFailingSubtest(methodName='dummy_test') + subtest_test.run(result=result) + + events = result.events + self.assertEqual(len(events), 4) + + event = events[1] + self.assertEqual(event[0], 'addSubTest') + self.assertEqual(str(event[2]), 'dummy_test (test_runner.test_parallel.SampleFailingSubtest) (index=0)') + self.assertEqual(repr(event[3][1]), "AssertionError('0 != 1',)") + + event = events[2] + self.assertEqual(repr(event[3][1]), "AssertionError('2 != 1',)")