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