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/deadline_test.rb
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "test_helper"
|
|
4
|
-
|
|
5
|
-
class DeadlineTest < ExpectTest
|
|
6
|
-
def test_absolute_deadline_caps_timeout_resets_from_incoming_data
|
|
7
|
-
session, writer = pipe_session(reset_timeout_on_read: true)
|
|
8
|
-
with_clock([[4, writer, "."], [8, writer, "."], [12, writer, "done"]]) do |clock|
|
|
9
|
-
result = session.expect_result("done", timeout: 5, deadline: 10)
|
|
10
|
-
assert result.timeout?
|
|
11
|
-
assert_equal 10, clock[0]
|
|
12
|
-
assert_equal "..", session.buffer
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def test_class_api_and_nil_timeout_use_the_absolute_deadline
|
|
17
|
-
session, writer = pipe_session
|
|
18
|
-
with_clock([[4, writer, "ready"]]) do |clock|
|
|
19
|
-
assert_equal 1, Expect.expect("ready", from: session, timeout: nil, deadline: 5)
|
|
20
|
-
result = Expect.expect_result("missing", from: session, timeout: nil, deadline: 5)
|
|
21
|
-
assert result.timeout?
|
|
22
|
-
assert_equal 5, clock[0]
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def test_shorter_relative_timeout_still_wins
|
|
27
|
-
session, = pipe_session
|
|
28
|
-
with_clock do |clock|
|
|
29
|
-
assert session.expect_result("missing", timeout: 2, deadline: 10).timeout?
|
|
30
|
-
assert_equal 2, clock[0]
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def test_continuing_match_cannot_extend_the_hard_deadline
|
|
35
|
-
session, = pipe_session
|
|
36
|
-
session.buffer = "first second"
|
|
37
|
-
with_clock do |clock|
|
|
38
|
-
result = session.expect_result(timeout: 10, deadline: 5) do
|
|
39
|
-
on("first") do
|
|
40
|
-
clock[0] = 6
|
|
41
|
-
Expect.continue
|
|
42
|
-
end
|
|
43
|
-
on("second") { flunk "must not consume text after the deadline" }
|
|
44
|
-
end
|
|
45
|
-
assert result.timeout?
|
|
46
|
-
assert_equal " second", session.buffer
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def test_timeout_callback_cannot_restart_an_expired_hard_deadline
|
|
51
|
-
session, = pipe_session
|
|
52
|
-
calls = 0
|
|
53
|
-
with_clock do |clock|
|
|
54
|
-
result = session.expect_result(timeout: 3, deadline: 5) do
|
|
55
|
-
timeout do
|
|
56
|
-
calls += 1
|
|
57
|
-
Expect.continue
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
assert result.timeout?
|
|
61
|
-
assert_equal 2, calls
|
|
62
|
-
assert_equal 5, clock[0]
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def test_expired_deadline_does_not_scan_or_read_and_preserves_input
|
|
67
|
-
session, writer = pipe_session
|
|
68
|
-
session.buffer = "buffered"
|
|
69
|
-
writer.write("unread")
|
|
70
|
-
with_clock do |clock|
|
|
71
|
-
clock[0] = 5
|
|
72
|
-
assert session.expect_result("buffered", timeout: 0, deadline: 5).timeout?
|
|
73
|
-
assert_equal "buffered", session.buffer
|
|
74
|
-
assert_equal "unread", session.to_io.read_nonblock(6)
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def test_zero_relative_timeout_still_polls_with_a_future_hard_deadline
|
|
79
|
-
session, writer = pipe_session
|
|
80
|
-
writer.write("ready")
|
|
81
|
-
assert_equal 1, session.expect("ready", timeout: 0, deadline: Expect.monotonic + 1)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def test_repeated_interruptions_cannot_extend_a_deadline
|
|
85
|
-
session, = pipe_session
|
|
86
|
-
clock = [0]
|
|
87
|
-
Expect.stub(:monotonic, -> { clock[0] }) do
|
|
88
|
-
IO.stub(:select, lambda { |*|
|
|
89
|
-
clock[0] += 1
|
|
90
|
-
raise Errno::EINTR
|
|
91
|
-
}) do
|
|
92
|
-
assert session.expect_result("missing", deadline: 3).timeout?
|
|
93
|
-
end
|
|
94
|
-
assert_equal 3, clock[0]
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def test_known_eof_events_are_delivered_before_timeout_of_live_sources
|
|
99
|
-
ended, = pipe_session
|
|
100
|
-
live, = pipe_session
|
|
101
|
-
ended.close
|
|
102
|
-
live.buffer = "ready"
|
|
103
|
-
seen = []
|
|
104
|
-
with_clock do
|
|
105
|
-
result = Expect.expect_result(from: [ended, live], deadline: 0) do
|
|
106
|
-
on("ready") { flunk "deadline already expired" }
|
|
107
|
-
eof(from: ended) do
|
|
108
|
-
seen << :eof
|
|
109
|
-
Expect.continue
|
|
110
|
-
end
|
|
111
|
-
timeout { |sessions| seen << sessions }
|
|
112
|
-
end
|
|
113
|
-
assert result.timeout?
|
|
114
|
-
assert_same live, result.session
|
|
115
|
-
assert_equal [:eof, [live]], seen
|
|
116
|
-
assert_equal "ready", live.buffer
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def test_all_known_eof_returns_eof_even_at_the_hard_deadline
|
|
121
|
-
first, = pipe_session
|
|
122
|
-
second, = pipe_session
|
|
123
|
-
[first, second].each(&:close)
|
|
124
|
-
seen = []
|
|
125
|
-
with_clock do
|
|
126
|
-
result = Expect.expect_result(from: [first, second], deadline: 0) do
|
|
127
|
-
eof do |session|
|
|
128
|
-
seen << session
|
|
129
|
-
Expect.continue
|
|
130
|
-
end
|
|
131
|
-
timeout { flunk "all sources ended" }
|
|
132
|
-
end
|
|
133
|
-
assert result.eof?
|
|
134
|
-
assert_equal [first, second], seen
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def test_deadline_reached_during_regexp_does_not_consume_the_match
|
|
139
|
-
session, = pipe_session
|
|
140
|
-
session.buffer = "ready"
|
|
141
|
-
pattern = Regexp.new("ready")
|
|
142
|
-
match = pattern.method(:match)
|
|
143
|
-
with_clock do |clock|
|
|
144
|
-
pattern.stub(:match, lambda { |text|
|
|
145
|
-
clock[0] = 5
|
|
146
|
-
match.call(text)
|
|
147
|
-
}) do
|
|
148
|
-
assert session.expect_result(pattern, deadline: 5).timeout?
|
|
149
|
-
end
|
|
150
|
-
assert_equal "ready", session.buffer
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def test_preserved_timeout_at_hard_deadline_still_dispatches_all_known_eof
|
|
155
|
-
first, = pipe_session
|
|
156
|
-
second, = pipe_session
|
|
157
|
-
[first, second].each(&:close)
|
|
158
|
-
first.buffer = "ready"
|
|
159
|
-
seen = []
|
|
160
|
-
with_clock do |clock|
|
|
161
|
-
result = Expect.expect_result(from: [first, second], deadline: 5) do
|
|
162
|
-
on("ready") do
|
|
163
|
-
clock[0] = 5
|
|
164
|
-
Expect.continue(reset_timeout: false)
|
|
165
|
-
end
|
|
166
|
-
eof do |session|
|
|
167
|
-
seen << session
|
|
168
|
-
Expect.continue(reset_timeout: false)
|
|
169
|
-
end
|
|
170
|
-
timeout { seen << :timeout }
|
|
171
|
-
end
|
|
172
|
-
assert result.eof?
|
|
173
|
-
assert_equal [first, second], seen
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def test_preserved_timeout_at_hard_deadline_only_times_out_live_sources
|
|
178
|
-
ended, = pipe_session
|
|
179
|
-
live, = pipe_session
|
|
180
|
-
ended.close
|
|
181
|
-
ended.buffer = "ready"
|
|
182
|
-
seen = []
|
|
183
|
-
with_clock do |clock|
|
|
184
|
-
result = Expect.expect_result(from: [ended, live], deadline: 5) do
|
|
185
|
-
on("ready") do
|
|
186
|
-
clock[0] = 5
|
|
187
|
-
Expect.continue(reset_timeout: false)
|
|
188
|
-
end
|
|
189
|
-
eof(from: ended) do
|
|
190
|
-
seen << :eof
|
|
191
|
-
Expect.continue(reset_timeout: false)
|
|
192
|
-
end
|
|
193
|
-
timeout { |sessions| seen << sessions }
|
|
194
|
-
end
|
|
195
|
-
assert result.timeout?
|
|
196
|
-
assert_same live, result.session
|
|
197
|
-
assert_equal [:eof, [live]], seen
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def test_invalid_deadlines_are_rejected_before_the_definition_or_io
|
|
202
|
-
session, writer = pipe_session
|
|
203
|
-
writer.write("ready")
|
|
204
|
-
[Float::INFINITY, -Float::INFINITY, Float::NAN, "invalid"].each do |deadline|
|
|
205
|
-
assert_raises(ArgumentError) do
|
|
206
|
-
session.expect_result(deadline: deadline) { flunk "invalid deadline must reject before registration" }
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
assert_equal "ready", session.to_io.read_nonblock(5)
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
private
|
|
213
|
-
|
|
214
|
-
# 虚拟时间只控制等待,数据仍经真实管道;精确断言上限,不依赖线程调度和 sleep。
|
|
215
|
-
def with_clock(events = [])
|
|
216
|
-
clock = [0.0]
|
|
217
|
-
pending = events.dup
|
|
218
|
-
poll = IO.method(:select)
|
|
219
|
-
select = lambda do |readers, writers, errors, timeout|
|
|
220
|
-
due = timeout && (clock[0] + timeout)
|
|
221
|
-
if pending.any? && (!due || pending.first[0] <= due)
|
|
222
|
-
at, writer, data = pending.shift
|
|
223
|
-
clock[0] = at
|
|
224
|
-
writer.write(data)
|
|
225
|
-
poll.call(readers, writers, errors, 0)
|
|
226
|
-
else
|
|
227
|
-
raise "unbounded wait without input" unless due
|
|
228
|
-
|
|
229
|
-
clock[0] = due
|
|
230
|
-
nil
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
Expect.stub(:monotonic, -> { clock[0] }) do
|
|
234
|
-
IO.stub(:select, select) { yield clock }
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
end
|
data/test/diagnostics_test.rb
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "test_helper"
|
|
4
|
-
require "logger"
|
|
5
|
-
|
|
6
|
-
class DiagnosticsTest < ExpectTest
|
|
7
|
-
def test_logger_receives_lifecycle_and_payload_at_separate_levels
|
|
8
|
-
session, writer = pipe_session(debug_level: 2)
|
|
9
|
-
output = StringIO.new
|
|
10
|
-
logger = Logger.new(output)
|
|
11
|
-
logger.level = Logger::INFO
|
|
12
|
-
session.diagnostic_output = logger
|
|
13
|
-
writer.write("ready")
|
|
14
|
-
session.expect("ready", timeout: 1)
|
|
15
|
-
assert_includes output.string, "matched pattern 1"
|
|
16
|
-
refute_includes output.string, "received"
|
|
17
|
-
logger.level = Logger::DEBUG
|
|
18
|
-
writer.write("next")
|
|
19
|
-
session.expect("next", timeout: 1)
|
|
20
|
-
assert_includes output.string, 'received "next"'
|
|
21
|
-
session.close
|
|
22
|
-
refute output.closed?
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def test_constructor_accepts_borrowed_diagnostic_io_and_keeps_transcript_separate
|
|
26
|
-
diagnostics = StringIO.new
|
|
27
|
-
session, writer = pipe_session(debug_level: 2, diagnostic_output: diagnostics)
|
|
28
|
-
transcript = StringIO.new
|
|
29
|
-
session.log_to(transcript)
|
|
30
|
-
writer.write("ready")
|
|
31
|
-
session.expect("ready", timeout: 1)
|
|
32
|
-
assert_equal "ready", transcript.string
|
|
33
|
-
assert_includes diagnostics.string, 'received "ready"'
|
|
34
|
-
session.close
|
|
35
|
-
refute diagnostics.closed?
|
|
36
|
-
refute transcript.closed?
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def test_callable_diagnostics_receive_immutable_metadata_without_the_session
|
|
40
|
-
events = []
|
|
41
|
-
session, writer = pipe_session(debug_level: 2, diagnostic_output: ->(event) { events << event })
|
|
42
|
-
writer.write("ready")
|
|
43
|
-
session.expect("ready", timeout: 1)
|
|
44
|
-
event = events.find { |item| item[:event] == :received }
|
|
45
|
-
assert event.frozen?
|
|
46
|
-
assert event[:message].frozen?
|
|
47
|
-
assert_equal :debug, event[:level]
|
|
48
|
-
assert_equal session.fileno, event[:fd]
|
|
49
|
-
assert_equal 'received "ready"', event[:message]
|
|
50
|
-
refute_includes event.values, session
|
|
51
|
-
assert_equal :info, events.last[:level]
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def test_invalid_diagnostic_target_preserves_the_previous_target
|
|
55
|
-
session, = pipe_session
|
|
56
|
-
output = StringIO.new
|
|
57
|
-
session.diagnostic_output = output
|
|
58
|
-
assert_raises(ArgumentError) { session.diagnostic_output = Object.new }
|
|
59
|
-
assert_same output, session.diagnostic_output
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def test_diagnostic_failure_keeps_original_input_available
|
|
63
|
-
session, writer = pipe_session(debug_level: 2)
|
|
64
|
-
failure = IOError.new("diagnostic failed")
|
|
65
|
-
session.diagnostic_output = ->(_) { raise failure }
|
|
66
|
-
writer.write("ready")
|
|
67
|
-
result = session.expect_result("ready", timeout: 1)
|
|
68
|
-
assert_same failure, result.error
|
|
69
|
-
assert_equal "ready", session.buffer
|
|
70
|
-
session.debug_level = 0
|
|
71
|
-
assert_equal 1, session.expect("ready", timeout: 0)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def test_nested_diagnostic_wait_preserves_the_outer_match_result
|
|
75
|
-
session, = pipe_session(debug_level: 1)
|
|
76
|
-
session.buffer = "ready"
|
|
77
|
-
nested = nil
|
|
78
|
-
notified = false
|
|
79
|
-
callback_result = nil
|
|
80
|
-
session.diagnostic_output = lambda do |event|
|
|
81
|
-
next unless event[:event] == :matched && !notified
|
|
82
|
-
|
|
83
|
-
notified = true
|
|
84
|
-
nested = session.expect_result("missing", timeout: 0)
|
|
85
|
-
end
|
|
86
|
-
result = session.expect_result(timeout: 0) do |patterns|
|
|
87
|
-
patterns.on("ready") { |source| callback_result = source.last_result }
|
|
88
|
-
end
|
|
89
|
-
assert nested.timeout?
|
|
90
|
-
assert result.matched?
|
|
91
|
-
assert_equal "ready", result.match
|
|
92
|
-
assert_same result, callback_result
|
|
93
|
-
assert_same result, session.last_result
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def test_redaction_handles_every_two_chunk_split_and_overlapping_secrets
|
|
97
|
-
(0..11).each do |split|
|
|
98
|
-
session, = pipe_session
|
|
99
|
-
output = StringIO.new
|
|
100
|
-
session.log_to(output)
|
|
101
|
-
session.redact("abc", "bcd", "secret")
|
|
102
|
-
text = "xabcdsecret!"
|
|
103
|
-
session.write_log(text.byteslice(0, split))
|
|
104
|
-
session.write_log(text.byteslice(split..))
|
|
105
|
-
session.log_output = nil
|
|
106
|
-
assert_equal "x[FILTERED]!", output.string, "split #{split}"
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def test_byte_at_a_time_redaction_preserves_matching_and_listeners
|
|
111
|
-
session, writer = pipe_session(debug_level: 3)
|
|
112
|
-
events = []
|
|
113
|
-
log = StringIO.new
|
|
114
|
-
forwarded = StringIO.new
|
|
115
|
-
session.diagnostic_output = ->(event) { events << event }
|
|
116
|
-
session.log_to(log)
|
|
117
|
-
session.listeners = [forwarded]
|
|
118
|
-
session.redact("password")
|
|
119
|
-
"password!".each_char do |byte|
|
|
120
|
-
writer.write(byte)
|
|
121
|
-
session.__send__(:read_available)
|
|
122
|
-
refute_includes log.string, "password"
|
|
123
|
-
end
|
|
124
|
-
assert_equal 1, session.expect("password!", timeout: 0)
|
|
125
|
-
writer.close
|
|
126
|
-
assert session.expect_result(:eof, timeout: 1).eof?
|
|
127
|
-
assert_equal "[FILTERED]!", log.string
|
|
128
|
-
assert_equal "password!", forwarded.string
|
|
129
|
-
received = events.select { |event| event[:event] == :received }.map { |event| event[:message] }.join
|
|
130
|
-
assert_includes received, "[FILTERED]"
|
|
131
|
-
refute_includes events.inspect, "password"
|
|
132
|
-
buffers = events.select { |event| event[:event] == :buffer }
|
|
133
|
-
assert(buffers.all? { |event| event[:message] == "buffer [FILTERED]" })
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def test_sending_diagnostics_are_filtered_across_separate_writes
|
|
137
|
-
client, peer = Socket.pair(:UNIX, :STREAM, 0)
|
|
138
|
-
@ios.push(client, peer)
|
|
139
|
-
output = StringIO.new
|
|
140
|
-
session = Expect.open(client, debug_level: 2, diagnostic_output: output)
|
|
141
|
-
@sessions << session
|
|
142
|
-
session.redact("password")
|
|
143
|
-
%w[pa ss wo rd].each { |part| session.write(part) }
|
|
144
|
-
assert_equal "password", peer.read(8)
|
|
145
|
-
session.close
|
|
146
|
-
refute_includes output.string, "password"
|
|
147
|
-
refute_includes output.string, '"pa"'
|
|
148
|
-
assert_includes output.string, "[FILTERED]"
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def test_registration_copies_secrets_is_additive_and_isolated_between_sessions
|
|
152
|
-
first, = pipe_session
|
|
153
|
-
second, = pipe_session
|
|
154
|
-
first_log = StringIO.new
|
|
155
|
-
second_log = StringIO.new
|
|
156
|
-
first.log_to(first_log)
|
|
157
|
-
second.log_to(second_log)
|
|
158
|
-
secret = +"first"
|
|
159
|
-
assert_same first, first.redact(secret)
|
|
160
|
-
secret.replace("changed")
|
|
161
|
-
first.write_log("fi")
|
|
162
|
-
first.redact("second")
|
|
163
|
-
first.write_log("rst second")
|
|
164
|
-
second.write_log("first second")
|
|
165
|
-
first.close
|
|
166
|
-
assert_equal "[FILTERED] [FILTERED]", first_log.string
|
|
167
|
-
assert_equal "first second", second_log.string
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def test_binary_secrets_are_matched_before_diagnostic_escaping
|
|
171
|
-
session, = pipe_session(debug_level: 2)
|
|
172
|
-
log = StringIO.new
|
|
173
|
-
diagnostics = StringIO.new
|
|
174
|
-
session.log_to(log)
|
|
175
|
-
session.diagnostic_output = diagnostics
|
|
176
|
-
secret = "\xff\0\n".b
|
|
177
|
-
session.redact(secret)
|
|
178
|
-
session.write_log("before#{secret}after".b)
|
|
179
|
-
session.__send__(:trace_data, :received, secret, level: 2)
|
|
180
|
-
session.close
|
|
181
|
-
assert_equal "before[FILTERED]after", log.string
|
|
182
|
-
refute_includes diagnostics.string, '\\xFF'
|
|
183
|
-
assert_includes diagnostics.string, "[FILTERED]"
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def test_eof_and_close_flush_benign_tails_but_hide_incomplete_secret_prefixes
|
|
187
|
-
session, writer = pipe_session
|
|
188
|
-
log = StringIO.new
|
|
189
|
-
session.log_to(log)
|
|
190
|
-
session.redact("password")
|
|
191
|
-
writer.write("hello pass")
|
|
192
|
-
writer.close
|
|
193
|
-
assert session.expect_result(:eof, timeout: 1).eof?
|
|
194
|
-
assert_equal "hello [FILTERED]", log.string
|
|
195
|
-
session.close
|
|
196
|
-
assert_equal "hello [FILTERED]", log.string
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def test_replacing_log_finishes_the_original_target_and_keeps_it_borrowed
|
|
200
|
-
session, = pipe_session
|
|
201
|
-
first = StringIO.new
|
|
202
|
-
second = StringIO.new
|
|
203
|
-
session.redact("secret")
|
|
204
|
-
session.log_to(first)
|
|
205
|
-
session.write_log("hello")
|
|
206
|
-
session.log_to(second)
|
|
207
|
-
session.write_log("secret!")
|
|
208
|
-
session.close
|
|
209
|
-
assert_equal "hello", first.string
|
|
210
|
-
assert_equal "[FILTERED]!", second.string
|
|
211
|
-
refute first.closed?
|
|
212
|
-
refute second.closed?
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def test_invalid_registration_does_not_partially_add_secrets
|
|
216
|
-
session, = pipe_session
|
|
217
|
-
output = StringIO.new
|
|
218
|
-
session.log_to(output)
|
|
219
|
-
[[], [""], [nil], ["valid", 42]].each do |values|
|
|
220
|
-
assert_raises(ArgumentError) { session.redact(*values) }
|
|
221
|
-
end
|
|
222
|
-
session.write_log("valid")
|
|
223
|
-
assert_equal "valid", output.string
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def test_reopening_the_same_log_path_finishes_the_old_stream_before_truncating
|
|
227
|
-
session, = pipe_session
|
|
228
|
-
session.redact("password")
|
|
229
|
-
Dir.mktmpdir do |directory|
|
|
230
|
-
path = File.join(directory, "session.log")
|
|
231
|
-
previous = session.log_to(path, mode: "w")
|
|
232
|
-
session.write_log("hello")
|
|
233
|
-
assert_equal "", File.binread(path)
|
|
234
|
-
session.log_to(path, mode: "w")
|
|
235
|
-
assert previous.closed?
|
|
236
|
-
session.write_log("x")
|
|
237
|
-
session.close
|
|
238
|
-
assert_equal "x", File.binread(path)
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def test_failed_old_log_flush_does_not_truncate_the_new_path
|
|
243
|
-
session, = pipe_session
|
|
244
|
-
session.redact("password")
|
|
245
|
-
failure = IOError.new("old log failed")
|
|
246
|
-
session.log_to { raise failure }
|
|
247
|
-
session.write_log("tail")
|
|
248
|
-
Dir.mktmpdir do |directory|
|
|
249
|
-
path = File.join(directory, "existing.log")
|
|
250
|
-
File.write(path, "keep")
|
|
251
|
-
assert_same failure, assert_raises(IOError) { session.log_to(path, mode: "w") }
|
|
252
|
-
assert_equal "keep", File.binread(path)
|
|
253
|
-
end
|
|
254
|
-
session.log_output = nil
|
|
255
|
-
assert_nil session.log_output
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
def test_flush_failure_does_not_prevent_handle_and_child_cleanup
|
|
259
|
-
session = child('puts "ready"; sleep 60', raw_pty: true)
|
|
260
|
-
session.expect("ready", timeout: 2)
|
|
261
|
-
session.redact("password")
|
|
262
|
-
failure = IOError.new("flush failed")
|
|
263
|
-
session.log_to { raise failure }
|
|
264
|
-
session.write_log("hi") # 小于保留窗口,失败应发生在关闭时的尾部交付。
|
|
265
|
-
assert_same failure, assert_raises(IOError) { session.close }
|
|
266
|
-
assert session.closed?
|
|
267
|
-
refute session.alive?
|
|
268
|
-
session.log_output = nil
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def test_unexpected_diagnostic_flush_failure_still_closes_owned_log
|
|
272
|
-
session, = pipe_session(debug_level: 2)
|
|
273
|
-
session.redact("password")
|
|
274
|
-
Dir.mktmpdir do |directory|
|
|
275
|
-
log = session.log_to(File.join(directory, "session.log"))
|
|
276
|
-
failure = RuntimeError.new("diagnostic callback failed")
|
|
277
|
-
session.diagnostic_output = ->(_) { raise failure }
|
|
278
|
-
session.__send__(:trace_data, :sending, "hi", level: 2)
|
|
279
|
-
assert_same failure, assert_raises(RuntimeError) { session.close }
|
|
280
|
-
assert log.closed?
|
|
281
|
-
assert session.closed?
|
|
282
|
-
end
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def test_replacing_diagnostics_finishes_the_previous_stream
|
|
286
|
-
session, = pipe_session(debug_level: 2)
|
|
287
|
-
session.redact("password")
|
|
288
|
-
first = StringIO.new
|
|
289
|
-
second = StringIO.new
|
|
290
|
-
session.diagnostic_output = first
|
|
291
|
-
session.__send__(:trace_data, :received, "pass", level: 2)
|
|
292
|
-
session.diagnostic_output = second
|
|
293
|
-
session.__send__(:trace_data, :received, "password!", level: 2)
|
|
294
|
-
session.close
|
|
295
|
-
assert_includes first.string, "[FILTERED]"
|
|
296
|
-
refute_includes first.string, "pass"
|
|
297
|
-
assert_includes second.string, "[FILTERED]"
|
|
298
|
-
refute_includes second.string, "password"
|
|
299
|
-
refute first.closed?
|
|
300
|
-
refute second.closed?
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
def test_log_callback_can_rotate_the_target_during_close
|
|
304
|
-
session, = pipe_session
|
|
305
|
-
session.redact("long-secret")
|
|
306
|
-
delivered = []
|
|
307
|
-
rotated = nil
|
|
308
|
-
Dir.mktmpdir do |directory|
|
|
309
|
-
path = File.join(directory, "rotated.log")
|
|
310
|
-
session.log_to do |data|
|
|
311
|
-
delivered << data
|
|
312
|
-
rotated = session.log_to(path, mode: "w")
|
|
313
|
-
session.write_log("body")
|
|
314
|
-
end
|
|
315
|
-
session.write_log("xyz")
|
|
316
|
-
session.close
|
|
317
|
-
assert_equal ["xyz"], delivered
|
|
318
|
-
assert rotated.closed?
|
|
319
|
-
assert_equal "body", File.binread(path)
|
|
320
|
-
assert_nil session.log_output
|
|
321
|
-
end
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
def test_diagnostic_flush_callback_can_create_or_revisit_the_other_direction
|
|
325
|
-
[nil, "seed"].each do |initial_send|
|
|
326
|
-
client, peer = Socket.pair(:UNIX, :STREAM, 0)
|
|
327
|
-
@ios.push(client, peer)
|
|
328
|
-
session = Expect.open(client, debug_level: 2)
|
|
329
|
-
@sessions << session
|
|
330
|
-
session.redact("long-secret")
|
|
331
|
-
events = []
|
|
332
|
-
session.diagnostic_output = lambda do |event|
|
|
333
|
-
events << event
|
|
334
|
-
session.write("ack") if event[:event] == :received
|
|
335
|
-
end
|
|
336
|
-
if initial_send
|
|
337
|
-
session.write(initial_send)
|
|
338
|
-
assert_equal initial_send, peer.read(initial_send.bytesize)
|
|
339
|
-
end
|
|
340
|
-
peer.write("xyz")
|
|
341
|
-
assert_equal 1, session.expect("xyz", timeout: 0)
|
|
342
|
-
replacement = StringIO.new
|
|
343
|
-
session.diagnostic_output = replacement
|
|
344
|
-
assert_equal "ack", peer.read(3)
|
|
345
|
-
expected = initial_send ? %i[matched sending received sending] : %i[matched received sending]
|
|
346
|
-
assert_equal(expected, events.map { |event| event[:event] })
|
|
347
|
-
assert_includes events.last[:message], "ack"
|
|
348
|
-
assert_equal "", replacement.string
|
|
349
|
-
session.close
|
|
350
|
-
assert_equal "", replacement.string
|
|
351
|
-
end
|
|
352
|
-
end
|
|
353
|
-
end
|