expect-pty 0.5.3 → 0.7.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 +55 -1
- data/README.md +122 -130
- data/docs/API.md +113 -0
- data/docs/MIGRATION.md +68 -0
- data/lib/expect/cleanup.rb +34 -0
- data/lib/expect/interaction.rb +228 -210
- data/lib/expect/logging.rb +117 -162
- data/lib/expect/matcher.rb +42 -38
- data/lib/expect/pattern.rb +10 -3
- data/lib/expect/pattern_list.rb +39 -9
- data/lib/expect/redactor.rb +12 -5
- data/lib/expect/relay.rb +106 -81
- data/lib/expect/relay_writer.rb +7 -5
- data/lib/expect/result.rb +33 -9
- data/lib/expect/session.rb +554 -0
- data/lib/expect/session_resources.rb +19 -18
- data/lib/expect/version.rb +2 -2
- data/lib/expect.rb +68 -578
- data/sig/expect.rbs +279 -0
- metadata +14 -123
- 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/lib/expect/configuration.rb +0 -122
- data/lib/expect/terminal.rb +0 -102
- 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/interact_test.rb
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "test_helper"
|
|
4
|
-
require_relative "support/interact_probe"
|
|
5
|
-
require "etc"
|
|
6
|
-
|
|
7
|
-
class InteractTest < ExpectTest
|
|
8
|
-
def shell_session
|
|
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
|
-
InteractProbe.prepare(session)
|
|
14
|
-
# Unlike SSH, the simulated remote shell shares the transport's PTY.
|
|
15
|
-
# Keep its canonical/ISIG settings while interact raws the local keyboard.
|
|
16
|
-
session.raw_terminal = false
|
|
17
|
-
session
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def local_terminal
|
|
21
|
-
master, slave = PTY.open
|
|
22
|
-
@ios.push(master, slave)
|
|
23
|
-
source = Expect.open(slave)
|
|
24
|
-
@sessions << source
|
|
25
|
-
[master, slave, source]
|
|
26
|
-
end
|
|
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
|
-
|
|
93
|
-
def test_real_terminal_handoff_ctrl_c_escape_and_resume
|
|
94
|
-
session = shell_session
|
|
95
|
-
log = StringIO.new
|
|
96
|
-
session.log_to(log)
|
|
97
|
-
report = bounded(20) { InteractProbe.verify(session, user: Etc.getpwuid.name) }
|
|
98
|
-
assert_equal 4, report[:cases].length
|
|
99
|
-
assert(report[:cases].all? { |result| result[:passed] })
|
|
100
|
-
assert report[:local_terminal_restored]
|
|
101
|
-
assert report[:transport_terminal_restored]
|
|
102
|
-
text = ScriptProbe.normalize(log.string)
|
|
103
|
-
%w[AUTOMATION_RESUMED SECOND_HANDOFF_OK STDOUT_FIRST STDERR_SECOND].each do |line|
|
|
104
|
-
assert_equal 1, text.scan(/^#{line}$/).length
|
|
105
|
-
end
|
|
106
|
-
refute_includes text, InteractProbe::TAIL
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def test_timeout_restores_local_tty_and_session_settings
|
|
110
|
-
session = shell_session
|
|
111
|
-
_, slave, source = local_terminal
|
|
112
|
-
saved = InteractProbe.configuration(source)
|
|
113
|
-
state = InteractProbe.settings(session)
|
|
114
|
-
source.on_sequence("original")
|
|
115
|
-
source_state = InteractProbe.settings(source)
|
|
116
|
-
assert_nil session.interact(input: source, escape: "\x1d", output: slave, timeout: 0.02)
|
|
117
|
-
assert_equal saved, InteractProbe.configuration(source)
|
|
118
|
-
assert_equal state, InteractProbe.settings(session)
|
|
119
|
-
assert_equal source_state, InteractProbe.settings(source)
|
|
120
|
-
assert session.alive?
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def test_manual_typing_is_visible_before_enter_and_automation_resumes_without_echo
|
|
124
|
-
session = shell_session
|
|
125
|
-
InteractProbe.prepare(session, echo: true)
|
|
126
|
-
master, slave, source = local_terminal
|
|
127
|
-
screen = Expect.open(master, write_timeout: 3)
|
|
128
|
-
@sessions << screen
|
|
129
|
-
saved = InteractProbe.configuration(source)
|
|
130
|
-
session.write("printf '\\n%s\\n' 'MANUAL_READY'\n")
|
|
131
|
-
keyboard = Thread.new do
|
|
132
|
-
ScriptProbe.check(screen.expect(/\r?\nMANUAL_READY\r?\n/, timeout: 3), "manual handoff was not ready")
|
|
133
|
-
ScriptProbe.check(screen.expect(ScriptProbe::PROMPT, timeout: 3), "manual prompt missing")
|
|
134
|
-
command = "printf 'VISIBLE_COMMAND\\n'"
|
|
135
|
-
screen.write(command)
|
|
136
|
-
ScriptProbe.check(screen.expect(command, timeout: 3), "typed command was invisible before Enter")
|
|
137
|
-
screen.write("\n")
|
|
138
|
-
ScriptProbe.check(screen.expect(/\r?\nVISIBLE_COMMAND\r?\n/, timeout: 3), "manual command output missing")
|
|
139
|
-
ScriptProbe.check(screen.expect(ScriptProbe::PROMPT, timeout: 3), "manual command did not return to shell")
|
|
140
|
-
screen.write(InteractProbe::ESCAPE)
|
|
141
|
-
true
|
|
142
|
-
rescue Exception # rubocop:disable Lint/RescueException -- Clean up terminal drivers even on Interrupt or SystemExit.
|
|
143
|
-
begin
|
|
144
|
-
screen.write(InteractProbe::ESCAPE)
|
|
145
|
-
rescue StandardError
|
|
146
|
-
nil
|
|
147
|
-
end
|
|
148
|
-
raise
|
|
149
|
-
end
|
|
150
|
-
keyboard.report_on_exception = false
|
|
151
|
-
assert_same source, session.interact(input: source, escape: InteractProbe::ESCAPE, output: slave, timeout: 12)
|
|
152
|
-
assert keyboard.join(2), "manual keyboard driver did not finish"
|
|
153
|
-
assert keyboard.value
|
|
154
|
-
assert_equal saved, InteractProbe.configuration(source)
|
|
155
|
-
InteractProbe.prepare(session)
|
|
156
|
-
log = StringIO.new
|
|
157
|
-
session.log_to(log)
|
|
158
|
-
result = ScriptProbe::Runner.new(session).run("manual_resume", "printf 'RESUMED_WITHOUT_ECHO\\n'",
|
|
159
|
-
expected_status: 0, expected_output: "RESUMED_WITHOUT_ECHO\n")
|
|
160
|
-
assert result[:passed]
|
|
161
|
-
refute_includes log.string, "printf"
|
|
162
|
-
ensure
|
|
163
|
-
keyboard&.kill&.join if keyboard&.alive?
|
|
164
|
-
end
|
|
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
|
-
|
|
203
|
-
def test_remote_eof_restores_input_terminal_and_keeps_borrowed_io_open
|
|
204
|
-
session = child('STDIN.gets; print "FINAL_REMOTE_OUTPUT"', raw_pty: true)
|
|
205
|
-
_, slave, source = local_terminal
|
|
206
|
-
saved = InteractProbe.configuration(source)
|
|
207
|
-
output = StringIO.new
|
|
208
|
-
session.write("go\n")
|
|
209
|
-
assert_same session, session.interact(input: source, escape: "\x1d", output: output, timeout: 3)
|
|
210
|
-
assert_equal "FINAL_REMOTE_OUTPUT", output.string
|
|
211
|
-
assert_equal saved, InteractProbe.configuration(source)
|
|
212
|
-
refute slave.closed?
|
|
213
|
-
assert_equal 0, session.wait(timeout: 1).exitstatus
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def test_local_eof_returns_without_closing_remote_session
|
|
217
|
-
session = shell_session
|
|
218
|
-
source, writer = pipe_session
|
|
219
|
-
writer.close
|
|
220
|
-
assert_same source, session.interact(input: source, escape: "\x1d", output: StringIO.new, timeout: 2)
|
|
221
|
-
assert session.alive?
|
|
222
|
-
session.write("printf 'AFTER_LOCAL_EOF\\n'\n")
|
|
223
|
-
assert_equal 1, session.expect(/AFTER_LOCAL_EOF\r?\n/, timeout: 2)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def test_remote_callback_exception_restores_both_terminals
|
|
227
|
-
session = shell_session
|
|
228
|
-
_, slave, source = local_terminal
|
|
229
|
-
local_mode = InteractProbe.configuration(source)
|
|
230
|
-
remote_mode = InteractProbe.configuration(session)
|
|
231
|
-
old_settings = InteractProbe.settings(session)
|
|
232
|
-
session.on_sequence("CALLBACK_FAILURE") { raise "test callback failed" }
|
|
233
|
-
session.write("printf 'CALLBACK_FAILURE\\n'\n")
|
|
234
|
-
error = assert_raises(RuntimeError) { session.interact(input: source, escape: "\x1d", output: slave, timeout: 2) }
|
|
235
|
-
assert_equal "test callback failed", error.message
|
|
236
|
-
assert_equal local_mode, InteractProbe.configuration(source)
|
|
237
|
-
assert_equal remote_mode, InteractProbe.configuration(session)
|
|
238
|
-
assert_equal old_settings[0..3], InteractProbe.settings(session)[0..3]
|
|
239
|
-
assert session.alive?
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def test_output_io_failure_restores_input_terminal
|
|
243
|
-
session = shell_session
|
|
244
|
-
_, slave, source = local_terminal
|
|
245
|
-
saved = InteractProbe.configuration(source)
|
|
246
|
-
output = StringIO.new
|
|
247
|
-
output.close
|
|
248
|
-
session.write("printf 'OUTPUT_FAILURE\\n'\n")
|
|
249
|
-
assert_raises(IOError) { session.interact(input: source, escape: "\x1d", output: output, timeout: 2) }
|
|
250
|
-
assert_equal saved, InteractProbe.configuration(source)
|
|
251
|
-
refute slave.closed?
|
|
252
|
-
end
|
|
253
|
-
end
|
data/test/interconnect_test.rb
DELETED
|
@@ -1,425 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "test_helper"
|
|
4
|
-
|
|
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
|
-
|
|
113
|
-
def test_interconnect_forwards_and_strips_split_escape
|
|
114
|
-
session, writer = pipe_session
|
|
115
|
-
listener = StringIO.new
|
|
116
|
-
session.listeners = [listener]
|
|
117
|
-
session.on_sequence("STOP")
|
|
118
|
-
background do
|
|
119
|
-
writer.write("helloST")
|
|
120
|
-
sleep 0.03
|
|
121
|
-
writer.write("OPtail")
|
|
122
|
-
end
|
|
123
|
-
assert_same(session, bounded { Expect.interconnect(session) })
|
|
124
|
-
assert_equal "hello", listener.string
|
|
125
|
-
assert_equal "tail", session.buffer
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def test_sequence_callback_parameters_and_resume
|
|
129
|
-
session, writer = pipe_session
|
|
130
|
-
listener = StringIO.new
|
|
131
|
-
values = []
|
|
132
|
-
session.listeners = [listener]
|
|
133
|
-
session.on_sequence("!") do
|
|
134
|
-
values << :event
|
|
135
|
-
true
|
|
136
|
-
end
|
|
137
|
-
session.on_sequence("?")
|
|
138
|
-
writer.write("one!two?three")
|
|
139
|
-
Expect.interconnect(session, timeout: 1)
|
|
140
|
-
assert_equal "onetwo", listener.string
|
|
141
|
-
assert_equal [:event], values
|
|
142
|
-
assert_equal "three", session.buffer
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def test_zero_is_truthy_and_uppercase_eof_is_a_literal_sequence
|
|
146
|
-
session, writer = pipe_session
|
|
147
|
-
output = StringIO.new
|
|
148
|
-
session.listeners = [output]
|
|
149
|
-
session.on_sequence("!") { 0 }
|
|
150
|
-
session.on_sequence("EOF")
|
|
151
|
-
writer.write("one!twoEOFtail")
|
|
152
|
-
assert_same session, Expect.interconnect(session, timeout: 1)
|
|
153
|
-
assert_equal "onetwo", output.string
|
|
154
|
-
assert_equal "tail", session.buffer
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def test_eof_flushes_partial_escape
|
|
158
|
-
session, writer = pipe_session
|
|
159
|
-
listener = StringIO.new
|
|
160
|
-
session.listeners = [listener]
|
|
161
|
-
session.on_sequence("STOP")
|
|
162
|
-
writer.write("helloST")
|
|
163
|
-
writer.close
|
|
164
|
-
assert_same session, Expect.interconnect(session, timeout: 1)
|
|
165
|
-
assert_equal "helloST", listener.string
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def test_eof_handler_can_continue_other_sessions
|
|
169
|
-
first, first_writer = pipe_session
|
|
170
|
-
second, second_writer = pipe_session
|
|
171
|
-
listener = StringIO.new
|
|
172
|
-
second.listeners = [listener]
|
|
173
|
-
seen = []
|
|
174
|
-
first.on_sequence(:eof) do
|
|
175
|
-
seen << :first
|
|
176
|
-
true
|
|
177
|
-
end
|
|
178
|
-
second.on_sequence("!")
|
|
179
|
-
first_writer.close
|
|
180
|
-
second_writer.write("second!")
|
|
181
|
-
assert_same second, Expect.interconnect(first, second, timeout: 1)
|
|
182
|
-
assert_equal [:first], seen
|
|
183
|
-
assert_equal "second", listener.string
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def test_interact_roundtrip_escape_and_restored_settings
|
|
187
|
-
session = child(<<~'RUBY', raw_pty: true)
|
|
188
|
-
while (value = STDIN.gets)
|
|
189
|
-
puts "reply:#{value.strip}"
|
|
190
|
-
end
|
|
191
|
-
RUBY
|
|
192
|
-
source, writer = pipe_session
|
|
193
|
-
sink = StringIO.new
|
|
194
|
-
listener = StringIO.new
|
|
195
|
-
session.listeners = [listener]
|
|
196
|
-
session.log_listeners = false
|
|
197
|
-
source.on_sequence("original")
|
|
198
|
-
background do
|
|
199
|
-
writer.write("hello\n")
|
|
200
|
-
# 收到真实响应后才发送退出键,避免将子进程启动速度当成交互完成条件。
|
|
201
|
-
bounded { sleep 0.001 until sink.string == "reply:hello\n" }
|
|
202
|
-
writer.write("\x1dtail")
|
|
203
|
-
end
|
|
204
|
-
assert_same(source, bounded { session.interact(input: source, escape: "\x1d", output: sink, timeout: 2) })
|
|
205
|
-
assert_equal "reply:hello\n", sink.string
|
|
206
|
-
assert_equal "tail", source.buffer
|
|
207
|
-
assert_equal [listener], session.listeners
|
|
208
|
-
refute session.log_listeners
|
|
209
|
-
assert_equal ["original"], source.instance_variable_get(:@sequences).keys
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def test_interconnect_leaves_terminal_modes_to_the_caller
|
|
213
|
-
session = child('print "!"; sleep 30')
|
|
214
|
-
original = terminal_configuration(session)
|
|
215
|
-
seen = nil
|
|
216
|
-
session.on_sequence("!") do
|
|
217
|
-
seen = terminal_configuration(session)
|
|
218
|
-
raise "stop"
|
|
219
|
-
end
|
|
220
|
-
assert_raises(RuntimeError) { Expect.interconnect(session, timeout: 1) }
|
|
221
|
-
assert_equal original, seen
|
|
222
|
-
assert_equal original, terminal_configuration(session)
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def test_manual_stty_leaves_terminal_mode_to_caller
|
|
226
|
-
session = child('print "!"; sleep 30')
|
|
227
|
-
session.raw_terminal = false
|
|
228
|
-
original = session.stty
|
|
229
|
-
seen = nil
|
|
230
|
-
session.on_sequence("!") do
|
|
231
|
-
seen = session.stty
|
|
232
|
-
false
|
|
233
|
-
end
|
|
234
|
-
Expect.interconnect(session, timeout: 1)
|
|
235
|
-
assert_equal original, seen
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
def test_timeout_flushes_pending_escape_prefix
|
|
239
|
-
session, writer = pipe_session
|
|
240
|
-
listener = StringIO.new
|
|
241
|
-
session.listeners = [listener]
|
|
242
|
-
session.on_sequence("STOP")
|
|
243
|
-
writer.write("helloST")
|
|
244
|
-
assert_nil Expect.interconnect(session, timeout: 0.02)
|
|
245
|
-
assert_equal "helloST", listener.string
|
|
246
|
-
assert_empty session.buffer
|
|
247
|
-
end
|
|
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
|
-
|
|
293
|
-
def test_regexp_escape_matches_and_preserves_trailing_input
|
|
294
|
-
session, writer = pipe_session
|
|
295
|
-
output = StringIO.new
|
|
296
|
-
session.listeners = [output]
|
|
297
|
-
session.on_sequence(/STOP\d+;/)
|
|
298
|
-
writer.write("beforeSTOP42;after")
|
|
299
|
-
assert_same session, Expect.interconnect(session, timeout: 1)
|
|
300
|
-
assert_equal "before", output.string
|
|
301
|
-
assert_equal "after", session.buffer
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
def test_regexp_escape_tracks_prior_reads_without_delaying_forwarding
|
|
305
|
-
session, writer = pipe_session
|
|
306
|
-
seen = []
|
|
307
|
-
output = StringIO.new
|
|
308
|
-
session.listeners = [output]
|
|
309
|
-
session.on_sequence(/STOP\d+;/) do
|
|
310
|
-
seen << :stopped
|
|
311
|
-
false
|
|
312
|
-
end
|
|
313
|
-
# Await actual forwarding before writing the continuation, ensuring the
|
|
314
|
-
# test cannot accidentally pass with a single combined read.
|
|
315
|
-
background do
|
|
316
|
-
writer.write("beforeSTOP")
|
|
317
|
-
bounded { sleep 0.001 until output.string == "beforeSTOP" }
|
|
318
|
-
writer.write("42;after")
|
|
319
|
-
end
|
|
320
|
-
assert_same(session, bounded { Expect.interconnect(session, timeout: 1) })
|
|
321
|
-
assert_equal [:stopped], seen
|
|
322
|
-
assert_equal "beforeSTOP", output.string
|
|
323
|
-
assert_equal "after", session.buffer
|
|
324
|
-
end
|
|
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
|
-
|
|
356
|
-
def test_regexp_escape_continuation_does_not_rematch_history
|
|
357
|
-
session, writer = pipe_session
|
|
358
|
-
seen = []
|
|
359
|
-
output = StringIO.new
|
|
360
|
-
session.listeners = [output]
|
|
361
|
-
session.on_sequence(/\[[0-9]+\]/) do
|
|
362
|
-
seen << :hit
|
|
363
|
-
true
|
|
364
|
-
end
|
|
365
|
-
session.on_sequence("!")
|
|
366
|
-
writer.write("one[12]two[34]three!tail")
|
|
367
|
-
Expect.interconnect(session, timeout: 1)
|
|
368
|
-
assert_equal %i[hit hit], seen
|
|
369
|
-
assert_equal "onetwothree", output.string
|
|
370
|
-
assert_equal "tail", session.buffer
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
def test_zero_width_regexp_escape_is_rejected_without_spinning
|
|
374
|
-
session, writer = pipe_session
|
|
375
|
-
session.on_sequence(/(?=a)/) { true }
|
|
376
|
-
writer.write("abc")
|
|
377
|
-
assert_raises(ArgumentError) { bounded { Expect.interconnect(session, timeout: 1) } }
|
|
378
|
-
assert_equal "abc", session.buffer
|
|
379
|
-
end
|
|
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
|
-
|
|
396
|
-
def test_logs_received_bytes_once_across_expect_interconnect_and_expect
|
|
397
|
-
session, writer = pipe_session
|
|
398
|
-
log = StringIO.new
|
|
399
|
-
output = StringIO.new
|
|
400
|
-
session.log_to(log)
|
|
401
|
-
session.listeners = [output]
|
|
402
|
-
session.on_sequence("!")
|
|
403
|
-
writer.write("prompt>before!")
|
|
404
|
-
session.expect("prompt>", timeout: 1)
|
|
405
|
-
Expect.interconnect(session, timeout: 1)
|
|
406
|
-
writer.write("after")
|
|
407
|
-
session.expect("after", timeout: 1)
|
|
408
|
-
assert_equal "prompt>before!after", log.string
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
def test_log_includes_escape_and_tail_before_next_expect
|
|
412
|
-
session, writer = pipe_session
|
|
413
|
-
log = StringIO.new
|
|
414
|
-
output = StringIO.new
|
|
415
|
-
session.log_to(log)
|
|
416
|
-
session.listeners = [output]
|
|
417
|
-
session.on_sequence("!")
|
|
418
|
-
writer.write("before!tail")
|
|
419
|
-
Expect.interconnect(session, timeout: 1)
|
|
420
|
-
assert_equal "before", output.string
|
|
421
|
-
assert_equal "before!tail", log.string
|
|
422
|
-
assert_equal 1, session.expect("tail", timeout: 0)
|
|
423
|
-
assert_equal "before!tail", log.string
|
|
424
|
-
end
|
|
425
|
-
end
|