ione 1.0.0.pre0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ module Ione
4
+ VERSION = '1.0.0.pre0'.freeze
5
+ end
@@ -0,0 +1,283 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+
6
+ describe 'An IO reactor' do
7
+ let :io_reactor do
8
+ Ione::Io::IoReactor.new
9
+ end
10
+
11
+ context 'with a generic server' do
12
+ let :protocol_handler_factory do
13
+ lambda { |c| IoSpec::TestConnection.new(c) }
14
+ end
15
+
16
+ let :fake_server do
17
+ FakeServer.new
18
+ end
19
+
20
+ before do
21
+ fake_server.start!
22
+ io_reactor.start
23
+ end
24
+
25
+ after do
26
+ io_reactor.stop
27
+ fake_server.stop!
28
+ end
29
+
30
+ it 'connects to the server' do
31
+ io_reactor.connect(ENV['SERVER_HOST'], fake_server.port, 1).map(&protocol_handler_factory)
32
+ fake_server.await_connects!(1)
33
+ end
34
+
35
+ it 'receives data' do
36
+ protocol_handler = io_reactor.connect(ENV['SERVER_HOST'], fake_server.port, 1).map(&protocol_handler_factory).value
37
+ fake_server.await_connects!(1)
38
+ fake_server.broadcast!('hello world')
39
+ await { protocol_handler.data.bytesize > 0 }
40
+ protocol_handler.data.should == 'hello world'
41
+ end
42
+
43
+ it 'receives data on multiple connections' do
44
+ protocol_handlers = Array.new(10) { io_reactor.connect(ENV['SERVER_HOST'], fake_server.port, 1).map(&protocol_handler_factory).value }
45
+ fake_server.await_connects!(10)
46
+ fake_server.broadcast!('hello world')
47
+ await { protocol_handlers.all? { |c| c.data.bytesize > 0 } }
48
+ protocol_handlers.sample.data.should == 'hello world'
49
+ end
50
+ end
51
+
52
+ context 'when talking to Redis' do
53
+ let :protocol_handler do
54
+ begin
55
+ io_reactor.connect(ENV['SERVER_HOST'], 6379, 1).map { |c| IoSpec::RedisProtocolHandler.new(c) }.value
56
+ rescue Ione::Io::ConnectionError
57
+ nil
58
+ end
59
+ end
60
+
61
+ before do
62
+ io_reactor.start.value
63
+ end
64
+
65
+ after do
66
+ io_reactor.stop.value
67
+ end
68
+
69
+ it 'can set a value' do
70
+ pending('Redis not running', unless: protocol_handler)
71
+ response = protocol_handler.send_request('SET', 'foo', 'bar').value
72
+ response.should == 'OK'
73
+ end
74
+
75
+ it 'can get a value' do
76
+ pending('Redis not running', unless: protocol_handler)
77
+ f = protocol_handler.send_request('SET', 'foo', 'bar').flat_map do
78
+ protocol_handler.send_request('GET', 'foo')
79
+ end
80
+ f.value.should == 'bar'
81
+ end
82
+
83
+ it 'can delete values' do
84
+ pending('Redis not running', unless: protocol_handler)
85
+ f = protocol_handler.send_request('SET', 'hello', 'world').flat_map do
86
+ protocol_handler.send_request('DEL', 'hello')
87
+ end
88
+ f.value.should == 1
89
+ end
90
+
91
+ it 'handles nil values' do
92
+ pending('Redis not running', unless: protocol_handler)
93
+ f = protocol_handler.send_request('DEL', 'hello').flat_map do
94
+ protocol_handler.send_request('GET', 'hello')
95
+ end
96
+ f.value.should be_nil
97
+ end
98
+
99
+ it 'handles errors' do
100
+ pending('Redis not running', unless: protocol_handler)
101
+ f = protocol_handler.send_request('SET', 'foo')
102
+ expect { f.value }.to raise_error("ERR wrong number of arguments for 'set' command")
103
+ end
104
+
105
+ it 'handles replies with multiple elements' do
106
+ pending('Redis not running', unless: protocol_handler)
107
+ f = protocol_handler.send_request('DEL', 'stuff')
108
+ f.value
109
+ f = protocol_handler.send_request('RPUSH', 'stuff', 'hello', 'world')
110
+ f.value.should == 2
111
+ f = protocol_handler.send_request('LRANGE', 'stuff', 0, 2)
112
+ f.value.should == ['hello', 'world']
113
+ end
114
+
115
+ it 'handles nil values when reading multiple elements' do
116
+ pending('Redis not running', unless: protocol_handler)
117
+ protocol_handler.send_request('DEL', 'things')
118
+ protocol_handler.send_request('HSET', 'things', 'hello', 'world')
119
+ f = protocol_handler.send_request('HMGET', 'things', 'hello', 'foo')
120
+ f.value.should == ['world', nil]
121
+ end
122
+ end
123
+ end
124
+
125
+ module IoSpec
126
+ class TestConnection
127
+ def initialize(connection)
128
+ @connection = connection
129
+ @connection.on_data(&method(:receive_data))
130
+ @lock = Mutex.new
131
+ @data = Ione::ByteBuffer.new
132
+ end
133
+
134
+ def data
135
+ @lock.synchronize { @data.to_s }
136
+ end
137
+
138
+ private
139
+
140
+ def receive_data(new_data)
141
+ @lock.synchronize { @data << new_data }
142
+ end
143
+ end
144
+
145
+ class LineProtocolHandler
146
+ def initialize(connection)
147
+ @connection = connection
148
+ @connection.on_data(&method(:process_data))
149
+ @lock = Mutex.new
150
+ @buffer = ''
151
+ @requests = []
152
+ end
153
+
154
+ def on_line(&listener)
155
+ @line_listener = listener
156
+ end
157
+
158
+ def write(command_string)
159
+ @connection.write(command_string)
160
+ end
161
+
162
+ def process_data(new_data)
163
+ lines = []
164
+ @lock.synchronize do
165
+ @buffer << new_data
166
+ while newline_index = @buffer.index("\r\n")
167
+ line = @buffer.slice!(0, newline_index + 2)
168
+ line.chomp!
169
+ lines << line
170
+ end
171
+ end
172
+ lines.each do |line|
173
+ @line_listener.call(line) if @line_listener
174
+ end
175
+ end
176
+ end
177
+
178
+ class RedisProtocolHandler
179
+ def initialize(connection)
180
+ @line_protocol = LineProtocolHandler.new(connection)
181
+ @line_protocol.on_line(&method(:handle_line))
182
+ @lock = Mutex.new
183
+ @responses = []
184
+ @state = BaseState.new(method(:handle_response))
185
+ end
186
+
187
+ def send_request(*args)
188
+ promise = Ione::Promise.new
189
+ @lock.synchronize do
190
+ @responses << promise
191
+ end
192
+ request = "*#{args.size}\r\n"
193
+ args.each do |arg|
194
+ arg_str = arg.to_s
195
+ request << "$#{arg_str.bytesize}\r\n#{arg_str}\r\n"
196
+ end
197
+ @line_protocol.write(request)
198
+ promise.future
199
+ end
200
+
201
+ def handle_response(result, error=false)
202
+ promise = @lock.synchronize do
203
+ @responses.shift
204
+ end
205
+ if error
206
+ promise.fail(StandardError.new(result))
207
+ else
208
+ promise.fulfill(result)
209
+ end
210
+ end
211
+
212
+ def handle_line(line)
213
+ @state = @state.handle_line(line)
214
+ end
215
+
216
+ class State
217
+ def initialize(result_handler)
218
+ @result_handler = result_handler
219
+ end
220
+
221
+ def complete!(result)
222
+ @result_handler.call(result)
223
+ end
224
+
225
+ def fail!(message)
226
+ @result_handler.call(message, true)
227
+ end
228
+ end
229
+
230
+ class BulkState < State
231
+ def handle_line(line)
232
+ complete!(line)
233
+ BaseState.new(@result_handler)
234
+ end
235
+ end
236
+
237
+ class MultiBulkState < State
238
+ def initialize(result_handler, expected_elements)
239
+ super(result_handler)
240
+ @expected_elements = expected_elements
241
+ @elements = []
242
+ end
243
+
244
+ def handle_line(line)
245
+ if line.start_with?('$')
246
+ line.slice!(0, 1)
247
+ if line.to_i == -1
248
+ @elements << nil
249
+ end
250
+ else
251
+ @elements << line
252
+ end
253
+ if @elements.size == @expected_elements
254
+ complete!(@elements)
255
+ BaseState.new(@result_handler)
256
+ else
257
+ self
258
+ end
259
+ end
260
+ end
261
+
262
+ class BaseState < State
263
+ def handle_line(line)
264
+ next_state = self
265
+ first_char = line.slice!(0, 1)
266
+ case first_char
267
+ when '+' then complete!(line)
268
+ when ':' then complete!(line.to_i)
269
+ when '-' then fail!(line)
270
+ when '$'
271
+ if line.to_i == -1
272
+ complete!(nil)
273
+ else
274
+ next_state = BulkState.new(@result_handler)
275
+ end
276
+ when '*'
277
+ next_state = MultiBulkState.new(@result_handler, line.to_i)
278
+ end
279
+ next_state
280
+ end
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,342 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+
6
+ module Ione
7
+ describe ByteBuffer do
8
+ let :buffer do
9
+ described_class.new
10
+ end
11
+
12
+ describe '#initialize' do
13
+ it 'can be inititialized empty' do
14
+ described_class.new.should be_empty
15
+ end
16
+
17
+ it 'can be initialized with bytes' do
18
+ described_class.new('hello').length.should == 5
19
+ end
20
+ end
21
+
22
+ describe '#length/#size/#bytesize' do
23
+ it 'returns the number of bytes in the buffer' do
24
+ buffer << 'foo'
25
+ buffer.length.should == 3
26
+ end
27
+
28
+ it 'is zero initially' do
29
+ buffer.length.should == 0
30
+ end
31
+
32
+ it 'is aliased as #size' do
33
+ buffer << 'foo'
34
+ buffer.size.should == 3
35
+ end
36
+
37
+ it 'is aliased as #bytesize' do
38
+ buffer << 'foo'
39
+ buffer.bytesize.should == 3
40
+ end
41
+ end
42
+
43
+ describe '#empty?' do
44
+ it 'is true initially' do
45
+ buffer.should be_empty
46
+ end
47
+
48
+ it 'is false when there are bytes in the buffer' do
49
+ buffer << 'foo'
50
+ buffer.should_not be_empty
51
+ end
52
+ end
53
+
54
+ describe '#append/#<<' do
55
+ it 'adds bytes to the buffer' do
56
+ buffer.append('foo')
57
+ buffer.should_not be_empty
58
+ end
59
+
60
+ it 'can be used as <<' do
61
+ buffer << 'foo'
62
+ buffer.should_not be_empty
63
+ end
64
+
65
+ it 'returns itself' do
66
+ buffer.append('foo').should eql(buffer)
67
+ end
68
+
69
+ it 'stores its bytes as binary' do
70
+ buffer.append('hällö').length.should == 7
71
+ buffer.to_s.encoding.should == ::Encoding::BINARY
72
+ end
73
+
74
+ it 'handles appending with multibyte strings' do
75
+ buffer.append('hello')
76
+ buffer.append('würld')
77
+ buffer.to_s.should == 'hellowürld'.force_encoding(::Encoding::BINARY)
78
+ end
79
+
80
+ it 'handles appending with another byte buffer' do
81
+ buffer.append('hello ').append(ByteBuffer.new('world'))
82
+ buffer.to_s.should == 'hello world'
83
+ end
84
+ end
85
+
86
+ describe '#eql?' do
87
+ it 'is equal to another buffer with the same contents' do
88
+ b1 = described_class.new
89
+ b2 = described_class.new
90
+ b1.append('foo')
91
+ b2.append('foo')
92
+ b1.should eql(b2)
93
+ end
94
+
95
+ it 'is not equal to another buffer with other contents' do
96
+ b1 = described_class.new
97
+ b2 = described_class.new
98
+ b1.append('foo')
99
+ b2.append('bar')
100
+ b1.should_not eql(b2)
101
+ end
102
+
103
+ it 'is aliased as #==' do
104
+ b1 = described_class.new
105
+ b2 = described_class.new
106
+ b1.append('foo')
107
+ b2.append('foo')
108
+ b1.should == b2
109
+ end
110
+
111
+ it 'is equal to another buffer when both are empty' do
112
+ b1 = described_class.new
113
+ b2 = described_class.new
114
+ b1.should eql(b2)
115
+ end
116
+ end
117
+
118
+ describe '#hash' do
119
+ it 'has the same hash code as another buffer with the same contents' do
120
+ b1 = described_class.new
121
+ b2 = described_class.new
122
+ b1.append('foo')
123
+ b2.append('foo')
124
+ b1.hash.should == b2.hash
125
+ end
126
+
127
+ it 'is not equal to the hash code of another buffer with other contents' do
128
+ b1 = described_class.new
129
+ b2 = described_class.new
130
+ b1.append('foo')
131
+ b2.append('bar')
132
+ b1.hash.should_not == b2.hash
133
+ end
134
+
135
+ it 'is equal to the hash code of another buffer when both are empty' do
136
+ b1 = described_class.new
137
+ b2 = described_class.new
138
+ b1.hash.should == b2.hash
139
+ end
140
+ end
141
+
142
+ describe '#to_s' do
143
+ it 'returns the bytes' do
144
+ buffer.append('hello world').to_s.should == 'hello world'
145
+ end
146
+ end
147
+
148
+ describe '#to_str' do
149
+ it 'returns the bytes' do
150
+ buffer.append('hello world').to_str.should == 'hello world'
151
+ end
152
+ end
153
+
154
+ describe '#inspect' do
155
+ it 'returns the bytes wrapped in ByteBuffer(...)' do
156
+ buffer.append("\xca\xfe")
157
+ buffer.inspect.should == '#<Ione::ByteBuffer: "\xCA\xFE">'
158
+ end
159
+ end
160
+
161
+ describe '#discard' do
162
+ it 'discards the specified number of bytes from the front of the buffer' do
163
+ buffer.append('hello world')
164
+ buffer.discard(4)
165
+ buffer.should == ByteBuffer.new('o world')
166
+ end
167
+
168
+ it 'returns the byte buffer' do
169
+ buffer.append('hello world')
170
+ buffer.discard(4).should == ByteBuffer.new('o world')
171
+ end
172
+
173
+ it 'raises an error if the number of bytes in the buffer is fewer than the number to discard' do
174
+ expect { buffer.discard(1) }.to raise_error(RangeError)
175
+ buffer.append('hello')
176
+ expect { buffer.discard(7) }.to raise_error(RangeError)
177
+ end
178
+ end
179
+
180
+ describe '#read' do
181
+ it 'returns the specified number of bytes, as a string' do
182
+ buffer.append('hello')
183
+ buffer.read(4).should == 'hell'
184
+ end
185
+
186
+ it 'removes the bytes from the buffer' do
187
+ buffer.append('hello')
188
+ buffer.read(3)
189
+ buffer.should == ByteBuffer.new('lo')
190
+ buffer.read(2).should == 'lo'
191
+ end
192
+
193
+ it 'raises an error if there are not enough bytes' do
194
+ buffer.append('hello')
195
+ expect { buffer.read(23423543) }.to raise_error(RangeError)
196
+ expect { buffer.discard(5).read(1) }.to raise_error(RangeError)
197
+ end
198
+
199
+ it 'returns a string with binary encoding' do
200
+ buffer.append('hello')
201
+ buffer.read(4).encoding.should == ::Encoding::BINARY
202
+ buffer.append('∆')
203
+ buffer.read(2).encoding.should == ::Encoding::BINARY
204
+ end
205
+ end
206
+
207
+ describe '#read_int' do
208
+ it 'returns the first four bytes interpreted as an int' do
209
+ buffer.append("\xca\xfe\xba\xbe\x01")
210
+ buffer.read_int.should == 0xcafebabe
211
+ end
212
+
213
+ it 'removes the bytes from the buffer' do
214
+ buffer.append("\xca\xfe\xba\xbe\x01")
215
+ buffer.read_int
216
+ buffer.should == ByteBuffer.new("\x01")
217
+ end
218
+
219
+ it 'raises an error if there are not enough bytes' do
220
+ buffer.append("\xca\xfe\xba")
221
+ expect { buffer.read_int }.to raise_error(RangeError)
222
+ end
223
+ end
224
+
225
+ describe '#read_short' do
226
+ it 'returns the first two bytes interpreted as a short' do
227
+ buffer.append("\xca\xfe\x01")
228
+ buffer.read_short.should == 0xcafe
229
+ end
230
+
231
+ it 'removes the bytes from the buffer' do
232
+ buffer.append("\xca\xfe\x01")
233
+ buffer.read_short
234
+ buffer.should == ByteBuffer.new("\x01")
235
+ end
236
+
237
+ it 'raises an error if there are not enough bytes' do
238
+ buffer.append("\xca")
239
+ expect { buffer.read_short }.to raise_error(RangeError)
240
+ end
241
+ end
242
+
243
+ describe '#read_byte' do
244
+ it 'returns the first bytes interpreted as an int' do
245
+ buffer.append("\x10\x01")
246
+ buffer.read_byte.should == 0x10
247
+ buffer.read_byte.should == 0x01
248
+ end
249
+
250
+ it 'removes the byte from the buffer' do
251
+ buffer.append("\x10\x01")
252
+ buffer.read_byte
253
+ buffer.should == ByteBuffer.new("\x01")
254
+ end
255
+
256
+ it 'raises an error if there are no bytes' do
257
+ expect { buffer.read_byte }.to raise_error(RangeError)
258
+ end
259
+
260
+ it 'can interpret the byte as signed' do
261
+ buffer.append("\x81\x02")
262
+ buffer.read_byte(true).should == -127
263
+ buffer.read_byte(true).should == 2
264
+ end
265
+ end
266
+
267
+ describe '#update' do
268
+ it 'changes the bytes at the specified location' do
269
+ buffer.append('foo bar')
270
+ buffer.update(4, 'baz')
271
+ buffer.to_s.should == 'foo baz'
272
+ end
273
+
274
+ it 'handles updates after a read' do
275
+ buffer.append('foo bar')
276
+ buffer.read(1)
277
+ buffer.update(3, 'baz')
278
+ buffer.to_s.should == 'oo baz'
279
+ end
280
+
281
+ it 'handles updates after multiple reads and appends' do
282
+ buffer.append('foo bar')
283
+ buffer.read(1)
284
+ buffer.append('x')
285
+ buffer.update(4, 'baz')
286
+ buffer.append('yyyy')
287
+ buffer.read(1)
288
+ buffer.to_s.should == 'o bbazyyyy'
289
+ end
290
+
291
+ it 'returns itself' do
292
+ buffer.append('foo')
293
+ buffer.update(0, 'bar').should equal(buffer)
294
+ end
295
+ end
296
+
297
+ describe '#dup' do
298
+ it 'returns a copy' do
299
+ buffer.append('hello world')
300
+ copy = buffer.dup
301
+ copy.should eql(buffer)
302
+ end
303
+
304
+ it 'returns a copy which can be modified without modifying the original' do
305
+ buffer.append('hello world')
306
+ copy = buffer.dup
307
+ copy.append('goodbye')
308
+ copy.should_not eql(buffer)
309
+ end
310
+ end
311
+
312
+ describe '#cheap_peek' do
313
+ it 'returns a prefix of the buffer' do
314
+ buffer.append('foo')
315
+ buffer.append('bar')
316
+ buffer.read_byte
317
+ buffer.append('hello')
318
+ x = buffer.cheap_peek
319
+ x.bytesize.should be > 0
320
+ x.bytesize.should be <= buffer.bytesize
321
+ buffer.to_str.should start_with(x)
322
+ end
323
+ end
324
+
325
+ context 'when reading and appending' do
326
+ it 'handles heavy churn' do
327
+ 1000.times do
328
+ buffer.append('x' * 6)
329
+ buffer.read_byte
330
+ buffer.append('y')
331
+ buffer.read_int
332
+ buffer.read_short
333
+ buffer.append('z' * 4)
334
+ buffer.read_byte
335
+ buffer.append('z')
336
+ buffer.read_int
337
+ buffer.should be_empty
338
+ end
339
+ end
340
+ end
341
+ end
342
+ end