expect-pty 0.5.3 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +28 -19
  4. data/docs/API.md +88 -0
  5. data/docs/MIGRATION.md +15 -0
  6. data/lib/expect/cleanup.rb +33 -0
  7. data/lib/expect/configuration.rb +87 -66
  8. data/lib/expect/interaction.rb +226 -209
  9. data/lib/expect/logging.rb +157 -156
  10. data/lib/expect/matcher.rb +30 -29
  11. data/lib/expect/pattern.rb +8 -2
  12. data/lib/expect/pattern_list.rb +35 -5
  13. data/lib/expect/redactor.rb +11 -4
  14. data/lib/expect/relay.rb +97 -73
  15. data/lib/expect/relay_writer.rb +2 -1
  16. data/lib/expect/result.rb +32 -8
  17. data/lib/expect/session.rb +508 -0
  18. data/lib/expect/session_resources.rb +10 -2
  19. data/lib/expect/terminal.rb +78 -78
  20. data/lib/expect/version.rb +1 -1
  21. data/lib/expect.rb +333 -520
  22. data/sig/expect.rbs +355 -0
  23. metadata +13 -106
  24. data/.rubocop.yml +0 -66
  25. data/CONTRIBUTING.md +0 -29
  26. data/Gemfile +0 -28
  27. data/Rakefile +0 -38
  28. data/benchmark/matching.rb +0 -98
  29. data/benchmark/redactor.rb +0 -64
  30. data/benchmark/relay.rb +0 -48
  31. data/benchmark/scaling.rb +0 -70
  32. data/benchmark/send_slow.rb +0 -41
  33. data/benchmark/support.rb +0 -114
  34. data/docs/COMPATIBILITY.md +0 -98
  35. data/docs/INTERNAL_CONTRACTS.md +0 -154
  36. data/docs/PERFORMANCE.md +0 -165
  37. data/docs/RELEASING.md +0 -84
  38. data/docs/VERIFICATION.md +0 -456
  39. data/examples/dialogue.rb +0 -30
  40. data/examples/kibitz/README.md +0 -81
  41. data/examples/kibitz/kibitz.rb +0 -142
  42. data/examples/kibitz/test_kibitz.rb +0 -37
  43. data/examples/ssh_auto.rb +0 -94
  44. data/examples/ssh_interact.rb +0 -159
  45. data/examples/ssh_login.rb +0 -64
  46. data/expect-pty.gemspec +0 -33
  47. data/script/ci +0 -121
  48. data/script/release.rb +0 -319
  49. data/test/buffer_accounting_test.rb +0 -83
  50. data/test/cleanup_test.rb +0 -251
  51. data/test/compare_upstream.rb +0 -157
  52. data/test/configuration_test.rb +0 -133
  53. data/test/deadline_test.rb +0 -237
  54. data/test/diagnostics_test.rb +0 -353
  55. data/test/edge_case_test.rb +0 -262
  56. data/test/fixtures/ssh_scripts/01_identity.sh +0 -4
  57. data/test/fixtures/ssh_scripts/02_output.sh +0 -5
  58. data/test/fixtures/ssh_scripts/03_delayed.sh +0 -6
  59. data/test/fixtures/ssh_scripts/04_failure.sh +0 -2
  60. data/test/fixtures/ssh_scripts/05_recovery.sh +0 -3
  61. data/test/initialization_failure_test.rb +0 -92
  62. data/test/integration/README.md +0 -109
  63. data/test/integration/ssh_scripts.rb +0 -94
  64. data/test/interact_test.rb +0 -253
  65. data/test/interconnect_test.rb +0 -425
  66. data/test/io_test.rb +0 -321
  67. data/test/kibitz_test.rb +0 -45
  68. data/test/lifecycle_contract_test.rb +0 -71
  69. data/test/literal_scan_test.rb +0 -97
  70. data/test/matching_test.rb +0 -211
  71. data/test/multi_session_test.rb +0 -66
  72. data/test/ownership_sequence_test.rb +0 -208
  73. data/test/pattern_offset_test.rb +0 -43
  74. data/test/process_interruption_test.rb +0 -296
  75. data/test/process_test.rb +0 -291
  76. data/test/redactor_test.rb +0 -183
  77. data/test/relay_recovery_test.rb +0 -451
  78. data/test/relay_reentrancy_test.rb +0 -159
  79. data/test/release_test.rb +0 -375
  80. data/test/ruby_api_test.rb +0 -515
  81. data/test/scan_reuse_test.rb +0 -109
  82. data/test/script_logging_test.rb +0 -124
  83. data/test/support/interact_probe.rb +0 -125
  84. data/test/support/kibitz_probe.rb +0 -177
  85. data/test/support/script_probe.rb +0 -158
  86. data/test/terminal_cleanup_test.rb +0 -349
  87. data/test/test_helper.rb +0 -58
  88. data/test/timeout_test.rb +0 -289
  89. data/test/write_contract_test.rb +0 -105
@@ -1,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
@@ -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
@@ -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