Skip to content

Commit a52be73

Browse files
committed
gh-160543: Isolate cleanup stacks in parallel test workers
Shallow-copying a TestCase also shares its mutable _cleanups list, allowing worker threads to consume callbacks registered by other workers. Copy the cleanup stack separately for each worker while preserving callbacks registered before the run.
1 parent dec26ce commit a52be73

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

‎Lib/test/libregrtest/parallel_case.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ def run(self, result=None):
3636
result.startTest(self)
3737

3838
cases = [copy.copy(self.test_case) for _ in range(self.num_threads)]
39+
for case in cases:
40+
# Each worker must run its own cleanups, not another worker's.
41+
case._cleanups = case._cleanups.copy()
3942
results = [unittest.TestResult() for _ in range(self.num_threads)]
4043

4144
barrier = threading.Barrier(self.num_threads)

‎Lib/test/test_regrtest.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@
2929

3030
from test.libregrtest.findtests import collect_cases
3131
from test.libregrtest.filter import set_match_tests
32+
from test.libregrtest.parallel_case import ParallelTestCase
3233
from test.libregrtest.run_workers import MultiprocessIterator
3334
from test import support
3435
from test.support import import_helper
3536
from test.support import os_helper
37+
from test.support import threading_helper
3638
from test.libregrtest import cmdline
3739
from test.libregrtest import main
3840
from test.libregrtest import setup
@@ -2747,6 +2749,61 @@ def test_collect_cases_skiptest(self):
27472749
self.assertIn(testname, skipped)
27482750

27492751

2752+
@threading_helper.requires_working_threading()
2753+
class ParallelTestCaseTestCase(unittest.TestCase):
2754+
def test_cleanups_are_thread_local(self):
2755+
num_threads = 4
2756+
barrier = threading.Barrier(num_threads)
2757+
cases = []
2758+
lock = threading.Lock()
2759+
2760+
class Test(unittest.TestCase):
2761+
def setUp(self):
2762+
self.cleanup_calls = []
2763+
with lock:
2764+
cases.append(self)
2765+
thread = threading.get_ident()
2766+
self.addCleanup(self.cleanup, thread, 1)
2767+
self.addCleanup(self.cleanup, thread, 2)
2768+
2769+
def cleanup(self, thread, value):
2770+
self.assertEqual(threading.get_ident(), thread)
2771+
self.cleanup_calls.append(value)
2772+
2773+
def test(self):
2774+
# All workers must register their cleanups before any runs them.
2775+
barrier.wait(timeout=support.SHORT_TIMEOUT)
2776+
2777+
case = Test('test')
2778+
result = unittest.TestResult()
2779+
ParallelTestCase(case, num_threads).run(result)
2780+
self.assertTrue(result.wasSuccessful(), (result.errors, result.failures))
2781+
self.assertEqual(result.testsRun, 1)
2782+
self.assertEqual(len(cases), num_threads)
2783+
self.assertEqual(len({id(case._cleanups) for case in cases}), num_threads)
2784+
for worker_case in cases:
2785+
self.assertEqual(worker_case.cleanup_calls, [2, 1])
2786+
self.assertEqual(worker_case._cleanups, [])
2787+
self.assertEqual(case._cleanups, [])
2788+
2789+
def test_existing_cleanups_are_preserved(self):
2790+
num_threads = 4
2791+
cleanup_calls = []
2792+
case = unittest.FunctionTestCase(lambda: None)
2793+
case.addCleanup(cleanup_calls.append, 1)
2794+
case.addCleanup(cleanup_calls.append, 2)
2795+
cleanups = case._cleanups.copy()
2796+
2797+
for _ in range(2):
2798+
cleanup_calls.clear()
2799+
result = unittest.TestResult()
2800+
ParallelTestCase(case, num_threads).run(result)
2801+
self.assertTrue(result.wasSuccessful(),
2802+
(result.errors, result.failures))
2803+
self.assertCountEqual(cleanup_calls, [1, 2] * num_threads)
2804+
self.assertEqual(case._cleanups, cleanups)
2805+
2806+
27502807
class MultiprocessIteratorTestCase(unittest.TestCase):
27512808
def test_yields_all_groups_once(self):
27522809
groups = [("mod_a", ("mod_a.A.t1", "mod_a.A.t2")),

0 commit comments

Comments
 (0)