expect-pty 0.3.2 → 0.4.0
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/.rubocop.yml +18 -10
- data/CHANGELOG.md +42 -12
- data/CONTRIBUTING.md +3 -1
- data/Gemfile +1 -0
- data/README.md +194 -80
- data/benchmark/matching.rb +26 -0
- data/benchmark/scaling.rb +70 -0
- data/benchmark/support.rb +23 -3
- data/docs/COMPATIBILITY.md +68 -57
- data/docs/INTERNAL_CONTRACTS.md +95 -24
- data/docs/PERFORMANCE.md +71 -9
- data/docs/RELEASING.md +8 -6
- data/docs/VERIFICATION.md +213 -81
- data/lib/expect/configuration.rb +3 -1
- data/lib/expect/interaction.rb +18 -3
- data/lib/expect/logging.rb +200 -0
- data/lib/expect/matcher.rb +70 -8
- data/lib/expect/pattern.rb +11 -4
- data/lib/expect/pattern_list.rb +3 -0
- data/lib/expect/redactor.rb +81 -0
- data/lib/expect/relay.rb +12 -0
- data/lib/expect/relay_writer.rb +9 -0
- data/lib/expect/result.rb +2 -0
- data/lib/expect/session_resources.rb +6 -2
- data/lib/expect/terminal.rb +36 -0
- data/lib/expect/version.rb +1 -1
- data/lib/expect.rb +84 -152
- data/script/ci +30 -3
- data/script/release.rb +37 -5
- data/test/buffer_accounting_test.rb +83 -0
- data/test/cleanup_test.rb +88 -0
- data/test/deadline_test.rb +237 -0
- data/test/diagnostics_test.rb +353 -0
- data/test/lifecycle_contract_test.rb +71 -0
- data/test/literal_scan_test.rb +97 -0
- data/test/pattern_offset_test.rb +3 -1
- data/test/process_test.rb +46 -0
- data/test/release_test.rb +94 -16
- data/test/timeout_test.rb +119 -0
- metadata +11 -2
data/script/release.rb
CHANGED
|
@@ -83,11 +83,16 @@ class Release
|
|
|
83
83
|
|
|
84
84
|
private
|
|
85
85
|
|
|
86
|
-
def capture(*
|
|
87
|
-
|
|
86
|
+
def capture(*)
|
|
87
|
+
capture_bytes(*).strip
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Git blob 是任意字节,不能沿用命令文本的 strip,尾部换行和 NUL 也属于发布内容。
|
|
91
|
+
def capture_bytes(*arguments)
|
|
92
|
+
output, error, status = Open3.capture3(*arguments, binmode: true)
|
|
88
93
|
raise "#{arguments.first} failed: #{error.strip}" unless status.success?
|
|
89
94
|
|
|
90
|
-
output
|
|
95
|
+
output
|
|
91
96
|
end
|
|
92
97
|
|
|
93
98
|
def command(*arguments)
|
|
@@ -124,7 +129,7 @@ class Release
|
|
|
124
129
|
def verify_package
|
|
125
130
|
package = Gem::Package.new(@artifact)
|
|
126
131
|
expected = Gem::Specification.load(File.expand_path("expect-pty.gemspec"))
|
|
127
|
-
fields = %i[name version platform summary description authors licenses required_ruby_version
|
|
132
|
+
fields = %i[name version platform summary description authors licenses homepage required_ruby_version
|
|
128
133
|
required_rubygems_version require_paths metadata dependencies extensions executables bindir
|
|
129
134
|
post_install_message]
|
|
130
135
|
unless fields.all? { |field| package.spec.public_send(field) == expected.public_send(field) }
|
|
@@ -132,6 +137,7 @@ class Release
|
|
|
132
137
|
end
|
|
133
138
|
raise "Artifact file list differs from the source" unless package.contents.sort == expected.files.sort
|
|
134
139
|
|
|
140
|
+
committed = committed_files if @commit
|
|
135
141
|
# 直接检查归档中的权限,避免解包时本机 umask 改写执行位。
|
|
136
142
|
File.open(@artifact, "rb") do |io|
|
|
137
143
|
Gem::Package::TarReader.new(io) do |archive|
|
|
@@ -139,7 +145,10 @@ class Release
|
|
|
139
145
|
package.open_tar_gz(data) do |tar|
|
|
140
146
|
tar.each do |entry|
|
|
141
147
|
file = entry.full_name
|
|
142
|
-
|
|
148
|
+
bytes = entry.read
|
|
149
|
+
raise "Artifact differs from source: #{file}" unless entry.file? && bytes == File.binread(file)
|
|
150
|
+
|
|
151
|
+
verify_committed_file(file, bytes, entry.header.mode, committed) if committed
|
|
143
152
|
next if entry.header.mode & 0o111 == File.stat(file).mode & 0o111
|
|
144
153
|
|
|
145
154
|
raise "Artifact executable permissions differ: #{file}"
|
|
@@ -149,6 +158,29 @@ class Release
|
|
|
149
158
|
end
|
|
150
159
|
end
|
|
151
160
|
|
|
161
|
+
# 工作区干净不代表所有打包文件都来自提交:ignore、assume-unchanged 和 filemode 均可隐藏差异。
|
|
162
|
+
# 只在正式发布时核对提交树;dry-run 仍允许检验尚未提交的开发源码。
|
|
163
|
+
def committed_files
|
|
164
|
+
capture_bytes("git", "ls-tree", "-rz", "--full-tree", @commit).split("\0").to_h do |record|
|
|
165
|
+
metadata, file = record.split("\t", 2)
|
|
166
|
+
mode, type, object = metadata.split
|
|
167
|
+
[file, [mode.to_i(8), type, object]]
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def verify_committed_file(file, bytes, mode, committed)
|
|
172
|
+
committed_mode, type, object = committed[file.b]
|
|
173
|
+
unless type == "blob" && committed_mode & 0o170000 == 0o100000
|
|
174
|
+
raise "Artifact file is not in release commit: #{file}"
|
|
175
|
+
end
|
|
176
|
+
unless bytes == capture_bytes("git", "cat-file", "blob", object)
|
|
177
|
+
raise "Artifact differs from release commit: #{file}"
|
|
178
|
+
end
|
|
179
|
+
return if mode & 0o111 == committed_mode & 0o111
|
|
180
|
+
|
|
181
|
+
raise "Artifact executable permissions differ from release commit: #{file}"
|
|
182
|
+
end
|
|
183
|
+
|
|
152
184
|
def get(path)
|
|
153
185
|
uri = URI("#{GEM_HOST}#{path}")
|
|
154
186
|
Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 30) do |http|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class BufferAccountingTest < ExpectTest
|
|
6
|
+
def test_limit_discards_are_cumulative_and_distinct_from_consumption
|
|
7
|
+
session, = pipe_session(buffer_limit: 4)
|
|
8
|
+
assert_equal 0, session.buffer_discarded_bytes
|
|
9
|
+
session.buffer = "abcdef"
|
|
10
|
+
assert_equal "cdef", session.buffer
|
|
11
|
+
assert_equal 2, session.buffer_discarded_bytes
|
|
12
|
+
session.buffer_limit = 2
|
|
13
|
+
assert_equal "ef", session.buffer
|
|
14
|
+
assert_equal 4, session.buffer_discarded_bytes
|
|
15
|
+
assert_equal 1, session.expect("e", timeout: 0)
|
|
16
|
+
assert_equal "f", session.clear_buffer
|
|
17
|
+
session.close
|
|
18
|
+
assert_equal 4, session.buffer_discarded_bytes
|
|
19
|
+
refute_respond_to session, :buffer_discarded_bytes=
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_invalid_limit_and_buffer_leave_accounting_unchanged
|
|
23
|
+
session, = pipe_session(buffer_limit: 4)
|
|
24
|
+
session.buffer = "abcdef"
|
|
25
|
+
assert_raises(ArgumentError) { session.buffer_limit = 0 }
|
|
26
|
+
assert_raises(ArgumentError) { session.buffer = nil }
|
|
27
|
+
assert_equal "cdef", session.buffer
|
|
28
|
+
assert_equal 2, session.buffer_discarded_bytes
|
|
29
|
+
session.buffer_limit = nil
|
|
30
|
+
session.buffer = "unlimited"
|
|
31
|
+
assert_equal 2, session.buffer_discarded_bytes
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_continuous_output_keeps_a_bounded_window_and_complete_log
|
|
35
|
+
session, writer = pipe_session(buffer_limit: 64)
|
|
36
|
+
logged = 0
|
|
37
|
+
session.log_to { |data| logged += data.bytesize }
|
|
38
|
+
32.times do
|
|
39
|
+
writer.write("x" * 4096)
|
|
40
|
+
assert_nil session.expect("missing", timeout: 0)
|
|
41
|
+
assert_equal 64, session.buffer.bytesize
|
|
42
|
+
end
|
|
43
|
+
assert_equal 131_072, logged
|
|
44
|
+
assert_equal logged - 64, session.buffer_discarded_bytes
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_preserved_match_does_not_count_as_discard
|
|
48
|
+
session, = pipe_session(buffer_limit: 4, preserve_buffer: true)
|
|
49
|
+
session.buffer = "abcdef"
|
|
50
|
+
2.times { assert_equal 1, session.expect("ef", timeout: 0) }
|
|
51
|
+
assert_equal "cdef", session.buffer
|
|
52
|
+
assert_equal 2, session.buffer_discarded_bytes
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_relay_tail_is_only_counted_when_matching_applies_the_limit
|
|
56
|
+
session, writer = pipe_session(buffer_limit: 2)
|
|
57
|
+
output = StringIO.new
|
|
58
|
+
session.listeners = [output]
|
|
59
|
+
session.on_sequence("STOP")
|
|
60
|
+
writer.write("prefixSTOPtail")
|
|
61
|
+
assert_same session, Expect.interconnect(session, timeout: 1)
|
|
62
|
+
assert_equal "prefix", output.string
|
|
63
|
+
assert_equal "tail", session.buffer
|
|
64
|
+
assert_equal 0, session.buffer_discarded_bytes
|
|
65
|
+
assert_equal 1, session.expect("il", timeout: 0)
|
|
66
|
+
assert_equal 2, session.buffer_discarded_bytes
|
|
67
|
+
session.expect(timeout: 0)
|
|
68
|
+
assert_equal 2, session.buffer_discarded_bytes
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_logging_failure_preserves_the_retained_input_and_discard_count
|
|
72
|
+
session, writer = pipe_session(buffer_limit: 4)
|
|
73
|
+
failure = IOError.new("log failed")
|
|
74
|
+
session.log_to { raise failure }
|
|
75
|
+
writer.write("abcdef")
|
|
76
|
+
assert_same failure, session.expect_result("ef", timeout: 1).error
|
|
77
|
+
assert_equal "cdef", session.buffer
|
|
78
|
+
assert_equal 2, session.buffer_discarded_bytes
|
|
79
|
+
session.log_output = nil
|
|
80
|
+
assert_equal 1, session.expect("ef", timeout: 0)
|
|
81
|
+
assert_equal 2, session.buffer_discarded_bytes
|
|
82
|
+
end
|
|
83
|
+
end
|
data/test/cleanup_test.rb
CHANGED
|
@@ -3,6 +3,94 @@
|
|
|
3
3
|
require_relative "test_helper"
|
|
4
4
|
|
|
5
5
|
class CleanupTest < ExpectTest
|
|
6
|
+
def test_failed_open_initialization_attempts_all_owned_handles_and_preserves_validation_error
|
|
7
|
+
reader, writer = IO.pipe
|
|
8
|
+
@ios.push(reader, writer)
|
|
9
|
+
reader.stub(:close, -> { raise IOError, "reader close failed" }) do
|
|
10
|
+
error = assert_raises(ArgumentError) { Expect.open(reader, writer: writer, own: true, unknown: true) }
|
|
11
|
+
assert_match(/unknown/, error.message)
|
|
12
|
+
assert writer.closed?
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_failed_new_initialization_attempts_all_pty_handles_and_preserves_validation_error
|
|
17
|
+
master, slave = PTY.open
|
|
18
|
+
@ios.push(master, slave)
|
|
19
|
+
PTY.stub(:open, [master, slave]) do
|
|
20
|
+
master.stub(:close, -> { raise IOError, "master close failed" }) do
|
|
21
|
+
error = assert_raises(ArgumentError) { Expect.new(unknown: true) }
|
|
22
|
+
assert_match(/unknown/, error.message)
|
|
23
|
+
assert slave.closed?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_failed_open_closes_real_owned_handles_when_an_argument_is_not_io
|
|
29
|
+
[true, false].each do |invalid_reader|
|
|
30
|
+
reader, writer = IO.pipe
|
|
31
|
+
@ios.push(reader, writer)
|
|
32
|
+
invalid = Object.new
|
|
33
|
+
invalid.define_singleton_method(:close) { raise "invalid IO must not be closed" }
|
|
34
|
+
incoming = invalid_reader ? invalid : reader
|
|
35
|
+
outgoing = invalid_reader ? writer : invalid
|
|
36
|
+
assert_raises(ArgumentError) { Expect.open(incoming, writer: outgoing, own: true) }
|
|
37
|
+
assert(invalid_reader ? writer.closed? : reader.closed?)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_open_block_error_is_preserved_when_owned_cleanup_also_fails
|
|
42
|
+
[ArgumentError.new("block failed"), Interrupt.new("interrupted"), SystemExit.new(17)].each do |failure|
|
|
43
|
+
reader, writer = IO.pipe
|
|
44
|
+
@ios.push(reader, writer)
|
|
45
|
+
reader.stub(:close, -> { raise IOError, "reader close failed" }) do
|
|
46
|
+
error = assert_raises(failure.class) do
|
|
47
|
+
Expect.open(reader, writer: writer, own: true) { raise failure }
|
|
48
|
+
end
|
|
49
|
+
assert_same failure, error
|
|
50
|
+
assert writer.closed?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_open_block_break_still_reports_cleanup_failure
|
|
56
|
+
reader, writer = IO.pipe
|
|
57
|
+
@ios.push(reader, writer)
|
|
58
|
+
failure = IOError.new("reader close failed")
|
|
59
|
+
reader.stub(:close, -> { raise failure }) do
|
|
60
|
+
error = assert_raises(IOError) { Expect.open(reader, writer: writer, own: true) { break :done } }
|
|
61
|
+
assert_same failure, error
|
|
62
|
+
assert writer.closed?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_spawn_block_error_is_preserved_and_child_reaped_when_cleanup_also_fails
|
|
67
|
+
session = stubborn_child
|
|
68
|
+
failure = ArgumentError.new("block failed")
|
|
69
|
+
Expect.stub(:new, session) do
|
|
70
|
+
session.stub(:spawn, session) do
|
|
71
|
+
session.to_io.stub(:close, -> { raise IOError, "reader close failed" }) do
|
|
72
|
+
error = assert_raises(ArgumentError) { Expect.spawn("already started") { raise failure } }
|
|
73
|
+
assert_same failure, error
|
|
74
|
+
assert_nil session.pid
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_graceful_close_preserves_logging_error_when_handle_cleanup_also_fails
|
|
81
|
+
session = stubborn_child
|
|
82
|
+
failure = ArgumentError.new("log failed")
|
|
83
|
+
session.log_output = ->(_) { raise failure }
|
|
84
|
+
session.to_io.stub(:wait_readable, true) do
|
|
85
|
+
session.to_io.stub(:read_nonblock, "last output") do
|
|
86
|
+
session.to_io.stub(:close, -> { raise IOError, "reader close failed" }) do
|
|
87
|
+
assert_same failure, assert_raises(ArgumentError) { session.close(graceful: true) }
|
|
88
|
+
assert_nil session.pid
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
6
94
|
def test_close_handles_attempts_every_owned_handle_and_preserves_first_error
|
|
7
95
|
reader, writer = IO.pipe
|
|
8
96
|
extra, peer = IO.pipe
|
|
@@ -0,0 +1,237 @@
|
|
|
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
|