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
data/test/diagnostics_test.rb
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
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
|
data/test/edge_case_test.rb
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "test_helper"
|
|
4
|
-
require "open3"
|
|
5
|
-
|
|
6
|
-
class EdgeCaseTest < ExpectTest
|
|
7
|
-
def test_zero_width_continuation_observes_original_deadline
|
|
8
|
-
session, = pipe_session
|
|
9
|
-
session.buffer = "ready"
|
|
10
|
-
result = bounded(1) do
|
|
11
|
-
session.expect_result(timeout: 0.01) { on(/(?=ready)/) { Expect.continue(reset_timeout: false) } }
|
|
12
|
-
end
|
|
13
|
-
assert result.timeout?
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def test_zero_width_continuation_waits_for_new_input_before_matching_again
|
|
17
|
-
session, = pipe_session
|
|
18
|
-
session.buffer = "ready"
|
|
19
|
-
calls = 0
|
|
20
|
-
|
|
21
|
-
result = bounded(1) do
|
|
22
|
-
session.expect_result(timeout: 0.01) do
|
|
23
|
-
on(/(?=ready)/) do
|
|
24
|
-
calls += 1
|
|
25
|
-
Expect.continue
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
assert result.timeout?
|
|
31
|
-
assert_equal 1, calls
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def test_stalled_pattern_can_match_again_after_new_input
|
|
35
|
-
session, writer = pipe_session
|
|
36
|
-
session.buffer = "ready"
|
|
37
|
-
first_match = Queue.new
|
|
38
|
-
background do
|
|
39
|
-
first_match.pop
|
|
40
|
-
writer.write("!")
|
|
41
|
-
end
|
|
42
|
-
calls = 0
|
|
43
|
-
|
|
44
|
-
result = bounded(1) do
|
|
45
|
-
session.expect_result(timeout: 0.5) do
|
|
46
|
-
on(/(?=ready)/) do
|
|
47
|
-
calls += 1
|
|
48
|
-
first_match << true if calls == 1
|
|
49
|
-
calls == 1 ? Expect.continue : nil
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
assert result.matched?
|
|
55
|
-
assert_equal 2, calls
|
|
56
|
-
assert_equal "ready!", session.buffer
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def test_stalled_pattern_can_match_again_after_timeout_callback_changes_buffer
|
|
60
|
-
session, = pipe_session
|
|
61
|
-
session.buffer = "ready"
|
|
62
|
-
calls = 0
|
|
63
|
-
timeouts = 0
|
|
64
|
-
|
|
65
|
-
result = bounded(1) do
|
|
66
|
-
session.expect_result(timeout: 0.01) do
|
|
67
|
-
on(/(?=ready)/) do
|
|
68
|
-
calls += 1
|
|
69
|
-
calls == 1 ? Expect.continue : nil
|
|
70
|
-
end
|
|
71
|
-
timeout do
|
|
72
|
-
timeouts += 1
|
|
73
|
-
session.buffer = "ready!"
|
|
74
|
-
timeouts == 1 ? Expect.continue : nil
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
assert result.matched?
|
|
80
|
-
assert_equal 2, calls
|
|
81
|
-
assert_equal 1, timeouts
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def test_stdout_works_with_utf8_banner_and_ascii_regexp
|
|
85
|
-
session, writer = pipe_session
|
|
86
|
-
writer.write("欢迎登录\nprompt>")
|
|
87
|
-
assert_equal 1, session.expect(/prompt>/, timeout: 1)
|
|
88
|
-
assert_equal "欢迎登录\n".b, session.before
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def test_invalid_fixed_encoding_regexp_data_raises
|
|
92
|
-
session, = pipe_session
|
|
93
|
-
session.buffer = "\xffinvalid".b
|
|
94
|
-
assert_raises(EncodingError) { session.expect(/中文/, timeout: 0) }
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def test_replacing_log_with_invalid_target_preserves_current_log
|
|
98
|
-
session, = pipe_session
|
|
99
|
-
log = StringIO.new
|
|
100
|
-
session.log_to(log)
|
|
101
|
-
assert_raises(ArgumentError) { session.log_to(42) }
|
|
102
|
-
assert_same log, session.log_output
|
|
103
|
-
session.write_log("still open")
|
|
104
|
-
assert_equal "still open", log.string
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def test_new_session_predicates_use_ruby_truthiness
|
|
108
|
-
Expect.configure(raw_pty: 0, log_stdout: nil)
|
|
109
|
-
session = Expect.new
|
|
110
|
-
@sessions << session
|
|
111
|
-
assert session.raw_pty?
|
|
112
|
-
refute session.log_stdout?
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def test_read_only_regular_file
|
|
116
|
-
Tempfile.create("expect-input") do |file|
|
|
117
|
-
file.write("first\nsecond\n")
|
|
118
|
-
file.rewind
|
|
119
|
-
Expect.open(file) do |session|
|
|
120
|
-
assert_equal 1, session.expect(/^second$/, timeout: 0)
|
|
121
|
-
assert_equal "first\n", session.before
|
|
122
|
-
assert session.expect_result(:eof, timeout: 1).eof?
|
|
123
|
-
end
|
|
124
|
-
refute file.closed?
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def test_existing_pty_eio_is_eof
|
|
129
|
-
master, slave = PTY.open
|
|
130
|
-
@ios.push(master, slave)
|
|
131
|
-
session = Expect.open(master)
|
|
132
|
-
@sessions << session
|
|
133
|
-
slave.write("end")
|
|
134
|
-
slave.flush
|
|
135
|
-
assert_equal 1, session.expect("end", timeout: 1)
|
|
136
|
-
slave.close
|
|
137
|
-
assert bounded { session.expect_result(:eof, timeout: 1) }.eof?
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def test_distinct_reader_writer_io
|
|
141
|
-
incoming, producer = IO.pipe
|
|
142
|
-
consumer, outgoing = IO.pipe
|
|
143
|
-
@ios.push(incoming, producer, consumer, outgoing)
|
|
144
|
-
session = Expect.open(incoming, writer: outgoing)
|
|
145
|
-
@sessions << session
|
|
146
|
-
producer.write("ready")
|
|
147
|
-
assert_equal 1, session.expect("ready", timeout: 1)
|
|
148
|
-
assert_equal 3, session.write("abc")
|
|
149
|
-
assert_equal "abc", consumer.read(3)
|
|
150
|
-
session.close
|
|
151
|
-
refute incoming.closed?
|
|
152
|
-
refute outgoing.closed?
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def test_control_character_delivers_signal_to_foreground_child
|
|
156
|
-
session = child('Signal.trap("INT") { puts "interrupted"; exit 0 }; puts "ready"; sleep 30')
|
|
157
|
-
session.expect("ready", timeout: 2)
|
|
158
|
-
session.write("\x03")
|
|
159
|
-
assert_equal 1, session.expect("interrupted", timeout: 2)
|
|
160
|
-
assert_equal 0, session.wait(timeout: 1).exitstatus
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def test_multiple_real_pty_processes
|
|
164
|
-
first = child('puts "one"')
|
|
165
|
-
second = child('STDIN.gets; puts "two"', raw_pty: true)
|
|
166
|
-
seen = []
|
|
167
|
-
record_session = lambda do |session|
|
|
168
|
-
seen << session
|
|
169
|
-
Expect.continue(reset_timeout: false)
|
|
170
|
-
end
|
|
171
|
-
result = Expect.expect_result(timeout: 2) do
|
|
172
|
-
on(/one/, from: first, &record_session)
|
|
173
|
-
eof(from: first) do
|
|
174
|
-
# 明确建立两个进程的顺序,不能通过 sleep 推断启动和输出的先后。
|
|
175
|
-
second.puts("continue")
|
|
176
|
-
Expect.continue(reset_timeout: false)
|
|
177
|
-
end
|
|
178
|
-
on("two", from: second)
|
|
179
|
-
end
|
|
180
|
-
assert_equal [first], seen
|
|
181
|
-
assert_same second, result.session
|
|
182
|
-
assert_equal 3, result.number
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def test_signal_interruption_preserves_deadline
|
|
186
|
-
session, writer = pipe_session
|
|
187
|
-
old_handler = Signal.trap("USR1") { nil }
|
|
188
|
-
background do
|
|
189
|
-
4.times do
|
|
190
|
-
sleep 0.01
|
|
191
|
-
Process.kill("USR1", Process.pid)
|
|
192
|
-
end
|
|
193
|
-
writer.write("ready")
|
|
194
|
-
end
|
|
195
|
-
assert_equal(1, bounded { session.expect("ready", timeout: 1) })
|
|
196
|
-
ensure
|
|
197
|
-
Signal.trap("USR1", old_handler) if old_handler
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def test_gc_reclaims_abandoned_child
|
|
201
|
-
assert_gc_reclaims_abandoned_child
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
def test_gc_reclaims_abandoned_child_with_log_callback_capturing_session
|
|
205
|
-
assert_gc_reclaims_abandoned_child(log_callback: true)
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
def test_invalid_options_raise_before_spawning
|
|
209
|
-
assert_raises(ArgumentError) { Expect.new(typo: true) }
|
|
210
|
-
session, = pipe_session
|
|
211
|
-
assert_raises(ArgumentError) { session.expect(0, "x") }
|
|
212
|
-
assert_raises(ArgumentError) { session.expect(["-unknown", "x"], timeout: 0) }
|
|
213
|
-
assert_raises(ArgumentError) { session.on_sequence("") }
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def test_entrypoint_can_coexist_with_standard_library_expect
|
|
217
|
-
entrypoint = File.expand_path("../lib/expect/pty.rb", __dir__)
|
|
218
|
-
script = <<~RUBY
|
|
219
|
-
require "rbconfig"
|
|
220
|
-
require File.join(RbConfig::CONFIG.fetch("rubylibdir"), "expect.rb")
|
|
221
|
-
require ARGV.fetch(0)
|
|
222
|
-
puts [IO.method_defined?(:expect), defined?(Expect), Expect::VERSION].join(":")
|
|
223
|
-
RUBY
|
|
224
|
-
environment = { "RUBYOPT" => nil, "RUBYLIB" => nil, "BUNDLE_GEMFILE" => nil }
|
|
225
|
-
output, status = Open3.capture2e(environment, RbConfig.ruby, "-e", script, entrypoint)
|
|
226
|
-
assert status.success?, output
|
|
227
|
-
assert_equal "true:constant:#{Expect::VERSION}\n", output
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
private
|
|
231
|
-
|
|
232
|
-
def assert_gc_reclaims_abandoned_child(log_callback: false)
|
|
233
|
-
script = <<~RUBY
|
|
234
|
-
require "expect"
|
|
235
|
-
require "rbconfig"
|
|
236
|
-
def abandoned
|
|
237
|
-
session = Expect.spawn(RbConfig.ruby, "--disable-gems", "-e", "sleep 60", log_stdout: false)
|
|
238
|
-
session.log_to { |bytes| [session.pid, bytes] } if #{log_callback}
|
|
239
|
-
session.pid
|
|
240
|
-
end
|
|
241
|
-
# Ruby 的保守 GC 可能扫描到创建线程栈上残留的引用;先结束该线程,确保会话确实不可达。
|
|
242
|
-
pid = Thread.new { abandoned }.value
|
|
243
|
-
20.times do
|
|
244
|
-
GC.start
|
|
245
|
-
sleep 0.02
|
|
246
|
-
begin
|
|
247
|
-
Process.kill(0, pid)
|
|
248
|
-
rescue Errno::ESRCH
|
|
249
|
-
puts "reaped"
|
|
250
|
-
exit 0
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
Process.kill("KILL", pid) rescue nil
|
|
254
|
-
Process.waitpid(pid) rescue nil
|
|
255
|
-
abort "abandoned child survived GC"
|
|
256
|
-
RUBY
|
|
257
|
-
output, status = Open3.capture2e(RbConfig.ruby, "--disable-gems", "-I", File.expand_path("../lib", __dir__), "-e",
|
|
258
|
-
script)
|
|
259
|
-
assert status.success?, output
|
|
260
|
-
assert_equal "reaped\n", output
|
|
261
|
-
end
|
|
262
|
-
end
|