carrot 0.7.0
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.
- data/.gitignore +3 -0
- data/LICENSE +20 -0
- data/README.markdown +34 -0
- data/Rakefile +41 -0
- data/VERSION.yml +4 -0
- data/carrot.gemspec +62 -0
- data/lib/amqp/buffer.rb +401 -0
- data/lib/amqp/exchange.rb +51 -0
- data/lib/amqp/frame.rb +121 -0
- data/lib/amqp/header.rb +27 -0
- data/lib/amqp/protocol.rb +209 -0
- data/lib/amqp/queue.rb +97 -0
- data/lib/amqp/server.rb +185 -0
- data/lib/amqp/spec.rb +820 -0
- data/lib/carrot.rb +87 -0
- data/lib/examples/simple_pop.rb +13 -0
- data/protocol/amqp-0.8.json +617 -0
- data/protocol/amqp-0.8.xml +3908 -0
- data/protocol/codegen.rb +173 -0
- data/protocol/doc.txt +281 -0
- data/test/carrot_test.rb +15 -0
- data/test/test_helper.rb +18 -0
- metadata +78 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Amos Elliston
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# Carrot
|
2
|
+
|
3
|
+
A synchronous amqp client. Based on Aman's amqp client:
|
4
|
+
|
5
|
+
[http://github.com/tmm1/amqp/tree/master] (http://github.com/tmm1/amqp/tree/master)
|
6
|
+
|
7
|
+
## Motivation
|
8
|
+
|
9
|
+
This client does not use eventmachine so no background thread necessary. As a result, it is much easier to use from script/console and Passenger. It also solves the problem of buffering messages and ack responses. For more details see [this thread] (http://groups.google.com/group/ruby-amqp/browse_thread/thread/fdae324a0ebb1961/fa185fdce1841b68).
|
10
|
+
|
11
|
+
There is currently no way to prevent buffering using eventmachine. Support for prefetch is still unreliable.
|
12
|
+
|
13
|
+
|
14
|
+
## Example
|
15
|
+
|
16
|
+
require 'carrot'
|
17
|
+
|
18
|
+
q = Carrot.queue('name')
|
19
|
+
10.times do |num|
|
20
|
+
q.publish(num.to_s)
|
21
|
+
end
|
22
|
+
|
23
|
+
puts "Queued #{q.message_count} messages"
|
24
|
+
puts
|
25
|
+
|
26
|
+
while msg = q.pop(:ack => true)
|
27
|
+
puts "Popping: #{msg}"
|
28
|
+
q.ack
|
29
|
+
end
|
30
|
+
Carrot.stop
|
31
|
+
|
32
|
+
# LICENSE
|
33
|
+
|
34
|
+
Copyright (c) 2009 Amos Elliston, Geni.com; Published under The MIT License, see License
|
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'rcov/rcovtask'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'jeweler'
|
8
|
+
Jeweler::Tasks.new do |s|
|
9
|
+
s.name = "carrot"
|
10
|
+
s.email = "amos@geni.com"
|
11
|
+
s.homepage = "http://github.com/famoseagle/carrot"
|
12
|
+
s.description = "A synchronous version of the ruby amqp client"
|
13
|
+
s.summary = "A synchronous version of the ruby amqp client"
|
14
|
+
s.authors = ["Amos Elliston"]
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
19
|
+
end
|
20
|
+
|
21
|
+
Rake::TestTask.new do |t|
|
22
|
+
t.libs << 'lib'
|
23
|
+
t.pattern = 'test/**/*_test.rb'
|
24
|
+
t.verbose = false
|
25
|
+
end
|
26
|
+
|
27
|
+
Rake::RDocTask.new do |rdoc|
|
28
|
+
rdoc.rdoc_dir = 'rdoc'
|
29
|
+
rdoc.title = 'carrot'
|
30
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
31
|
+
rdoc.rdoc_files.include('README*')
|
32
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
33
|
+
end
|
34
|
+
|
35
|
+
Rcov::RcovTask.new do |t|
|
36
|
+
t.libs << 'test'
|
37
|
+
t.test_files = FileList['test/**/*_test.rb']
|
38
|
+
t.verbose = true
|
39
|
+
end
|
40
|
+
|
41
|
+
task :default => :test
|
data/VERSION.yml
ADDED
data/carrot.gemspec
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{carrot}
|
8
|
+
s.version = "0.7.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Amos Elliston"]
|
12
|
+
s.date = %q{2009-10-19}
|
13
|
+
s.description = %q{A synchronous version of the ruby amqp client}
|
14
|
+
s.email = %q{amos@geni.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.markdown"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".gitignore",
|
21
|
+
"LICENSE",
|
22
|
+
"README.markdown",
|
23
|
+
"Rakefile",
|
24
|
+
"VERSION.yml",
|
25
|
+
"carrot.gemspec",
|
26
|
+
"lib/amqp/buffer.rb",
|
27
|
+
"lib/amqp/exchange.rb",
|
28
|
+
"lib/amqp/frame.rb",
|
29
|
+
"lib/amqp/header.rb",
|
30
|
+
"lib/amqp/protocol.rb",
|
31
|
+
"lib/amqp/queue.rb",
|
32
|
+
"lib/amqp/server.rb",
|
33
|
+
"lib/amqp/spec.rb",
|
34
|
+
"lib/carrot.rb",
|
35
|
+
"lib/examples/simple_pop.rb",
|
36
|
+
"protocol/amqp-0.8.json",
|
37
|
+
"protocol/amqp-0.8.xml",
|
38
|
+
"protocol/codegen.rb",
|
39
|
+
"protocol/doc.txt",
|
40
|
+
"test/carrot_test.rb",
|
41
|
+
"test/test_helper.rb"
|
42
|
+
]
|
43
|
+
s.homepage = %q{http://github.com/famoseagle/carrot}
|
44
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
45
|
+
s.require_paths = ["lib"]
|
46
|
+
s.rubygems_version = %q{1.3.5}
|
47
|
+
s.summary = %q{A synchronous version of the ruby amqp client}
|
48
|
+
s.test_files = [
|
49
|
+
"test/carrot_test.rb",
|
50
|
+
"test/test_helper.rb"
|
51
|
+
]
|
52
|
+
|
53
|
+
if s.respond_to? :specification_version then
|
54
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
55
|
+
s.specification_version = 3
|
56
|
+
|
57
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
58
|
+
else
|
59
|
+
end
|
60
|
+
else
|
61
|
+
end
|
62
|
+
end
|
data/lib/amqp/buffer.rb
ADDED
@@ -0,0 +1,401 @@
|
|
1
|
+
if [].map.respond_to? :with_index
|
2
|
+
class Array #:nodoc:
|
3
|
+
def enum_with_index
|
4
|
+
each.with_index
|
5
|
+
end
|
6
|
+
end
|
7
|
+
else
|
8
|
+
require 'enumerator'
|
9
|
+
end
|
10
|
+
|
11
|
+
module Carrot::AMQP
|
12
|
+
class Buffer #:nodoc: all
|
13
|
+
class Overflow < StandardError; end
|
14
|
+
class InvalidType < StandardError; end
|
15
|
+
|
16
|
+
def initialize data = ''
|
17
|
+
@data = data
|
18
|
+
@pos = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :pos
|
22
|
+
|
23
|
+
def data
|
24
|
+
@data.clone
|
25
|
+
end
|
26
|
+
alias :contents :data
|
27
|
+
alias :to_s :data
|
28
|
+
|
29
|
+
def << data
|
30
|
+
@data << data.to_s
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def length
|
35
|
+
@data.length
|
36
|
+
end
|
37
|
+
|
38
|
+
def empty?
|
39
|
+
pos == length
|
40
|
+
end
|
41
|
+
|
42
|
+
def rewind
|
43
|
+
@pos = 0
|
44
|
+
end
|
45
|
+
|
46
|
+
def read_properties *types
|
47
|
+
types.shift if types.first == :properties
|
48
|
+
|
49
|
+
i = 0
|
50
|
+
values = []
|
51
|
+
|
52
|
+
while props = read(:short)
|
53
|
+
(0..14).each do |n|
|
54
|
+
# no more property types
|
55
|
+
break unless types[i]
|
56
|
+
|
57
|
+
# if flag is set
|
58
|
+
if props & (1<<(15-n)) != 0
|
59
|
+
if types[i] == :bit
|
60
|
+
# bit values exist in flags only
|
61
|
+
values << true
|
62
|
+
else
|
63
|
+
# save type name for later reading
|
64
|
+
values << types[i]
|
65
|
+
end
|
66
|
+
else
|
67
|
+
# property not set or is false bit
|
68
|
+
values << (types[i] == :bit ? false : nil)
|
69
|
+
end
|
70
|
+
|
71
|
+
i+=1
|
72
|
+
end
|
73
|
+
|
74
|
+
# bit(0) == 0 means no more property flags
|
75
|
+
break unless props & 1 == 1
|
76
|
+
end
|
77
|
+
|
78
|
+
values.map do |value|
|
79
|
+
value.is_a?(Symbol) ? read(value) : value
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def read *types
|
84
|
+
if types.first == :properties
|
85
|
+
return read_properties(*types)
|
86
|
+
end
|
87
|
+
|
88
|
+
values = types.map do |type|
|
89
|
+
case type
|
90
|
+
when :octet
|
91
|
+
_read(1, 'C')
|
92
|
+
when :short
|
93
|
+
_read(2, 'n')
|
94
|
+
when :long
|
95
|
+
_read(4, 'N')
|
96
|
+
when :longlong
|
97
|
+
upper, lower = _read(8, 'NN')
|
98
|
+
upper << 32 | lower
|
99
|
+
when :shortstr
|
100
|
+
_read read(:octet)
|
101
|
+
when :longstr
|
102
|
+
_read read(:long)
|
103
|
+
when :timestamp
|
104
|
+
Time.at read(:longlong)
|
105
|
+
when :table
|
106
|
+
t = Hash.new
|
107
|
+
|
108
|
+
table = Buffer.new(read(:longstr))
|
109
|
+
until table.empty?
|
110
|
+
key, type = table.read(:shortstr, :octet)
|
111
|
+
key = key.intern
|
112
|
+
t[key] ||= case type
|
113
|
+
when 83 # 'S'
|
114
|
+
table.read(:longstr)
|
115
|
+
when 73 # 'I'
|
116
|
+
table.read(:long)
|
117
|
+
when 68 # 'D'
|
118
|
+
exp = table.read(:octet)
|
119
|
+
num = table.read(:long)
|
120
|
+
num / 10.0**exp
|
121
|
+
when 84 # 'T'
|
122
|
+
table.read(:timestamp)
|
123
|
+
when 70 # 'F'
|
124
|
+
table.read(:table)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
t
|
129
|
+
when :bit
|
130
|
+
if (@bits ||= []).empty?
|
131
|
+
val = read(:octet)
|
132
|
+
@bits = (0..7).map{|i| (val & 1<<i) != 0 }
|
133
|
+
end
|
134
|
+
|
135
|
+
@bits.shift
|
136
|
+
else
|
137
|
+
raise InvalidType, "Cannot read data of type #{type}"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
types.size == 1 ? values.first : values
|
142
|
+
end
|
143
|
+
|
144
|
+
def write type, data
|
145
|
+
case type
|
146
|
+
when :octet
|
147
|
+
_write(data, 'C')
|
148
|
+
when :short
|
149
|
+
_write(data, 'n')
|
150
|
+
when :long
|
151
|
+
_write(data, 'N')
|
152
|
+
when :longlong
|
153
|
+
lower = data & 0xffffffff
|
154
|
+
upper = (data & ~0xffffffff) >> 32
|
155
|
+
_write([upper, lower], 'NN')
|
156
|
+
when :shortstr
|
157
|
+
data = (data || '').to_s
|
158
|
+
_write([data.length, data], 'Ca*')
|
159
|
+
when :longstr
|
160
|
+
if data.is_a? Hash
|
161
|
+
write(:table, data)
|
162
|
+
else
|
163
|
+
data = (data || '').to_s
|
164
|
+
_write([data.length, data], 'Na*')
|
165
|
+
end
|
166
|
+
when :timestamp
|
167
|
+
write(:longlong, data.to_i)
|
168
|
+
when :table
|
169
|
+
data ||= {}
|
170
|
+
write :longstr, (data.inject(Buffer.new) do |table, (key, value)|
|
171
|
+
table.write(:shortstr, key.to_s)
|
172
|
+
|
173
|
+
case value
|
174
|
+
when String
|
175
|
+
table.write(:octet, 83) # 'S'
|
176
|
+
table.write(:longstr, value.to_s)
|
177
|
+
when Fixnum
|
178
|
+
table.write(:octet, 73) # 'I'
|
179
|
+
table.write(:long, value)
|
180
|
+
when Float
|
181
|
+
table.write(:octet, 68) # 'D'
|
182
|
+
# XXX there's gotta be a better way to do this..
|
183
|
+
exp = value.to_s.split('.').last.length
|
184
|
+
num = value * 10**exp
|
185
|
+
table.write(:octet, exp)
|
186
|
+
table.write(:long, num)
|
187
|
+
when Time
|
188
|
+
table.write(:octet, 84) # 'T'
|
189
|
+
table.write(:timestamp, value)
|
190
|
+
when Hash
|
191
|
+
table.write(:octet, 70) # 'F'
|
192
|
+
table.write(:table, value)
|
193
|
+
end
|
194
|
+
|
195
|
+
table
|
196
|
+
end)
|
197
|
+
when :bit
|
198
|
+
[*data].to_enum(:each_slice, 8).each{|bits|
|
199
|
+
write(:octet, bits.enum_with_index.inject(0){ |byte, (bit, i)|
|
200
|
+
byte |= 1<<i if bit
|
201
|
+
byte
|
202
|
+
})
|
203
|
+
}
|
204
|
+
when :properties
|
205
|
+
values = []
|
206
|
+
data.enum_with_index.inject(0) do |short, ((type, value), i)|
|
207
|
+
n = i % 15
|
208
|
+
last = i+1 == data.size
|
209
|
+
|
210
|
+
if (n == 0 and i != 0) or last
|
211
|
+
if data.size > i+1
|
212
|
+
short |= 1<<0
|
213
|
+
elsif last and value
|
214
|
+
values << [type,value]
|
215
|
+
short |= 1<<(15-n)
|
216
|
+
end
|
217
|
+
|
218
|
+
write(:short, short)
|
219
|
+
short = 0
|
220
|
+
end
|
221
|
+
|
222
|
+
if value and !last
|
223
|
+
values << [type,value]
|
224
|
+
short |= 1<<(15-n)
|
225
|
+
end
|
226
|
+
|
227
|
+
short
|
228
|
+
end
|
229
|
+
|
230
|
+
values.each do |type, value|
|
231
|
+
write(type, value) unless type == :bit
|
232
|
+
end
|
233
|
+
else
|
234
|
+
raise InvalidType, "Cannot write data of type #{type}"
|
235
|
+
end
|
236
|
+
|
237
|
+
self
|
238
|
+
end
|
239
|
+
|
240
|
+
def extract
|
241
|
+
begin
|
242
|
+
cur_data, cur_pos = @data.clone, @pos
|
243
|
+
yield self
|
244
|
+
rescue Overflow
|
245
|
+
@data, @pos = cur_data, cur_pos
|
246
|
+
nil
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def _read(size, pack = nil)
|
251
|
+
if @data.is_a?(Server)
|
252
|
+
raw = @data.read(size)
|
253
|
+
return raw if raw.nil? or pack.nil?
|
254
|
+
return raw.unpack(pack).first
|
255
|
+
end
|
256
|
+
|
257
|
+
if @pos + size > length
|
258
|
+
raise Overflow
|
259
|
+
else
|
260
|
+
data = @data[@pos,size]
|
261
|
+
@data[@pos,size] = ''
|
262
|
+
if pack
|
263
|
+
data = data.unpack(pack)
|
264
|
+
data = data.pop if data.size == 1
|
265
|
+
end
|
266
|
+
data
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def _write data, pack = nil
|
271
|
+
data = [*data].pack(pack) if pack
|
272
|
+
@data[@pos,0] = data
|
273
|
+
@pos += data.length
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
if $0 =~ /bacon/ or $0 == __FILE__
|
279
|
+
require 'bacon'
|
280
|
+
include AMQP
|
281
|
+
|
282
|
+
describe Buffer do
|
283
|
+
before do
|
284
|
+
@buf = Buffer.new
|
285
|
+
end
|
286
|
+
|
287
|
+
should 'have contents' do
|
288
|
+
@buf.contents.should == ''
|
289
|
+
end
|
290
|
+
|
291
|
+
should 'initialize with data' do
|
292
|
+
@buf = Buffer.new('abc')
|
293
|
+
@buf.contents.should == 'abc'
|
294
|
+
end
|
295
|
+
|
296
|
+
should 'append raw data' do
|
297
|
+
@buf << 'abc'
|
298
|
+
@buf << 'def'
|
299
|
+
@buf.contents.should == 'abcdef'
|
300
|
+
end
|
301
|
+
|
302
|
+
should 'append other buffers' do
|
303
|
+
@buf << Buffer.new('abc')
|
304
|
+
@buf.data.should == 'abc'
|
305
|
+
end
|
306
|
+
|
307
|
+
should 'have a position' do
|
308
|
+
@buf.pos.should == 0
|
309
|
+
end
|
310
|
+
|
311
|
+
should 'have a length' do
|
312
|
+
@buf.length.should == 0
|
313
|
+
@buf << 'abc'
|
314
|
+
@buf.length.should == 3
|
315
|
+
end
|
316
|
+
|
317
|
+
should 'know the end' do
|
318
|
+
@buf.empty?.should == true
|
319
|
+
end
|
320
|
+
|
321
|
+
should 'read and write data' do
|
322
|
+
@buf._write('abc')
|
323
|
+
@buf.rewind
|
324
|
+
@buf._read(2).should == 'ab'
|
325
|
+
@buf._read(1).should == 'c'
|
326
|
+
end
|
327
|
+
|
328
|
+
should 'raise on overflow' do
|
329
|
+
lambda{ @buf._read(1) }.should.raise Buffer::Overflow
|
330
|
+
end
|
331
|
+
|
332
|
+
should 'raise on invalid types' do
|
333
|
+
lambda{ @buf.read(:junk) }.should.raise Buffer::InvalidType
|
334
|
+
lambda{ @buf.write(:junk, 1) }.should.raise Buffer::InvalidType
|
335
|
+
end
|
336
|
+
|
337
|
+
{ :octet => 0b10101010,
|
338
|
+
:short => 100,
|
339
|
+
:long => 100_000_000,
|
340
|
+
:longlong => 666_555_444_333_222_111,
|
341
|
+
:shortstr => 'hello',
|
342
|
+
:longstr => 'bye'*500,
|
343
|
+
:timestamp => time = Time.at(Time.now.to_i),
|
344
|
+
:table => { :this => 'is', :a => 'hash', :with => {:nested => 123, :and => time, :also => 123.456} },
|
345
|
+
:bit => true
|
346
|
+
}.each do |type, value|
|
347
|
+
|
348
|
+
should "read and write a #{type}" do
|
349
|
+
@buf.write(type, value)
|
350
|
+
@buf.rewind
|
351
|
+
@buf.read(type).should == value
|
352
|
+
@buf.should.be.empty
|
353
|
+
end
|
354
|
+
|
355
|
+
end
|
356
|
+
|
357
|
+
should 'read and write multiple bits' do
|
358
|
+
bits = [true, false, false, true, true, false, false, true, true, false]
|
359
|
+
@buf.write(:bit, bits)
|
360
|
+
@buf.write(:octet, 100)
|
361
|
+
|
362
|
+
@buf.rewind
|
363
|
+
|
364
|
+
bits.map do
|
365
|
+
@buf.read(:bit)
|
366
|
+
end.should == bits
|
367
|
+
@buf.read(:octet).should == 100
|
368
|
+
end
|
369
|
+
|
370
|
+
should 'read and write properties' do
|
371
|
+
properties = ([
|
372
|
+
[:octet, 1],
|
373
|
+
[:shortstr, 'abc'],
|
374
|
+
[:bit, true],
|
375
|
+
[:bit, false],
|
376
|
+
[:shortstr, nil],
|
377
|
+
[:timestamp, nil],
|
378
|
+
[:table, { :a => 'hash' }],
|
379
|
+
]*5).sort_by{rand}
|
380
|
+
|
381
|
+
@buf.write(:properties, properties)
|
382
|
+
@buf.rewind
|
383
|
+
@buf.read(:properties, *properties.map{|type,_| type }).should == properties.map{|_,value| value }
|
384
|
+
@buf.should.be.empty
|
385
|
+
end
|
386
|
+
|
387
|
+
should 'do transactional reads with #extract' do
|
388
|
+
@buf.write :octet, 8
|
389
|
+
orig = @buf.to_s
|
390
|
+
|
391
|
+
@buf.rewind
|
392
|
+
@buf.extract do |b|
|
393
|
+
b.read :octet
|
394
|
+
b.read :short
|
395
|
+
end
|
396
|
+
|
397
|
+
@buf.pos.should == 0
|
398
|
+
@buf.data.should == orig
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|