2026-03-21 23:08:21 -04:00
|
|
|
import pickle
|
2026-03-12 14:23:09 -04:00
|
|
|
import time
|
2026-03-23 22:56:10 -04:00
|
|
|
from django.test.runner import DiscoverRunner, ParallelTestSuite, RemoteTestResult, RemoteTestRunner
|
2026-03-21 23:08:21 -04:00
|
|
|
|
|
|
|
|
|
2026-03-23 22:56:10 -04:00
|
|
|
class _Py313SafeRemoteTestResult(RemoteTestResult):
|
2026-03-21 23:08:21 -04:00
|
|
|
"""Prevents Python 3.13 'cannot pickle traceback' crash in parallel workers.
|
|
|
|
|
|
|
|
|
|
In Python 3.13, traceback objects are not picklable. Django's parallel
|
2026-03-23 22:56:10 -04:00
|
|
|
runner serialises test errors via multiprocessing (pickle) to send them
|
|
|
|
|
from worker subprocesses back to the main process. When a Selenium test
|
|
|
|
|
crashes the browser, the resulting exception traceback fails to pickle,
|
|
|
|
|
aborting the entire parallel run with a confusing TypeError.
|
2026-03-21 23:08:21 -04:00
|
|
|
|
|
|
|
|
Fix: strip the traceback (replace with None) before it reaches pickle.
|
|
|
|
|
Error type and message are preserved; only the traceback is lost in transit.
|
2026-03-23 22:56:10 -04:00
|
|
|
|
|
|
|
|
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.
|
2026-03-21 23:08:21 -04:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
2026-03-23 22:56:10 -04:00
|
|
|
def _safe_err(err):
|
2026-03-21 23:08:21 -04:00
|
|
|
exc_type, exc_value, exc_tb = err
|
|
|
|
|
try:
|
|
|
|
|
pickle.dumps((exc_type, exc_value, exc_tb))
|
|
|
|
|
except (TypeError, AttributeError):
|
|
|
|
|
return (exc_type, exc_value, None)
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
def addError(self, test, err):
|
2026-03-23 22:56:10 -04:00
|
|
|
super().addError(test, self._safe_err(err))
|
2026-03-21 23:08:21 -04:00
|
|
|
|
|
|
|
|
def addFailure(self, test, err):
|
2026-03-23 22:56:10 -04:00
|
|
|
super().addFailure(test, self._safe_err(err))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _Py313SafeRemoteTestRunner(RemoteTestRunner):
|
|
|
|
|
resultclass = _Py313SafeRemoteTestResult
|
2026-03-21 23:08:21 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class _SafeParallelTestSuite(ParallelTestSuite):
|
|
|
|
|
runner_class = _Py313SafeRemoteTestRunner
|
2026-03-12 14:23:09 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class RobustCompressorTestRunner(DiscoverRunner):
|
2026-03-21 23:08:21 -04:00
|
|
|
parallel_test_suite = _SafeParallelTestSuite
|
|
|
|
|
|
2026-03-12 14:23:09 -04:00
|
|
|
def setup_test_environment(self, **kwargs):
|
|
|
|
|
super().setup_test_environment(**kwargs)
|
|
|
|
|
from compressor.storage import CompressorFileStorage
|
|
|
|
|
_orig_save = CompressorFileStorage.save
|
|
|
|
|
def _robust_save(self, name, content):
|
|
|
|
|
for _ in range(5):
|
|
|
|
|
try: return _orig_save(self, name, content)
|
|
|
|
|
except PermissionError: time.sleep(0.05)
|
|
|
|
|
raise
|
|
|
|
|
CompressorFileStorage.save = _robust_save
|