Skip to content

Commit 54f9336

Browse files
gpsheadclaude
andcommitted
Add stderr capture and assertions to forkserver preload tests
Replace suppress_forkserver_stderr() with capture_forkserver_stderr() that writes stderr to a temp file instead of /dev/null. This allows tests to assert on the actual warning/error messages. The capture module also enables ImportWarning via filterwarnings() since it's ignored by default in Python. Line buffering ensures output is flushed, and forkserver.main() calls _flush_std_streams() after preloading which guarantees content is written before we read. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 128e7f0 commit 54f9336

1 file changed

Lines changed: 33 additions & 10 deletions

File tree

Lib/test/test_multiprocessing_forkserver/test_preload.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,28 @@ def _send_value(conn, value):
3737
conn.send(value)
3838

3939
@contextlib.contextmanager
40-
def suppress_forkserver_stderr(self):
41-
"""Suppress stderr in forkserver by preloading a module that redirects it."""
40+
def capture_forkserver_stderr(self):
41+
"""Capture stderr from forkserver by preloading a module that redirects it.
42+
43+
Yields (module_name, capture_file_path). The capture file can be read
44+
after the forkserver has processed preloads. This works because
45+
forkserver.main() calls util._flush_std_streams() after preloading,
46+
ensuring captured output is written before we read it.
47+
"""
4248
tmpdir = tempfile.mkdtemp()
43-
suppress_file = os.path.join(tmpdir, '_suppress_stderr.py')
49+
capture_module = os.path.join(tmpdir, '_capture_stderr.py')
50+
capture_file = os.path.join(tmpdir, 'stderr.txt')
4451
try:
45-
with open(suppress_file, 'w') as f:
46-
f.write('import sys, os; sys.stderr = open(os.devnull, "w")\n')
52+
with open(capture_module, 'w') as f:
53+
# Use line buffering (buffering=1) to ensure warnings are written.
54+
# Enable ImportWarning since it's ignored by default.
55+
f.write(
56+
f'import sys, warnings; '
57+
f'sys.stderr = open({capture_file!r}, "w", buffering=1); '
58+
f'warnings.filterwarnings("always", category=ImportWarning)\n'
59+
)
4760
sys.path.insert(0, tmpdir)
48-
yield '_suppress_stderr'
61+
yield '_capture_stderr', capture_file
4962
finally:
5063
sys.path.remove(tmpdir)
5164
shutil.rmtree(tmpdir, ignore_errors=True)
@@ -82,9 +95,9 @@ def test_preload_on_error_ignore_explicit(self):
8295

8396
def test_preload_on_error_warn(self):
8497
"""Test that invalid modules emit warnings with on_error='warn'."""
85-
with self.suppress_forkserver_stderr() as suppress_mod:
98+
with self.capture_forkserver_stderr() as (capture_mod, stderr_file):
8699
self.ctx.set_forkserver_preload(
87-
[suppress_mod, 'nonexistent_module_xyz'], on_error='warn')
100+
[capture_mod, 'nonexistent_module_xyz'], on_error='warn')
88101

89102
r, w = self.ctx.Pipe(duplex=False)
90103
p = self.ctx.Process(target=self._send_value, args=(w, 123))
@@ -97,11 +110,16 @@ def test_preload_on_error_warn(self):
97110
self.assertEqual(result, 123)
98111
self.assertEqual(p.exitcode, 0)
99112

113+
with open(stderr_file) as f:
114+
stderr_output = f.read()
115+
self.assertIn('nonexistent_module_xyz', stderr_output)
116+
self.assertIn('ImportWarning', stderr_output)
117+
100118
def test_preload_on_error_fail_breaks_context(self):
101119
"""Test that invalid modules with on_error='fail' breaks the forkserver."""
102-
with self.suppress_forkserver_stderr() as suppress_mod:
120+
with self.capture_forkserver_stderr() as (capture_mod, stderr_file):
103121
self.ctx.set_forkserver_preload(
104-
[suppress_mod, 'nonexistent_module_xyz'], on_error='fail')
122+
[capture_mod, 'nonexistent_module_xyz'], on_error='fail')
105123

106124
r, w = self.ctx.Pipe(duplex=False)
107125
try:
@@ -111,6 +129,11 @@ def test_preload_on_error_fail_breaks_context(self):
111129
notes = getattr(cm.exception, '__notes__', [])
112130
self.assertTrue(notes, "Expected exception to have __notes__")
113131
self.assertIn('Forkserver process may have crashed', notes[0])
132+
133+
with open(stderr_file) as f:
134+
stderr_output = f.read()
135+
self.assertIn('nonexistent_module_xyz', stderr_output)
136+
self.assertIn('ModuleNotFoundError', stderr_output)
114137
finally:
115138
w.close()
116139
r.close()

0 commit comments

Comments
 (0)