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/compare_upstream.rb
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Optional differential check. The ordinary test suite has no Perl dependency.
|
|
4
|
-
require "json"
|
|
5
|
-
require "open3"
|
|
6
|
-
require "rbconfig"
|
|
7
|
-
require_relative "../lib/expect"
|
|
8
|
-
|
|
9
|
-
upstream = ARGV.fetch(0) { abort "usage: ruby test/compare_upstream.rb /path/to/expect.pm" }
|
|
10
|
-
perl = <<~'PERL'
|
|
11
|
-
use strict;
|
|
12
|
-
use warnings;
|
|
13
|
-
use Expect;
|
|
14
|
-
use JSON::PP;
|
|
15
|
-
my %results;
|
|
16
|
-
sub fresh {
|
|
17
|
-
my ($buffer) = @_;
|
|
18
|
-
my $e = Expect->new;
|
|
19
|
-
$e->log_stdout(0);
|
|
20
|
-
$e->set_accum($buffer);
|
|
21
|
-
return $e;
|
|
22
|
-
}
|
|
23
|
-
sub snapshot {
|
|
24
|
-
my ($e) = @_;
|
|
25
|
-
my $captures = $e->exp_matchlist;
|
|
26
|
-
return { number => $e->match_number, before => $e->before,
|
|
27
|
-
match => $e->match, after => $e->after, captures => $captures || [],
|
|
28
|
-
accum => $e->get_accum };
|
|
29
|
-
}
|
|
30
|
-
my $e = fresh('before a.c after');
|
|
31
|
-
$e->expect(0, 'absent', 'a.c');
|
|
32
|
-
$results{literal} = snapshot($e);
|
|
33
|
-
$e = fresh('prefix value=42 tail');
|
|
34
|
-
$e->expect(0, ['value=(\d+)']);
|
|
35
|
-
$results{regexp_array} = snapshot($e);
|
|
36
|
-
$e = fresh("first\nsecond\nthird");
|
|
37
|
-
$e->expect(0, '-re', '^second$');
|
|
38
|
-
$results{multiline} = snapshot($e);
|
|
39
|
-
$e = fresh('second first');
|
|
40
|
-
$e->expect(0, 'first', 'second');
|
|
41
|
-
$results{priority} = snapshot($e);
|
|
42
|
-
$e = fresh('before token tail');
|
|
43
|
-
$e->notransfer(1);
|
|
44
|
-
$e->expect(0, 'token');
|
|
45
|
-
$results{notransfer} = snapshot($e);
|
|
46
|
-
$e = fresh('discard tail');
|
|
47
|
-
$e->max_accum(4);
|
|
48
|
-
$e->expect(0, 'tail');
|
|
49
|
-
$results{max_accum} = snapshot($e);
|
|
50
|
-
$e = fresh('keep this');
|
|
51
|
-
$e->expect(0, 'missing');
|
|
52
|
-
$results{timeout} = { error => $e->exp_error, before => $e->before, accum => $e->get_accum };
|
|
53
|
-
$e = fresh('A B C End');
|
|
54
|
-
my @states;
|
|
55
|
-
$e->expect(1, ['[ABC]', sub { push @states, $_[0]->match; exp_continue }], 'End');
|
|
56
|
-
$results{continue} = { states => \@states, final => $e->match, number => $e->match_number };
|
|
57
|
-
$e = fresh('');
|
|
58
|
-
$e->raw_pty(1);
|
|
59
|
-
$e->spawn($^X, '-e', '$|=1; while (<STDIN>) { chomp; print scalar(reverse($_)), "\n" }');
|
|
60
|
-
$e->send("crate\n");
|
|
61
|
-
$e->expect(3, 'etarc');
|
|
62
|
-
$results{pty_dialogue} = { before => $e->before, match => $e->match, number => $e->match_number };
|
|
63
|
-
$e->hard_close;
|
|
64
|
-
pipe(my $reader, my $writer) or die $!;
|
|
65
|
-
$writer->autoflush(1);
|
|
66
|
-
my $input = Expect->exp_init($reader);
|
|
67
|
-
my $idle = fresh('');
|
|
68
|
-
print $writer 'beforeSTOP42;after';
|
|
69
|
-
$results{readiness} = [Expect::test_handles(1, $idle, $input)];
|
|
70
|
-
my @escapes;
|
|
71
|
-
$input->set_seq('STOP\d+;', sub { push @escapes, 'stopped'; return 0; });
|
|
72
|
-
Expect::interconnect($input);
|
|
73
|
-
$results{regexp_escape} = \@escapes;
|
|
74
|
-
close $writer;
|
|
75
|
-
print JSON::PP->new->canonical->encode(\%results);
|
|
76
|
-
PERL
|
|
77
|
-
|
|
78
|
-
output, errors, status = Open3.capture3("perl", "-I#{File.join(upstream, "lib")}", "-e", perl)
|
|
79
|
-
abort "Perl fixture failed: #{errors}" unless status.success?
|
|
80
|
-
expected = JSON.parse(output)
|
|
81
|
-
actual = {}
|
|
82
|
-
sessions = []
|
|
83
|
-
fresh = lambda do |buffer|
|
|
84
|
-
session = Expect.new(log_stdout: false)
|
|
85
|
-
sessions << session
|
|
86
|
-
session.buffer = buffer
|
|
87
|
-
session
|
|
88
|
-
end
|
|
89
|
-
snapshot = lambda do |s|
|
|
90
|
-
{ "number" => s.match_number, "before" => s.before, "match" => s.match,
|
|
91
|
-
"after" => s.after, "captures" => s.captures, "accum" => s.buffer }
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
begin
|
|
95
|
-
s = fresh.call("before a.c after")
|
|
96
|
-
s.expect("absent", "a.c", timeout: 0)
|
|
97
|
-
actual["literal"] = snapshot.call(s)
|
|
98
|
-
s = fresh.call("prefix value=42 tail")
|
|
99
|
-
s.expect(/value=(\d+)/, timeout: 0)
|
|
100
|
-
actual["regexp_array"] = snapshot.call(s)
|
|
101
|
-
s = fresh.call("first\nsecond\nthird")
|
|
102
|
-
s.expect(/^second$/, timeout: 0)
|
|
103
|
-
actual["multiline"] = snapshot.call(s)
|
|
104
|
-
s = fresh.call("second first")
|
|
105
|
-
s.expect("first", "second", timeout: 0)
|
|
106
|
-
actual["priority"] = snapshot.call(s)
|
|
107
|
-
s = fresh.call("before token tail")
|
|
108
|
-
s.preserve_buffer = true
|
|
109
|
-
s.expect("token", timeout: 0)
|
|
110
|
-
actual["notransfer"] = snapshot.call(s)
|
|
111
|
-
s = fresh.call("discard tail")
|
|
112
|
-
s.buffer_limit = 4
|
|
113
|
-
s.expect("tail", timeout: 0)
|
|
114
|
-
actual["max_accum"] = snapshot.call(s)
|
|
115
|
-
s = fresh.call("keep this")
|
|
116
|
-
s.expect("missing", timeout: 0)
|
|
117
|
-
actual["timeout"] =
|
|
118
|
-
{ "error" => (s.error == :timeout ? "1:TIMEOUT" : s.error), "before" => s.before, "accum" => s.buffer }
|
|
119
|
-
s = fresh.call("A B C End")
|
|
120
|
-
states = []
|
|
121
|
-
s.expect(timeout: 1) do
|
|
122
|
-
on(/[ABC]/) do |object|
|
|
123
|
-
states << object.match
|
|
124
|
-
Expect.continue
|
|
125
|
-
end
|
|
126
|
-
on("End")
|
|
127
|
-
end
|
|
128
|
-
actual["continue"] = { "states" => states, "final" => s.match, "number" => s.match_number }
|
|
129
|
-
s = fresh.call("")
|
|
130
|
-
s.raw_pty = true
|
|
131
|
-
s.spawn(RbConfig.ruby, "--disable-gems", "-e",
|
|
132
|
-
"STDOUT.sync = true; while line = STDIN.gets; puts line.chomp.reverse; end")
|
|
133
|
-
s.write("crate\n")
|
|
134
|
-
s.expect("etarc", timeout: 3)
|
|
135
|
-
actual["pty_dialogue"] = { "before" => s.before, "match" => s.match, "number" => s.match_number }
|
|
136
|
-
reader, writer = IO.pipe
|
|
137
|
-
input = Expect.open(reader, own: true)
|
|
138
|
-
sessions << input
|
|
139
|
-
idle = fresh.call("")
|
|
140
|
-
writer.write("beforeSTOP42;after")
|
|
141
|
-
actual["readiness"] = Expect.readable_sessions(idle, input, timeout: 1).map { |session| [idle, input].index(session) }
|
|
142
|
-
escapes = []
|
|
143
|
-
input.on_sequence(/STOP\d+;/) do
|
|
144
|
-
escapes << "stopped"
|
|
145
|
-
false
|
|
146
|
-
end
|
|
147
|
-
Expect.interconnect(input, timeout: 1)
|
|
148
|
-
actual["regexp_escape"] = escapes
|
|
149
|
-
writer.close
|
|
150
|
-
|
|
151
|
-
failures = expected.keys.reject { |key| actual.fetch(key) == expected.fetch(key) }
|
|
152
|
-
failures.each { |key| warn "#{key}: Perl=#{expected[key].inspect} Ruby=#{actual[key].inspect}" }
|
|
153
|
-
abort "#{failures.length} upstream comparisons failed" unless failures.empty?
|
|
154
|
-
puts "#{expected.length} upstream comparisons passed (Expect.pm #{File.basename(upstream)})"
|
|
155
|
-
ensure
|
|
156
|
-
sessions.reverse_each { |session| session.hard_close(timeout: 0.03) }
|
|
157
|
-
end
|
data/test/configuration_test.rb
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "test_helper"
|
|
4
|
-
|
|
5
|
-
class ConfigurationTest < ExpectTest
|
|
6
|
-
def test_configuration_is_frozen_and_sessions_have_independent_settings
|
|
7
|
-
Expect.configure(timeout: 0.1) do |config|
|
|
8
|
-
config.buffer_limit = 4
|
|
9
|
-
config.log_stdout = false
|
|
10
|
-
end
|
|
11
|
-
first, = pipe_session
|
|
12
|
-
second, = pipe_session
|
|
13
|
-
first.timeout = 1
|
|
14
|
-
first.buffer_limit = nil
|
|
15
|
-
Expect.configure(timeout: 2)
|
|
16
|
-
third, = pipe_session
|
|
17
|
-
|
|
18
|
-
assert_equal 1, first.timeout
|
|
19
|
-
assert_equal 0.1, second.timeout
|
|
20
|
-
assert_equal 2, third.timeout
|
|
21
|
-
assert_equal 4, second.buffer_limit
|
|
22
|
-
assert_nil first.buffer_limit
|
|
23
|
-
assert_equal 2, Expect.configuration.timeout
|
|
24
|
-
assert_raises(FrozenError) { Expect.configuration.timeout = 10 }
|
|
25
|
-
Expect.configuration.to_h.clear
|
|
26
|
-
assert_equal 2, Expect.configuration.timeout
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def test_configuration_errors_do_not_publish_partial_changes
|
|
30
|
-
previous = Expect.configuration
|
|
31
|
-
assert_raises(ArgumentError) { Expect.configure(unknown: true) }
|
|
32
|
-
assert_same previous, Expect.configuration
|
|
33
|
-
assert_raises(ArgumentError) do
|
|
34
|
-
Expect.configure do |config|
|
|
35
|
-
config.timeout = 5
|
|
36
|
-
config.debug_level = 4
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
assert_same previous, Expect.configuration
|
|
40
|
-
assert_raises(RuntimeError) { Expect.configure(timeout: 3) { raise "cancelled" } }
|
|
41
|
-
assert_same previous, Expect.configuration
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def test_subclasses_inherit_configuration_and_can_override_it_independently
|
|
45
|
-
subclass = Class.new(Expect)
|
|
46
|
-
Expect.configure(timeout: 0.5)
|
|
47
|
-
assert_equal 0.5, subclass.configuration.timeout
|
|
48
|
-
subclass.configure(timeout: 1)
|
|
49
|
-
Expect.configure(timeout: 2)
|
|
50
|
-
reader, writer = IO.pipe
|
|
51
|
-
@ios.push(reader, writer)
|
|
52
|
-
subclass.open(reader) do |session|
|
|
53
|
-
assert_equal 1, session.timeout
|
|
54
|
-
assert_instance_of subclass, session
|
|
55
|
-
end
|
|
56
|
-
assert_equal 2, Expect.configuration.timeout
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def test_concurrent_configure_updates_are_serialized
|
|
60
|
-
subclass = Class.new(Expect)
|
|
61
|
-
entered = Queue.new
|
|
62
|
-
second_started = Queue.new
|
|
63
|
-
release = Queue.new
|
|
64
|
-
first = Thread.new do
|
|
65
|
-
subclass.configure do |config|
|
|
66
|
-
entered << true
|
|
67
|
-
release.pop
|
|
68
|
-
config.timeout = 1
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
entered.pop
|
|
72
|
-
second = Thread.new do
|
|
73
|
-
second_started << true
|
|
74
|
-
subclass.configure(debug_level: 2)
|
|
75
|
-
end
|
|
76
|
-
second_started.pop
|
|
77
|
-
# 第一轮尚未发布时,第二轮不能从同一旧快照完成更新。
|
|
78
|
-
refute second.join(0.05), "concurrent configure published before the first update finished"
|
|
79
|
-
release << true
|
|
80
|
-
first.value
|
|
81
|
-
second.value
|
|
82
|
-
assert_equal 1, subclass.configuration.timeout
|
|
83
|
-
assert_equal 2, subclass.configuration.debug_level
|
|
84
|
-
ensure
|
|
85
|
-
release&.push(true)
|
|
86
|
-
first&.join
|
|
87
|
-
second&.join
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def test_nested_configure_fails_without_publishing_partial_changes
|
|
91
|
-
subclass = Class.new(Expect)
|
|
92
|
-
previous = subclass.configuration
|
|
93
|
-
assert_raises(ThreadError) do
|
|
94
|
-
subclass.configure do |config|
|
|
95
|
-
config.timeout = 1
|
|
96
|
-
subclass.configure(debug_level: 2)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
assert_same previous, subclass.configuration
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def test_attribute_validation_preserves_the_current_value_and_buffer
|
|
103
|
-
session, = pipe_session(buffer_limit: 8, timeout: 1, write_timeout: 2, debug_level: 1)
|
|
104
|
-
session.buffer = "contents"
|
|
105
|
-
[0, -1, 1.5, "4"].each do |limit|
|
|
106
|
-
assert_raises(ArgumentError) { session.buffer_limit = limit }
|
|
107
|
-
end
|
|
108
|
-
assert_equal 8, session.buffer_limit
|
|
109
|
-
assert_equal "contents", session.buffer
|
|
110
|
-
assert_raises(ArgumentError) { session.timeout = Float::INFINITY }
|
|
111
|
-
assert_raises(ArgumentError) { session.write_timeout = -1 }
|
|
112
|
-
assert_raises(ArgumentError) { session.debug_level = 1.5 }
|
|
113
|
-
assert_equal [1, 2, 1], [session.timeout, session.write_timeout, session.debug_level]
|
|
114
|
-
session.buffer_limit = 4
|
|
115
|
-
assert_equal "ents", session.buffer
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def test_predicates_use_ruby_truthiness_and_normalize_boolean_attributes
|
|
119
|
-
Expect.configure(raw_pty: 0, log_stdout: nil)
|
|
120
|
-
config = Expect.configuration
|
|
121
|
-
assert_equal true, config.raw_pty
|
|
122
|
-
assert_equal false, config.log_stdout
|
|
123
|
-
session, = pipe_session
|
|
124
|
-
assert session.raw_pty?
|
|
125
|
-
refute session.log_stdout?
|
|
126
|
-
session.preserve_buffer = 0
|
|
127
|
-
assert session.preserve_buffer?
|
|
128
|
-
session.preserve_buffer = nil
|
|
129
|
-
refute session.preserve_buffer?
|
|
130
|
-
assert_raises(ArgumentError) { session.timeout(1) }
|
|
131
|
-
assert_raises(ArgumentError) { session.log_stdout(true) }
|
|
132
|
-
end
|
|
133
|
-
end
|
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
|