@@ -159,6 +159,67 @@ def test_popen2e_noblock
159159 t . join
160160 end
161161
162+ def test_popen_spawn_failure_closes_pipes
163+ [ :popen3 , :popen2 , :popen2e ] . each do |method |
164+ assert_no_fd_leak ( method ) do
165+ assert_raise ( Errno ::ENOENT ) do
166+ Open3 . public_send ( method , "/open3-command-does-not-exist" )
167+ end
168+ end
169+ end
170+ end
171+
172+ def test_popen_spawn_throw_closes_pipes
173+ tag = Object . new
174+ assert_no_fd_leak ( :popen3 ) do
175+ stub_open3_spawn ( -> ( *) { throw tag } ) do
176+ assert_throw ( tag ) do
177+ Open3 . popen3 ( "unused" )
178+ end
179+ end
180+ end
181+ end
182+
183+ def test_pipeline_spawn_failure_closes_pipes
184+ first = [ RUBY , '-e' , '' ]
185+ missing = [ "/open3-command-does-not-exist" ]
186+
187+ [ :pipeline_rw , :pipeline_r , :pipeline_w ,
188+ :pipeline_start , :pipeline ] . each do |method |
189+ threads = Thread . list
190+ assert_no_fd_leak ( method ) do
191+ assert_raise ( Errno ::ENOENT ) do
192+ Open3 . public_send ( method , first , missing )
193+ end
194+ end
195+ ( Thread . list - threads ) . each do |thread |
196+ thread . join if Process ::Waiter === thread
197+ end
198+ end
199+ end
200+
201+ def test_pipeline_spawn_throw_closes_pipes
202+ tag = Object . new
203+ spawn = Open3 . method ( :spawn )
204+ calls = 0
205+ threads = Thread . list
206+
207+ assert_no_fd_leak ( :pipeline_rw ) do
208+ stub_open3_spawn ( -> ( *args ) {
209+ calls += 1
210+ throw tag if calls == 2
211+ spawn . call ( *args )
212+ } ) do
213+ assert_throw ( tag ) do
214+ Open3 . pipeline_rw ( [ RUBY , '-e' , '' ] , [ "unused" ] )
215+ end
216+ end
217+ end
218+ ( Thread . list - threads ) . each do |thread |
219+ thread . join if Process ::Waiter === thread
220+ end
221+ end
222+
162223 def test_capture3
163224 o , e , s = Open3 . capture3 ( RUBY , '-e' , 'i=STDIN.read; print i+"o"; STDOUT.flush; STDERR.print i+"e"' , :stdin_data => "i" )
164225 assert_equal ( "io" , o )
@@ -332,4 +393,33 @@ def test_integer_and_symbol_key
332393 assert_equal ( "test_integer_and_symbol_key\n " , out )
333394 assert_predicate ( status , :success? )
334395 end
396+
397+ private
398+
399+ def stub_open3_spawn ( spawn )
400+ Open3 . define_singleton_method ( :spawn , spawn )
401+ yield
402+ ensure
403+ Open3 . singleton_class . send ( :remove_method , :spawn )
404+ end
405+
406+ def assert_no_fd_leak ( method )
407+ fd_dir = [ "/proc/self/fd" , "/dev/fd" ] . find { |dir | File . directory? ( dir ) }
408+ omit "cannot inspect open file descriptors" unless fd_dir
409+
410+ gc_was_disabled = GC . disable
411+ before = open_fds ( fd_dir )
412+ yield
413+ assert_equal ( before , open_fds ( fd_dir ) ,
414+ "#{ method } leaked file descriptors" )
415+ ensure
416+ GC . enable unless gc_was_disabled
417+ end
418+
419+ def open_fds ( fd_dir )
420+ Dir . open ( fd_dir ) do |dir |
421+ fds = dir . children ( &:to_i ) . sort
422+ fds -= [ dir . fileno ] if dir . respond_to? :fileno
423+ end
424+ end
335425end
0 commit comments