ione 1.0.0.pre0

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.
@@ -0,0 +1,360 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+
6
+ module Ione
7
+ module Io
8
+ describe IoReactor do
9
+ let :reactor do
10
+ described_class.new(selector: selector, clock: clock)
11
+ end
12
+
13
+ let! :selector do
14
+ IoReactorSpec::FakeSelector.new
15
+ end
16
+
17
+ let :clock do
18
+ double(:clock, now: 0)
19
+ end
20
+
21
+ shared_context 'running_reactor' do
22
+ before do
23
+ selector.handler do |readables, writables, _, _|
24
+ writables.each do |writable|
25
+ fake_connected(writable)
26
+ end
27
+ [[], writables, []]
28
+ end
29
+ end
30
+
31
+ def fake_connected(connection)
32
+ connection.to_io.stub(:connect_nonblock)
33
+ end
34
+
35
+ after do
36
+ reactor.stop if reactor.running?
37
+ end
38
+ end
39
+
40
+ describe '#start' do
41
+ after do
42
+ reactor.stop.value if reactor.running?
43
+ end
44
+
45
+ it 'returns a future that is resolved when the reactor has started' do
46
+ reactor.start.value
47
+ end
48
+
49
+ it 'returns a future that resolves to the reactor' do
50
+ reactor.start.value.should equal(reactor)
51
+ end
52
+
53
+ it 'is running after being started' do
54
+ reactor.start.value
55
+ reactor.should be_running
56
+ end
57
+
58
+ it 'cannot be started again once stopped' do
59
+ reactor.start.value
60
+ reactor.stop.value
61
+ expect { reactor.start }.to raise_error(ReactorError)
62
+ end
63
+
64
+ it 'calls the selector' do
65
+ called = false
66
+ selector.handler { called = true; [[], [], []] }
67
+ reactor.start.value
68
+ await { called }
69
+ reactor.stop.value
70
+ called.should be_true, 'expected the selector to have been called'
71
+ end
72
+ end
73
+
74
+ describe '#stop' do
75
+ include_context 'running_reactor'
76
+
77
+ it 'returns a future that is resolved when the reactor has stopped' do
78
+ reactor.start.value
79
+ reactor.stop.value
80
+ end
81
+
82
+ it 'returns a future which resolves to the reactor' do
83
+ reactor.start.value
84
+ reactor.stop.value.should equal(reactor)
85
+ end
86
+
87
+ it 'is not running after being stopped' do
88
+ reactor.start.value
89
+ reactor.stop.value
90
+ reactor.should_not be_running
91
+ end
92
+
93
+ it 'closes all sockets' do
94
+ reactor.start.value
95
+ connection = reactor.connect('example.com', 9999, 5).value
96
+ reactor.stop.value
97
+ connection.should be_closed
98
+ end
99
+
100
+ it 'cancels all active timers' do
101
+ reactor.start.value
102
+ clock.stub(:now).and_return(1)
103
+ expired_timer = reactor.schedule_timer(1)
104
+ active_timer1 = reactor.schedule_timer(999)
105
+ active_timer2 = reactor.schedule_timer(111)
106
+ expired_timer.should_not_receive(:fail)
107
+ clock.stub(:now).and_return(2)
108
+ await { expired_timer.completed? }
109
+ reactor.stop.value
110
+ active_timer1.should be_failed
111
+ active_timer2.should be_failed
112
+ end
113
+ end
114
+
115
+ describe '#on_error' do
116
+ before do
117
+ selector.handler { raise 'Blurgh' }
118
+ end
119
+
120
+ it 'calls the listeners when the reactor crashes' do
121
+ error = nil
122
+ reactor.on_error { |e| error = e }
123
+ reactor.start
124
+ await { error }
125
+ error.message.should == 'Blurgh'
126
+ end
127
+
128
+ it 'calls the listener immediately when the reactor has already crashed' do
129
+ error = nil
130
+ reactor.start.value
131
+ await { !reactor.running? }
132
+ reactor.on_error { |e| error = e }
133
+ await { error }
134
+ end
135
+
136
+ it 'ignores errors raised by listeners' do
137
+ called = false
138
+ reactor.on_error { raise 'Blurgh' }
139
+ reactor.on_error { called = true }
140
+ reactor.start
141
+ await { called }
142
+ called.should be_true, 'expected all close listeners to have been called'
143
+ end
144
+ end
145
+
146
+ describe '#connect' do
147
+ include_context 'running_reactor'
148
+
149
+ it 'returns the connected connection when no block is given' do
150
+ reactor.start.value
151
+ reactor.connect('example.com', 9999, 5).value.should be_a(Connection)
152
+ end
153
+ end
154
+
155
+ describe '#schedule_timer' do
156
+ before do
157
+ reactor.start.value
158
+ end
159
+
160
+ after do
161
+ reactor.stop.value
162
+ end
163
+
164
+ it 'returns a future that is resolved after the specified duration' do
165
+ clock.stub(:now).and_return(1)
166
+ f = reactor.schedule_timer(0.1)
167
+ clock.stub(:now).and_return(1.1)
168
+ await { f.resolved? }
169
+ end
170
+ end
171
+
172
+ describe '#to_s' do
173
+ context 'returns a string that' do
174
+ it 'includes the class name' do
175
+ reactor.to_s.should include('Ione::Io::IoReactor')
176
+ end
177
+
178
+ it 'includes a list of its connections' do
179
+ reactor.to_s.should include('@connections=[')
180
+ reactor.to_s.should include('#<Ione::Io::Unblocker>')
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ describe IoLoopBody do
187
+ let :loop_body do
188
+ described_class.new(selector: selector, clock: clock)
189
+ end
190
+
191
+ let :selector do
192
+ double(:selector)
193
+ end
194
+
195
+ let :clock do
196
+ double(:clock, now: 0)
197
+ end
198
+
199
+ let :socket do
200
+ double(:socket, connected?: false, connecting?: false, writable?: false, closed?: false)
201
+ end
202
+
203
+ describe '#tick' do
204
+ before do
205
+ loop_body.add_socket(socket)
206
+ end
207
+
208
+ it 'passes connected sockets as readables to the selector' do
209
+ socket.stub(:connected?).and_return(true)
210
+ selector.should_receive(:select).with([socket], anything, anything, anything).and_return([nil, nil, nil])
211
+ loop_body.tick
212
+ end
213
+
214
+ it 'passes writable sockets as writable to the selector' do
215
+ socket.stub(:writable?).and_return(true)
216
+ selector.should_receive(:select).with(anything, [socket], anything, anything).and_return([nil, nil, nil])
217
+ loop_body.tick
218
+ end
219
+
220
+ it 'passes connecting sockets as writable to the selector' do
221
+ socket.stub(:connecting?).and_return(true)
222
+ socket.stub(:connect)
223
+ selector.should_receive(:select).with(anything, [socket], anything, anything).and_return([nil, nil, nil])
224
+ loop_body.tick
225
+ end
226
+
227
+ it 'filters out closed sockets' do
228
+ socket.stub(:closed?).and_return(true)
229
+ selector.should_receive(:select).with([], [], anything, anything).and_return([nil, nil, nil])
230
+ loop_body.tick
231
+ socket.stub(:connected?).and_return(true)
232
+ selector.should_receive(:select).with([], [], anything, anything).and_return([nil, nil, nil])
233
+ loop_body.tick
234
+ end
235
+
236
+ it 'calls #read on all readable sockets returned by the selector' do
237
+ socket.stub(:connected?).and_return(true)
238
+ socket.should_receive(:read)
239
+ selector.stub(:select) do |r, w, _, _|
240
+ [[socket], nil, nil]
241
+ end
242
+ loop_body.tick
243
+ end
244
+
245
+ it 'calls #connect on all connecting sockets' do
246
+ socket.stub(:connecting?).and_return(true)
247
+ socket.should_receive(:connect)
248
+ selector.stub(:select).and_return([nil, nil, nil])
249
+ loop_body.tick
250
+ end
251
+
252
+ it 'calls #flush on all writable sockets returned by the selector' do
253
+ socket.stub(:writable?).and_return(true)
254
+ socket.should_receive(:flush)
255
+ selector.stub(:select) do |r, w, _, _|
256
+ [nil, [socket], nil]
257
+ end
258
+ loop_body.tick
259
+ end
260
+
261
+ it 'allows the caller to specify a custom timeout' do
262
+ selector.should_receive(:select).with(anything, anything, anything, 99).and_return([[], [], []])
263
+ loop_body.tick(99)
264
+ end
265
+
266
+ it 'completes timers that have expired' do
267
+ selector.stub(:select).and_return([nil, nil, nil])
268
+ clock.stub(:now).and_return(1)
269
+ promise = Promise.new
270
+ loop_body.schedule_timer(1, promise)
271
+ loop_body.tick
272
+ promise.future.should_not be_completed
273
+ clock.stub(:now).and_return(2)
274
+ loop_body.tick
275
+ promise.future.should be_completed
276
+ end
277
+
278
+ it 'clears out timers that have expired' do
279
+ selector.stub(:select).and_return([nil, nil, nil])
280
+ clock.stub(:now).and_return(1)
281
+ promise = Promise.new
282
+ loop_body.schedule_timer(1, promise)
283
+ clock.stub(:now).and_return(2)
284
+ loop_body.tick
285
+ promise.future.should be_completed
286
+ promise.should_not_receive(:fulfill)
287
+ loop_body.tick
288
+ end
289
+ end
290
+
291
+ describe '#close_sockets' do
292
+ it 'closes all sockets' do
293
+ socket1 = double(:socket1, closed?: false)
294
+ socket2 = double(:socket2, closed?: false)
295
+ socket1.should_receive(:close)
296
+ socket2.should_receive(:close)
297
+ loop_body.add_socket(socket1)
298
+ loop_body.add_socket(socket2)
299
+ loop_body.close_sockets
300
+ end
301
+
302
+ it 'closes all sockets, even when one of them raises an error' do
303
+ socket1 = double(:socket1, closed?: false)
304
+ socket2 = double(:socket2, closed?: false)
305
+ socket1.stub(:close).and_raise('Blurgh')
306
+ socket2.should_receive(:close)
307
+ loop_body.add_socket(socket1)
308
+ loop_body.add_socket(socket2)
309
+ loop_body.close_sockets
310
+ end
311
+
312
+ it 'does not close already closed sockets' do
313
+ socket.stub(:closed?).and_return(true)
314
+ socket.should_not_receive(:close)
315
+ loop_body.add_socket(socket)
316
+ loop_body.close_sockets
317
+ end
318
+ end
319
+
320
+ describe '#cancel_timers' do
321
+ before do
322
+ selector.stub(:select).and_return([nil, nil, nil])
323
+ end
324
+
325
+ it 'fails all active timers with a CancelledError' do
326
+ p1 = Promise.new
327
+ p2 = Promise.new
328
+ p3 = Promise.new
329
+ clock.stub(:now).and_return(1)
330
+ loop_body.schedule_timer(1, p1)
331
+ loop_body.schedule_timer(3, p2)
332
+ loop_body.schedule_timer(3, p3)
333
+ clock.stub(:now).and_return(2)
334
+ loop_body.tick
335
+ loop_body.cancel_timers
336
+ p1.future.should be_completed
337
+ p2.future.should be_failed
338
+ p3.future.should be_failed
339
+ expect { p3.future.value }.to raise_error(CancelledError)
340
+ end
341
+ end
342
+ end
343
+ end
344
+ end
345
+
346
+ module IoReactorSpec
347
+ class FakeSelector
348
+ def initialize
349
+ handler { [[], [], []] }
350
+ end
351
+
352
+ def handler(&body)
353
+ @body = body
354
+ end
355
+
356
+ def select(*args)
357
+ @body.call(*args)
358
+ end
359
+ end
360
+ end
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+
3
+ require 'ione'
4
+
5
+ require 'support/fake_server'
6
+ require 'support/await_helper'
7
+
8
+ ENV['SERVER_HOST'] ||= '127.0.0.1'.freeze
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ module AwaitHelper
4
+ def await(timeout=5, &test)
5
+ started_at = Time.now
6
+ until test.call
7
+ yield
8
+ time_taken = Time.now - started_at
9
+ if time_taken > timeout
10
+ fail('Test took more than %.1fs' % [time_taken.to_f])
11
+ else
12
+ sleep(0.01)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ RSpec.configure do |c|
19
+ c.include(AwaitHelper)
20
+ end
@@ -0,0 +1,106 @@
1
+ # encoding: utf-8
2
+
3
+ class FakeServer
4
+ attr_reader :port, :connects, :disconnects
5
+
6
+ def initialize(port=(2**15 + rand(2**15)))
7
+ @port = port
8
+ @state = {}
9
+ @lock = Mutex.new
10
+ @connects = 0
11
+ @disconnects = 0
12
+ @connections = []
13
+ @received_bytes = ''
14
+ end
15
+
16
+ def start!(options={})
17
+ @lock.synchronize do
18
+ return if @running
19
+ @running = true
20
+ end
21
+ @sockets = [TCPServer.new(@port)]
22
+ @started = Ione::Promise.new
23
+ @thread = Thread.start do
24
+ Thread.current.abort_on_exception = true
25
+ sleep(options[:accept_delay] || 0)
26
+ @started.fulfill
27
+ io_loop
28
+ end
29
+ @started.future.value
30
+ self
31
+ end
32
+
33
+ def stop!
34
+ @lock.synchronize do
35
+ return unless @running
36
+ @running = false
37
+ end
38
+ if defined? @started
39
+ @thread.join
40
+ @sockets.each(&:close)
41
+ end
42
+ end
43
+
44
+ def broadcast!(bytes)
45
+ @lock.synchronize do
46
+ @connections.each { |c| c.write_nonblock(bytes) }
47
+ end
48
+ end
49
+
50
+ def await_connects!(n=1)
51
+ started_at = Time.now
52
+ until @connects >= n
53
+ sleep(0.01)
54
+ raise 'Waited longer than 5s!' if (Time.now - started_at) > 5
55
+ end
56
+ end
57
+
58
+ def await_disconnects!(n=1)
59
+ started_at = Time.now
60
+ until @disconnects >= n
61
+ sleep(0.01)
62
+ raise 'Waited longer than 5s!' if (Time.now - started_at) > 5
63
+ end
64
+ end
65
+
66
+ def received_bytes
67
+ @lock.synchronize do
68
+ return @received_bytes.dup
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def io_loop
75
+ while @running
76
+ acceptables, _ = IO.select(@sockets, @connections, nil, 0)
77
+ readables, writables, _ = IO.select(@connections, @connections, nil, 0)
78
+
79
+ if acceptables
80
+ acceptables.each do |socket|
81
+ connection, _ = socket.accept_nonblock
82
+ @lock.synchronize do
83
+ @connects += 1
84
+ @connections << connection
85
+ end
86
+ end
87
+ end
88
+
89
+ if readables
90
+ readables.each do |readable|
91
+ begin
92
+ bytes = readable.read_nonblock(2**16)
93
+ @lock.synchronize do
94
+ @received_bytes << bytes
95
+ end
96
+ rescue EOFError
97
+ @lock.synchronize do
98
+ @connections.delete(readable)
99
+ @disconnects += 1
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end