expect-pty 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,353 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+ require "logger"
5
+
6
+ class DiagnosticsTest < ExpectTest
7
+ def test_logger_receives_lifecycle_and_payload_at_separate_levels
8
+ session, writer = pipe_session(debug_level: 2)
9
+ output = StringIO.new
10
+ logger = Logger.new(output)
11
+ logger.level = Logger::INFO
12
+ session.diagnostic_output = logger
13
+ writer.write("ready")
14
+ session.expect("ready", timeout: 1)
15
+ assert_includes output.string, "matched pattern 1"
16
+ refute_includes output.string, "received"
17
+ logger.level = Logger::DEBUG
18
+ writer.write("next")
19
+ session.expect("next", timeout: 1)
20
+ assert_includes output.string, 'received "next"'
21
+ session.close
22
+ refute output.closed?
23
+ end
24
+
25
+ def test_constructor_accepts_borrowed_diagnostic_io_and_keeps_transcript_separate
26
+ diagnostics = StringIO.new
27
+ session, writer = pipe_session(debug_level: 2, diagnostic_output: diagnostics)
28
+ transcript = StringIO.new
29
+ session.log_to(transcript)
30
+ writer.write("ready")
31
+ session.expect("ready", timeout: 1)
32
+ assert_equal "ready", transcript.string
33
+ assert_includes diagnostics.string, 'received "ready"'
34
+ session.close
35
+ refute diagnostics.closed?
36
+ refute transcript.closed?
37
+ end
38
+
39
+ def test_callable_diagnostics_receive_immutable_metadata_without_the_session
40
+ events = []
41
+ session, writer = pipe_session(debug_level: 2, diagnostic_output: ->(event) { events << event })
42
+ writer.write("ready")
43
+ session.expect("ready", timeout: 1)
44
+ event = events.find { |item| item[:event] == :received }
45
+ assert event.frozen?
46
+ assert event[:message].frozen?
47
+ assert_equal :debug, event[:level]
48
+ assert_equal session.fileno, event[:fd]
49
+ assert_equal 'received "ready"', event[:message]
50
+ refute_includes event.values, session
51
+ assert_equal :info, events.last[:level]
52
+ end
53
+
54
+ def test_invalid_diagnostic_target_preserves_the_previous_target
55
+ session, = pipe_session
56
+ output = StringIO.new
57
+ session.diagnostic_output = output
58
+ assert_raises(ArgumentError) { session.diagnostic_output = Object.new }
59
+ assert_same output, session.diagnostic_output
60
+ end
61
+
62
+ def test_diagnostic_failure_keeps_original_input_available
63
+ session, writer = pipe_session(debug_level: 2)
64
+ failure = IOError.new("diagnostic failed")
65
+ session.diagnostic_output = ->(_) { raise failure }
66
+ writer.write("ready")
67
+ result = session.expect_result("ready", timeout: 1)
68
+ assert_same failure, result.error
69
+ assert_equal "ready", session.buffer
70
+ session.debug_level = 0
71
+ assert_equal 1, session.expect("ready", timeout: 0)
72
+ end
73
+
74
+ def test_nested_diagnostic_wait_preserves_the_outer_match_result
75
+ session, = pipe_session(debug_level: 1)
76
+ session.buffer = "ready"
77
+ nested = nil
78
+ notified = false
79
+ callback_result = nil
80
+ session.diagnostic_output = lambda do |event|
81
+ next unless event[:event] == :matched && !notified
82
+
83
+ notified = true
84
+ nested = session.expect_result("missing", timeout: 0)
85
+ end
86
+ result = session.expect_result(timeout: 0) do |patterns|
87
+ patterns.on("ready") { |source| callback_result = source.last_result }
88
+ end
89
+ assert nested.timeout?
90
+ assert result.matched?
91
+ assert_equal "ready", result.match
92
+ assert_same result, callback_result
93
+ assert_same result, session.last_result
94
+ end
95
+
96
+ def test_redaction_handles_every_two_chunk_split_and_overlapping_secrets
97
+ (0..11).each do |split|
98
+ session, = pipe_session
99
+ output = StringIO.new
100
+ session.log_to(output)
101
+ session.redact("abc", "bcd", "secret")
102
+ text = "xabcdsecret!"
103
+ session.write_log(text.byteslice(0, split))
104
+ session.write_log(text.byteslice(split..))
105
+ session.log_output = nil
106
+ assert_equal "x[FILTERED]!", output.string, "split #{split}"
107
+ end
108
+ end
109
+
110
+ def test_byte_at_a_time_redaction_preserves_matching_and_listeners
111
+ session, writer = pipe_session(debug_level: 3)
112
+ events = []
113
+ log = StringIO.new
114
+ forwarded = StringIO.new
115
+ session.diagnostic_output = ->(event) { events << event }
116
+ session.log_to(log)
117
+ session.listeners = [forwarded]
118
+ session.redact("password")
119
+ "password!".each_char do |byte|
120
+ writer.write(byte)
121
+ session.__send__(:read_available)
122
+ refute_includes log.string, "password"
123
+ end
124
+ assert_equal 1, session.expect("password!", timeout: 0)
125
+ writer.close
126
+ assert session.expect_result(:eof, timeout: 1).eof?
127
+ assert_equal "[FILTERED]!", log.string
128
+ assert_equal "password!", forwarded.string
129
+ received = events.select { |event| event[:event] == :received }.map { |event| event[:message] }.join
130
+ assert_includes received, "[FILTERED]"
131
+ refute_includes events.inspect, "password"
132
+ buffers = events.select { |event| event[:event] == :buffer }
133
+ assert(buffers.all? { |event| event[:message] == "buffer [FILTERED]" })
134
+ end
135
+
136
+ def test_sending_diagnostics_are_filtered_across_separate_writes
137
+ client, peer = Socket.pair(:UNIX, :STREAM, 0)
138
+ @ios.push(client, peer)
139
+ output = StringIO.new
140
+ session = Expect.open(client, debug_level: 2, diagnostic_output: output)
141
+ @sessions << session
142
+ session.redact("password")
143
+ %w[pa ss wo rd].each { |part| session.write(part) }
144
+ assert_equal "password", peer.read(8)
145
+ session.close
146
+ refute_includes output.string, "password"
147
+ refute_includes output.string, '"pa"'
148
+ assert_includes output.string, "[FILTERED]"
149
+ end
150
+
151
+ def test_registration_copies_secrets_is_additive_and_isolated_between_sessions
152
+ first, = pipe_session
153
+ second, = pipe_session
154
+ first_log = StringIO.new
155
+ second_log = StringIO.new
156
+ first.log_to(first_log)
157
+ second.log_to(second_log)
158
+ secret = +"first"
159
+ assert_same first, first.redact(secret)
160
+ secret.replace("changed")
161
+ first.write_log("fi")
162
+ first.redact("second")
163
+ first.write_log("rst second")
164
+ second.write_log("first second")
165
+ first.close
166
+ assert_equal "[FILTERED] [FILTERED]", first_log.string
167
+ assert_equal "first second", second_log.string
168
+ end
169
+
170
+ def test_binary_secrets_are_matched_before_diagnostic_escaping
171
+ session, = pipe_session(debug_level: 2)
172
+ log = StringIO.new
173
+ diagnostics = StringIO.new
174
+ session.log_to(log)
175
+ session.diagnostic_output = diagnostics
176
+ secret = "\xff\0\n".b
177
+ session.redact(secret)
178
+ session.write_log("before#{secret}after".b)
179
+ session.__send__(:trace_data, :received, secret, level: 2)
180
+ session.close
181
+ assert_equal "before[FILTERED]after", log.string
182
+ refute_includes diagnostics.string, '\\xFF'
183
+ assert_includes diagnostics.string, "[FILTERED]"
184
+ end
185
+
186
+ def test_eof_and_close_flush_benign_tails_but_hide_incomplete_secret_prefixes
187
+ session, writer = pipe_session
188
+ log = StringIO.new
189
+ session.log_to(log)
190
+ session.redact("password")
191
+ writer.write("hello pass")
192
+ writer.close
193
+ assert session.expect_result(:eof, timeout: 1).eof?
194
+ assert_equal "hello [FILTERED]", log.string
195
+ session.close
196
+ assert_equal "hello [FILTERED]", log.string
197
+ end
198
+
199
+ def test_replacing_log_finishes_the_original_target_and_keeps_it_borrowed
200
+ session, = pipe_session
201
+ first = StringIO.new
202
+ second = StringIO.new
203
+ session.redact("secret")
204
+ session.log_to(first)
205
+ session.write_log("hello")
206
+ session.log_to(second)
207
+ session.write_log("secret!")
208
+ session.close
209
+ assert_equal "hello", first.string
210
+ assert_equal "[FILTERED]!", second.string
211
+ refute first.closed?
212
+ refute second.closed?
213
+ end
214
+
215
+ def test_invalid_registration_does_not_partially_add_secrets
216
+ session, = pipe_session
217
+ output = StringIO.new
218
+ session.log_to(output)
219
+ [[], [""], [nil], ["valid", 42]].each do |values|
220
+ assert_raises(ArgumentError) { session.redact(*values) }
221
+ end
222
+ session.write_log("valid")
223
+ assert_equal "valid", output.string
224
+ end
225
+
226
+ def test_reopening_the_same_log_path_finishes_the_old_stream_before_truncating
227
+ session, = pipe_session
228
+ session.redact("password")
229
+ Dir.mktmpdir do |directory|
230
+ path = File.join(directory, "session.log")
231
+ previous = session.log_to(path, mode: "w")
232
+ session.write_log("hello")
233
+ assert_equal "", File.binread(path)
234
+ session.log_to(path, mode: "w")
235
+ assert previous.closed?
236
+ session.write_log("x")
237
+ session.close
238
+ assert_equal "x", File.binread(path)
239
+ end
240
+ end
241
+
242
+ def test_failed_old_log_flush_does_not_truncate_the_new_path
243
+ session, = pipe_session
244
+ session.redact("password")
245
+ failure = IOError.new("old log failed")
246
+ session.log_to { raise failure }
247
+ session.write_log("tail")
248
+ Dir.mktmpdir do |directory|
249
+ path = File.join(directory, "existing.log")
250
+ File.write(path, "keep")
251
+ assert_same failure, assert_raises(IOError) { session.log_to(path, mode: "w") }
252
+ assert_equal "keep", File.binread(path)
253
+ end
254
+ session.log_output = nil
255
+ assert_nil session.log_output
256
+ end
257
+
258
+ def test_flush_failure_does_not_prevent_handle_and_child_cleanup
259
+ session = child('puts "ready"; sleep 60', raw_pty: true)
260
+ session.expect("ready", timeout: 2)
261
+ session.redact("password")
262
+ failure = IOError.new("flush failed")
263
+ session.log_to { raise failure }
264
+ session.write_log("hi") # 小于保留窗口,失败应发生在关闭时的尾部交付。
265
+ assert_same failure, assert_raises(IOError) { session.close }
266
+ assert session.closed?
267
+ refute session.alive?
268
+ session.log_output = nil
269
+ end
270
+
271
+ def test_unexpected_diagnostic_flush_failure_still_closes_owned_log
272
+ session, = pipe_session(debug_level: 2)
273
+ session.redact("password")
274
+ Dir.mktmpdir do |directory|
275
+ log = session.log_to(File.join(directory, "session.log"))
276
+ failure = RuntimeError.new("diagnostic callback failed")
277
+ session.diagnostic_output = ->(_) { raise failure }
278
+ session.__send__(:trace_data, :sending, "hi", level: 2)
279
+ assert_same failure, assert_raises(RuntimeError) { session.close }
280
+ assert log.closed?
281
+ assert session.closed?
282
+ end
283
+ end
284
+
285
+ def test_replacing_diagnostics_finishes_the_previous_stream
286
+ session, = pipe_session(debug_level: 2)
287
+ session.redact("password")
288
+ first = StringIO.new
289
+ second = StringIO.new
290
+ session.diagnostic_output = first
291
+ session.__send__(:trace_data, :received, "pass", level: 2)
292
+ session.diagnostic_output = second
293
+ session.__send__(:trace_data, :received, "password!", level: 2)
294
+ session.close
295
+ assert_includes first.string, "[FILTERED]"
296
+ refute_includes first.string, "pass"
297
+ assert_includes second.string, "[FILTERED]"
298
+ refute_includes second.string, "password"
299
+ refute first.closed?
300
+ refute second.closed?
301
+ end
302
+
303
+ def test_log_callback_can_rotate_the_target_during_close
304
+ session, = pipe_session
305
+ session.redact("long-secret")
306
+ delivered = []
307
+ rotated = nil
308
+ Dir.mktmpdir do |directory|
309
+ path = File.join(directory, "rotated.log")
310
+ session.log_to do |data|
311
+ delivered << data
312
+ rotated = session.log_to(path, mode: "w")
313
+ session.write_log("body")
314
+ end
315
+ session.write_log("xyz")
316
+ session.close
317
+ assert_equal ["xyz"], delivered
318
+ assert rotated.closed?
319
+ assert_equal "body", File.binread(path)
320
+ assert_nil session.log_output
321
+ end
322
+ end
323
+
324
+ def test_diagnostic_flush_callback_can_create_or_revisit_the_other_direction
325
+ [nil, "seed"].each do |initial_send|
326
+ client, peer = Socket.pair(:UNIX, :STREAM, 0)
327
+ @ios.push(client, peer)
328
+ session = Expect.open(client, debug_level: 2)
329
+ @sessions << session
330
+ session.redact("long-secret")
331
+ events = []
332
+ session.diagnostic_output = lambda do |event|
333
+ events << event
334
+ session.write("ack") if event[:event] == :received
335
+ end
336
+ if initial_send
337
+ session.write(initial_send)
338
+ assert_equal initial_send, peer.read(initial_send.bytesize)
339
+ end
340
+ peer.write("xyz")
341
+ assert_equal 1, session.expect("xyz", timeout: 0)
342
+ replacement = StringIO.new
343
+ session.diagnostic_output = replacement
344
+ assert_equal "ack", peer.read(3)
345
+ expected = initial_send ? %i[matched sending received sending] : %i[matched received sending]
346
+ assert_equal(expected, events.map { |event| event[:event] })
347
+ assert_includes events.last[:message], "ack"
348
+ assert_equal "", replacement.string
349
+ session.close
350
+ assert_equal "", replacement.string
351
+ end
352
+ end
353
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class LifecycleContractTest < ExpectTest
6
+ def test_external_reaping_releases_pid_without_fabricating_process_status
7
+ session = child('puts "ready"; exit 7', raw_pty: true)
8
+ assert_equal 1, session.expect("ready", timeout: 2)
9
+ pid = session.pid
10
+ assert_equal(7, bounded { Process.waitpid2(pid).last.exitstatus })
11
+ refute session.alive?
12
+ assert_nil session.pid
13
+ assert_nil session.process_status
14
+ assert_nil session.wait(timeout: 0)
15
+ Process.stub(:kill, ->(*) { flunk "must not signal an externally reaped PID" }) do
16
+ 2.times { session.close }
17
+ end
18
+ assert session.closed?
19
+ end
20
+
21
+ def test_external_reader_close_does_not_take_ownership_of_the_writer
22
+ reader, producer = IO.pipe
23
+ consumer, writer = IO.pipe
24
+ @ios.push(reader, producer, consumer, writer)
25
+ session = Expect.open(reader, writer: writer)
26
+ @sessions << session
27
+ session.buffer = "tail"
28
+ reader.close
29
+ assert session.closed?
30
+ assert session.eof?
31
+ refute session.alive?
32
+ assert_raises(IOError) { session.write("blocked") }
33
+ 2.times { session.close }
34
+ refute writer.closed?
35
+ assert_equal "tail", session.buffer
36
+ writer.write("borrowed")
37
+ assert_equal "borrowed", consumer.read(8)
38
+ end
39
+
40
+ def test_failed_handle_close_can_be_retried_without_losing_other_owned_handles
41
+ reader, writer = IO.pipe
42
+ @ios.push(reader, writer)
43
+ session = Expect.open(reader, writer: writer, own: true)
44
+ @sessions << session
45
+ failure = IOError.new("close failed")
46
+ reader.stub(:close, -> { raise failure }) do
47
+ assert_same failure, assert_raises(IOError) { session.close }
48
+ assert writer.closed?
49
+ refute reader.closed?
50
+ assert session.closed?
51
+ end
52
+ session.close
53
+ assert reader.closed?
54
+ assert_nil session.close
55
+ end
56
+
57
+ def test_timeout_and_eof_remain_events_and_write_errors_remain_io_errors
58
+ session, writer = pipe_session
59
+ timeout = session.expect_result("missing", timeout: 0)
60
+ assert timeout.timeout?
61
+ assert_equal :timeout, timeout.error
62
+ writer.close
63
+ eof = session.expect_result(:eof, timeout: 1)
64
+ assert eof.eof?
65
+ assert_equal :eof, eof.error
66
+ failure = Expect::WriteTimeout.new(bytes_written: 3)
67
+ assert_kind_of IOError, failure
68
+ assert_equal 3, failure.bytes_written
69
+ assert_operator Expect::SpawnError, :<, StandardError
70
+ end
71
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class LiteralScanTest < ExpectTest
6
+ def test_new_bytes_can_complete_a_previously_missing_literal
7
+ session, writer = pipe_session
8
+ list = Expect::PatternList.new([session], ["password:", "word:"])
9
+ matcher = Expect::Matcher.new(list, nil)
10
+ session.buffer = "prefix pass"
11
+ assert_nil matcher.__send__(:find_match)
12
+ writer.write("word:")
13
+ session.__send__(:read_available)
14
+ result = matcher.__send__(:find_match)
15
+ assert_equal 1, result[1].number
16
+ assert_equal [7, 9, []], result[2]
17
+ end
18
+
19
+ def test_later_text_for_an_earlier_pattern_keeps_declaration_priority
20
+ session, writer = pipe_session
21
+ list = Expect::PatternList.new([session], %w[preferred ready])
22
+ matcher = Expect::Matcher.new(list, nil)
23
+ session.buffer = "ready"
24
+ assert_equal 2, matcher.__send__(:find_match)[1].number
25
+ writer.write(" preferred")
26
+ session.__send__(:read_available)
27
+ assert_equal 1, matcher.__send__(:find_match)[1].number
28
+ end
29
+
30
+ def test_destructive_buffer_changes_invalidate_old_misses
31
+ session, writer = pipe_session
32
+ list = Expect::PatternList.new([session], ["start", "token", /END/])
33
+ matcher = Expect::Matcher.new(list, nil)
34
+ session.buffer = "x" * 100
35
+ assert_nil matcher.__send__(:find_match)
36
+ session.buffer = "start"
37
+ assert_equal 1, matcher.__send__(:find_match)[1].number
38
+ session.clear_buffer
39
+ assert_nil matcher.__send__(:find_match)
40
+ writer.write("token")
41
+ session.__send__(:read_available)
42
+ assert_equal 2, matcher.__send__(:find_match)[1].number
43
+ session.expect("token", timeout: 0)
44
+ session.buffer_limit = 8
45
+ writer.write("xxxxstart")
46
+ session.__send__(:read_available)
47
+ assert_equal [3, 5, []], matcher.__send__(:find_match)[2]
48
+ session.clear_buffer
49
+ session.__send__(:restore_relay_buffer, "token")
50
+ assert_equal 2, matcher.__send__(:find_match)[1].number
51
+ end
52
+
53
+ def test_repeated_scans_equal_a_fresh_matcher_across_operation_sequences
54
+ session, writer = pipe_session
55
+ patterns = ["abc", "bc", "\0\xff".b, /END/n]
56
+ list = Expect::PatternList.new([session], patterns)
57
+ matcher = Expect::Matcher.new(list, nil)
58
+ random = Random.new(40_033)
59
+ chunks = ["a", "b", "c", "x", "END", "\0", "\xff".b]
60
+ 150.times do
61
+ case random.rand(6)
62
+ when 0
63
+ session.buffer = chunks.sample(random: random) * random.rand(1..4)
64
+ when 1
65
+ session.clear_buffer
66
+ when 2
67
+ session.buffer_limit = [nil, 2, 5, 12].sample(random: random)
68
+ when 3
69
+ session.__send__(:restore_relay_buffer, chunks.sample(random: random).b)
70
+ when 4
71
+ session.expect("x", timeout: 0)
72
+ else
73
+ writer.write(chunks.sample(random: random))
74
+ session.__send__(:read_available)
75
+ end
76
+ expected = Expect::Matcher.new(list, nil).__send__(:find_match)
77
+ actual = matcher.__send__(:find_match)
78
+ assert_equal signature(expected), signature(actual), "buffer=#{session.buffer.inspect}"
79
+ end
80
+ end
81
+
82
+ def test_a_dynamically_registered_pattern_is_scanned_from_the_start
83
+ session, = pipe_session
84
+ session.buffer = "prefix ready"
85
+ list = Expect::PatternList.new([session], ["missing"])
86
+ matcher = Expect::Matcher.new(list, nil)
87
+ assert_nil matcher.__send__(:find_match)
88
+ list.on("prefix")
89
+ assert_equal [0, 6, []], matcher.__send__(:find_match)[2]
90
+ end
91
+
92
+ private
93
+
94
+ def signature(result)
95
+ result ? [result[0], result[1].number, result[2]] : []
96
+ end
97
+ end
@@ -16,8 +16,10 @@ class PatternOffsetTest < ExpectTest
16
16
  ]
17
17
  session, = pipe_session
18
18
  cases.each do |text, regexp, offset, length, captures|
19
- bytes = text.b
19
+ # 扫描只读调用者输入;省去正则副本后也不能改变编码标记或冻结字符串。
20
+ bytes = text.b.freeze
20
21
  assert_equal [offset, length, captures], Expect::Pattern.new(value: regexp).locate(bytes)
22
+ assert_equal Encoding::BINARY, bytes.encoding
21
23
  session.buffer = bytes
22
24
  result = session.expect_result(regexp, timeout: 0)
23
25
  assert_equal bytes.byteslice(0, offset), result.before
data/test/process_test.rb CHANGED
@@ -77,6 +77,14 @@ class ProcessTest < ExpectTest
77
77
  assert_raises(Expect::SpawnError) { session.spawn("cat") }
78
78
  end
79
79
 
80
+ def test_spawn_cleans_child_when_post_exec_diagnostics_fail
81
+ assert_failed_construction_cleans_child(:spawn)
82
+ end
83
+
84
+ def test_new_cleans_child_when_post_exec_diagnostics_fail
85
+ assert_failed_construction_cleans_child(:new)
86
+ end
87
+
80
88
  def test_block_closes_child_on_exception
81
89
  pid = nil
82
90
  assert_raises(RuntimeError) do
@@ -228,6 +236,17 @@ class ProcessTest < ExpectTest
228
236
  assert_equal configuration, terminal_configuration(session)
229
237
  end
230
238
 
239
+ def test_stty_reports_a_missing_system_command_and_preserves_the_cause
240
+ previous_path = ENV.fetch("PATH", nil)
241
+ session = child("sleep 60")
242
+ ENV["PATH"] = ""
243
+ error = assert_raises(IOError) { session.stty }
244
+ assert_match "stty executable not found", error.message
245
+ assert_instance_of Errno::ENOENT, error.cause
246
+ ensure
247
+ ENV["PATH"] = previous_path
248
+ end
249
+
231
250
  def test_configuration_and_session_attributes
232
251
  Expect.configure(timeout: 0.01, log_stdout: false)
233
252
  session = Expect.new
@@ -242,4 +261,31 @@ class ProcessTest < ExpectTest
242
261
  assert_equal 30, session.buffer_limit
243
262
  assert_match(/\A#<Expect .*closed=false>\z/, session.inspect)
244
263
  end
264
+
265
+ private
266
+
267
+ def assert_failed_construction_cleans_child(factory)
268
+ connection = nil
269
+ session_class = Class.new(Expect)
270
+ session_class.define_method(:initialize) do |*command, **options|
271
+ connection = self
272
+ super(*command, **options)
273
+ end
274
+ reader, diagnostic_output = IO.pipe
275
+ @ios.push(reader, diagnostic_output)
276
+ reader.close
277
+ previous_stderr = $stderr
278
+ begin
279
+ $stderr = diagnostic_output
280
+ assert_raises(Errno::EPIPE) do
281
+ session_class.public_send(factory, RbConfig.ruby, "--disable-gems", "-e", "sleep 60", debug_level: 1)
282
+ end
283
+ ensure
284
+ $stderr = previous_stderr
285
+ @sessions << connection if connection
286
+ end
287
+ assert connection.closed?
288
+ assert_nil connection.pid
289
+ assert_instance_of Process::Status, connection.process_status
290
+ end
245
291
  end