summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Wobrock <david.wobrock@gmail.com>2023-12-27 17:02:27 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2024-02-21 12:04:03 +0100
commitc09e8f5fd8f977bf16e9ec5d11b370151fc81ea8 (patch)
tree8f001d9a2a2beb792cbfb3a25dbebf99a43c8165
parenta269d8d1d8cec2f9ae7ecc0c97a41fcf9efee580 (diff)
Fixed #32114 -- Fixed parallel test crash on non-picklable objects in subtests.
-rw-r--r--django/test/testcases.py30
-rw-r--r--tests/test_runner/test_parallel.py17
-rw-r--r--tests/test_utils/test_testcase.py10
3 files changed, 56 insertions, 1 deletions
diff --git a/django/test/testcases.py b/django/test/testcases.py
index bf035bd531..911a2d50ac 100644
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -1,6 +1,7 @@
import difflib
import json
import logging
+import pickle
import posixpath
import sys
import threading
@@ -92,6 +93,18 @@ def to_list(value):
return value
+def is_pickable(obj):
+ """
+ Returns true if the object can be dumped and loaded through the pickle
+ module.
+ """
+ try:
+ pickle.loads(pickle.dumps(obj))
+ except (AttributeError, TypeError):
+ return False
+ return True
+
+
def assert_and_parse_html(self, html, user_msg, msg):
try:
dom = parse_html(html)
@@ -303,6 +316,23 @@ class SimpleTestCase(unittest.TestCase):
"""
self._setup_and_call(result)
+ def __getstate__(self):
+ """
+ Make SimpleTestCase picklable for parallel tests using subtests.
+ """
+ state = super().__dict__
+ # _outcome and _subtest cannot be tested on picklability, since they
+ # contain the TestCase itself, leading to an infinite recursion.
+ if state["_outcome"]:
+ pickable_state = {"_outcome": None, "_subtest": None}
+ for key, value in state.items():
+ if key in pickable_state or not is_pickable(value):
+ continue
+ pickable_state[key] = value
+ return pickable_state
+
+ return state
+
def debug(self):
"""Perform the same as __call__(), without catching the exception."""
debug_result = _DebugResult()
diff --git a/tests/test_runner/test_parallel.py b/tests/test_runner/test_parallel.py
index e83f53bf4e..8f7bfd5cf8 100644
--- a/tests/test_runner/test_parallel.py
+++ b/tests/test_runner/test_parallel.py
@@ -51,6 +51,13 @@ class SampleFailingSubtest(SimpleTestCase):
with self.subTest(index=i):
self.assertEqual(i, 1)
+ # This method name doesn't begin with "test" to prevent test discovery
+ # from seeing it.
+ def pickle_error_test(self):
+ with self.subTest("TypeError: cannot pickle memoryview object"):
+ self.x = memoryview(b"")
+ self.fail("expected failure")
+
class RemoteTestResultTest(SimpleTestCase):
def _test_error_exc_info(self):
@@ -106,6 +113,16 @@ class RemoteTestResultTest(SimpleTestCase):
with self.assertRaisesMessage(TypeError, msg):
result._confirm_picklable(not_unpicklable_error)
+ def test_unpicklable_subtest(self):
+ result = RemoteTestResult()
+ subtest_test = SampleFailingSubtest(methodName="pickle_error_test")
+ subtest_test.run(result=result)
+
+ events = result.events
+ subtest_event = events[1]
+ assertion_error = subtest_event[3]
+ self.assertEqual(str(assertion_error[1]), "expected failure")
+
@unittest.skipUnless(tblib is not None, "requires tblib to be installed")
def test_add_failing_subtests(self):
"""
diff --git a/tests/test_utils/test_testcase.py b/tests/test_utils/test_testcase.py
index eb6ca80036..0f41f29a23 100644
--- a/tests/test_utils/test_testcase.py
+++ b/tests/test_utils/test_testcase.py
@@ -1,12 +1,20 @@
+import pickle
from functools import wraps
from django.db import IntegrityError, connections, transaction
from django.test import TestCase, skipUnlessDBFeature
-from django.test.testcases import DatabaseOperationForbidden, TestData
+from django.test.testcases import DatabaseOperationForbidden, SimpleTestCase, TestData
from .models import Car, Person, PossessedCar
+class TestSimpleTestCase(SimpleTestCase):
+ def test_is_picklable_with_non_picklable_properties(self):
+ """ParallelTestSuite requires that all TestCases are picklable."""
+ self.non_picklable = lambda: 0
+ self.assertEqual(self, pickle.loads(pickle.dumps(self)))
+
+
class TestTestCase(TestCase):
@skipUnlessDBFeature("can_defer_constraint_checks")
@skipUnlessDBFeature("supports_foreign_keys")