expect-pty 0.5.3 → 0.6.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +28 -19
- data/docs/API.md +88 -0
- data/docs/MIGRATION.md +15 -0
- data/lib/expect/cleanup.rb +33 -0
- data/lib/expect/configuration.rb +87 -66
- data/lib/expect/interaction.rb +226 -209
- data/lib/expect/logging.rb +157 -156
- data/lib/expect/matcher.rb +30 -29
- data/lib/expect/pattern.rb +8 -2
- data/lib/expect/pattern_list.rb +35 -5
- data/lib/expect/redactor.rb +11 -4
- data/lib/expect/relay.rb +97 -73
- data/lib/expect/relay_writer.rb +2 -1
- data/lib/expect/result.rb +32 -8
- data/lib/expect/session.rb +508 -0
- data/lib/expect/session_resources.rb +10 -2
- data/lib/expect/terminal.rb +78 -78
- data/lib/expect/version.rb +1 -1
- data/lib/expect.rb +333 -520
- data/sig/expect.rbs +355 -0
- metadata +13 -106
- data/.rubocop.yml +0 -66
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -28
- data/Rakefile +0 -38
- data/benchmark/matching.rb +0 -98
- data/benchmark/redactor.rb +0 -64
- data/benchmark/relay.rb +0 -48
- data/benchmark/scaling.rb +0 -70
- data/benchmark/send_slow.rb +0 -41
- data/benchmark/support.rb +0 -114
- data/docs/COMPATIBILITY.md +0 -98
- data/docs/INTERNAL_CONTRACTS.md +0 -154
- data/docs/PERFORMANCE.md +0 -165
- data/docs/RELEASING.md +0 -84
- data/docs/VERIFICATION.md +0 -456
- data/examples/dialogue.rb +0 -30
- data/examples/kibitz/README.md +0 -81
- data/examples/kibitz/kibitz.rb +0 -142
- data/examples/kibitz/test_kibitz.rb +0 -37
- data/examples/ssh_auto.rb +0 -94
- data/examples/ssh_interact.rb +0 -159
- data/examples/ssh_login.rb +0 -64
- data/expect-pty.gemspec +0 -33
- data/script/ci +0 -121
- data/script/release.rb +0 -319
- data/test/buffer_accounting_test.rb +0 -83
- data/test/cleanup_test.rb +0 -251
- data/test/compare_upstream.rb +0 -157
- data/test/configuration_test.rb +0 -133
- data/test/deadline_test.rb +0 -237
- data/test/diagnostics_test.rb +0 -353
- data/test/edge_case_test.rb +0 -262
- data/test/fixtures/ssh_scripts/01_identity.sh +0 -4
- data/test/fixtures/ssh_scripts/02_output.sh +0 -5
- data/test/fixtures/ssh_scripts/03_delayed.sh +0 -6
- data/test/fixtures/ssh_scripts/04_failure.sh +0 -2
- data/test/fixtures/ssh_scripts/05_recovery.sh +0 -3
- data/test/initialization_failure_test.rb +0 -92
- data/test/integration/README.md +0 -109
- data/test/integration/ssh_scripts.rb +0 -94
- data/test/interact_test.rb +0 -253
- data/test/interconnect_test.rb +0 -425
- data/test/io_test.rb +0 -321
- data/test/kibitz_test.rb +0 -45
- data/test/lifecycle_contract_test.rb +0 -71
- data/test/literal_scan_test.rb +0 -97
- data/test/matching_test.rb +0 -211
- data/test/multi_session_test.rb +0 -66
- data/test/ownership_sequence_test.rb +0 -208
- data/test/pattern_offset_test.rb +0 -43
- data/test/process_interruption_test.rb +0 -296
- data/test/process_test.rb +0 -291
- data/test/redactor_test.rb +0 -183
- data/test/relay_recovery_test.rb +0 -451
- data/test/relay_reentrancy_test.rb +0 -159
- data/test/release_test.rb +0 -375
- data/test/ruby_api_test.rb +0 -515
- data/test/scan_reuse_test.rb +0 -109
- data/test/script_logging_test.rb +0 -124
- data/test/support/interact_probe.rb +0 -125
- data/test/support/kibitz_probe.rb +0 -177
- data/test/support/script_probe.rb +0 -158
- data/test/terminal_cleanup_test.rb +0 -349
- data/test/test_helper.rb +0 -58
- data/test/timeout_test.rb +0 -289
- data/test/write_contract_test.rb +0 -105
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "test_helper"
|
|
4
|
-
require "minitest/mock"
|
|
5
|
-
|
|
6
|
-
class ProcessInterruptionTest < ExpectTest
|
|
7
|
-
def test_hard_close_reaps_a_real_child_after_one_wait_interruption
|
|
8
|
-
session = child('Signal.trap("HUP", "IGNORE"); Signal.trap("TERM", "IGNORE"); puts "ready"; sleep 60',
|
|
9
|
-
raw_pty: true)
|
|
10
|
-
assert_equal 1, session.expect("ready", timeout: 2)
|
|
11
|
-
pid = session.pid
|
|
12
|
-
original = Process.method(:waitpid2)
|
|
13
|
-
interrupted = false
|
|
14
|
-
Process.stub(:waitpid2, lambda { |*args|
|
|
15
|
-
if args.first == pid && !interrupted
|
|
16
|
-
interrupted = true
|
|
17
|
-
raise Errno::EINTR
|
|
18
|
-
end
|
|
19
|
-
original.call(*args)
|
|
20
|
-
}) do
|
|
21
|
-
status = bounded { session.hard_close(timeout: 0) }
|
|
22
|
-
assert_instance_of Process::Status, status
|
|
23
|
-
assert_equal Signal.list.fetch("KILL"), status.termsig
|
|
24
|
-
assert_same status, session.process_status
|
|
25
|
-
end
|
|
26
|
-
assert interrupted
|
|
27
|
-
assert_nil session.pid
|
|
28
|
-
assert_raises(Errno::ECHILD) { Process.waitpid(pid, Process::WNOHANG) }
|
|
29
|
-
ensure
|
|
30
|
-
session&.hard_close(timeout: 0)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def test_continuous_wait_interruptions_keep_the_original_deadline_and_pid
|
|
34
|
-
with_fake_child do |session, resources, calls|
|
|
35
|
-
with_clock(session) do |now|
|
|
36
|
-
Process.stub(:waitpid2, lambda { |*|
|
|
37
|
-
calls << :wait
|
|
38
|
-
raise Errno::EINTR
|
|
39
|
-
}) do
|
|
40
|
-
assert_nil(bounded { session.wait(timeout: 0.03) })
|
|
41
|
-
assert_equal 0.03, now.call
|
|
42
|
-
assert_operator calls.size, :<=, 5
|
|
43
|
-
assert_operator calls.size, :>=, 3
|
|
44
|
-
assert_equal 123_456, resources.pid
|
|
45
|
-
assert_nil resources.status
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def test_continuous_wait_and_signal_interruptions_are_bounded_during_hard_close
|
|
52
|
-
with_fake_child do |session, resources, calls|
|
|
53
|
-
with_clock(session) do |now|
|
|
54
|
-
Process.stub(:waitpid2, lambda { |*|
|
|
55
|
-
calls << :wait
|
|
56
|
-
raise Errno::EINTR
|
|
57
|
-
}) do
|
|
58
|
-
Process.stub(:kill, lambda { |signal, *|
|
|
59
|
-
calls << signal
|
|
60
|
-
raise Errno::EINTR
|
|
61
|
-
}) do
|
|
62
|
-
assert_nil(bounded { session.hard_close(timeout: 0.02) })
|
|
63
|
-
assert_in_delta 1.04, now.call, 0.000001
|
|
64
|
-
assert_operator calls.count("TERM"), :>, 1
|
|
65
|
-
assert_operator calls.count("KILL"), :>, 1
|
|
66
|
-
assert_operator calls.size, :<, 230
|
|
67
|
-
assert_equal 123_456, resources.pid
|
|
68
|
-
assert_nil resources.status
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def test_signal_interruption_retries_without_extending_the_soft_close_budget
|
|
76
|
-
with_fake_child do |session, resources, calls|
|
|
77
|
-
with_clock(session) do |now|
|
|
78
|
-
Process.stub(:kill, lambda { |signal, _pid|
|
|
79
|
-
calls << signal
|
|
80
|
-
raise Errno::EINTR if calls.count(signal) == 1
|
|
81
|
-
}) do
|
|
82
|
-
assert_nil session.soft_close(timeout: 0, term_timeout: 0.03)
|
|
83
|
-
assert_equal 0.03, now.call
|
|
84
|
-
assert_equal %w[TERM TERM], calls.grep(String)
|
|
85
|
-
assert_equal 123_456, resources.pid
|
|
86
|
-
assert_nil resources.status
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def test_echild_clears_ownership_before_any_signal_or_detach
|
|
93
|
-
with_fake_child do |session, resources, calls|
|
|
94
|
-
Process.stub(:waitpid2, ->(*) { raise Errno::ECHILD }) do
|
|
95
|
-
assert_nil session.hard_close(timeout: 0)
|
|
96
|
-
resources.finalize
|
|
97
|
-
assert_nil resources.pid
|
|
98
|
-
assert_nil resources.status
|
|
99
|
-
assert_empty calls
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def test_finalizer_continues_to_kill_and_detach_after_interrupted_reap
|
|
105
|
-
with_fake_child do |_session, resources, calls|
|
|
106
|
-
Process.stub(:waitpid2, lambda { |*|
|
|
107
|
-
calls << :wait
|
|
108
|
-
raise Errno::EINTR
|
|
109
|
-
}) do
|
|
110
|
-
bounded { resources.finalize }
|
|
111
|
-
end
|
|
112
|
-
assert_equal [:wait, "KILL", :detach], calls
|
|
113
|
-
assert_nil resources.pid
|
|
114
|
-
assert_nil resources.status
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def test_finalizer_detaches_even_if_kill_is_continuously_interrupted
|
|
119
|
-
with_fake_child do |session, resources, calls|
|
|
120
|
-
session.log_to { flunk "finalizer invoked a user callback" }
|
|
121
|
-
Process.stub(:kill, lambda { |signal, *|
|
|
122
|
-
calls << signal
|
|
123
|
-
raise Errno::EINTR
|
|
124
|
-
}) do
|
|
125
|
-
bounded { resources.finalize }
|
|
126
|
-
end
|
|
127
|
-
assert_equal :detach, calls.last
|
|
128
|
-
assert_operator calls.count("KILL"), :>=, 1
|
|
129
|
-
assert_operator calls.count("KILL"), :<=, 2
|
|
130
|
-
assert_nil resources.pid
|
|
131
|
-
assert_nil resources.status
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def test_finalizer_retains_unknown_pid_if_detach_also_fails
|
|
136
|
-
with_fake_child do |_session, resources, _calls|
|
|
137
|
-
Process.stub(:detach, ->(*) { raise Errno::EINTR }) { bounded { resources.finalize } }
|
|
138
|
-
assert_equal 123_456, resources.pid
|
|
139
|
-
assert_nil resources.status
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def test_non_owner_never_waits_signals_or_detaches
|
|
144
|
-
with_fake_child do |session, resources, calls|
|
|
145
|
-
Process.stub(:pid, resources.owner + 1) do
|
|
146
|
-
resources.finalize
|
|
147
|
-
assert_nil session.wait(timeout: 0)
|
|
148
|
-
assert_nil session.hard_close(timeout: 0)
|
|
149
|
-
end
|
|
150
|
-
assert_empty calls
|
|
151
|
-
assert_equal 123_456, resources.pid
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def test_forked_copy_cannot_reap_signal_or_detach_the_parent_child
|
|
156
|
-
session = child('puts "ready"; sleep 60', raw_pty: true)
|
|
157
|
-
assert_equal 1, session.expect("ready", timeout: 2)
|
|
158
|
-
reader, writer = IO.pipe
|
|
159
|
-
@ios.push(reader, writer)
|
|
160
|
-
probe = fork do
|
|
161
|
-
reader.close
|
|
162
|
-
forbidden = ->(*) { raise "non-owner attempted process cleanup" }
|
|
163
|
-
Process.stub(:waitpid2, forbidden) do
|
|
164
|
-
Process.stub(:kill, forbidden) do
|
|
165
|
-
Process.stub(:detach, forbidden) do
|
|
166
|
-
session.instance_variable_get(:@resources).finalize
|
|
167
|
-
session.hard_close(timeout: 0)
|
|
168
|
-
writer.write("ok")
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
exit! 0
|
|
173
|
-
rescue Exception # rubocop:disable Lint/RescueException -- 子进程只回传验证失败,父进程负责回收。
|
|
174
|
-
exit! 1
|
|
175
|
-
end
|
|
176
|
-
writer.close
|
|
177
|
-
assert_equal("ok", bounded { reader.read })
|
|
178
|
-
status = bounded { Process.waitpid2(probe).last }
|
|
179
|
-
probe = nil
|
|
180
|
-
assert status.success?
|
|
181
|
-
assert session.alive?
|
|
182
|
-
ensure
|
|
183
|
-
if probe
|
|
184
|
-
begin
|
|
185
|
-
Process.kill("KILL", probe)
|
|
186
|
-
rescue Errno::ESRCH
|
|
187
|
-
nil
|
|
188
|
-
end
|
|
189
|
-
begin
|
|
190
|
-
Process.waitpid(probe)
|
|
191
|
-
rescue Errno::ECHILD
|
|
192
|
-
nil
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
session&.hard_close(timeout: 0)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def test_finalizer_rechecks_ownership_before_detaching_after_a_signal
|
|
199
|
-
with_fake_child do |_session, resources, calls|
|
|
200
|
-
owner = resources.owner
|
|
201
|
-
Process.stub(:pid, -> { owner }) do
|
|
202
|
-
Process.stub(:kill, lambda { |signal, *|
|
|
203
|
-
calls << signal
|
|
204
|
-
owner += 1
|
|
205
|
-
}) do
|
|
206
|
-
resources.finalize
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
assert_equal [:wait, "KILL"], calls
|
|
210
|
-
assert_equal 123_456, resources.pid
|
|
211
|
-
assert_nil resources.status
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def test_esrch_during_signal_still_reaps_without_inventing_status
|
|
216
|
-
with_fake_child do |session, resources, calls|
|
|
217
|
-
gone = false
|
|
218
|
-
Process.stub(:waitpid2, lambda { |*|
|
|
219
|
-
calls << :wait
|
|
220
|
-
raise Errno::ECHILD if gone
|
|
221
|
-
}) do
|
|
222
|
-
Process.stub(:kill, lambda { |signal, *|
|
|
223
|
-
calls << signal
|
|
224
|
-
gone = true
|
|
225
|
-
raise Errno::ESRCH
|
|
226
|
-
}) do
|
|
227
|
-
assert_nil session.hard_close(timeout: 0)
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
|
-
assert_equal [:wait, :wait, "TERM", :wait], calls
|
|
231
|
-
assert_nil resources.pid
|
|
232
|
-
assert_nil resources.status
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def test_zero_budget_close_returns_status_reaped_after_esrch
|
|
237
|
-
status = child("exit 7").wait(timeout: 2)
|
|
238
|
-
assert_instance_of Process::Status, status
|
|
239
|
-
%i[soft_close hard_close].each do |method|
|
|
240
|
-
with_fake_child do |session, resources, calls|
|
|
241
|
-
gone = false
|
|
242
|
-
Process.stub(:waitpid2, lambda { |pid, *|
|
|
243
|
-
calls << :wait
|
|
244
|
-
[pid, status] if gone
|
|
245
|
-
}) do
|
|
246
|
-
Process.stub(:kill, lambda { |signal, *|
|
|
247
|
-
calls << signal
|
|
248
|
-
gone = true
|
|
249
|
-
raise Errno::ESRCH
|
|
250
|
-
}) do
|
|
251
|
-
options = { timeout: 0 }
|
|
252
|
-
options[:term_timeout] = 0 if method == :soft_close
|
|
253
|
-
assert_same status, session.public_send(method, **options)
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
assert_nil resources.pid
|
|
257
|
-
assert_same status, session.process_status
|
|
258
|
-
assert_equal [:wait, :wait, "TERM", :wait], calls
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
private
|
|
264
|
-
|
|
265
|
-
# 假 PID 只存在于系统调用全部被替换的作用域;ensure 先撤销 PID,再交给通用 teardown。
|
|
266
|
-
def with_fake_child
|
|
267
|
-
session, = pipe_session
|
|
268
|
-
resources = session.instance_variable_get(:@resources)
|
|
269
|
-
calls = []
|
|
270
|
-
originals = %i[waitpid2 kill detach].to_h { |name| [name, Process.method(name)] }
|
|
271
|
-
Process.define_singleton_method(:waitpid2) do |*|
|
|
272
|
-
calls << :wait
|
|
273
|
-
nil
|
|
274
|
-
end
|
|
275
|
-
Process.define_singleton_method(:kill) do |signal, *|
|
|
276
|
-
calls << signal
|
|
277
|
-
1
|
|
278
|
-
end
|
|
279
|
-
Process.define_singleton_method(:detach) do |*|
|
|
280
|
-
calls << :detach
|
|
281
|
-
nil
|
|
282
|
-
end
|
|
283
|
-
resources.pid = 123_456
|
|
284
|
-
yield session, resources, calls
|
|
285
|
-
ensure
|
|
286
|
-
resources.pid = nil if resources
|
|
287
|
-
originals&.each { |name, method| Process.define_singleton_method(name, method) }
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
def with_clock(session)
|
|
291
|
-
now = 0.0
|
|
292
|
-
Expect.stub(:monotonic, -> { now }) do
|
|
293
|
-
session.stub(:sleep, ->(seconds) { now += seconds }) { yield -> { now } }
|
|
294
|
-
end
|
|
295
|
-
end
|
|
296
|
-
end
|
data/test/process_test.rb
DELETED
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "test_helper"
|
|
4
|
-
|
|
5
|
-
class ProcessTest < ExpectTest
|
|
6
|
-
def test_spawn_has_controlling_terminal_and_merged_stderr
|
|
7
|
-
session = child(<<~RUBY)
|
|
8
|
-
terminal = File.open("/dev/tty", &:tty?)
|
|
9
|
-
puts [STDIN.tty?, STDOUT.tty?, STDERR.tty?, terminal].join(":")
|
|
10
|
-
STDERR.puts "error-stream"
|
|
11
|
-
RUBY
|
|
12
|
-
assert_equal 1, session.expect("true:true:true:true", timeout: 2)
|
|
13
|
-
assert_equal 1, session.expect("error-stream", timeout: 2)
|
|
14
|
-
session.soft_close(timeout: 1)
|
|
15
|
-
assert_equal 0, session.exit_code
|
|
16
|
-
assert_equal 0, session.process_status.to_i
|
|
17
|
-
assert session.closed?
|
|
18
|
-
refute session.alive?
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def test_raw_mode_is_applied_before_child_starts
|
|
22
|
-
session = child(<<~'RUBY', raw_pty: true)
|
|
23
|
-
print "name: "
|
|
24
|
-
value = STDIN.gets
|
|
25
|
-
puts "reply=#{value.strip.reverse}"
|
|
26
|
-
RUBY
|
|
27
|
-
session.expect("name: ", timeout: 2)
|
|
28
|
-
session.write("crate\n")
|
|
29
|
-
assert_equal 1, session.expect("reply=etarc\n", timeout: 2)
|
|
30
|
-
assert_equal "", session.before
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def test_new_pty_supports_slave_configuration_before_spawn
|
|
34
|
-
session = Expect.new(log_stdout: false)
|
|
35
|
-
@sessions << session
|
|
36
|
-
assert session.slave.tty?
|
|
37
|
-
session.slave.echo = false
|
|
38
|
-
session.slave.winsize = [37, 111]
|
|
39
|
-
# 子进程需要 io-console gem,保留 RubyGems 以使用 Bundler 选定的版本。
|
|
40
|
-
session.spawn(RbConfig.ruby, "-rio/console", "-e",
|
|
41
|
-
'STDOUT.sync = true; puts STDIN.winsize.join(":"); puts STDIN.gets')
|
|
42
|
-
assert_equal 1, session.expect("37:111", timeout: 2), session.before.inspect
|
|
43
|
-
assert_equal [37, 111], session.winsize
|
|
44
|
-
session.winsize = [24, 80]
|
|
45
|
-
assert_equal [24, 80], session.winsize
|
|
46
|
-
session.clear_buffer
|
|
47
|
-
session.write("line\n")
|
|
48
|
-
session.expect("line", timeout: 2)
|
|
49
|
-
session.expect(timeout: 2)
|
|
50
|
-
assert_equal "\r\n", session.before
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def test_command_arguments_environment_and_directory
|
|
54
|
-
Dir.mktmpdir do |dir|
|
|
55
|
-
session = Expect.spawn(RbConfig.ruby, "--disable-gems", "-e",
|
|
56
|
-
'puts ARGV.first; puts ENV.fetch("EXPECT_TEST_VALUE"); puts Dir.pwd', "a;$(exit) b",
|
|
57
|
-
env: { "EXPECT_TEST_VALUE" => "environment" }, chdir: dir,
|
|
58
|
-
log_stdout: false, raw_pty: true)
|
|
59
|
-
@sessions << session
|
|
60
|
-
assert_equal 1, session.expect("a;$(exit) b\nenvironment\n#{File.realpath(dir)}\n", timeout: 2)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def test_string_command_supports_shell_semantics
|
|
65
|
-
session = Expect.new("printf 'shell-one'; printf 'shell-two'", log_stdout: false)
|
|
66
|
-
@sessions << session
|
|
67
|
-
assert_equal 1, session.expect("shell-oneshell-two", timeout: 2)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def test_exec_failure_is_synchronous_and_reaped
|
|
71
|
-
session = Expect.new(log_stdout: false)
|
|
72
|
-
@sessions << session
|
|
73
|
-
error = assert_raises(Expect::SpawnError) { session.spawn("/no/such/expect-test-command") }
|
|
74
|
-
assert_match(/ENOENT/, error.message)
|
|
75
|
-
assert session.closed?
|
|
76
|
-
assert_nil session.pid
|
|
77
|
-
assert_raises(Expect::SpawnError) { session.spawn("cat") }
|
|
78
|
-
end
|
|
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
|
-
|
|
88
|
-
def test_block_closes_child_on_exception
|
|
89
|
-
pid = nil
|
|
90
|
-
assert_raises(RuntimeError) do
|
|
91
|
-
Expect.spawn(RbConfig.ruby, "--disable-gems", "-e", "sleep 60", log_stdout: false) do |session|
|
|
92
|
-
pid = session.pid
|
|
93
|
-
raise "stop"
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
assert_raises(Errno::ECHILD) { Process.waitpid(pid, Process::WNOHANG) }
|
|
97
|
-
assert_raises(Errno::ESRCH) { Process.kill(0, pid) }
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def test_soft_close_drains_output_before_exit_and_returns_process_status
|
|
101
|
-
session = child('sleep 0.03; print "last output"; exit 7', raw_pty: true)
|
|
102
|
-
log = StringIO.new
|
|
103
|
-
session.log_to(log)
|
|
104
|
-
assert_equal 7, session.soft_close(timeout: 1).exitstatus
|
|
105
|
-
assert_equal "last output", log.string
|
|
106
|
-
assert_equal "last output", session.buffer
|
|
107
|
-
assert_equal 7, session.exit_code
|
|
108
|
-
refute log.closed?
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def test_hard_close_escalates_uncooperative_child_and_reaps
|
|
112
|
-
session = child('Signal.trap("HUP", "IGNORE"); Signal.trap("TERM", "IGNORE"); puts "ready"; loop { sleep 1 }')
|
|
113
|
-
session.expect("ready", timeout: 2)
|
|
114
|
-
pid = session.pid
|
|
115
|
-
bounded(2) { session.hard_close(timeout: 0.03) }
|
|
116
|
-
assert_equal Signal.list.fetch("KILL"), session.process_status.termsig
|
|
117
|
-
assert_raises(Errno::ECHILD) { Process.waitpid(pid, Process::WNOHANG) }
|
|
118
|
-
assert_equal session.process_status, session.hard_close
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def test_soft_close_sends_term_and_returns_the_exit_status
|
|
122
|
-
session = child(<<~RUBY, raw_pty: true)
|
|
123
|
-
Signal.trap("HUP", "IGNORE")
|
|
124
|
-
Signal.trap("TERM") { exit 7 }
|
|
125
|
-
puts "ready"
|
|
126
|
-
sleep 60
|
|
127
|
-
RUBY
|
|
128
|
-
session.expect("ready", timeout: 2)
|
|
129
|
-
status = bounded { session.soft_close(timeout: 0, term_timeout: 1) }
|
|
130
|
-
assert_equal 7, status.exitstatus
|
|
131
|
-
assert session.closed?
|
|
132
|
-
refute session.alive?
|
|
133
|
-
assert_same status, session.soft_close(timeout: 0)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def test_soft_close_never_sends_kill_and_retains_a_live_pid_for_hard_close
|
|
137
|
-
session = child(<<~RUBY)
|
|
138
|
-
Signal.trap("HUP", "IGNORE")
|
|
139
|
-
Signal.trap("TERM", "IGNORE")
|
|
140
|
-
puts "ready"
|
|
141
|
-
sleep 60
|
|
142
|
-
RUBY
|
|
143
|
-
session.expect("ready", timeout: 2)
|
|
144
|
-
pid = session.pid
|
|
145
|
-
assert_nil(bounded { session.soft_close(timeout: 0, term_timeout: 0.03) })
|
|
146
|
-
assert session.closed?
|
|
147
|
-
assert session.alive?
|
|
148
|
-
assert_equal pid, session.pid
|
|
149
|
-
assert_nil session.wait(timeout: 0)
|
|
150
|
-
status = bounded { session.hard_close(timeout: 0.03) }
|
|
151
|
-
assert_equal Signal.list.fetch("KILL"), status.termsig
|
|
152
|
-
assert_nil session.pid
|
|
153
|
-
assert_raises(Errno::ECHILD) { Process.waitpid(pid, Process::WNOHANG) }
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def test_block_cleanup_uses_the_configured_graceful_close_and_drains_output
|
|
157
|
-
log = StringIO.new
|
|
158
|
-
connection = nil
|
|
159
|
-
value = Expect.spawn(RbConfig.ruby, "--disable-gems", "-e", 'sleep 0.03; puts "tail"',
|
|
160
|
-
raw_pty: true, graceful_close: true) do |session|
|
|
161
|
-
connection = session
|
|
162
|
-
session.log_output = log
|
|
163
|
-
:finished
|
|
164
|
-
end
|
|
165
|
-
assert_equal :finished, value
|
|
166
|
-
assert_equal "tail\n", log.string
|
|
167
|
-
assert connection.closed?
|
|
168
|
-
assert_equal 0, connection.exit_code
|
|
169
|
-
refute log.closed?
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def test_explicit_graceful_close_returns_nil_like_io_close
|
|
173
|
-
session = child('sleep 0.03; print "tail"', raw_pty: true)
|
|
174
|
-
assert_nil session.close(graceful: true)
|
|
175
|
-
assert_equal "tail", session.buffer
|
|
176
|
-
assert_equal 0, session.exit_code
|
|
177
|
-
assert_nil session.close
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def test_close_still_reaps_the_child_when_graceful_logging_fails
|
|
181
|
-
session = child('puts "ready"; sleep 60', raw_pty: true)
|
|
182
|
-
pid = session.pid
|
|
183
|
-
session.log_to { raise IOError, "log failed" }
|
|
184
|
-
assert_raises(IOError) { bounded { session.close(graceful: true) } }
|
|
185
|
-
assert session.closed?
|
|
186
|
-
refute session.alive?
|
|
187
|
-
assert_raises(Errno::ECHILD) { Process.waitpid(pid, Process::WNOHANG) }
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def test_invalid_close_timeout_does_not_close_or_signal_the_child
|
|
191
|
-
session = child('puts "ready"; sleep 60')
|
|
192
|
-
session.expect("ready", timeout: 2)
|
|
193
|
-
assert_raises(ArgumentError) { session.soft_close(timeout: 0, term_timeout: nil) }
|
|
194
|
-
assert_raises(ArgumentError) { session.soft_close(timeout: 0, term_timeout: -1) }
|
|
195
|
-
assert_raises(ArgumentError) { session.hard_close(timeout: nil) }
|
|
196
|
-
refute session.closed?
|
|
197
|
-
assert session.alive?
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def test_eof_does_not_prevent_buffered_match
|
|
201
|
-
session = child('print "final"; exit 4', raw_pty: true)
|
|
202
|
-
assert_equal 1, session.expect("final", timeout: 2)
|
|
203
|
-
assert session.expect_result(:eof, timeout: 2).eof?
|
|
204
|
-
assert_equal 4, session.wait(timeout: 1).exitstatus
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def test_wait_timeout_and_explicit_close
|
|
208
|
-
session = child("sleep 0.15; exit 0")
|
|
209
|
-
assert_nil session.wait(timeout: 0)
|
|
210
|
-
assert session.alive?
|
|
211
|
-
assert_equal 0, session.wait(timeout: 1).exitstatus
|
|
212
|
-
refute session.alive?
|
|
213
|
-
session.close
|
|
214
|
-
assert_raises(IOError) { session.write("x") }
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def test_borrowed_and_owned_io_lifetimes
|
|
218
|
-
session, writer = pipe_session
|
|
219
|
-
reader = session.to_io
|
|
220
|
-
session.close
|
|
221
|
-
refute reader.closed?
|
|
222
|
-
writer.write("x")
|
|
223
|
-
assert_equal "x", reader.read(1)
|
|
224
|
-
owned = Expect.open(reader, own: true)
|
|
225
|
-
owned.close
|
|
226
|
-
assert reader.closed?
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
def test_stty_roundtrip
|
|
230
|
-
session = child("sleep 30")
|
|
231
|
-
initial = session.stty
|
|
232
|
-
configuration = terminal_configuration(session)
|
|
233
|
-
session.stty("raw -echo")
|
|
234
|
-
refute session.to_io.echo?
|
|
235
|
-
session.stty(initial)
|
|
236
|
-
assert_equal configuration, terminal_configuration(session)
|
|
237
|
-
end
|
|
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
|
-
|
|
250
|
-
def test_configuration_and_session_attributes
|
|
251
|
-
Expect.configure(timeout: 0.01, log_stdout: false)
|
|
252
|
-
session = Expect.new
|
|
253
|
-
@sessions << session
|
|
254
|
-
refute session.log_stdout?
|
|
255
|
-
assert_equal 0.01, session.timeout
|
|
256
|
-
session.log_stdout = true
|
|
257
|
-
assert session.log_stdout
|
|
258
|
-
session.log_stdout = false
|
|
259
|
-
refute session.log_stdout
|
|
260
|
-
session.buffer_limit = 30
|
|
261
|
-
assert_equal 30, session.buffer_limit
|
|
262
|
-
assert_match(/\A#<Expect .*closed=false>\z/, session.inspect)
|
|
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
|
|
291
|
-
end
|