@@ -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