expect-pty 0.2.0 → 0.3.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 +15 -0
- data/Gemfile +21 -5
- data/README.md +40 -15
- data/docs/COMPATIBILITY.md +23 -1
- data/docs/RELEASING.md +26 -13
- data/docs/VERIFICATION.md +65 -1
- data/expect-pty.gemspec +8 -1
- data/lib/expect/configuration.rb +7 -0
- data/lib/expect/interaction.rb +249 -0
- data/lib/expect/{engine.rb → matcher.rb} +40 -5
- data/lib/expect/pattern.rb +12 -20
- data/lib/expect/relay.rb +133 -0
- data/lib/expect/relay_writer.rb +66 -0
- data/lib/expect/result.rb +2 -0
- data/lib/expect/{resources.rb → session_resources.rb} +3 -3
- data/lib/expect/version.rb +1 -1
- data/lib/expect.rb +105 -30
- data/script/ci +44 -7
- data/script/release.rb +22 -10
- data/test/edge_case_test.rb +101 -22
- data/test/interact_test.rb +102 -0
- data/test/interconnect_test.rb +203 -4
- data/test/io_test.rb +102 -0
- data/test/matching_test.rb +33 -0
- data/test/process_test.rb +3 -2
- data/test/relay_recovery_test.rb +414 -0
- data/test/release_test.rb +98 -1
- data/test/support/script_probe.rb +1 -0
- metadata +98 -7
- data/lib/expect/interconnect.rb +0 -170
data/test/interact_test.rb
CHANGED
|
@@ -25,6 +25,71 @@ class InteractTest < ExpectTest
|
|
|
25
25
|
[master, slave, source]
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def socket_session
|
|
29
|
+
local, peer = Socket.pair(:UNIX, :STREAM, 0)
|
|
30
|
+
@ios.push(local, peer)
|
|
31
|
+
session = Expect.open(local)
|
|
32
|
+
@sessions << session
|
|
33
|
+
[session, peer]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_changed_interact_regexp_does_not_match_previously_forwarded_input
|
|
37
|
+
session, peer = socket_session
|
|
38
|
+
input, keyboard = pipe_session
|
|
39
|
+
keyboard.write("old")
|
|
40
|
+
assert_nil session.interact(input: input.to_io, output: StringIO.new, escape: /STOP/, timeout: 0)
|
|
41
|
+
assert_equal "old", peer.read_nonblock(100)
|
|
42
|
+
|
|
43
|
+
assert_nil session.interact(input: input.to_io, output: StringIO.new, escape: /old/, timeout: 0)
|
|
44
|
+
keyboard.write("oldtail")
|
|
45
|
+
stopped = session.interact(input: input.to_io, output: StringIO.new, escape: /old/, timeout: 0.1)
|
|
46
|
+
assert_same input.to_io, stopped.to_io
|
|
47
|
+
assert_equal "tail", stopped.buffer
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_same_interact_regexp_keeps_cross_call_history_and_unread_tail
|
|
51
|
+
session, peer = socket_session
|
|
52
|
+
input, keyboard = pipe_session
|
|
53
|
+
keyboard.write("ST")
|
|
54
|
+
assert_nil session.interact(input: input.to_io, output: StringIO.new, escape: /STOP/, timeout: 0)
|
|
55
|
+
assert_equal "ST", peer.read_nonblock(100)
|
|
56
|
+
|
|
57
|
+
keyboard.write("OPtail")
|
|
58
|
+
stopped = session.interact(input: input.to_io, output: StringIO.new, escape: /STOP/, timeout: 0.1)
|
|
59
|
+
assert_same input.to_io, stopped.to_io
|
|
60
|
+
assert_equal "tail", stopped.buffer
|
|
61
|
+
assert_nil session.interact(input: input.to_io, output: StringIO.new, escape: /STOP/, timeout: 0)
|
|
62
|
+
assert_equal "tail", peer.read_nonblock(100)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_repeated_interact_keeps_split_crlf_on_the_same_terminal
|
|
66
|
+
session, = socket_session
|
|
67
|
+
master, slave, source = local_terminal
|
|
68
|
+
session.buffer = "first\r"
|
|
69
|
+
assert_nil session.interact(input: source, output: slave, timeout: 0)
|
|
70
|
+
assert_equal "first\r", master.read_nonblock(100)
|
|
71
|
+
|
|
72
|
+
session.buffer = "\nsecond"
|
|
73
|
+
assert_nil session.interact(input: source, output: slave, timeout: 0)
|
|
74
|
+
assert_equal "\nsecond", master.read_nonblock(100)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_changed_interact_regexp_preserves_pending_output_and_tail
|
|
78
|
+
session, peer = socket_session
|
|
79
|
+
input, keyboard = pipe_session
|
|
80
|
+
loop { break if session.writer.write_nonblock("x" * 4096, exception: false) == :wait_writable }
|
|
81
|
+
keyboard.write("old")
|
|
82
|
+
assert_nil session.interact(input: input, output: StringIO.new, escape: /STOP/, timeout: 0)
|
|
83
|
+
assert input.pending_output?
|
|
84
|
+
input.buffer = "tail"
|
|
85
|
+
loop { break if peer.read_nonblock(65_536, exception: false) == :wait_readable }
|
|
86
|
+
|
|
87
|
+
assert_nil session.interact(input: input, output: StringIO.new, escape: /old/, timeout: 0.01)
|
|
88
|
+
refute input.pending_output?
|
|
89
|
+
assert_empty input.buffer
|
|
90
|
+
assert_equal "oldtail", peer.read_nonblock(100)
|
|
91
|
+
end
|
|
92
|
+
|
|
28
93
|
def test_real_terminal_handoff_ctrl_c_escape_and_resume
|
|
29
94
|
session = shell_session
|
|
30
95
|
log = StringIO.new
|
|
@@ -98,6 +163,43 @@ class InteractTest < ExpectTest
|
|
|
98
163
|
keyboard&.kill&.join if keyboard&.alive?
|
|
99
164
|
end
|
|
100
165
|
|
|
166
|
+
def test_interact_preserves_newline_processing_on_the_shared_local_terminal
|
|
167
|
+
session = child(<<~'RUBY', raw_pty: true)
|
|
168
|
+
STDOUT.sync = true
|
|
169
|
+
while STDIN.gets
|
|
170
|
+
STDOUT.write("first\nsecond\r")
|
|
171
|
+
STDIN.gets
|
|
172
|
+
STDOUT.write("\nFW# ")
|
|
173
|
+
end
|
|
174
|
+
RUBY
|
|
175
|
+
master, _, source = local_terminal
|
|
176
|
+
screen = Expect.open(master, write_timeout: 3)
|
|
177
|
+
@sessions << screen
|
|
178
|
+
driver = Thread.new do
|
|
179
|
+
screen.write("show\n")
|
|
180
|
+
ScriptProbe.check(screen.expect("first\r\nsecond\r", timeout: 3),
|
|
181
|
+
"interact disabled output newline processing")
|
|
182
|
+
screen.write("continue\n")
|
|
183
|
+
ScriptProbe.check(screen.expect("\nFW# ", timeout: 3), "interact duplicated a split CRLF sequence")
|
|
184
|
+
screen.write(InteractProbe::ESCAPE)
|
|
185
|
+
true
|
|
186
|
+
rescue Exception # rubocop:disable Lint/RescueException -- Release interact when the assertion fails.
|
|
187
|
+
begin
|
|
188
|
+
screen.write(InteractProbe::ESCAPE)
|
|
189
|
+
rescue StandardError
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
raise
|
|
193
|
+
end
|
|
194
|
+
driver.report_on_exception = false
|
|
195
|
+
|
|
196
|
+
assert_same source, session.interact(input: source, escape: InteractProbe::ESCAPE, output: source.to_io, timeout: 5)
|
|
197
|
+
assert driver.join(2), "terminal driver did not finish"
|
|
198
|
+
assert driver.value
|
|
199
|
+
ensure
|
|
200
|
+
driver&.kill&.join if driver&.alive?
|
|
201
|
+
end
|
|
202
|
+
|
|
101
203
|
def test_remote_eof_restores_input_terminal_and_keeps_borrowed_io_open
|
|
102
204
|
session = child('STDIN.gets; print "FINAL_REMOTE_OUTPUT"', raw_pty: true)
|
|
103
205
|
_, slave, source = local_terminal
|
data/test/interconnect_test.rb
CHANGED
|
@@ -3,6 +3,113 @@
|
|
|
3
3
|
require_relative "test_helper"
|
|
4
4
|
|
|
5
5
|
class InterconnectTest < ExpectTest
|
|
6
|
+
def test_escape_callback_can_match_new_input_and_return_its_tail
|
|
7
|
+
session, writer = pipe_session
|
|
8
|
+
output = StringIO.new
|
|
9
|
+
session.listeners = [output]
|
|
10
|
+
result = nil
|
|
11
|
+
session.on_sequence("!") do
|
|
12
|
+
writer.write("ready tail")
|
|
13
|
+
result = session.expect("ready", timeout: 0.05)
|
|
14
|
+
false
|
|
15
|
+
end
|
|
16
|
+
session.buffer = "!"
|
|
17
|
+
|
|
18
|
+
assert_same(session, bounded { Expect.interconnect(session, timeout: 1) })
|
|
19
|
+
assert_equal 1, result
|
|
20
|
+
assert_equal " tail", session.buffer
|
|
21
|
+
assert_empty output.string
|
|
22
|
+
Expect.interconnect(session, timeout: 0)
|
|
23
|
+
assert_equal " tail", output.string
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_escape_callback_can_match_previously_read_tail
|
|
27
|
+
session, = pipe_session
|
|
28
|
+
result = nil
|
|
29
|
+
session.on_sequence("!") do
|
|
30
|
+
result = session.expect("ready", timeout: 0)
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
session.buffer = "!ready tail"
|
|
34
|
+
|
|
35
|
+
assert_same session, Expect.interconnect(session, timeout: 1)
|
|
36
|
+
assert_equal 1, result
|
|
37
|
+
assert_equal " tail", session.buffer
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_callback_match_failure_returns_unconsumed_bytes_to_relay
|
|
41
|
+
session, writer = pipe_session
|
|
42
|
+
session.on_sequence("!") do
|
|
43
|
+
writer.write("\xff".b)
|
|
44
|
+
session.expect(/ready/u, timeout: 0.05)
|
|
45
|
+
end
|
|
46
|
+
session.buffer = "!prefix"
|
|
47
|
+
|
|
48
|
+
assert_raises(EncodingError) { bounded { Expect.interconnect(session, timeout: 1) } }
|
|
49
|
+
assert_equal "prefix\xff".b, session.buffer
|
|
50
|
+
assert_nil session.__send__(:interaction_buffer)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_timeout_flush_leaves_blocked_output_pending_without_reading_after_deadline
|
|
54
|
+
reader, writer = IO.pipe
|
|
55
|
+
sink, sink_writer = IO.pipe
|
|
56
|
+
@ios.push(reader, writer, sink, sink_writer)
|
|
57
|
+
loop { break if sink_writer.write_nonblock("x" * 4096, exception: false) == :wait_writable }
|
|
58
|
+
session = Expect.open(reader, writer: sink_writer, write_timeout: 1)
|
|
59
|
+
@sessions << session
|
|
60
|
+
# A loopback listener must not wait or read more input after the deadline.
|
|
61
|
+
session.listeners = [session]
|
|
62
|
+
session.on_sequence("STOP")
|
|
63
|
+
session.buffer = "ST"
|
|
64
|
+
writer.write("tail")
|
|
65
|
+
original = IO.method(:select)
|
|
66
|
+
poll = true
|
|
67
|
+
select = lambda do |*arguments|
|
|
68
|
+
if poll
|
|
69
|
+
poll = false
|
|
70
|
+
nil
|
|
71
|
+
else
|
|
72
|
+
original.call(*arguments)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
IO.stub(:select, select) { assert_nil Expect.interconnect(session, timeout: 0) }
|
|
77
|
+
assert session.pending_output?
|
|
78
|
+
assert_empty session.buffer
|
|
79
|
+
session.listeners = []
|
|
80
|
+
assert_equal 1, session.expect("tail", timeout: 0)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_backpressure_reads_still_apply_escape_sequences
|
|
84
|
+
source, = pipe_session
|
|
85
|
+
reader, writer = IO.pipe
|
|
86
|
+
sink, sink_writer = IO.pipe
|
|
87
|
+
@ios.push(reader, writer, sink, sink_writer)
|
|
88
|
+
loop { break if sink_writer.write_nonblock("x" * 4096, exception: false) == :wait_writable }
|
|
89
|
+
target = Expect.open(reader, writer: sink_writer, write_timeout: 1)
|
|
90
|
+
@sessions << target
|
|
91
|
+
output = StringIO.new
|
|
92
|
+
received = Queue.new
|
|
93
|
+
target.log_to { received << true }
|
|
94
|
+
target.listeners = [output]
|
|
95
|
+
target.on_sequence("STOP")
|
|
96
|
+
source.listeners = [target]
|
|
97
|
+
source.buffer = "request"
|
|
98
|
+
writer.write("beforeSTOPtail")
|
|
99
|
+
background do
|
|
100
|
+
received.pop
|
|
101
|
+
sink.readpartial(65_536)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
assert_same(target, bounded { Expect.interconnect(target, source, timeout: 0.2) })
|
|
105
|
+
assert_equal "before", output.string
|
|
106
|
+
assert_equal "tail", target.buffer
|
|
107
|
+
target.log_output = nil
|
|
108
|
+
writer.write("ready")
|
|
109
|
+
assert_equal 1, target.expect("ready", timeout: 1)
|
|
110
|
+
assert_equal "tail", target.before
|
|
111
|
+
end
|
|
112
|
+
|
|
6
113
|
def test_interconnect_forwards_and_strips_split_escape
|
|
7
114
|
session, writer = pipe_session
|
|
8
115
|
listener = StringIO.new
|
|
@@ -102,14 +209,17 @@ class InterconnectTest < ExpectTest
|
|
|
102
209
|
assert_equal ["original"], source.instance_variable_get(:@sequences).keys
|
|
103
210
|
end
|
|
104
211
|
|
|
105
|
-
def
|
|
212
|
+
def test_interconnect_leaves_terminal_modes_to_the_caller
|
|
106
213
|
session = child('print "!"; sleep 30')
|
|
107
|
-
mode = session.to_io.console_mode
|
|
108
214
|
original = terminal_configuration(session)
|
|
109
|
-
|
|
215
|
+
seen = nil
|
|
216
|
+
session.on_sequence("!") do
|
|
217
|
+
seen = terminal_configuration(session)
|
|
218
|
+
raise "stop"
|
|
219
|
+
end
|
|
110
220
|
assert_raises(RuntimeError) { Expect.interconnect(session, timeout: 1) }
|
|
221
|
+
assert_equal original, seen
|
|
111
222
|
assert_equal original, terminal_configuration(session)
|
|
112
|
-
refute_nil mode
|
|
113
223
|
end
|
|
114
224
|
|
|
115
225
|
def test_manual_stty_leaves_terminal_mode_to_caller
|
|
@@ -136,6 +246,50 @@ class InterconnectTest < ExpectTest
|
|
|
136
246
|
assert_empty session.buffer
|
|
137
247
|
end
|
|
138
248
|
|
|
249
|
+
def test_interconnect_retries_an_interrupted_select
|
|
250
|
+
session, writer = pipe_session
|
|
251
|
+
output = StringIO.new
|
|
252
|
+
session.listeners = [output]
|
|
253
|
+
session.on_sequence("!")
|
|
254
|
+
writer.write("ready!")
|
|
255
|
+
original = IO.method(:select)
|
|
256
|
+
interrupted = true
|
|
257
|
+
select = lambda do |*arguments|
|
|
258
|
+
if interrupted
|
|
259
|
+
interrupted = false
|
|
260
|
+
raise Errno::EINTR
|
|
261
|
+
end
|
|
262
|
+
original.call(*arguments)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
IO.stub(:select, select) do
|
|
266
|
+
assert_same session, Expect.interconnect(session, timeout: 1)
|
|
267
|
+
end
|
|
268
|
+
assert_equal "ready", output.string
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def test_interconnect_retries_an_interrupted_read
|
|
272
|
+
session, writer = pipe_session
|
|
273
|
+
output = StringIO.new
|
|
274
|
+
session.listeners = [output]
|
|
275
|
+
session.on_sequence("!")
|
|
276
|
+
writer.write("ready!")
|
|
277
|
+
original = session.to_io.method(:read_nonblock)
|
|
278
|
+
interrupted = true
|
|
279
|
+
read = lambda do |*arguments, **options|
|
|
280
|
+
if interrupted
|
|
281
|
+
interrupted = false
|
|
282
|
+
raise Errno::EINTR
|
|
283
|
+
end
|
|
284
|
+
original.call(*arguments, **options)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
session.to_io.stub(:read_nonblock, read) do
|
|
288
|
+
assert_same session, Expect.interconnect(session, timeout: 1)
|
|
289
|
+
end
|
|
290
|
+
assert_equal "ready", output.string
|
|
291
|
+
end
|
|
292
|
+
|
|
139
293
|
def test_regexp_escape_matches_and_preserves_trailing_input
|
|
140
294
|
session, writer = pipe_session
|
|
141
295
|
output = StringIO.new
|
|
@@ -169,6 +323,36 @@ class InterconnectTest < ExpectTest
|
|
|
169
323
|
assert_equal "after", session.buffer
|
|
170
324
|
end
|
|
171
325
|
|
|
326
|
+
def test_regexp_escape_history_has_a_finite_default_window
|
|
327
|
+
session, = pipe_session
|
|
328
|
+
session.on_sequence(/UNLIKELY_ESCAPE/)
|
|
329
|
+
buffers = { session => "".b }
|
|
330
|
+
|
|
331
|
+
5.times do
|
|
332
|
+
buffers[session] << ("x" * 16_384)
|
|
333
|
+
Expect.send(:relay_buffer, session, buffers)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
assert_operator session.__send__(:relay_history).bytesize, :<=, 65_536
|
|
337
|
+
assert_empty buffers.fetch(session)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def test_utf8_regexp_escape_history_keeps_a_valid_leading_character
|
|
341
|
+
session, = pipe_session
|
|
342
|
+
session.on_sequence(/终止/u)
|
|
343
|
+
buffers = { session => "".b }
|
|
344
|
+
|
|
345
|
+
6.times do
|
|
346
|
+
buffers[session] << ("中" * 5461).b
|
|
347
|
+
assert Expect.send(:relay_buffer, session, buffers)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
assert_operator session.__send__(:relay_history).bytesize, :<=, 65_536
|
|
351
|
+
assert session.__send__(:relay_history).dup.force_encoding(Encoding::UTF_8).valid_encoding?
|
|
352
|
+
buffers[session] << "终止".b
|
|
353
|
+
refute Expect.send(:relay_buffer, session, buffers)
|
|
354
|
+
end
|
|
355
|
+
|
|
172
356
|
def test_regexp_escape_continuation_does_not_rematch_history
|
|
173
357
|
session, writer = pipe_session
|
|
174
358
|
seen = []
|
|
@@ -194,6 +378,21 @@ class InterconnectTest < ExpectTest
|
|
|
194
378
|
assert_equal "abc", session.buffer
|
|
195
379
|
end
|
|
196
380
|
|
|
381
|
+
def test_log_failure_preserves_received_bytes_for_recovery
|
|
382
|
+
session, writer = pipe_session
|
|
383
|
+
session.log_to { raise IOError, "log failed" }
|
|
384
|
+
writer.write("before!tail")
|
|
385
|
+
|
|
386
|
+
error = assert_raises(IOError) { Expect.interconnect(session, timeout: 1) }
|
|
387
|
+
assert_equal "log failed", error.message
|
|
388
|
+
assert_equal "before!tail", session.buffer
|
|
389
|
+
refute session.eof?
|
|
390
|
+
|
|
391
|
+
session.log_output = nil
|
|
392
|
+
assert_equal 1, session.expect("before!", timeout: 0)
|
|
393
|
+
assert_equal "tail", session.buffer
|
|
394
|
+
end
|
|
395
|
+
|
|
197
396
|
def test_logs_received_bytes_once_across_expect_interconnect_and_expect
|
|
198
397
|
session, writer = pipe_session
|
|
199
398
|
log = StringIO.new
|
data/test/io_test.rb
CHANGED
|
@@ -49,6 +49,26 @@ class IOTest < ExpectTest
|
|
|
49
49
|
assert_equal "\ncba\n", chunks.join
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
def test_new_log_files_are_private_without_changing_existing_permissions
|
|
53
|
+
session, = pipe_session
|
|
54
|
+
Dir.mktmpdir do |directory|
|
|
55
|
+
previous_umask = File.umask(0o022)
|
|
56
|
+
begin
|
|
57
|
+
%w[a w].each do |mode|
|
|
58
|
+
path = File.join(directory, "session-#{mode}.log")
|
|
59
|
+
session.log_to(path, mode: mode)
|
|
60
|
+
assert_equal 0o600, File.stat(path).mode & 0o777
|
|
61
|
+
session.log_output = nil
|
|
62
|
+
File.chmod(0o640, path)
|
|
63
|
+
session.log_to(path, mode: mode)
|
|
64
|
+
assert_equal 0o640, File.stat(path).mode & 0o777
|
|
65
|
+
end
|
|
66
|
+
ensure
|
|
67
|
+
File.umask(previous_umask)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
52
72
|
def test_group_and_stdout_logging_can_be_controlled_separately
|
|
53
73
|
session, writer = pipe_session
|
|
54
74
|
listener = StringIO.new
|
|
@@ -85,6 +105,25 @@ class IOTest < ExpectTest
|
|
|
85
105
|
assert_match(/received "more"/, diagnostics)
|
|
86
106
|
end
|
|
87
107
|
|
|
108
|
+
def test_quiet_mode_does_not_format_byte_content_for_diagnostics
|
|
109
|
+
session, peer = Socket.pair(:UNIX, :STREAM, 0)
|
|
110
|
+
@ios.push(session, peer)
|
|
111
|
+
connection = Expect.open(session, debug_level: 0)
|
|
112
|
+
@sessions << connection
|
|
113
|
+
inspected = 0
|
|
114
|
+
probe = TracePoint.new(:c_call) do |event|
|
|
115
|
+
inspected += 1 if event.defined_class == String && event.method_id == :inspect
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
probe.enable do
|
|
119
|
+
connection.write("hello")
|
|
120
|
+
peer.write("reply")
|
|
121
|
+
connection.expect("reply", timeout: 1)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
assert_equal 0, inspected
|
|
125
|
+
end
|
|
126
|
+
|
|
88
127
|
def test_send_slow_collects_replies_even_when_logging_disabled
|
|
89
128
|
session = child("loop { char = STDIN.read(1); break unless char; print char.upcase }", raw_pty: true)
|
|
90
129
|
session.log_listeners = false
|
|
@@ -121,6 +160,69 @@ class IOTest < ExpectTest
|
|
|
121
160
|
assert_empty Expect.readable_sessions(first, second)
|
|
122
161
|
end
|
|
123
162
|
|
|
163
|
+
def test_readiness_retries_an_interrupted_select
|
|
164
|
+
session, writer = pipe_session
|
|
165
|
+
writer.write("ready")
|
|
166
|
+
original = IO.method(:select)
|
|
167
|
+
interrupted = true
|
|
168
|
+
select = lambda do |*arguments|
|
|
169
|
+
if interrupted
|
|
170
|
+
interrupted = false
|
|
171
|
+
raise Errno::EINTR
|
|
172
|
+
end
|
|
173
|
+
original.call(*arguments)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
IO.stub(:select, select) do
|
|
177
|
+
assert_equal [session], Expect.readable_sessions(session, timeout: 1)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def test_write_retries_an_interrupted_nonblocking_write
|
|
182
|
+
reader, writer = Socket.pair(:UNIX, :STREAM, 0)
|
|
183
|
+
@ios.push(reader, writer)
|
|
184
|
+
session = Expect.open(reader)
|
|
185
|
+
@sessions << session
|
|
186
|
+
original = reader.method(:write_nonblock)
|
|
187
|
+
interrupted = true
|
|
188
|
+
write = lambda do |*arguments, **options|
|
|
189
|
+
if interrupted
|
|
190
|
+
interrupted = false
|
|
191
|
+
raise Errno::EINTR
|
|
192
|
+
end
|
|
193
|
+
original.call(*arguments, **options)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
reader.stub(:write_nonblock, write) do
|
|
197
|
+
assert_equal 5, session.write("hello")
|
|
198
|
+
end
|
|
199
|
+
assert_equal "hello", writer.read(5)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def test_write_retries_an_interrupted_writable_wait
|
|
203
|
+
reader, writer = Socket.pair(:UNIX, :STREAM, 0)
|
|
204
|
+
@ios.push(reader, writer)
|
|
205
|
+
session = Expect.open(reader, write_timeout: 1)
|
|
206
|
+
@sessions << session
|
|
207
|
+
original = reader.method(:write_nonblock)
|
|
208
|
+
waiting = true
|
|
209
|
+
write = lambda do |*arguments, **options|
|
|
210
|
+
if waiting
|
|
211
|
+
waiting = false
|
|
212
|
+
:wait_writable
|
|
213
|
+
else
|
|
214
|
+
original.call(*arguments, **options)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
reader.stub(:write_nonblock, write) do
|
|
219
|
+
IO.stub(:select, ->(*) { raise Errno::EINTR }) do
|
|
220
|
+
assert_equal 5, session.write("hello")
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
assert_equal "hello", writer.read(5)
|
|
224
|
+
end
|
|
225
|
+
|
|
124
226
|
def test_readiness_waits_for_data_and_does_not_consume_data
|
|
125
227
|
first, = pipe_session
|
|
126
228
|
second, writer = pipe_session
|
data/test/matching_test.rb
CHANGED
|
@@ -126,6 +126,19 @@ class MatchingTest < ExpectTest
|
|
|
126
126
|
assert_equal 1, session.expect("结束", timeout: 1)
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
def test_utf8_regexp_waits_for_incomplete_trailing_character_before_matching
|
|
130
|
+
session, writer = pipe_session
|
|
131
|
+
session.buffer = "ready\xE4".b
|
|
132
|
+
|
|
133
|
+
assert_nil session.expect(/ready\z/u, timeout: 0)
|
|
134
|
+
assert_nil session.expect(/ready/u, timeout: 0)
|
|
135
|
+
assert_equal "ready\xE4".b, session.buffer
|
|
136
|
+
|
|
137
|
+
writer.write("\xB8\xAD".b)
|
|
138
|
+
assert_equal 1, session.expect(/ready中\z/u, timeout: 1)
|
|
139
|
+
assert_equal "ready中".b, session.match
|
|
140
|
+
end
|
|
141
|
+
|
|
129
142
|
def test_binary_nul_and_invalid_utf8
|
|
130
143
|
session, writer = pipe_session
|
|
131
144
|
writer.write("\xff\x00abc\xfe".b)
|
|
@@ -134,6 +147,26 @@ class MatchingTest < ExpectTest
|
|
|
134
147
|
assert_equal ["abc"], session.captures
|
|
135
148
|
end
|
|
136
149
|
|
|
150
|
+
def test_utf8_regexp_rejects_impossible_partial_characters
|
|
151
|
+
session, = pipe_session
|
|
152
|
+
["\xE0\x80", "\xED\xA0", "\xF0\x80", "\xF4\x90"].each do |bytes|
|
|
153
|
+
session.buffer = bytes.b
|
|
154
|
+
assert_raises(EncodingError) { session.expect(/ready/u, timeout: 0) }
|
|
155
|
+
assert_equal bytes.b, session.buffer
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def test_utf8_regexp_accepts_valid_partial_characters
|
|
160
|
+
session, = pipe_session
|
|
161
|
+
["¢", "中", "😀", "\u{10FFFF}"].each do |character|
|
|
162
|
+
1.upto(character.bytesize - 1) do |length|
|
|
163
|
+
session.buffer = character.b.byteslice(0, length)
|
|
164
|
+
assert_nil session.expect(/./u, timeout: 0)
|
|
165
|
+
assert_equal character.b.byteslice(0, length), session.buffer
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
137
170
|
def test_native_regexp_anchors_and_flags
|
|
138
171
|
session, = pipe_session
|
|
139
172
|
session.buffer = "a\nb\nc"
|
data/test/process_test.rb
CHANGED
|
@@ -36,9 +36,10 @@ class ProcessTest < ExpectTest
|
|
|
36
36
|
assert session.slave.tty?
|
|
37
37
|
session.slave.echo = false
|
|
38
38
|
session.slave.winsize = [37, 111]
|
|
39
|
-
|
|
39
|
+
# 子进程需要 io-console gem,保留 RubyGems 以使用 Bundler 选定的版本。
|
|
40
|
+
session.spawn(RbConfig.ruby, "-rio/console", "-e",
|
|
40
41
|
'STDOUT.sync = true; puts STDIN.winsize.join(":"); puts STDIN.gets')
|
|
41
|
-
assert_equal 1, session.expect("37:111", timeout: 2)
|
|
42
|
+
assert_equal 1, session.expect("37:111", timeout: 2), session.before.inspect
|
|
42
43
|
assert_equal [37, 111], session.winsize
|
|
43
44
|
session.winsize = [24, 80]
|
|
44
45
|
assert_equal [24, 80], session.winsize
|