amqp 0.5.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/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