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,183 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "minitest/autorun"
4
- require "open3"
5
- require "rbconfig"
6
- require_relative "../lib/expect/redactor"
7
-
8
- class RedactorTest < Minitest::Test
9
- def test_standalone_require_does_not_load_pty_or_allocate_a_session
10
- program = <<~RUBY
11
- require "expect/redactor"
12
- abort "PTY loaded" if defined?(PTY)
13
- abort "unexpected result" unless Expect::Redactor.redact("secret!", ["secret"]) == "[FILTERED]!"
14
- RUBY
15
- output, status = Open3.capture2e(RbConfig.ruby, "--disable-gems", "-I", File.expand_path("../lib", __dir__),
16
- "-e", program)
17
- assert status.success?, output
18
- end
19
-
20
- def test_empty_patterns_pass_binary_input_without_buffering
21
- filter = Expect::Redactor.new([])
22
- assert_equal "\xff\0raw".b, filter.append("\xff\0raw".b)
23
- assert_empty filter.finish
24
- assert_equal "plain", Expect::Redactor.redact("plain", [])
25
- end
26
-
27
- def test_one_shot_redaction_does_not_treat_an_ordinary_suffix_as_a_secret_prefix
28
- assert_equal "pass", Expect::Redactor.redact("pass", ["password"])
29
- assert_equal "[REDACTED]!", Expect::Redactor.redact("password!", ["password"], replacement: "[REDACTED]")
30
- end
31
-
32
- def test_stream_finish_masks_partial_secrets_by_default
33
- filter = Expect::Redactor.new(["password"])
34
- assert_equal "prefix [FILTERED]", filter.append("prefix pass") + filter.finish
35
- assert_empty filter.finish
36
- assert_equal "[FILTERED]!", filter.append("password!") + filter.finish
37
- end
38
-
39
- def test_exact_finish_is_explicit_and_validated
40
- filter = Expect::Redactor.new(["password"])
41
- filter.append("pass")
42
- assert_raises(ArgumentError) { filter.finish(partial: nil) }
43
- assert_equal "pass", filter.finish(partial: false)
44
- end
45
-
46
- def test_custom_marker_and_overlapping_patterns_at_every_boundary
47
- patterns = ["alpha[REDACTED]omega", "[REDACTED]", "REDACTED", "[", "abc", "cde"]
48
- input = "alpha[REDACTED]omega | [REDACTED] | abcde!"
49
- expected = "[REDACTED] | [REDACTED] | [REDACTED]!"
50
- (0..input.bytesize).each do |split|
51
- filter = Expect::Redactor.new(patterns, replacement: "[REDACTED]")
52
- actual = filter.append(input.byteslice(0, split)) + filter.append(input.byteslice(split..)) + filter.finish
53
- assert_equal expected, actual, "split #{split}"
54
- end
55
- filter = Expect::Redactor.new(patterns, replacement: "[REDACTED]")
56
- actual = input.bytes.map { |byte| filter.append(byte.chr) }.join + filter.finish
57
- assert_equal expected, actual
58
- assert_equal expected, Expect::Redactor.redact(input, patterns, replacement: "[REDACTED]")
59
- end
60
-
61
- def test_patterns_and_replacement_are_copied
62
- secret = +"secret"
63
- patterns = [secret]
64
- replacement = +"hidden"
65
- filter = Expect::Redactor.new(patterns, replacement: replacement)
66
- secret.replace("public")
67
- patterns.clear
68
- replacement.clear
69
- assert_equal "hidden!", filter.append("secret!") + filter.finish
70
- end
71
-
72
- def test_invalid_pattern_update_is_atomic_and_preserves_pending_data
73
- filter = Expect::Redactor.new(["secret"])
74
- filter.append("sec")
75
- [nil, "secret", [""], ["valid", 1]].each do |patterns|
76
- assert_raises(ArgumentError) { filter.patterns = patterns }
77
- end
78
- assert_equal "[FILTERED]!", filter.append("ret!") + filter.finish
79
- end
80
-
81
- def test_pattern_update_keeps_hidden_spans_and_matches_new_secret_across_chunks
82
- filter = Expect::Redactor.new(%w[abc long-pattern])
83
- filter.append("abcsec")
84
- filter.patterns = ["secret"]
85
- assert_equal "[FILTERED]!", filter.append("ret!") + filter.finish
86
- filter.patterns = []
87
- assert_equal "secret", filter.append("secret")
88
- assert_empty filter.finish
89
- end
90
-
91
- def test_invalid_input_and_replacement_do_not_mutate_stream
92
- [nil, "", 3].each do |replacement|
93
- assert_raises(ArgumentError) { Expect::Redactor.new([], replacement: replacement) }
94
- end
95
- filter = Expect::Redactor.new(["secret"])
96
- filter.append("sec")
97
- assert_raises(ArgumentError) { filter.append(nil) }
98
- assert_equal "[FILTERED]!", filter.append("ret!") + filter.finish
99
- end
100
-
101
- def test_inspect_does_not_expose_registered_or_pending_bytes
102
- filter = Expect::Redactor.new(["private-pattern"])
103
- filter.append("raw-pending")
104
- assert_equal "#<Expect::Redactor>", filter.inspect
105
- end
106
-
107
- def test_seeded_binary_chunks_match_a_naive_union_of_full_and_partial_spans
108
- random = Random.new(5000)
109
- 250.times do |index|
110
- alphabet = index.even? ? [97, 98] : [0, 97, 98, 99, 128, 255]
111
- input = Array.new(random.rand(0..120)) { alphabet.sample(random: random) }.pack("C*")
112
- patterns = Array.new(random.rand(0..6)) do
113
- Array.new(random.rand(1..8)) { alphabet.sample(random: random) }.pack("C*")
114
- end
115
- chunks = []
116
- offset = 0
117
- while offset < input.bytesize
118
- chunks << input.byteslice(offset, random.rand(1..15))
119
- offset += chunks.last.bytesize
120
- end
121
- [false, true].each do |partial|
122
- expected = reference_redact(input, patterns, partial: partial)
123
- [[input], input.bytes.map(&:chr), chunks].each_with_index do |partition, kind|
124
- filter = Expect::Redactor.new(patterns)
125
- actual = partition.map { |chunk| filter.append(chunk) }.join.b + filter.finish(partial: partial)
126
- assert_equal expected, actual, "seed=5000 case=#{index} partition=#{kind} partial=#{partial}"
127
- assert_empty filter.finish(partial: partial)
128
- end
129
- end
130
- end
131
- end
132
-
133
- def test_long_overlaps_merge_across_chunks_without_repeating_markers
134
- filter = Expect::Redactor.new(["a" * 1024, "a" * 32, "ab"])
135
- output = +""
136
- 256.times { output << filter.append("a" * 17) }
137
- output << filter.append("b!") << filter.finish
138
- assert_equal "[FILTERED]!", output
139
- end
140
-
141
- def test_rule_removal_keeps_hidden_pending_bytes_and_partial_finish_is_separate
142
- filter = Expect::Redactor.new(%w[aaaa long-pattern])
143
- assert_empty filter.append("aaaaa")
144
- filter.patterns = ["abc"]
145
- assert_equal "[FILTERED]", filter.append("ab")
146
- assert_empty filter.finish # 新的疑似前缀紧邻已隐藏区域,不能重复输出替换标记。
147
-
148
- filter = Expect::Redactor.new(%w[aaaa long-pattern])
149
- assert_empty filter.append("aaaaa")
150
- filter.patterns = []
151
- assert_equal "[FILTERED]!", filter.append("!") + filter.finish
152
- end
153
-
154
- private
155
-
156
- # 独立参考模型:逐偏移比较完整秘密,再合并布尔掩码;不复用生产代码的扫描/输出实现。
157
- def reference_redact(input, patterns, partial:)
158
- hidden = Array.new(input.bytesize, false)
159
- patterns.each do |pattern|
160
- input.bytesize.times do |offset|
161
- next unless input.byteslice(offset, pattern.bytesize) == pattern
162
-
163
- pattern.bytesize.times { |length| hidden[offset + length] = true }
164
- end
165
- next unless partial
166
-
167
- (1...pattern.bytesize).each do |length|
168
- next unless input.end_with?(pattern.byteslice(0, length))
169
-
170
- ((input.bytesize - length)...input.bytesize).each { |offset| hidden[offset] = true }
171
- end
172
- end
173
- output = +"".b
174
- input.bytes.each_with_index do |byte, offset|
175
- if hidden[offset]
176
- output << "[FILTERED]" if offset.zero? || !hidden[offset - 1]
177
- else
178
- output << byte
179
- end
180
- end
181
- output
182
- end
183
- end
@@ -1,451 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "test_helper"
4
-
5
- class RelayRecoveryTest < ExpectTest
6
- def sink_session(**)
7
- reader, peer = IO.pipe
8
- sink, writer = IO.pipe
9
- @ios.push(reader, peer, sink, writer)
10
- session = Expect.open(reader, writer: writer, **)
11
- @sessions << session
12
- [session, sink, peer]
13
- end
14
-
15
- def fill(writer)
16
- total = 0
17
- loop do
18
- count = writer.write_nonblock("x" * 4096, exception: false)
19
- return total if count == :wait_writable
20
-
21
- total += count
22
- end
23
- end
24
-
25
- def drain(reader)
26
- data = "".b
27
- loop do
28
- chunk = reader.read_nonblock(65_536, exception: false)
29
- return data if chunk == :wait_readable || chunk.nil?
30
-
31
- data << chunk
32
- end
33
- end
34
-
35
- def test_outer_deadline_bounds_raw_io_and_expect_listeners
36
- [false, true].each do |wrapped|
37
- source, = pipe_session
38
- target, = sink_session
39
- fill(target.writer)
40
- source.listeners = [wrapped ? target : target.writer]
41
- source.buffer = "request"
42
- started = Expect.monotonic
43
- assert_nil bounded(0.5) { Expect.interconnect(source, target, timeout: 0.02) }
44
- assert_operator Expect.monotonic - started, :<, 0.3
45
- end
46
- end
47
-
48
- def test_partial_timeout_and_retry_deliver_exactly_once_to_each_listener
49
- source, source_writer = pipe_session
50
- target, sink = sink_session(write_timeout: 0.02)
51
- first = StringIO.new
52
- source.listeners = [first, target]
53
- payload = (0..255).to_a.pack("C*") * 2048
54
- source.buffer = payload
55
- assert_raises(Expect::WriteTimeout) { bounded { Expect.interconnect(source, timeout: 1) } }
56
- received = drain(sink)
57
- assert_operator received.bytesize, :>, 0
58
- assert_operator received.bytesize, :<, payload.bytesize
59
- target.write_timeout = 2
60
- receiver = background do
61
- loop { received << sink.readpartial(65_536) }
62
- rescue EOFError
63
- received
64
- end
65
- source_writer.close
66
- assert_same(source, bounded { Expect.interconnect(source, timeout: 2) })
67
- target.writer.close
68
- assert receiver.join(2), "receiver did not finish"
69
- assert_equal payload.bytesize, receiver.value.bytesize
70
- assert payload == receiver.value, "resumed pipe output differs from the original bytes"
71
- assert_equal payload.bytesize, first.string.bytesize
72
- assert payload == first.string.b, "first listener output differs from the original bytes"
73
- end
74
-
75
- def test_short_custom_writes_preserve_suffix
76
- source, = pipe_session
77
- received = "".b
78
- target = Object.new
79
- target.define_singleton_method(:write) do |data|
80
- count = [2, data.bytesize].min
81
- received << data.byteslice(0, count)
82
- count
83
- end
84
- source.listeners = [target]
85
- source.buffer = "abcdef"
86
- Expect.interconnect(source, timeout: 0.02)
87
- assert_equal "abcdef", received
88
- end
89
-
90
- def test_failed_second_listener_does_not_replay_first_listener
91
- source, = pipe_session
92
- first = StringIO.new
93
- second = StringIO.new
94
- failing = true
95
- original = second.method(:write)
96
- second.define_singleton_method(:write) do |data|
97
- raise IOError, "listener failed" if failing
98
-
99
- original.call(data)
100
- end
101
- source.listeners = [first, second]
102
- source.buffer = "payload"
103
- assert_raises(IOError) { Expect.interconnect(source, timeout: 0.02) }
104
- failing = false
105
- Expect.interconnect(source, timeout: 0.02)
106
- assert_equal "payload", first.string
107
- assert_equal "payload", second.string
108
- end
109
-
110
- def test_repeated_interact_with_raw_input_keeps_read_ahead
111
- session, sink = sink_session
112
- input, keyboard = IO.pipe
113
- @ios.push(input, keyboard)
114
- keyboard.write("one!two!")
115
- first = session.interact(input: input, output: StringIO.new, escape: "!", timeout: 0.05)
116
- second = session.interact(input: input, output: StringIO.new, escape: "!", timeout: 0.05)
117
- assert_same first, second
118
- assert_equal "onetwo", drain(sink)
119
- refute input.closed?
120
- end
121
-
122
- def test_write_retries_read_eintr_during_backpressure
123
- session, sink, peer = sink_session(write_timeout: 1)
124
- fill(session.writer)
125
- peer.write("reply")
126
- original = session.to_io.method(:read_nonblock)
127
- interrupted = false
128
- session.to_io.define_singleton_method(:read_nonblock) do |*args, **kwargs|
129
- unless interrupted
130
- interrupted = true
131
- raise Errno::EINTR
132
- end
133
- original.call(*args, **kwargs)
134
- end
135
- session.log_to { drain(sink) }
136
- assert_equal(5, bounded { session.write("hello") })
137
- assert interrupted
138
- assert_equal "reply", session.buffer
139
- assert_equal "hello", drain(sink)
140
- end
141
-
142
- def test_write_timeout_reports_accepted_bytes
143
- session, sink = sink_session(write_timeout: 0.02)
144
- error = assert_raises(Expect::WriteTimeout) { session.write("x" * 524_288) }
145
- received = drain(sink)
146
- assert_operator received.bytesize, :>, 0
147
- assert_equal received.bytesize, error.bytes_written
148
- end
149
-
150
- def test_nested_output_timeout_preserves_the_callers_write_progress
151
- %i[listeners log_output].each do |destination|
152
- session, sink, peer = sink_session(write_timeout: 1)
153
- target, = sink_session(write_timeout: 0)
154
- fill(target.writer)
155
- session.public_send(:"#{destination}=", destination == :listeners ? [target] : target)
156
- peer.write("reply")
157
-
158
- error = assert_raises(Expect::WriteTimeout) { bounded { session.write("x" * 1_048_576) } }
159
- received = drain(sink)
160
- assert_operator received.bytesize, :>, 0
161
- assert_equal received.bytesize, error.bytes_written
162
- assert_instance_of Expect::WriteTimeout, error.cause
163
- assert_equal 0, error.cause.bytes_written
164
- assert_equal "reply", session.buffer
165
- end
166
- end
167
-
168
- def test_truncated_utf8_at_eof_raises_without_losing_bytes
169
- session, writer = pipe_session
170
- writer.write("\xe4".b)
171
- writer.close
172
- assert_raises(EncodingError) { session.expect(/中/u, timeout: 1) }
173
- assert_equal "\xe4".b, session.buffer
174
- assert_equal 1, session.expect("\xe4".b, timeout: 0)
175
- end
176
-
177
- def test_blocked_target_does_not_starve_another_source
178
- slow, = pipe_session
179
- fast, writer = pipe_session
180
- target, = sink_session
181
- fill(target.writer)
182
- slow.listeners = [target]
183
- slow.buffer = "blocked"
184
- output = StringIO.new
185
- fast.listeners = [output]
186
- writer.write("ready")
187
- assert_nil bounded(0.5) { Expect.interconnect(slow, fast, timeout: 0.02) }
188
- assert_equal "ready", output.string
189
- assert slow.pending_output?
190
- end
191
-
192
- def test_outer_timeout_keeps_output_for_original_targets
193
- source, = pipe_session
194
- target, sink = sink_session
195
- fill(target.writer)
196
- source.listeners = [target.writer]
197
- source.buffer = "request"
198
- Expect.interconnect(source, timeout: 0.01)
199
- assert source.pending_output?
200
- assert_empty source.buffer
201
- drain(sink)
202
- replacement = StringIO.new
203
- source.listeners = [replacement]
204
- source.buffer = "new"
205
- Expect.interconnect(source, timeout: 0.02)
206
- refute source.pending_output?
207
- assert_equal "request", drain(sink)
208
- assert_equal "new", replacement.string
209
- end
210
-
211
- def test_every_short_write_failure_position_resumes_without_replay
212
- 0.upto(5) do |failure_offset|
213
- source, = pipe_session
214
- first = StringIO.new
215
- received = "".b
216
- failed = false
217
- target = Object.new
218
- target.define_singleton_method(:write) do |data|
219
- if !failed && received.bytesize == failure_offset
220
- failed = true
221
- raise IOError, "interrupted delivery"
222
- end
223
- received << data.byteslice(0, 1)
224
- 1
225
- end
226
- source.listeners = [first, target]
227
- source.buffer = "abcdef"
228
- assert_raises(IOError) { Expect.interconnect(source, timeout: 1) }
229
- assert source.pending_output?
230
- Expect.interconnect(source, timeout: 0.01)
231
- assert_equal "abcdef", received
232
- assert_equal "abcdef", first.string
233
- refute source.pending_output?
234
- end
235
- end
236
-
237
- def test_split_literal_and_short_write_failures_preserve_delivery_and_callback_order
238
- [1, 2, 3].product([1, 2, 3], [0, 2]).each do |split, step, failure_offset|
239
- source, writer = pipe_session
240
- fast = StringIO.new
241
- received = "".b
242
- failed = false
243
- slow = Object.new
244
- slow.define_singleton_method(:write) do |data|
245
- if !failed && received.bytesize == failure_offset
246
- failed = true
247
- raise IOError, "injected partial delivery"
248
- end
249
- count = [step, data.bytesize].min
250
- count = [count, failure_offset - received.bytesize].min unless failed
251
- received << data.byteslice(0, count)
252
- count
253
- end
254
- callbacks = []
255
- source.listeners = [fast, slow]
256
- source.on_sequence("STOP") do
257
- callbacks << [fast.string.dup, received.dup]
258
- false
259
- end
260
- source.buffer = "head#{"STOP".byteslice(0, split)}"
261
- writer.write("#{"STOP".byteslice(split..)}tail")
262
- assert_raises(IOError) { bounded { Expect.interconnect(source, timeout: 1) } }
263
- assert_empty callbacks
264
- assert source.pending_output?
265
- assert_same(source, bounded { Expect.interconnect(source, timeout: 1) })
266
- assert_equal [%w[head head]], callbacks
267
- assert_equal "tail", source.buffer
268
- assert_equal "head", fast.string
269
- assert_equal "head", received
270
- refute source.pending_output?
271
- end
272
- end
273
-
274
- def test_flush_failure_retries_flush_without_replaying_data
275
- source, = pipe_session
276
- output = StringIO.new
277
- failed = false
278
- output.define_singleton_method(:flush) do
279
- return if failed
280
-
281
- failed = true
282
- raise IOError, "flush failed"
283
- end
284
- source.listeners = [output]
285
- source.buffer = "once"
286
- assert_raises(IOError) { Expect.interconnect(source, timeout: 1) }
287
- Expect.interconnect(source, timeout: 0.01)
288
- assert_equal "once", output.string
289
- refute source.pending_output?
290
- end
291
-
292
- def test_invalid_write_count_raises_and_can_recover
293
- [0, nil, -1, 7, "6"].each do |invalid|
294
- source, = pipe_session
295
- output = StringIO.new
296
- source.listeners = [output]
297
- source.buffer = "abcdef"
298
- output.stub(:write, invalid) do
299
- assert_raises(IOError) { bounded { Expect.interconnect(source, timeout: 1) } }
300
- end
301
- Expect.interconnect(source, timeout: 0.01)
302
- assert_equal "abcdef", output.string
303
- end
304
- end
305
-
306
- def test_escape_callback_waits_for_prefix_delivery_across_timeout
307
- source, = pipe_session
308
- target, sink = sink_session
309
- fill(target.writer)
310
- source.listeners = [target]
311
- called = 0
312
- source.on_sequence(/END\z/) do
313
- called += 1
314
- false
315
- end
316
- source.buffer = "prefixEND"
317
- assert_nil Expect.interconnect(source, timeout: 0.01)
318
- assert_equal 0, called
319
- source.buffer = "tail"
320
- drain(sink)
321
- assert_same source, Expect.interconnect(source, timeout: 0.1)
322
- assert_equal 1, called
323
- assert_equal "prefix", drain(sink)
324
- assert_equal "tail", source.buffer
325
- end
326
-
327
- def test_matching_window_does_not_truncate_undelivered_relay_input
328
- source, writer = pipe_session(buffer_limit: 2)
329
- target, sink = sink_session
330
- fill(target.writer)
331
- source.listeners = [target]
332
- source.on_sequence("!")
333
- writer.write("prefix!tail")
334
- Expect.interconnect(source, timeout: 0.01)
335
- assert_equal "tail", source.buffer
336
- drain(sink)
337
- assert_same source, Expect.interconnect(source, timeout: 0.1)
338
- assert_equal "prefix", drain(sink)
339
- assert_equal "tail", source.buffer
340
- end
341
-
342
- def test_unlisted_target_output_is_drained_without_losing_bytes
343
- source, writer = pipe_session
344
- target = child('puts "ready"; data = STDIN.read(131072); print "reply" if data.bytesize == 131072',
345
- raw_pty: true, write_timeout: 1)
346
- source.listeners = [target]
347
- source.buffer = "x" * 131_072
348
- writer.close
349
- assert_same(source, bounded { Expect.interconnect(source, timeout: 2) })
350
- refute source.pending_output?
351
- assert_empty source.buffer
352
- assert_equal 1, target.expect("ready", timeout: 1)
353
- assert_equal 1, target.expect("reply", timeout: 1)
354
- end
355
-
356
- def test_eof_validates_truncated_utf8_in_escape_regex
357
- source, writer = pipe_session
358
- source.on_sequence(/中/u)
359
- writer.write("\xe4".b)
360
- writer.close
361
- assert_raises(EncodingError) { Expect.interconnect(source, timeout: 1) }
362
- end
363
-
364
- def test_instance_regexp_timeout_propagates_and_keeps_buffer
365
- source, = pipe_session
366
- source.buffer = "#{"a" * 30}!"
367
- regexp = Regexp.new('\\A(a+)+\\1\\z', timeout: 0.01)
368
- global_timeout = Regexp.timeout
369
- assert_raises(Regexp::TimeoutError) { bounded { source.expect(regexp, timeout: 0) } }
370
- assert_equal "#{"a" * 30}!", source.buffer
371
- assert Regexp.timeout == global_timeout, "matching must not change the process regexp timeout"
372
- assert_equal 0.01, regexp.timeout
373
- end
374
-
375
- def test_closed_cached_input_wrapper_can_be_reopened_without_closing_raw_io
376
- session, sink = sink_session
377
- input, keyboard = IO.pipe
378
- @ios.push(input, keyboard)
379
- keyboard.write("one!")
380
- source = session.interact(input: input, output: StringIO.new, escape: "!", timeout: 0.1)
381
- source.close
382
- keyboard.write("two!")
383
- resumed = session.interact(input: input, output: StringIO.new, escape: "!", timeout: 0.1)
384
- refute_same source, resumed
385
- assert_equal "onetwo", drain(sink)
386
- resumed.graceful_close = true
387
- bounded(0.5) { session.close }
388
- assert resumed.closed?
389
- refute input.closed?
390
- end
391
-
392
- def test_repeated_read_interruptions_do_not_extend_write_timeout
393
- session, = sink_session(write_timeout: 0.02)
394
- fill(session.writer)
395
- reads = 0
396
- session.to_io.stub(:read_nonblock, lambda { |*|
397
- reads += 1
398
- raise Errno::EINTR
399
- }) do
400
- IO.stub(:select, ->(*) { [[session.to_io], [], []] }) do
401
- error = assert_raises(Expect::WriteTimeout) { bounded(0.5) { session.write("request") } }
402
- assert_equal 0, error.bytes_written
403
- end
404
- end
405
- assert_operator reads, :>, 1
406
- end
407
-
408
- def test_zero_timeout_retains_a_blocked_escape_prefix_for_retry
409
- source, = pipe_session
410
- target, sink = sink_session
411
- fill(target.writer)
412
- source.on_sequence("STOP")
413
- source.listeners = [target]
414
- source.buffer = "ST"
415
- assert_nil bounded(0.5) { Expect.interconnect(source, timeout: 0) }
416
- assert source.pending_output?
417
- drain(sink)
418
- Expect.interconnect(source, timeout: 0.01)
419
- assert_equal "ST", drain(sink)
420
- refute source.pending_output?
421
- end
422
-
423
- def test_short_log_writes_keep_all_bytes
424
- source, = pipe_session
425
- output = StringIO.new
426
- original = output.method(:write)
427
- output.define_singleton_method(:write) { |data| original.call(data.byteslice(0, 1)) }
428
- source.log_to(output)
429
- source.write_log("complete")
430
- assert_equal "complete", output.string
431
- end
432
-
433
- def test_interrupted_custom_output_is_retried_without_waiting_for_new_input
434
- source, = pipe_session
435
- output = StringIO.new
436
- interrupted = false
437
- original = output.method(:write)
438
- output.define_singleton_method(:write) do |data|
439
- unless interrupted
440
- interrupted = true
441
- raise Errno::EINTR
442
- end
443
- original.call(data)
444
- end
445
- source.listeners = [output]
446
- source.on_sequence("!")
447
- source.buffer = "ready!"
448
- assert_same source, bounded(0.5) { Expect.interconnect(source, timeout: 0.05) }
449
- assert_equal "ready", output.string
450
- end
451
- end