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,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