Skip to content

Commit 128e7f0

Browse files
committed
Fix test hang by restoring __main__ state in TestHandlePreload
TestHandlePreload tests call spawn.import_main_path() which modifies sys.modules['__main__'] and appends to spawn.old_main_modules. This state persisted after tests, causing subsequent forkserver tests to try loading __main__ from a deleted temp file. With on_error='ignore', the forkserver stayed broken causing process spawn failures and hangs. Fix by adding setUp/tearDown to TestHandlePreload that saves and restores sys.modules['__main__'] and clears spawn.old_main_modules. Also add suppress_forkserver_stderr() context manager that injects a stderr-suppressing module via the preload mechanism itself, avoiding noisy output during tests that expect import failures. Thanks to Duane Griffin for identifying the root cause of the hang.
1 parent 5a8bfc6 commit 128e7f0

1 file changed

Lines changed: 52 additions & 24 deletions

File tree

Lib/test/test_multiprocessing_forkserver/test_preload.py

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""Tests for forkserver preload functionality."""
22

3+
import contextlib
34
import multiprocessing
5+
import os
6+
import shutil
47
import sys
58
import tempfile
69
import unittest
7-
from multiprocessing import forkserver
10+
from multiprocessing import forkserver, spawn
811

912

1013
class TestForkserverPreload(unittest.TestCase):
@@ -33,6 +36,20 @@ def _send_value(conn, value):
3336
"""Send value through connection. Static method to be picklable as Process target."""
3437
conn.send(value)
3538

39+
@contextlib.contextmanager
40+
def suppress_forkserver_stderr(self):
41+
"""Suppress stderr in forkserver by preloading a module that redirects it."""
42+
tmpdir = tempfile.mkdtemp()
43+
suppress_file = os.path.join(tmpdir, '_suppress_stderr.py')
44+
try:
45+
with open(suppress_file, 'w') as f:
46+
f.write('import sys, os; sys.stderr = open(os.devnull, "w")\n')
47+
sys.path.insert(0, tmpdir)
48+
yield '_suppress_stderr'
49+
finally:
50+
sys.path.remove(tmpdir)
51+
shutil.rmtree(tmpdir, ignore_errors=True)
52+
3653
def test_preload_on_error_ignore_default(self):
3754
"""Test that invalid modules are silently ignored by default."""
3855
self.ctx.set_forkserver_preload(['nonexistent_module_xyz'])
@@ -65,34 +82,38 @@ def test_preload_on_error_ignore_explicit(self):
6582

6683
def test_preload_on_error_warn(self):
6784
"""Test that invalid modules emit warnings with on_error='warn'."""
68-
self.ctx.set_forkserver_preload(['nonexistent_module_xyz'], on_error='warn')
85+
with self.suppress_forkserver_stderr() as suppress_mod:
86+
self.ctx.set_forkserver_preload(
87+
[suppress_mod, 'nonexistent_module_xyz'], on_error='warn')
6988

70-
r, w = self.ctx.Pipe(duplex=False)
71-
p = self.ctx.Process(target=self._send_value, args=(w, 123))
72-
p.start()
73-
w.close()
74-
result = r.recv()
75-
r.close()
76-
p.join()
89+
r, w = self.ctx.Pipe(duplex=False)
90+
p = self.ctx.Process(target=self._send_value, args=(w, 123))
91+
p.start()
92+
w.close()
93+
result = r.recv()
94+
r.close()
95+
p.join()
7796

78-
self.assertEqual(result, 123)
79-
self.assertEqual(p.exitcode, 0)
97+
self.assertEqual(result, 123)
98+
self.assertEqual(p.exitcode, 0)
8099

81100
def test_preload_on_error_fail_breaks_context(self):
82101
"""Test that invalid modules with on_error='fail' breaks the forkserver."""
83-
self.ctx.set_forkserver_preload(['nonexistent_module_xyz'], on_error='fail')
84-
85-
r, w = self.ctx.Pipe(duplex=False)
86-
try:
87-
p = self.ctx.Process(target=self._send_value, args=(w, 42))
88-
with self.assertRaises((EOFError, ConnectionError, BrokenPipeError)) as cm:
89-
p.start()
90-
notes = getattr(cm.exception, '__notes__', [])
91-
self.assertTrue(notes, "Expected exception to have __notes__")
92-
self.assertIn('Forkserver process may have crashed', notes[0])
93-
finally:
94-
w.close()
95-
r.close()
102+
with self.suppress_forkserver_stderr() as suppress_mod:
103+
self.ctx.set_forkserver_preload(
104+
[suppress_mod, 'nonexistent_module_xyz'], on_error='fail')
105+
106+
r, w = self.ctx.Pipe(duplex=False)
107+
try:
108+
p = self.ctx.Process(target=self._send_value, args=(w, 42))
109+
with self.assertRaises((EOFError, ConnectionError, BrokenPipeError)) as cm:
110+
p.start()
111+
notes = getattr(cm.exception, '__notes__', [])
112+
self.assertTrue(notes, "Expected exception to have __notes__")
113+
self.assertIn('Forkserver process may have crashed', notes[0])
114+
finally:
115+
w.close()
116+
r.close()
96117

97118
def test_preload_valid_modules_with_on_error_fail(self):
98119
"""Test that valid modules work fine with on_error='fail'."""
@@ -119,6 +140,13 @@ def test_preload_invalid_on_error_value(self):
119140
class TestHandlePreload(unittest.TestCase):
120141
"""Unit tests for _handle_preload() function."""
121142

143+
def setUp(self):
144+
self._saved_main = sys.modules['__main__']
145+
146+
def tearDown(self):
147+
spawn.old_main_modules.clear()
148+
sys.modules['__main__'] = self._saved_main
149+
122150
def test_handle_preload_main_on_error_fail(self):
123151
"""Test that __main__ import failures raise with on_error='fail'."""
124152
with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as f:

0 commit comments

Comments
 (0)