diff --git a/django/test/runner.py b/django/test/runner.py index 0610d303f8..8b4c608c08 100644 --- a/django/test/runner.py +++ b/django/test/runner.py @@ -89,6 +89,14 @@ class RemoteTestResult(object): def test_index(self): return self.testsRun - 1 + def _confirm_picklable(self, obj): + """ + Confirm that obj can be pickled and unpickled as multiprocessing will + need to pickle the exception in the child process and unpickle it in + the parent process. Let the exception rise, if not. + """ + pickle.loads(pickle.dumps(obj)) + def _print_unpicklable_subtest(self, test, subtest, pickle_exc): print(""" Subtest failed: @@ -113,7 +121,7 @@ with a cleaner failure message. # with the multiprocessing module. Since we're in a forked process, # our best chance to communicate with them is to print to stdout. try: - pickle.dumps(err) + self._confirm_picklable(err) except Exception as exc: original_exc_txt = repr(err[1]) original_exc_txt = textwrap.fill(original_exc_txt, 75, initial_indent=' ', subsequent_indent=' ') @@ -154,7 +162,7 @@ failure and get a correct traceback. def check_subtest_picklable(self, test, subtest): try: - pickle.dumps(subtest) + self._confirm_picklable(subtest) except Exception as exc: self._print_unpicklable_subtest(test, subtest, exc) raise diff --git a/tests/test_runner/test_parallel.py b/tests/test_runner/test_parallel.py index 529fe22092..8118734994 100644 --- a/tests/test_runner/test_parallel.py +++ b/tests/test_runner/test_parallel.py @@ -10,6 +10,15 @@ except ImportError: tblib = None +class ExceptionThatFailsUnpickling(Exception): + """ + After pickling, this class fails unpickling with an error about incorrect + arguments passed to __init__(). + """ + def __init__(self, arg): + super(ExceptionThatFailsUnpickling, self).__init__() + + class ParallelTestRunnerTest(SimpleTestCase): """ End-to-end tests of the parallel test runner. @@ -44,6 +53,19 @@ class SampleFailingSubtest(SimpleTestCase): class RemoteTestResultTest(SimpleTestCase): + def test_pickle_errors_detection(self): + picklable_error = RuntimeError('This is fine') + not_unpicklable_error = ExceptionThatFailsUnpickling('arg') + + result = RemoteTestResult() + result._confirm_picklable(picklable_error) + + msg = '__init__() missing 1 required positional argument' + if six.PY2: + msg = '__init__() takes exactly 2 arguments (1 given)' + with self.assertRaisesMessage(TypeError, msg): + result._confirm_picklable(not_unpicklable_error) + @unittest.skipUnless(six.PY3 and tblib is not None, 'requires tblib to be installed') def test_add_failing_subtests(self): """