expect-pty 0.2.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.
- checksums.yaml +7 -0
- data/.rubocop.yml +29 -0
- data/CHANGELOG.md +25 -0
- data/Gemfile +10 -0
- data/LICENSE +21 -0
- data/README.md +264 -0
- data/Rakefile +38 -0
- data/docs/COMPATIBILITY.md +65 -0
- data/docs/RELEASING.md +55 -0
- data/docs/VERIFICATION.md +146 -0
- data/examples/dialogue.rb +30 -0
- data/examples/kibitz/README.md +73 -0
- data/examples/kibitz/kibitz.rb +139 -0
- data/examples/kibitz/test_kibitz.rb +37 -0
- data/examples/ssh_auto.rb +94 -0
- data/examples/ssh_interact.rb +159 -0
- data/examples/ssh_login.rb +64 -0
- data/expect-pty.gemspec +25 -0
- data/lib/expect/configuration.rb +113 -0
- data/lib/expect/engine.rb +141 -0
- data/lib/expect/interconnect.rb +170 -0
- data/lib/expect/pattern.rb +62 -0
- data/lib/expect/pattern_list.rb +90 -0
- data/lib/expect/pty.rb +4 -0
- data/lib/expect/resources.rb +63 -0
- data/lib/expect/result.rb +14 -0
- data/lib/expect/version.rb +6 -0
- data/lib/expect.rb +591 -0
- data/script/ci +44 -0
- data/script/release.rb +267 -0
- data/test/compare_upstream.rb +157 -0
- data/test/configuration_test.rb +90 -0
- data/test/edge_case_test.rb +183 -0
- data/test/fixtures/ssh_scripts/01_identity.sh +4 -0
- data/test/fixtures/ssh_scripts/02_output.sh +5 -0
- data/test/fixtures/ssh_scripts/03_delayed.sh +6 -0
- data/test/fixtures/ssh_scripts/04_failure.sh +2 -0
- data/test/fixtures/ssh_scripts/05_recovery.sh +3 -0
- data/test/integration/README.md +90 -0
- data/test/integration/ssh_scripts.rb +94 -0
- data/test/interact_test.rb +151 -0
- data/test/interconnect_test.rb +226 -0
- data/test/io_test.rb +184 -0
- data/test/kibitz_test.rb +45 -0
- data/test/matching_test.rb +178 -0
- data/test/multi_session_test.rb +66 -0
- data/test/process_test.rb +244 -0
- data/test/release_test.rb +153 -0
- data/test/ruby_api_test.rb +515 -0
- data/test/script_logging_test.rb +124 -0
- data/test/support/interact_probe.rb +125 -0
- data/test/support/kibitz_probe.rb +177 -0
- data/test/support/script_probe.rb +157 -0
- data/test/test_helper.rb +58 -0
- data/test/timeout_test.rb +170 -0
- metadata +97 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class RubyAPITest < ExpectTest
|
|
6
|
+
def test_result_uses_native_struct_conversion_and_pattern_matching
|
|
7
|
+
session, = pipe_session
|
|
8
|
+
session.buffer = "before value=42 after"
|
|
9
|
+
session.expect(/value=(\d+)/, timeout: 0)
|
|
10
|
+
result = session.last_result
|
|
11
|
+
values = [1, nil, "value=42", "before ", " after", session, ["42"]]
|
|
12
|
+
|
|
13
|
+
assert_equal values, result.to_a
|
|
14
|
+
assert_equal values, result.deconstruct
|
|
15
|
+
assert_equal values, Array(result)
|
|
16
|
+
assert_equal ["42"], result.to_h.fetch(:captures)
|
|
17
|
+
refute_respond_to result, :to_ary
|
|
18
|
+
number, error, match, before, after, connection, captures = result.to_a
|
|
19
|
+
assert_equal values, [number, error, match, before, after, connection, captures]
|
|
20
|
+
assert_pattern { result => { number: 1, captures: ["42"] } }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_send_retains_ruby_reflection_and_write_sends_bytes
|
|
24
|
+
client, server = Socket.pair(:UNIX, :STREAM, 0)
|
|
25
|
+
@ios.push(client, server)
|
|
26
|
+
Expect.open(client) do |session|
|
|
27
|
+
session.buffer = "ready"
|
|
28
|
+
assert_equal "ready", session.send(:buffer)
|
|
29
|
+
assert_nil session.send(:timeout=, nil)
|
|
30
|
+
assert_equal 3, session.send(:write, "abc")
|
|
31
|
+
assert_equal("abc", bounded { server.read(3) })
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_concise_expect_dsl_drives_a_dialogue_without_a_result_object
|
|
36
|
+
session = child('print "name: "; puts "hello " + gets.strip', raw_pty: true)
|
|
37
|
+
name = "Ruby"
|
|
38
|
+
matched = session.expect(timeout: 2) do
|
|
39
|
+
on("name: ") do |connection|
|
|
40
|
+
connection.puts(name)
|
|
41
|
+
connection.continue
|
|
42
|
+
end
|
|
43
|
+
on(/hello (\w+)/)
|
|
44
|
+
end
|
|
45
|
+
assert_equal 2, matched
|
|
46
|
+
assert_equal "hello Ruby", session.match
|
|
47
|
+
assert_equal ["Ruby"], session.captures
|
|
48
|
+
assert_equal 0, session.wait(timeout: 1).exitstatus
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_concise_dsl_supports_events_multi_session_and_result_objects
|
|
52
|
+
first, writer = pipe_session
|
|
53
|
+
second, = pipe_session
|
|
54
|
+
writer.close
|
|
55
|
+
ended = []
|
|
56
|
+
timed_out = []
|
|
57
|
+
result = Expect.expect_result(timeout: 0.02) do
|
|
58
|
+
eof(from: first) do |connection|
|
|
59
|
+
ended << connection
|
|
60
|
+
connection.continue(reset_timeout: false)
|
|
61
|
+
end
|
|
62
|
+
on("missing", from: second)
|
|
63
|
+
timeout { |sessions| timed_out.concat(sessions) }
|
|
64
|
+
end
|
|
65
|
+
assert result.timeout?
|
|
66
|
+
assert_equal [first], ended
|
|
67
|
+
assert_equal [second], timed_out
|
|
68
|
+
assert_same second, result.session
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_explicit_block_parameter_preserves_the_callers_self
|
|
72
|
+
session, = pipe_session
|
|
73
|
+
session.buffer = "ready"
|
|
74
|
+
owner = self
|
|
75
|
+
matched = session.expect(timeout: 0) do |patterns|
|
|
76
|
+
assert_same owner, self
|
|
77
|
+
patterns.on("ready") { assert_same owner, self }
|
|
78
|
+
end
|
|
79
|
+
assert_equal 1, matched
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_optional_block_parameter_still_receives_the_pattern_builder
|
|
83
|
+
session, = pipe_session
|
|
84
|
+
session.buffer = "ready"
|
|
85
|
+
matched = session.expect(timeout: 0) { |patterns = nil| patterns.on("ready") }
|
|
86
|
+
assert_equal 1, matched
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_break_from_a_concise_definition_does_not_consume_input
|
|
90
|
+
session, writer = pipe_session
|
|
91
|
+
writer.write("ready")
|
|
92
|
+
result = session.expect(timeout: 0) do
|
|
93
|
+
on("ready")
|
|
94
|
+
break :cancelled
|
|
95
|
+
end
|
|
96
|
+
assert_equal :cancelled, result
|
|
97
|
+
assert_nil session.last_result
|
|
98
|
+
assert_equal "ready", session.to_io.read_nonblock(5)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_exception_in_a_concise_definition_does_not_consume_input
|
|
102
|
+
session, writer = pipe_session
|
|
103
|
+
writer.write("ready")
|
|
104
|
+
assert_raises(RuntimeError) do
|
|
105
|
+
session.expect(timeout: 0) do
|
|
106
|
+
on("ready")
|
|
107
|
+
raise "definition failed"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
assert_equal "ready", session.to_io.read_nonblock(5)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_block_patterns_drive_a_real_dialogue_with_captured_variables
|
|
114
|
+
session = child(<<~'RUBY', raw_pty: true)
|
|
115
|
+
print "name: "
|
|
116
|
+
name = gets.strip
|
|
117
|
+
print "code: "
|
|
118
|
+
puts "hello #{name}:#{gets.strip}"
|
|
119
|
+
RUBY
|
|
120
|
+
name = "Ruby"
|
|
121
|
+
result = session.expect_result(timeout: 2) do |patterns|
|
|
122
|
+
patterns.on("name: ") do |connection|
|
|
123
|
+
connection.puts(name)
|
|
124
|
+
connection.continue
|
|
125
|
+
end
|
|
126
|
+
patterns.on("code: ") do |connection|
|
|
127
|
+
connection.send_slow("123\n", delay: 0)
|
|
128
|
+
connection.continue(reset_timeout: false)
|
|
129
|
+
end
|
|
130
|
+
patterns.on(/hello (\w+):(\d+)/)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
assert_equal 3, result.number
|
|
134
|
+
assert_equal %w[Ruby 123], result.captures
|
|
135
|
+
assert_same session, result.session
|
|
136
|
+
assert_equal 0, session.wait(timeout: 1).exitstatus
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def test_keyword_timeout_and_default_timeout
|
|
140
|
+
session, writer = pipe_session
|
|
141
|
+
session.timeout = 0
|
|
142
|
+
assert session.expect_result("missing").timeout?
|
|
143
|
+
assert session.expect_result("missing", timeout: 0.01).timeout?
|
|
144
|
+
background do
|
|
145
|
+
sleep 0.02
|
|
146
|
+
writer.write("ready")
|
|
147
|
+
end
|
|
148
|
+
assert_equal(1, bounded { session.expect("ready", timeout: nil) })
|
|
149
|
+
assert_equal 0, session.timeout
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def test_block_uses_keyword_timeout_and_returns_pattern_number
|
|
153
|
+
session, = pipe_session
|
|
154
|
+
session.buffer = "ready"
|
|
155
|
+
number = session.expect(timeout: 0) { |patterns| patterns.on("ready") }
|
|
156
|
+
assert_equal 1, number
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def test_literal_patterns_are_unambiguous_even_with_callbacks
|
|
160
|
+
%w[-i -ex -unknown timeout eof a.c].each do |literal|
|
|
161
|
+
session, = pipe_session
|
|
162
|
+
session.buffer = "prefix #{literal} suffix"
|
|
163
|
+
observed = nil
|
|
164
|
+
result = session.expect_result(timeout: 0) do |patterns|
|
|
165
|
+
patterns.on(literal) { |connection| observed = connection.match }
|
|
166
|
+
end
|
|
167
|
+
assert result.matched?, literal
|
|
168
|
+
assert_equal literal, observed
|
|
169
|
+
assert_equal "prefix ", result.before
|
|
170
|
+
assert_equal " suffix", session.buffer
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def test_positional_strings_are_literal_and_do_not_parse_command_line_flags
|
|
175
|
+
%w[-i -ex -re timeout eof].each do |literal|
|
|
176
|
+
session, = pipe_session
|
|
177
|
+
session.buffer = "before #{literal} after"
|
|
178
|
+
result = session.expect_result(literal, timeout: 0)
|
|
179
|
+
assert result.matched?
|
|
180
|
+
assert_equal literal, result.match
|
|
181
|
+
assert_equal " after", session.buffer
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def test_duplicate_timeout_callbacks_are_rejected_before_reading
|
|
186
|
+
session, writer = pipe_session
|
|
187
|
+
writer.write("ready")
|
|
188
|
+
assert_raises(ArgumentError) do
|
|
189
|
+
session.expect(timeout: 0) do
|
|
190
|
+
timeout { nil }
|
|
191
|
+
timeout { nil }
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
assert_equal "ready", session.to_io.read_nonblock(5)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def test_timeout_callback_receives_all_active_sessions
|
|
198
|
+
first, = pipe_session
|
|
199
|
+
second, = pipe_session
|
|
200
|
+
observed = nil
|
|
201
|
+
Expect.expect(timeout: 0) do
|
|
202
|
+
on("first", from: first)
|
|
203
|
+
on("second", from: second)
|
|
204
|
+
timeout { |sessions| observed = sessions }
|
|
205
|
+
end
|
|
206
|
+
assert_equal [first, second], observed
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def test_patterns_keep_declaration_priority
|
|
210
|
+
session, = pipe_session
|
|
211
|
+
session.buffer = "second first"
|
|
212
|
+
result = session.expect_result(timeout: 0) do |patterns|
|
|
213
|
+
patterns.on("first")
|
|
214
|
+
patterns.on("second")
|
|
215
|
+
end
|
|
216
|
+
assert_equal 1, result.number
|
|
217
|
+
assert_equal "second ", result.before
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def test_invalid_patterns_and_positional_timeouts_do_not_consume_input
|
|
221
|
+
session, writer = pipe_session
|
|
222
|
+
writer.write("ready")
|
|
223
|
+
[nil, 0].each do |timeout|
|
|
224
|
+
assert_raises(ArgumentError) { session.expect(timeout, "ready", timeout: 1) }
|
|
225
|
+
assert_raises(ArgumentError) { Expect.expect(timeout, "-i", session, "ready", timeout: 1) }
|
|
226
|
+
end
|
|
227
|
+
assert_raises(ArgumentError) { session.expect("ready", timeout: 0) { |patterns| patterns.on("ready") } }
|
|
228
|
+
assert_raises(ArgumentError) { session.expect(timeout: 0) { |patterns| patterns.on(:eof) } }
|
|
229
|
+
assert_raises(ArgumentError) { session.expect(timeout: 0) { |patterns| patterns.on("ready", from: []) } }
|
|
230
|
+
assert_raises(ArgumentError) { Expect.expect(timeout: 0) { |patterns| patterns.on("ready") } }
|
|
231
|
+
assert_raises(ArgumentError) { Expect.expect(timeout: 0) { |patterns| patterns.timeout { nil } } }
|
|
232
|
+
assert_raises(ArgumentError) { session.expect(timeout: -1) { flunk "invalid timeout must fail before the block" } }
|
|
233
|
+
assert_empty session.buffer
|
|
234
|
+
assert_equal "ready", session.to_io.read_nonblock(5)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def test_definition_exception_does_not_run_callbacks_or_consume_input
|
|
238
|
+
session, writer = pipe_session
|
|
239
|
+
writer.write("ready")
|
|
240
|
+
assert_raises(RuntimeError) do
|
|
241
|
+
session.expect(timeout: 0) do |patterns|
|
|
242
|
+
patterns.on("ready") { flunk "callbacks must wait until configuration finishes" }
|
|
243
|
+
raise "configuration failed"
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
assert_nil session.last_result
|
|
247
|
+
assert_equal "ready", session.to_io.read_nonblock(5)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def test_callback_exception_preserves_the_match_and_unread_buffer
|
|
251
|
+
session, = pipe_session
|
|
252
|
+
session.buffer = "ready tail"
|
|
253
|
+
error = assert_raises(RuntimeError) do
|
|
254
|
+
session.expect(timeout: 0) do |patterns|
|
|
255
|
+
patterns.on("ready") { raise "callback failed" }
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
assert_equal "callback failed", error.message
|
|
259
|
+
assert_equal "ready", session.match
|
|
260
|
+
assert_equal " tail", session.buffer
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def test_timeout_callback_can_continue_with_active_sessions
|
|
264
|
+
session, = pipe_session
|
|
265
|
+
observed = nil
|
|
266
|
+
result = session.expect_result(timeout: 0) do |patterns|
|
|
267
|
+
patterns.on("ready")
|
|
268
|
+
patterns.timeout do |sessions|
|
|
269
|
+
observed = sessions
|
|
270
|
+
session.buffer = "ready"
|
|
271
|
+
Expect.continue
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
assert_equal [session], observed
|
|
275
|
+
assert_equal 1, result.number
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def test_continuation_without_reset_observes_the_deadline
|
|
279
|
+
session, = pipe_session
|
|
280
|
+
session.buffer = "ready"
|
|
281
|
+
result = bounded(1) do
|
|
282
|
+
session.expect_result(timeout: 0.01) do |patterns|
|
|
283
|
+
patterns.on(/(?=ready)/) { Expect.continue(reset_timeout: false) }
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
assert result.timeout?
|
|
287
|
+
assert_equal "ready", session.buffer
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def test_eof_block_receives_the_session_and_remaining_bytes
|
|
291
|
+
session, writer = pipe_session
|
|
292
|
+
writer.write("tail")
|
|
293
|
+
writer.close
|
|
294
|
+
observed = nil
|
|
295
|
+
result = session.expect_result(timeout: 1) do |patterns|
|
|
296
|
+
patterns.eof { |connection| observed = [connection, connection.before] }
|
|
297
|
+
end
|
|
298
|
+
assert result.eof?
|
|
299
|
+
assert_equal [session, "tail"], observed
|
|
300
|
+
assert_empty session.buffer
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def test_class_block_routes_patterns_to_multiple_sessions
|
|
304
|
+
first, = pipe_session
|
|
305
|
+
second, writer = pipe_session
|
|
306
|
+
writer.write("ready:42")
|
|
307
|
+
result = Expect.expect_result(timeout: 1) do |patterns|
|
|
308
|
+
patterns.on("missing", from: first)
|
|
309
|
+
patterns.on(/ready:(\d+)/, from: [first, second])
|
|
310
|
+
end
|
|
311
|
+
assert_equal 2, result.number
|
|
312
|
+
assert_same second, result.session
|
|
313
|
+
assert_equal ["42"], result.captures
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def test_class_keyword_from_selects_a_session
|
|
317
|
+
session, writer = pipe_session
|
|
318
|
+
writer.write("ready")
|
|
319
|
+
assert_equal 1, Expect.expect("ready", from: session, timeout: 1)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def test_eof_continuation_keeps_other_sessions_active
|
|
323
|
+
first, first_writer = pipe_session
|
|
324
|
+
second, second_writer = pipe_session
|
|
325
|
+
first_writer.close
|
|
326
|
+
observed = []
|
|
327
|
+
result = Expect.expect_result(timeout: 1) do |patterns|
|
|
328
|
+
patterns.eof(from: first) do |connection|
|
|
329
|
+
observed << connection
|
|
330
|
+
second_writer.write("ready")
|
|
331
|
+
connection.continue(reset_timeout: false)
|
|
332
|
+
end
|
|
333
|
+
patterns.on("ready", from: second)
|
|
334
|
+
end
|
|
335
|
+
assert_equal [first], observed
|
|
336
|
+
assert_same second, result.session
|
|
337
|
+
assert_equal 2, result.number
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def test_timeout_before_class_patterns_receives_active_sessions
|
|
341
|
+
session, = pipe_session
|
|
342
|
+
observed = nil
|
|
343
|
+
result = Expect.expect_result(timeout: 0) do |patterns|
|
|
344
|
+
patterns.timeout { |sessions| observed = sessions }
|
|
345
|
+
patterns.on("missing", from: session)
|
|
346
|
+
end
|
|
347
|
+
assert result.timeout?
|
|
348
|
+
assert_equal [session], observed
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def test_open_returns_block_value_and_preserves_borrowed_io
|
|
352
|
+
reader, writer = IO.pipe
|
|
353
|
+
@ios.push(reader, writer)
|
|
354
|
+
connection = nil
|
|
355
|
+
result = Expect.open(reader) do |session|
|
|
356
|
+
connection = session
|
|
357
|
+
:finished
|
|
358
|
+
end
|
|
359
|
+
assert_equal :finished, result
|
|
360
|
+
assert connection.closed?
|
|
361
|
+
refute reader.closed?
|
|
362
|
+
refute writer.closed?
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def test_open_closes_both_owned_handles_on_exception
|
|
366
|
+
reader, writer = IO.pipe
|
|
367
|
+
@ios.push(reader, writer)
|
|
368
|
+
assert_raises(RuntimeError) do
|
|
369
|
+
Expect.open(reader, writer: writer, own: true) { raise "stop" }
|
|
370
|
+
end
|
|
371
|
+
assert reader.closed?
|
|
372
|
+
assert writer.closed?
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def test_invalid_open_options_close_owned_io_and_preserve_borrowed_io
|
|
376
|
+
[false, true].each do |own|
|
|
377
|
+
reader, writer = IO.pipe
|
|
378
|
+
@ios.push(reader, writer)
|
|
379
|
+
assert_raises(ArgumentError) do
|
|
380
|
+
Expect.open(reader, writer: writer, own: own, unknown: true) { flunk "invalid options" }
|
|
381
|
+
end
|
|
382
|
+
assert_equal own, reader.closed?
|
|
383
|
+
assert_equal own, writer.closed?
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def test_boolean_predicates_use_ruby_truthiness_and_have_no_write_arguments
|
|
388
|
+
session, = pipe_session
|
|
389
|
+
refute session.log_stdout?
|
|
390
|
+
session.log_stdout = true
|
|
391
|
+
assert session.log_stdout?
|
|
392
|
+
session.log_stdout = 0
|
|
393
|
+
assert session.log_stdout?
|
|
394
|
+
session.log_stdout = nil
|
|
395
|
+
refute session.log_stdout?
|
|
396
|
+
assert_raises(ArgumentError) { session.log_stdout?(true) }
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def test_readiness_accepts_an_empty_group_with_a_keyword_timeout
|
|
400
|
+
assert_empty Expect.readable_sessions(timeout: 0)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def test_spawn_closes_on_nonlocal_block_exit
|
|
404
|
+
connection = nil
|
|
405
|
+
result = Expect.spawn(RbConfig.ruby, "--disable-gems", "-e", "sleep 60", log_stdout: false) do |session|
|
|
406
|
+
connection = session
|
|
407
|
+
break :finished
|
|
408
|
+
end
|
|
409
|
+
assert_equal :finished, result
|
|
410
|
+
assert connection.closed?
|
|
411
|
+
refute connection.alive?
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def test_buffer_setter_copies_bytes_and_applies_the_limit
|
|
415
|
+
session, = pipe_session(buffer_limit: 4)
|
|
416
|
+
value = +"before tail"
|
|
417
|
+
session.buffer = value
|
|
418
|
+
value.replace("changed")
|
|
419
|
+
session.buffer.clear
|
|
420
|
+
assert_equal "tail", session.buffer
|
|
421
|
+
assert_equal Encoding::BINARY, session.buffer.encoding
|
|
422
|
+
assert_equal "tail", session.clear_buffer
|
|
423
|
+
assert_empty session.buffer
|
|
424
|
+
assert_raises(ArgumentError) { session.buffer = nil }
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def test_listeners_setter_takes_a_snapshot_and_validates_before_replacement
|
|
428
|
+
session, writer = pipe_session
|
|
429
|
+
output = StringIO.new
|
|
430
|
+
listeners = [output]
|
|
431
|
+
session.listeners = listeners
|
|
432
|
+
listeners.clear
|
|
433
|
+
session.listeners.clear
|
|
434
|
+
assert_raises(ArgumentError) { session.listeners = [Object.new] }
|
|
435
|
+
writer.write("ready")
|
|
436
|
+
session.expect("ready", timeout: 1)
|
|
437
|
+
assert_equal "ready", output.string
|
|
438
|
+
session.listeners = []
|
|
439
|
+
assert_empty session.listeners
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def test_log_block_and_setter_receive_only_read_bytes
|
|
443
|
+
session, writer = pipe_session
|
|
444
|
+
chunks = []
|
|
445
|
+
session.log_to { |bytes| chunks << bytes }
|
|
446
|
+
assert_raises(ArgumentError) { session.log_to(StringIO.new) { nil } }
|
|
447
|
+
writer.write("first")
|
|
448
|
+
session.expect("first", timeout: 1)
|
|
449
|
+
output = StringIO.new
|
|
450
|
+
session.log_output = output
|
|
451
|
+
writer.write("second")
|
|
452
|
+
session.expect("second", timeout: 1)
|
|
453
|
+
session.log_output = nil
|
|
454
|
+
assert_equal "first", chunks.join
|
|
455
|
+
assert_equal "second", output.string
|
|
456
|
+
refute output.closed?
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def test_puts_matches_standard_ruby_io_for_nested_and_recursive_arrays
|
|
460
|
+
client, server = Socket.pair(:UNIX, :STREAM, 0)
|
|
461
|
+
@ios.push(client, server)
|
|
462
|
+
recursive = ["once"]
|
|
463
|
+
recursive << recursive
|
|
464
|
+
values = [nil, 42, "中文", "already\n", ["nested", ["last"]], recursive, "\xff".b]
|
|
465
|
+
expected = StringIO.new("".b)
|
|
466
|
+
expected.puts
|
|
467
|
+
expected.puts(*values)
|
|
468
|
+
Expect.open(client) do |session|
|
|
469
|
+
assert_nil session.puts
|
|
470
|
+
assert_nil session.puts(*values)
|
|
471
|
+
end
|
|
472
|
+
assert_equal(expected.string, bounded { server.read(expected.string.bytesize) })
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def test_assigning_the_current_log_preserves_file_ownership
|
|
476
|
+
session, = pipe_session
|
|
477
|
+
Dir.mktmpdir do |directory|
|
|
478
|
+
log = session.log_to(File.join(directory, "session.log"))
|
|
479
|
+
session.log_output = log
|
|
480
|
+
session.close
|
|
481
|
+
assert log.closed?
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def test_write_converts_objects_and_append_returns_the_session
|
|
486
|
+
client, server = Socket.pair(:UNIX, :STREAM, 0)
|
|
487
|
+
@ios.push(client, server)
|
|
488
|
+
values = [nil, 42, %w[a b]]
|
|
489
|
+
expected = StringIO.new
|
|
490
|
+
count = expected.write(*values)
|
|
491
|
+
Expect.open(client) do |session|
|
|
492
|
+
assert_equal count, session.write(*values)
|
|
493
|
+
assert_same session, session << "tail" << "\n"
|
|
494
|
+
end
|
|
495
|
+
expected << "tail\n"
|
|
496
|
+
assert_equal(expected.string, bounded { server.read(expected.string.bytesize) })
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def test_sequence_block_filters_escape_and_keeps_trailing_bytes
|
|
500
|
+
session, writer = pipe_session
|
|
501
|
+
output = StringIO.new
|
|
502
|
+
session.listeners = [output]
|
|
503
|
+
observed = []
|
|
504
|
+
value = :done
|
|
505
|
+
session.on_sequence("STOP") do
|
|
506
|
+
observed << value
|
|
507
|
+
false
|
|
508
|
+
end
|
|
509
|
+
writer.write("beforeSTOPafter")
|
|
510
|
+
assert_same session, Expect.interconnect(session, timeout: 1)
|
|
511
|
+
assert_equal [:done], observed
|
|
512
|
+
assert_equal "before", output.string
|
|
513
|
+
assert_equal "after", session.buffer
|
|
514
|
+
end
|
|
515
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
require_relative "support/script_probe"
|
|
5
|
+
require "etc"
|
|
6
|
+
|
|
7
|
+
class ScriptLoggingTest < ExpectTest
|
|
8
|
+
def shell_probe
|
|
9
|
+
session = Expect.spawn("/bin/sh", "-i", env: { "PS1" => ScriptProbe::PROMPT, "ENV" => nil, "LC_ALL" => "C" },
|
|
10
|
+
raw_pty: true, log_stdout: false, write_timeout: 3)
|
|
11
|
+
@sessions << session
|
|
12
|
+
ScriptProbe::Runner.new(session).ready!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_multiple_script_files_with_persisted_log_and_shutdown_drain
|
|
16
|
+
runner = shell_probe
|
|
17
|
+
Dir.mktmpdir do |dir|
|
|
18
|
+
report = ScriptProbe.verify_file_session(runner, File.join(dir, "session.log"), user: Etc.getpwuid.name)
|
|
19
|
+
assert_equal([0, 0, 0, 7, 0, 0, 0], report[:cases].map { |result| result[:status] })
|
|
20
|
+
assert(report[:cases].all? { |result| result[:passed] })
|
|
21
|
+
assert_operator report[:log_bytes], :>, 500
|
|
22
|
+
assert_equal 10, report[:checks].length
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_callback_log_records_multiple_scripts_exactly_once
|
|
27
|
+
runner = shell_probe
|
|
28
|
+
chunks = []
|
|
29
|
+
runner.session.log_to(->(bytes) { chunks << bytes.dup })
|
|
30
|
+
%w[first second].each do |value|
|
|
31
|
+
runner.run(value, "printf '#{value}\\n'\n", expected_status: 0, expected_output: "#{value}\n")
|
|
32
|
+
end
|
|
33
|
+
runner.finish!
|
|
34
|
+
log = ScriptProbe.normalize(chunks.join)
|
|
35
|
+
assert_equal 1, log.scan(/^first$/).length
|
|
36
|
+
assert_equal 1, log.scan(/^second$/).length
|
|
37
|
+
assert_operator log.index("[SEND] first"), :<, log.index("[SEND] second")
|
|
38
|
+
assert_includes log, "SESSION_FINAL_TAIL\n"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_borrowed_log_is_flushed_and_stays_open_after_multiple_scripts
|
|
42
|
+
runner = shell_probe
|
|
43
|
+
Tempfile.create("expect-borrowed-log") do |file|
|
|
44
|
+
runner.session.log_to(file)
|
|
45
|
+
runner.run("one", "printf 'BORROWED_ONE\\n'", expected_status: 0, expected_output: "BORROWED_ONE\n")
|
|
46
|
+
assert_includes File.binread(file.path), "BORROWED_ONE\n"
|
|
47
|
+
runner.run("two", "printf 'BORROWED_TWO\\n'", expected_status: 0, expected_output: "BORROWED_TWO\n")
|
|
48
|
+
runner.finish!
|
|
49
|
+
refute file.closed?
|
|
50
|
+
text = File.binread(file.path)
|
|
51
|
+
assert_includes text, "BORROWED_TWO\n"
|
|
52
|
+
assert_includes text, "SESSION_FINAL_TAIL\n"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_nonzero_exit_is_reported_even_when_expected_text_appears
|
|
57
|
+
runner = shell_probe
|
|
58
|
+
failure = assert_raises(ScriptProbe::Failure) do
|
|
59
|
+
runner.run("bad_status", "printf 'looks good\\n'; exit 9", expected_status: 0, expected_output: "looks good\n")
|
|
60
|
+
end
|
|
61
|
+
assert_match(/exit 9, expected 0/, failure.message)
|
|
62
|
+
refute runner.results.last[:passed]
|
|
63
|
+
result = runner.run("after_failure", "printf 'still alive\\n'", expected_status: 0,
|
|
64
|
+
expected_output: "still alive\n")
|
|
65
|
+
assert result[:passed]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_matching_output_is_required_even_with_zero_exit_status
|
|
69
|
+
runner = shell_probe
|
|
70
|
+
assert_raises(ScriptProbe::Failure) do
|
|
71
|
+
runner.run("wrong_output", "printf 'actual\\n'", expected_status: 0, expected_output: "expected\n")
|
|
72
|
+
end
|
|
73
|
+
refute runner.results.last[:passed]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_script_arguments_preserve_quotes_and_shell_metacharacters
|
|
77
|
+
runner = shell_probe
|
|
78
|
+
result = runner.run("quoting", "printf '%s\\n' \"a'b\" '$(not-a-command)' 'semi;colon'\n",
|
|
79
|
+
expected_status: 0, expected_output: "a'b\n$(not-a-command)\nsemi;colon\n")
|
|
80
|
+
assert result[:passed]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_crlf_terminal_output_preserves_exact_script_boundaries
|
|
84
|
+
runner = shell_probe
|
|
85
|
+
runner.session.write("stty opost onlcr\n")
|
|
86
|
+
assert_equal 1, runner.session.expect(ScriptProbe::PROMPT, timeout: 2)
|
|
87
|
+
["", "line\n", "no final newline", "two\n\n"].each do |output|
|
|
88
|
+
result = runner.run("crlf", "printf %s #{Shellwords.escape(output)}",
|
|
89
|
+
expected_status: 0, expected_output: output)
|
|
90
|
+
assert_equal output, result[:output]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_logger_error_is_visible_to_caller
|
|
95
|
+
runner = shell_probe
|
|
96
|
+
runner.session.log_to(->(_) { raise IOError, "log destination failed" })
|
|
97
|
+
error = assert_raises(IOError) do
|
|
98
|
+
runner.run("logger_failure", "printf 'output\\n'", expected_status: 0, expected_output: "output\n")
|
|
99
|
+
end
|
|
100
|
+
assert_equal "log destination failed", error.message
|
|
101
|
+
assert_empty runner.results
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_timeout_preserves_log_and_subsequent_output_is_captured
|
|
105
|
+
runner = shell_probe
|
|
106
|
+
session = runner.session
|
|
107
|
+
Dir.mktmpdir do |dir|
|
|
108
|
+
path = File.join(dir, "timeout.log")
|
|
109
|
+
session.log_to(path, mode: "w")
|
|
110
|
+
session.write("printf 'BEFORE_TIMEOUT\\n'; sleep 0.15; printf 'AFTER_TIMEOUT\\n'\n")
|
|
111
|
+
assert_equal 1, session.expect("BEFORE_TIMEOUT\n", timeout: 2)
|
|
112
|
+
assert_nil session.expect("AFTER_TIMEOUT", timeout: 0.02)
|
|
113
|
+
assert_equal :timeout, session.error
|
|
114
|
+
assert_includes File.binread(path), "BEFORE_TIMEOUT\n"
|
|
115
|
+
assert_equal 1, session.expect("AFTER_TIMEOUT\n", timeout: 2)
|
|
116
|
+
assert_equal 1, session.expect(ScriptProbe::PROMPT, timeout: 2)
|
|
117
|
+
runner.finish!
|
|
118
|
+
text = File.binread(path)
|
|
119
|
+
assert_equal 1, text.scan("BEFORE_TIMEOUT\n").length
|
|
120
|
+
assert_equal 1, text.scan("AFTER_TIMEOUT\n").length
|
|
121
|
+
assert_includes text, "SESSION_FINAL_TAIL\n"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|