amqp 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,30 @@
1
+ Simple AMQP client for Ruby/EventMachine.
2
+
3
+ To use with RabbitMQ, first run the server:
4
+
5
+ hg clone http://hg.rabbitmq.com/rabbitmq-codegen
6
+ hg clone http://hg.rabbitmq.com/rabbitmq-server
7
+ cd rabbitmq-server
8
+ make run
9
+
10
+ Then run the example client:
11
+
12
+ ruby examples/simple.rb
13
+
14
+ To run the specs:
15
+
16
+ rake spec
17
+
18
+ The lib/amqp/spec.rb file is generated automatically based on the AMQP specification. To generate it:
19
+
20
+ rake codegen
21
+
22
+ This project was inspired by py-amqplib, rabbitmq, qpid and rubbyt.
23
+ Special thanks to Dmitriy Samovskiy, Ben Hood and Tony Garnock-Jones.
24
+
25
+ Other AMQP resources:
26
+
27
+ Barry Pederson's py-amqplib: http://barryp.org/software/py-amqplib/
28
+ Ben Hood's article on writing an AMQP article: http://hopper.squarespace.com/blog/2008/6/21/build-your-own-amqp-client.html
29
+ Dmitriy Samovskiy's introduction to ruby+rabbitmq: http://somic-org.homelinux.org/blog/2008/06/24/ruby-amqp-rabbitmq-example/
30
+ Ben Hood's AMQP client in AS3: http://github.com/0x6e6562/as3-amqp
@@ -0,0 +1,53 @@
1
+ $:.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'mq'
3
+
4
+ EM.run{
5
+
6
+ def log *args
7
+ p args
8
+ end
9
+
10
+ # AMQP.logging = true
11
+
12
+ EM.add_periodic_timer(1){
13
+ puts
14
+
15
+ log :publishing, time = Time.now
16
+ MQ.new.fanout('clock').publish(Marshal.dump(time))
17
+ }
18
+
19
+ MQ.new.queue('every second').bind('clock').subscribe{ |time|
20
+ log 'every second', :received, Marshal.load(time)
21
+ }
22
+
23
+ MQ.new.queue('every 5 seconds').bind('clock').subscribe{ |time|
24
+ time = Marshal.load(time)
25
+ log 'every 5 seconds', :received, time if time.strftime('%S').to_i%5 == 0
26
+ }
27
+
28
+ }
29
+
30
+ __END__
31
+
32
+ [:publishing, Thu Jul 17 20:14:00 -0700 2008]
33
+ ["every 5 seconds", :received, Thu Jul 17 20:14:00 -0700 2008]
34
+ ["every second", :received, Thu Jul 17 20:14:00 -0700 2008]
35
+
36
+ [:publishing, Thu Jul 17 20:14:01 -0700 2008]
37
+ ["every second", :received, Thu Jul 17 20:14:01 -0700 2008]
38
+
39
+ [:publishing, Thu Jul 17 20:14:02 -0700 2008]
40
+ ["every second", :received, Thu Jul 17 20:14:02 -0700 2008]
41
+
42
+ [:publishing, Thu Jul 17 20:14:03 -0700 2008]
43
+ ["every second", :received, Thu Jul 17 20:14:03 -0700 2008]
44
+
45
+ [:publishing, Thu Jul 17 20:14:04 -0700 2008]
46
+ ["every second", :received, Thu Jul 17 20:14:04 -0700 2008]
47
+
48
+ [:publishing, Thu Jul 17 20:14:05 -0700 2008]
49
+ ["every 5 seconds", :received, Thu Jul 17 20:14:05 -0700 2008]
50
+ ["every second", :received, Thu Jul 17 20:14:05 -0700 2008]
51
+
52
+ [:publishing, Thu Jul 17 20:14:06 -0700 2008]
53
+ ["every second", :received, Thu Jul 17 20:14:06 -0700 2008]
@@ -0,0 +1,52 @@
1
+ $:.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'mq'
3
+
4
+ EM.run{
5
+
6
+ def log *args
7
+ p args
8
+ end
9
+
10
+ # AMQP.logging = true
11
+
12
+ class HashTable < Hash
13
+ def get key
14
+ log 'HashTable', :get, key
15
+ self[key]
16
+ end
17
+
18
+ def set key, value
19
+ log 'HashTable', :set, key => value
20
+ self[key] = value
21
+ end
22
+
23
+ def keys
24
+ log 'HashTable', :keys
25
+ super
26
+ end
27
+ end
28
+
29
+ server = MQ.new.rpc('hash table node', HashTable.new)
30
+
31
+ client = MQ.new.rpc('hash table node')
32
+ client.set(:now, time = Time.now)
33
+ client.get(:now) do |res|
34
+ log 'client', :now => res, :eql? => res == time
35
+ end
36
+
37
+ client.set(:one, 1)
38
+ client.keys do |res|
39
+ log 'client', :keys => res
40
+ EM.stop_event_loop
41
+ end
42
+
43
+ }
44
+
45
+ __END__
46
+
47
+ ["HashTable", :set, {:now=>Thu Jul 17 21:04:53 -0700 2008}]
48
+ ["HashTable", :get, :now]
49
+ ["HashTable", :set, {:one=>1}]
50
+ ["HashTable", :keys]
51
+ ["client", {:eql?=>true, :now=>Thu Jul 17 21:04:53 -0700 2008}]
52
+ ["client", {:keys=>[:one, :now]}]
@@ -0,0 +1,53 @@
1
+ $:.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'mq'
3
+
4
+ EM.run{
5
+
6
+ def log *args
7
+ p [ Time.now, *args ]
8
+ end
9
+
10
+ # AMQP.logging = true
11
+
12
+ amq = MQ.new
13
+ amq.queue('one').subscribe{ |headers, msg|
14
+ log 'one', :received, msg, :from => headers.reply_to
15
+
16
+ if headers.reply_to
17
+ msg[1] = 'o'
18
+
19
+ log 'one', :sending, msg, :to => headers.reply_to
20
+ amq.direct.publish(msg, :key => headers.reply_to)
21
+ else
22
+ puts
23
+ end
24
+ }
25
+
26
+ amq = MQ.new
27
+ amq.queue('two').subscribe{ |msg|
28
+ log 'two', :received, msg
29
+ puts
30
+ }
31
+
32
+ amq = MQ.new
33
+ amq.direct.publish('ding', :key => 'one')
34
+ EM.add_periodic_timer(1){
35
+ log :sending, 'ping', :to => 'one', :from => 'two'
36
+ amq.direct.publish('ping', :key => 'one', :reply_to => 'two')
37
+ }
38
+
39
+ }
40
+
41
+ __END__
42
+
43
+ [Thu Jul 17 21:23:55 -0700 2008, "one", :received, "ding", {:from=>nil}]
44
+
45
+ [Thu Jul 17 21:23:56 -0700 2008, :sending, "ping", {:from=>"two", :to=>"one"}]
46
+ [Thu Jul 17 21:23:56 -0700 2008, "one", :received, "ping", {:from=>"two"}]
47
+ [Thu Jul 17 21:23:56 -0700 2008, "one", :sending, "pong", {:to=>"two"}]
48
+ [Thu Jul 17 21:23:56 -0700 2008, "two", :received, "pong"]
49
+
50
+ [Thu Jul 17 21:23:57 -0700 2008, :sending, "ping", {:from=>"two", :to=>"one"}]
51
+ [Thu Jul 17 21:23:57 -0700 2008, "one", :received, "ping", {:from=>"two"}]
52
+ [Thu Jul 17 21:23:57 -0700 2008, "one", :sending, "pong", {:to=>"two"}]
53
+ [Thu Jul 17 21:23:57 -0700 2008, "two", :received, "pong"]
@@ -0,0 +1,77 @@
1
+ $:.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'amqp'
3
+
4
+ module SimpleClient
5
+ def process_frame frame
6
+ case frame
7
+ when Frame::Body
8
+ EM.stop_event_loop
9
+
10
+ when Frame::Method
11
+ case method = frame.payload
12
+ when Protocol::Connection::Start
13
+ send Protocol::Connection::StartOk.new({:platform => 'Ruby/EventMachine',
14
+ :product => 'AMQP',
15
+ :information => 'http://github.com/tmm1/amqp',
16
+ :version => '0.1.0'},
17
+ 'AMQPLAIN',
18
+ {:LOGIN => 'guest',
19
+ :PASSWORD => 'guest'},
20
+ 'en_US')
21
+
22
+ when Protocol::Connection::Tune
23
+ send Protocol::Connection::TuneOk.new(:channel_max => 0,
24
+ :frame_max => 131072,
25
+ :heartbeat => 0)
26
+
27
+ send Protocol::Connection::Open.new(:virtual_host => '/',
28
+ :capabilities => '',
29
+ :insist => false)
30
+
31
+ when Protocol::Connection::OpenOk
32
+ send Protocol::Channel::Open.new, :channel => 1
33
+
34
+ when Protocol::Channel::OpenOk
35
+ send Protocol::Access::Request.new(:realm => '/data',
36
+ :read => true,
37
+ :write => true,
38
+ :active => true), :channel => 1
39
+
40
+ when Protocol::Access::RequestOk
41
+ @ticket = method.ticket
42
+ send Protocol::Queue::Declare.new(:ticket => @ticket,
43
+ :queue => '',
44
+ :exclusive => false,
45
+ :auto_delete => true), :channel => 1
46
+
47
+ when Protocol::Queue::DeclareOk
48
+ @queue = method.queue
49
+ send Protocol::Queue::Bind.new(:ticket => @ticket,
50
+ :queue => @queue,
51
+ :exchange => '',
52
+ :routing_key => 'test_route'), :channel => 1
53
+
54
+ when Protocol::Queue::BindOk
55
+ send Protocol::Basic::Consume.new(:ticket => @ticket,
56
+ :queue => @queue,
57
+ :no_local => false,
58
+ :no_ack => true), :channel => 1
59
+
60
+ when Protocol::Basic::ConsumeOk
61
+ data = "this is a test!"
62
+
63
+ send Protocol::Basic::Publish.new(:ticket => @ticket,
64
+ :exchange => '',
65
+ :routing_key => 'test_route'), :channel => 1
66
+ send Protocol::Header.new(Protocol::Basic, data.length, :content_type => 'application/octet-stream',
67
+ :delivery_mode => 1,
68
+ :priority => 0), :channel => 1
69
+ send Frame::Body.new(data), :channel => 1
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ AMQP.logging = true
76
+ AMQP.client = SimpleClient
77
+ AMQP.start
@@ -0,0 +1,56 @@
1
+ $:.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'mq'
3
+
4
+ EM.run{
5
+
6
+ def log *args
7
+ p [ Time.now, *args ]
8
+ end
9
+
10
+ # AMQP.logging = true
11
+
12
+ EM.add_periodic_timer(1){
13
+ puts
14
+
15
+ log :publishing, 'stock.usd.appl', price = 170+rand(1000)/100.0
16
+ MQ.topic(:key => 'stock.usd.appl').publish(price, :headers => {:symbol => 'appl'})
17
+
18
+ log :publishing, 'stock.usd.msft', price = 22+rand(500)/100.0
19
+ MQ.topic.publish(price, :key => 'stock.usd.msft', :headers => {:symbol => 'msft'})
20
+ }
21
+
22
+ Thread.new{
23
+ amq = MQ.new
24
+ amq.queue('apple stock').bind(amq.topic, :key => 'stock.usd.appl').subscribe{ |price|
25
+ log 'apple stock', price
26
+ }
27
+ }
28
+
29
+ Thread.new{
30
+ amq = MQ.new
31
+ amq.queue('us stocks').bind(amq.topic, :key => 'stock.usd.*').subscribe{ |info, price|
32
+ log 'us stock', info.headers[:symbol], price
33
+ }
34
+ }
35
+
36
+ }
37
+
38
+ __END__
39
+
40
+ [Thu Jul 17 14:51:07 -0700 2008, :publishing, "stock.usd.appl", 170.84]
41
+ [Thu Jul 17 14:51:07 -0700 2008, :publishing, "stock.usd.msft", 23.68]
42
+ [Thu Jul 17 14:51:07 -0700 2008, "apple stock", "170.84"]
43
+ [Thu Jul 17 14:51:07 -0700 2008, "us stock", "appl", "170.84"]
44
+ [Thu Jul 17 14:51:07 -0700 2008, "us stock", "msft", "23.68"]
45
+
46
+ [Thu Jul 17 14:51:08 -0700 2008, :publishing, "stock.usd.appl", 173.61]
47
+ [Thu Jul 17 14:51:08 -0700 2008, :publishing, "stock.usd.msft", 25.8]
48
+ [Thu Jul 17 14:51:08 -0700 2008, "apple stock", "173.61"]
49
+ [Thu Jul 17 14:51:08 -0700 2008, "us stock", "appl", "173.61"]
50
+ [Thu Jul 17 14:51:08 -0700 2008, "us stock", "msft", "25.8"]
51
+
52
+ [Thu Jul 17 14:51:09 -0700 2008, :publishing, "stock.usd.appl", 173.94]
53
+ [Thu Jul 17 14:51:09 -0700 2008, :publishing, "stock.usd.msft", 24.88]
54
+ [Thu Jul 17 14:51:09 -0700 2008, "apple stock", "173.94"]
55
+ [Thu Jul 17 14:51:09 -0700 2008, "us stock", "appl", "173.94"]
56
+ [Thu Jul 17 14:51:09 -0700 2008, "us stock", "msft", "24.88"]
@@ -0,0 +1,14 @@
1
+ module AMQP
2
+ DIR = File.expand_path(File.dirname(File.expand_path(__FILE__)))
3
+
4
+ $:.unshift DIR
5
+
6
+ %w[ buffer spec protocol frame client ].each do |file|
7
+ require "amqp/#{file}"
8
+ end
9
+
10
+ class << self
11
+ @logging = false
12
+ attr_accessor :logging
13
+ end
14
+ end
@@ -0,0 +1,387 @@
1
+ require 'enumerator'
2
+
3
+ module AMQP
4
+ class Buffer
5
+ class Overflow < Exception; end
6
+ class InvalidType < Exception; end
7
+
8
+ def initialize data = ''
9
+ @data = data
10
+ @pos = 0
11
+ end
12
+
13
+ attr_reader :pos
14
+
15
+ def data
16
+ @data.clone
17
+ end
18
+ alias :contents :data
19
+ alias :to_s :data
20
+
21
+ def << data
22
+ @data << data.to_s
23
+ self
24
+ end
25
+
26
+ def length
27
+ @data.length
28
+ end
29
+
30
+ def empty?
31
+ pos == length
32
+ end
33
+
34
+ def rewind
35
+ @pos = 0
36
+ end
37
+
38
+ def read_properties *types
39
+ types.shift if types.first == :properties
40
+
41
+ i = 0
42
+ values = []
43
+
44
+ while props = read(:short)
45
+ (0..14).each do |n|
46
+ # no more property types
47
+ break unless types[i]
48
+
49
+ # if flag is set
50
+ if props & (1<<(15-n)) != 0
51
+ if types[i] == :bit
52
+ # bit values exist in flags only
53
+ values << true
54
+ else
55
+ # save type name for later reading
56
+ values << types[i]
57
+ end
58
+ else
59
+ # property not set or is false bit
60
+ values << (types[i] == :bit ? false : nil)
61
+ end
62
+
63
+ i+=1
64
+ end
65
+
66
+ # bit(0) == 0 means no more property flags
67
+ break unless props & 1 == 1
68
+ end
69
+
70
+ values.map do |value|
71
+ value.is_a?(Symbol) ? read(value) : value
72
+ end
73
+ end
74
+
75
+ def read *types
76
+ if types.first == :properties
77
+ return read_properties(*types)
78
+ end
79
+
80
+ values = types.map do |type|
81
+ case type
82
+ when :octet
83
+ _read(1, 'C')
84
+ when :short
85
+ _read(2, 'n')
86
+ when :long
87
+ _read(4, 'N')
88
+ when :longlong
89
+ upper, lower = _read(8, 'NN')
90
+ upper << 32 | lower
91
+ when :shortstr
92
+ _read read(:octet)
93
+ when :longstr
94
+ _read read(:long)
95
+ when :timestamp
96
+ Time.at read(:longlong)
97
+ when :table
98
+ t = Hash.new
99
+
100
+ table = Buffer.new(read(:longstr))
101
+ until table.empty?
102
+ key, type = table.read(:shortstr, :octet)
103
+ key = key.intern
104
+ t[key] ||= case type
105
+ when ?S
106
+ table.read(:longstr)
107
+ when ?I
108
+ table.read(:long)
109
+ when ?D
110
+ exp = table.read(:octet)
111
+ num = table.read(:long)
112
+ num / 10.0**exp
113
+ when ?T
114
+ table.read(:timestamp)
115
+ when ?F
116
+ table.read(:table)
117
+ end
118
+ end
119
+
120
+ t
121
+ when :bit
122
+ if (@bits ||= []).empty?
123
+ val = read(:octet)
124
+ @bits = (0..7).map{|i| (val & 1<<i) != 0 }
125
+ end
126
+
127
+ @bits.shift
128
+ else
129
+ raise InvalidType, "Cannot read data of type #{type}"
130
+ end
131
+ end
132
+
133
+ types.size == 1 ? values.first : values
134
+ end
135
+
136
+ def write type, data
137
+ case type
138
+ when :octet
139
+ _write(data, 'C')
140
+ when :short
141
+ _write(data, 'n')
142
+ when :long
143
+ _write(data, 'N')
144
+ when :longlong
145
+ lower = data & 0xffffffff
146
+ upper = (data & ~0xffffffff) >> 32
147
+ _write([upper, lower], 'NN')
148
+ when :shortstr
149
+ data = (data || '').to_s
150
+ _write([data.length, data], 'Ca*')
151
+ when :longstr
152
+ if data.is_a? Hash
153
+ write(:table, data)
154
+ else
155
+ data = (data || '').to_s
156
+ _write([data.length, data], 'Na*')
157
+ end
158
+ when :timestamp
159
+ write(:longlong, data.to_i)
160
+ when :table
161
+ data ||= {}
162
+ write :longstr, (data.inject(Buffer.new) do |table, (key, value)|
163
+ table.write(:shortstr, key.to_s)
164
+
165
+ case value
166
+ when String
167
+ table.write(:octet, ?S)
168
+ table.write(:longstr, value.to_s)
169
+ when Fixnum
170
+ table.write(:octet, ?I)
171
+ table.write(:long, value)
172
+ when Float
173
+ table.write(:octet, ?D)
174
+ # XXX there's gotta be a better way to do this..
175
+ exp = value.to_s.gsub(/^.+\./,'').length
176
+ num = value * 10**exp
177
+ table.write(:octet, exp)
178
+ table.write(:long, num)
179
+ when Time
180
+ table.write(:octet, ?T)
181
+ table.write(:timestamp, value)
182
+ when Hash
183
+ table.write(:octet, ?F)
184
+ table.write(:table, value)
185
+ end
186
+
187
+ table
188
+ end)
189
+ when :bit
190
+ [*data].to_enum(:each_slice, 8).each{|bits|
191
+ write(:octet, bits.enum_with_index.inject(0){ |byte, (bit, i)|
192
+ byte |= 1<<i if bit
193
+ byte
194
+ })
195
+ }
196
+ when :properties
197
+ values = []
198
+ data.enum_with_index.inject(0) do |short, ((type, value), i)|
199
+ n = i % 15
200
+ last = i+1 == data.size
201
+
202
+ if (n == 0 and i != 0) or last
203
+ if data.size > i+1
204
+ short |= 1<<0
205
+ elsif last and value
206
+ values << [type,value]
207
+ short |= 1<<(15-n)
208
+ end
209
+
210
+ write(:short, short)
211
+ short = 0
212
+ end
213
+
214
+ if value and !last
215
+ values << [type,value]
216
+ short |= 1<<(15-n)
217
+ end
218
+
219
+ short
220
+ end
221
+
222
+ values.each do |type, value|
223
+ write(type, value) unless type == :bit
224
+ end
225
+ else
226
+ raise InvalidType, "Cannot write data of type #{type}"
227
+ end
228
+
229
+ self
230
+ end
231
+
232
+ def extract
233
+ begin
234
+ cur_data, cur_pos = @data.clone, @pos
235
+ yield self
236
+ rescue Overflow
237
+ @data, @pos = cur_data, cur_pos
238
+ nil
239
+ end
240
+ end
241
+
242
+ def _read size, pack = nil
243
+ if @pos + size > length
244
+ raise Overflow
245
+ else
246
+ data = @data[@pos,size]
247
+ @data[@pos,size] = ''
248
+ if pack
249
+ data = data.unpack(pack)
250
+ data = data.pop if data.size == 1
251
+ end
252
+ data
253
+ end
254
+ end
255
+
256
+ def _write data, pack = nil
257
+ data = [*data].pack(pack) if pack
258
+ @data[@pos,0] = data
259
+ @pos += data.length
260
+ end
261
+ end
262
+ end
263
+
264
+ if $0 =~ /bacon/ or $0 == __FILE__
265
+ require 'bacon'
266
+ include AMQP
267
+
268
+ describe Buffer do
269
+ before do
270
+ @buf = Buffer.new
271
+ end
272
+
273
+ should 'have contents' do
274
+ @buf.contents.should == ''
275
+ end
276
+
277
+ should 'initialize with data' do
278
+ @buf = Buffer.new('abc')
279
+ @buf.contents.should == 'abc'
280
+ end
281
+
282
+ should 'append raw data' do
283
+ @buf << 'abc'
284
+ @buf << 'def'
285
+ @buf.contents.should == 'abcdef'
286
+ end
287
+
288
+ should 'append other buffers' do
289
+ @buf << Buffer.new('abc')
290
+ @buf.data.should == 'abc'
291
+ end
292
+
293
+ should 'have a position' do
294
+ @buf.pos.should == 0
295
+ end
296
+
297
+ should 'have a length' do
298
+ @buf.length.should == 0
299
+ @buf << 'abc'
300
+ @buf.length.should == 3
301
+ end
302
+
303
+ should 'know the end' do
304
+ @buf.empty?.should == true
305
+ end
306
+
307
+ should 'read and write data' do
308
+ @buf._write('abc')
309
+ @buf.rewind
310
+ @buf._read(2).should == 'ab'
311
+ @buf._read(1).should == 'c'
312
+ end
313
+
314
+ should 'raise on overflow' do
315
+ lambda{ @buf._read(1) }.should.raise Buffer::Overflow
316
+ end
317
+
318
+ should 'raise on invalid types' do
319
+ lambda{ @buf.read(:junk) }.should.raise Buffer::InvalidType
320
+ lambda{ @buf.write(:junk, 1) }.should.raise Buffer::InvalidType
321
+ end
322
+
323
+ { :octet => 0b10101010,
324
+ :short => 100,
325
+ :long => 100_000_000,
326
+ :longlong => 666_555_444_333_222_111,
327
+ :shortstr => 'hello',
328
+ :longstr => 'bye'*500,
329
+ :timestamp => time = Time.at(Time.now.to_i),
330
+ :table => { :this => 'is', :a => 'hash', :with => {:nested => 123, :and => time, :also => 123.456} },
331
+ :bit => true
332
+ }.each do |type, value|
333
+
334
+ should "read and write a #{type}" do
335
+ @buf.write(type, value)
336
+ @buf.rewind
337
+ @buf.read(type).should == value
338
+ @buf.should.be.empty
339
+ end
340
+
341
+ end
342
+
343
+ should 'read and write multiple bits' do
344
+ bits = [true, false, false, true, true, false, false, true, true, false]
345
+ @buf.write(:bit, bits)
346
+ @buf.write(:octet, 100)
347
+
348
+ @buf.rewind
349
+
350
+ bits.map do
351
+ @buf.read(:bit)
352
+ end.should == bits
353
+ @buf.read(:octet).should == 100
354
+ end
355
+
356
+ should 'read and write properties' do
357
+ properties = ([
358
+ [:octet, 1],
359
+ [:shortstr, 'abc'],
360
+ [:bit, true],
361
+ [:bit, false],
362
+ [:shortstr, nil],
363
+ [:timestamp, nil],
364
+ [:table, { :a => 'hash' }],
365
+ ]*5).sort_by{rand}
366
+
367
+ @buf.write(:properties, properties)
368
+ @buf.rewind
369
+ @buf.read(:properties, *properties.map{|type,_| type }).should == properties.map{|_,value| value }
370
+ @buf.should.be.empty
371
+ end
372
+
373
+ should 'do transactional reads with #extract' do
374
+ @buf.write :octet, 8
375
+ orig = @buf.to_s
376
+
377
+ @buf.rewind
378
+ @buf.extract do |b|
379
+ b.read :octet
380
+ b.read :short
381
+ end
382
+
383
+ @buf.pos.should == 0
384
+ @buf.data.should == orig
385
+ end
386
+ end
387
+ end