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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +28 -19
  4. data/docs/API.md +88 -0
  5. data/docs/MIGRATION.md +15 -0
  6. data/lib/expect/cleanup.rb +33 -0
  7. data/lib/expect/configuration.rb +87 -66
  8. data/lib/expect/interaction.rb +226 -209
  9. data/lib/expect/logging.rb +157 -156
  10. data/lib/expect/matcher.rb +30 -29
  11. data/lib/expect/pattern.rb +8 -2
  12. data/lib/expect/pattern_list.rb +35 -5
  13. data/lib/expect/redactor.rb +11 -4
  14. data/lib/expect/relay.rb +97 -73
  15. data/lib/expect/relay_writer.rb +2 -1
  16. data/lib/expect/result.rb +32 -8
  17. data/lib/expect/session.rb +508 -0
  18. data/lib/expect/session_resources.rb +10 -2
  19. data/lib/expect/terminal.rb +78 -78
  20. data/lib/expect/version.rb +1 -1
  21. data/lib/expect.rb +333 -520
  22. data/sig/expect.rbs +355 -0
  23. metadata +13 -106
  24. data/.rubocop.yml +0 -66
  25. data/CONTRIBUTING.md +0 -29
  26. data/Gemfile +0 -28
  27. data/Rakefile +0 -38
  28. data/benchmark/matching.rb +0 -98
  29. data/benchmark/redactor.rb +0 -64
  30. data/benchmark/relay.rb +0 -48
  31. data/benchmark/scaling.rb +0 -70
  32. data/benchmark/send_slow.rb +0 -41
  33. data/benchmark/support.rb +0 -114
  34. data/docs/COMPATIBILITY.md +0 -98
  35. data/docs/INTERNAL_CONTRACTS.md +0 -154
  36. data/docs/PERFORMANCE.md +0 -165
  37. data/docs/RELEASING.md +0 -84
  38. data/docs/VERIFICATION.md +0 -456
  39. data/examples/dialogue.rb +0 -30
  40. data/examples/kibitz/README.md +0 -81
  41. data/examples/kibitz/kibitz.rb +0 -142
  42. data/examples/kibitz/test_kibitz.rb +0 -37
  43. data/examples/ssh_auto.rb +0 -94
  44. data/examples/ssh_interact.rb +0 -159
  45. data/examples/ssh_login.rb +0 -64
  46. data/expect-pty.gemspec +0 -33
  47. data/script/ci +0 -121
  48. data/script/release.rb +0 -319
  49. data/test/buffer_accounting_test.rb +0 -83
  50. data/test/cleanup_test.rb +0 -251
  51. data/test/compare_upstream.rb +0 -157
  52. data/test/configuration_test.rb +0 -133
  53. data/test/deadline_test.rb +0 -237
  54. data/test/diagnostics_test.rb +0 -353
  55. data/test/edge_case_test.rb +0 -262
  56. data/test/fixtures/ssh_scripts/01_identity.sh +0 -4
  57. data/test/fixtures/ssh_scripts/02_output.sh +0 -5
  58. data/test/fixtures/ssh_scripts/03_delayed.sh +0 -6
  59. data/test/fixtures/ssh_scripts/04_failure.sh +0 -2
  60. data/test/fixtures/ssh_scripts/05_recovery.sh +0 -3
  61. data/test/initialization_failure_test.rb +0 -92
  62. data/test/integration/README.md +0 -109
  63. data/test/integration/ssh_scripts.rb +0 -94
  64. data/test/interact_test.rb +0 -253
  65. data/test/interconnect_test.rb +0 -425
  66. data/test/io_test.rb +0 -321
  67. data/test/kibitz_test.rb +0 -45
  68. data/test/lifecycle_contract_test.rb +0 -71
  69. data/test/literal_scan_test.rb +0 -97
  70. data/test/matching_test.rb +0 -211
  71. data/test/multi_session_test.rb +0 -66
  72. data/test/ownership_sequence_test.rb +0 -208
  73. data/test/pattern_offset_test.rb +0 -43
  74. data/test/process_interruption_test.rb +0 -296
  75. data/test/process_test.rb +0 -291
  76. data/test/redactor_test.rb +0 -183
  77. data/test/relay_recovery_test.rb +0 -451
  78. data/test/relay_reentrancy_test.rb +0 -159
  79. data/test/release_test.rb +0 -375
  80. data/test/ruby_api_test.rb +0 -515
  81. data/test/scan_reuse_test.rb +0 -109
  82. data/test/script_logging_test.rb +0 -124
  83. data/test/support/interact_probe.rb +0 -125
  84. data/test/support/kibitz_probe.rb +0 -177
  85. data/test/support/script_probe.rb +0 -158
  86. data/test/terminal_cleanup_test.rb +0 -349
  87. data/test/test_helper.rb +0 -58
  88. data/test/timeout_test.rb +0 -289
  89. data/test/write_contract_test.rb +0 -105
@@ -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