expect-pty 0.5.3 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +28 -19
- data/docs/API.md +88 -0
- data/docs/MIGRATION.md +15 -0
- data/lib/expect/cleanup.rb +33 -0
- data/lib/expect/configuration.rb +87 -66
- data/lib/expect/interaction.rb +226 -209
- data/lib/expect/logging.rb +157 -156
- data/lib/expect/matcher.rb +30 -29
- data/lib/expect/pattern.rb +8 -2
- data/lib/expect/pattern_list.rb +35 -5
- data/lib/expect/redactor.rb +11 -4
- data/lib/expect/relay.rb +97 -73
- data/lib/expect/relay_writer.rb +2 -1
- data/lib/expect/result.rb +32 -8
- data/lib/expect/session.rb +508 -0
- data/lib/expect/session_resources.rb +10 -2
- data/lib/expect/terminal.rb +78 -78
- data/lib/expect/version.rb +1 -1
- data/lib/expect.rb +333 -520
- data/sig/expect.rbs +355 -0
- metadata +13 -106
- data/.rubocop.yml +0 -66
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -28
- data/Rakefile +0 -38
- data/benchmark/matching.rb +0 -98
- data/benchmark/redactor.rb +0 -64
- data/benchmark/relay.rb +0 -48
- data/benchmark/scaling.rb +0 -70
- data/benchmark/send_slow.rb +0 -41
- data/benchmark/support.rb +0 -114
- data/docs/COMPATIBILITY.md +0 -98
- data/docs/INTERNAL_CONTRACTS.md +0 -154
- data/docs/PERFORMANCE.md +0 -165
- data/docs/RELEASING.md +0 -84
- data/docs/VERIFICATION.md +0 -456
- data/examples/dialogue.rb +0 -30
- data/examples/kibitz/README.md +0 -81
- data/examples/kibitz/kibitz.rb +0 -142
- data/examples/kibitz/test_kibitz.rb +0 -37
- data/examples/ssh_auto.rb +0 -94
- data/examples/ssh_interact.rb +0 -159
- data/examples/ssh_login.rb +0 -64
- data/expect-pty.gemspec +0 -33
- data/script/ci +0 -121
- data/script/release.rb +0 -319
- data/test/buffer_accounting_test.rb +0 -83
- data/test/cleanup_test.rb +0 -251
- data/test/compare_upstream.rb +0 -157
- data/test/configuration_test.rb +0 -133
- data/test/deadline_test.rb +0 -237
- data/test/diagnostics_test.rb +0 -353
- data/test/edge_case_test.rb +0 -262
- data/test/fixtures/ssh_scripts/01_identity.sh +0 -4
- data/test/fixtures/ssh_scripts/02_output.sh +0 -5
- data/test/fixtures/ssh_scripts/03_delayed.sh +0 -6
- data/test/fixtures/ssh_scripts/04_failure.sh +0 -2
- data/test/fixtures/ssh_scripts/05_recovery.sh +0 -3
- data/test/initialization_failure_test.rb +0 -92
- data/test/integration/README.md +0 -109
- data/test/integration/ssh_scripts.rb +0 -94
- data/test/interact_test.rb +0 -253
- data/test/interconnect_test.rb +0 -425
- data/test/io_test.rb +0 -321
- data/test/kibitz_test.rb +0 -45
- data/test/lifecycle_contract_test.rb +0 -71
- data/test/literal_scan_test.rb +0 -97
- data/test/matching_test.rb +0 -211
- data/test/multi_session_test.rb +0 -66
- data/test/ownership_sequence_test.rb +0 -208
- data/test/pattern_offset_test.rb +0 -43
- data/test/process_interruption_test.rb +0 -296
- data/test/process_test.rb +0 -291
- data/test/redactor_test.rb +0 -183
- data/test/relay_recovery_test.rb +0 -451
- data/test/relay_reentrancy_test.rb +0 -159
- data/test/release_test.rb +0 -375
- data/test/ruby_api_test.rb +0 -515
- data/test/scan_reuse_test.rb +0 -109
- data/test/script_logging_test.rb +0 -124
- data/test/support/interact_probe.rb +0 -125
- data/test/support/kibitz_probe.rb +0 -177
- data/test/support/script_probe.rb +0 -158
- data/test/terminal_cleanup_test.rb +0 -349
- data/test/test_helper.rb +0 -58
- data/test/timeout_test.rb +0 -289
- data/test/write_contract_test.rb +0 -105
data/test/redactor_test.rb
DELETED
|
@@ -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
|
data/test/relay_recovery_test.rb
DELETED
|
@@ -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
|