Opened 37 minutes ago
#37459 new Uncategorized
Parallel test runner crashes with MaybeEncodingError when a subTest fails in a test case with unpicklable attributes
| Reported by: | Oscar van Leusen | Owned by: | |
|---|---|---|---|
| Component: | Testing framework | Version: | 6.1 |
| Severity: | Normal | Keywords: | |
| Cc: | Oscar van Leusen | Triage Stage: | Unreviewed |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
Summary
With --parallel, a failing subTest in a test case that has any unpicklable instance attribute (for example, a MagicMock or a test Client assigned in setUp) aborts the whole run with multiprocessing.pool.MaybeEncodingError: Error sending result, instead of reporting the failure.
RemoteTestResult.check_subtest_picklable() runs while the test is executing. At that point SimpleTestCase.__getstate__ filters out unpicklable attributes, because self._outcome is set. The events are pickled again when the worker sends its results, after the test has finished. By then unittest has reset _outcome to None, so __getstate__ returns the full __dict__ and pickling fails. The check passes on different state from what is actually sent.
Storing mocks or extra clients on self in setUp is common, so any subtest failure in such a test crashes the parallel run. The built-in self.client is unaffected only because _pre_setup() assigns it on the class.
Minimal Reproduction
Reproduced on 6.1.2 and 5.2.18, Python 3.12, with both the spawn and fork start methods. Requires tblib (pip install django tblib)
settings.py:
SECRET_KEY = "x"
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}
tests.py:
from unittest.mock import MagicMock
from django.test import SimpleTestCase
class SubTestWithMock(SimpleTestCase):
def setUp(self):
self.mock = MagicMock()
def test_failing_subtest(self):
with self.subTest(i=1):
self.fail("expected failure")
class Other(SimpleTestCase): # A second class so the run actually uses workers
def test_ok(self):
pass
Run with: DJANGO_SETTINGS_MODULE=settings PYTHONPATH=. python -m django test tests --parallel 2
Expected: FAILED (failures=1).
Actual: the run aborts with
multiprocessing.pool.MaybeEncodingError: Error sending result: ...
Reason: 'PicklingError("Can't pickle <class 'unittest.mock.MagicMock'>: it's not the same object as unittest.mock.MagicMock")'
Workaround
With --parallel 1, or without including an un-pickleable attribute, the failure is reported normally.