actually bubbles up original error w.o pickling TypeErrors wrapping it
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Disco DeDisco
2026-03-23 22:56:10 -04:00
parent c331e72de6
commit cc02419e8d

View File

@@ -1,22 +1,28 @@
import pickle import pickle
import time import time
from django.test.runner import DiscoverRunner, ParallelTestSuite, RemoteTestRunner from django.test.runner import DiscoverRunner, ParallelTestSuite, RemoteTestResult, RemoteTestRunner
class _Py313SafeRemoteTestRunner(RemoteTestRunner): class _Py313SafeRemoteTestResult(RemoteTestResult):
"""Prevents Python 3.13 'cannot pickle traceback' crash in parallel workers. """Prevents Python 3.13 'cannot pickle traceback' crash in parallel workers.
In Python 3.13, traceback objects are not picklable. Django's parallel In Python 3.13, traceback objects are not picklable. Django's parallel
runner tries to pickle test errors to pass them back to the main process. runner serialises test errors via multiprocessing (pickle) to send them
When a Selenium test crashes the browser, the resulting exception traceback from worker subprocesses back to the main process. When a Selenium test
fails to pickle, aborting the entire parallel run with a confusing TypeError. crashes the browser, the resulting exception traceback fails to pickle,
aborting the entire parallel run with a confusing TypeError.
Fix: strip the traceback (replace with None) before it reaches pickle. Fix: strip the traceback (replace with None) before it reaches pickle.
Error type and message are preserved; only the traceback is lost in transit. Error type and message are preserved; only the traceback is lost in transit.
Note: the fix must be on RemoteTestResult (not RemoteTestRunner) because
addError/addFailure — where check_picklable is called — live on the result
object, not the runner. RemoteTestRunner.run() instantiates resultclass()
and passes it to the test suite; overriding it on the runner has no effect.
""" """
@staticmethod @staticmethod
def _strip_tb_if_needed(err): def _safe_err(err):
exc_type, exc_value, exc_tb = err exc_type, exc_value, exc_tb = err
try: try:
pickle.dumps((exc_type, exc_value, exc_tb)) pickle.dumps((exc_type, exc_value, exc_tb))
@@ -25,10 +31,14 @@ class _Py313SafeRemoteTestRunner(RemoteTestRunner):
return err return err
def addError(self, test, err): def addError(self, test, err):
super().addError(test, self._strip_tb_if_needed(err)) super().addError(test, self._safe_err(err))
def addFailure(self, test, err): def addFailure(self, test, err):
super().addFailure(test, self._strip_tb_if_needed(err)) super().addFailure(test, self._safe_err(err))
class _Py313SafeRemoteTestRunner(RemoteTestRunner):
resultclass = _Py313SafeRemoteTestResult
class _SafeParallelTestSuite(ParallelTestSuite): class _SafeParallelTestSuite(ParallelTestSuite):