ione 1.0.0.pre0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yardopts +4 -0
- data/lib/ione.rb +8 -0
- data/lib/ione/byte_buffer.rb +279 -0
- data/lib/ione/future.rb +509 -0
- data/lib/ione/io.rb +15 -0
- data/lib/ione/io/connection.rb +215 -0
- data/lib/ione/io/io_reactor.rb +321 -0
- data/lib/ione/version.rb +5 -0
- data/spec/integration/io_spec.rb +283 -0
- data/spec/ione/byte_buffer_spec.rb +342 -0
- data/spec/ione/future_spec.rb +737 -0
- data/spec/ione/io/connection_spec.rb +484 -0
- data/spec/ione/io/io_reactor_spec.rb +360 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/await_helper.rb +20 -0
- data/spec/support/fake_server.rb +106 -0
- metadata +70 -0
data/lib/ione/version.rb
ADDED
@@ -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
|