amqp 0.7.0.pre → 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.
Files changed (71) hide show
  1. data/.gitignore +4 -0
  2. data/.rspec +2 -0
  3. data/CHANGELOG +8 -2
  4. data/CONTRIBUTORS +22 -0
  5. data/Gemfile +3 -3
  6. data/README.md +20 -11
  7. data/Rakefile +30 -6
  8. data/amqp.gemspec +1 -1
  9. data/bin/cleanify.rb +50 -0
  10. data/examples/amqp/simple.rb +6 -4
  11. data/examples/mq/ack.rb +8 -6
  12. data/examples/mq/automatic_binding_for_default_direct_exchange.rb +65 -0
  13. data/examples/mq/callbacks.rb +9 -1
  14. data/examples/mq/clock.rb +17 -17
  15. data/examples/mq/hashtable.rb +19 -10
  16. data/examples/mq/internal.rb +13 -11
  17. data/examples/mq/logger.rb +38 -36
  18. data/examples/mq/multiclock.rb +16 -7
  19. data/examples/mq/pingpong.rb +16 -7
  20. data/examples/mq/pop.rb +8 -6
  21. data/examples/mq/primes-simple.rb +2 -0
  22. data/examples/mq/primes.rb +7 -5
  23. data/examples/mq/stocks.rb +14 -5
  24. data/lib/amqp.rb +12 -8
  25. data/lib/amqp/buffer.rb +35 -158
  26. data/lib/amqp/client.rb +34 -22
  27. data/lib/amqp/frame.rb +8 -64
  28. data/lib/amqp/protocol.rb +21 -70
  29. data/lib/amqp/server.rb +11 -9
  30. data/lib/amqp/spec.rb +8 -6
  31. data/lib/amqp/version.rb +2 -0
  32. data/lib/ext/blankslate.rb +3 -1
  33. data/lib/ext/em.rb +2 -0
  34. data/lib/ext/emfork.rb +13 -11
  35. data/lib/mq.rb +253 -156
  36. data/lib/mq/collection.rb +6 -88
  37. data/lib/mq/exchange.rb +70 -13
  38. data/lib/mq/header.rb +12 -6
  39. data/lib/mq/logger.rb +9 -7
  40. data/lib/mq/queue.rb +42 -30
  41. data/lib/mq/rpc.rb +6 -4
  42. data/protocol/codegen.rb +20 -18
  43. data/research/api.rb +10 -46
  44. data/research/primes-forked.rb +9 -7
  45. data/research/primes-processes.rb +74 -72
  46. data/research/primes-threaded.rb +9 -7
  47. data/spec/integration/automatic_binding_for_default_direct_exchange_spec.rb +61 -0
  48. data/spec/mq_helper.rb +70 -0
  49. data/spec/spec_helper.rb +84 -29
  50. data/spec/unit/amqp/buffer_spec.rb +178 -0
  51. data/spec/unit/amqp/client_spec.rb +472 -0
  52. data/spec/unit/amqp/frame_spec.rb +60 -0
  53. data/spec/unit/amqp/misc_spec.rb +123 -0
  54. data/spec/unit/amqp/protocol_spec.rb +53 -0
  55. data/spec/unit/mq/channel_close_spec.rb +15 -0
  56. data/spec/unit/mq/collection_spec.rb +129 -0
  57. data/spec/unit/mq/exchange_declaration_spec.rb +524 -0
  58. data/spec/unit/mq/misc_spec.rb +228 -0
  59. data/spec/unit/mq/mq_basic_spec.rb +39 -0
  60. data/spec/unit/mq/queue_declaration_spec.rb +97 -0
  61. data/spec/unit/mq/queue_spec.rb +71 -0
  62. metadata +33 -21
  63. data/Gemfile.lock +0 -16
  64. data/old/README +0 -30
  65. data/old/Rakefile +0 -12
  66. data/old/amqp-0.8.json +0 -606
  67. data/old/amqp_spec.rb +0 -796
  68. data/old/amqpc.rb +0 -695
  69. data/old/codegen.rb +0 -148
  70. data/spec/channel_close_spec.rb +0 -13
  71. data/spec/sync_async_spec.rb +0 -52
@@ -0,0 +1,472 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+ require "amqp/client"
5
+
6
+ describe AMQP::Client do
7
+ include AMQP::SpecHelper
8
+ include AMQP
9
+
10
+ em_after { AMQP.cleanup_state }
11
+
12
+ context 'with AMQP.client set to BasicClient (default)' do
13
+ describe 'creating new connection to AMQP broker using .connect:', :broker => true do
14
+ it 'when .connect is called outside EM event loop, raises error' do
15
+ expect { Client.connect }.to raise_error /eventmachine not initialized/
16
+ end
17
+
18
+ context 'when .connect is called inside EM event loop:' do
19
+ include AMQP::SpecHelper
20
+
21
+ it 'calls EM.connect with AMQP::Client as a handler class and AMQP options, and' do
22
+ em do
23
+ opts = AMQP.settings.merge(AMQP_OPTS)
24
+ EM.should_receive(:connect).with(opts[:host], opts[:port], Client, opts)
25
+ Client.connect AMQP_OPTS
26
+ done
27
+ end
28
+ end
29
+
30
+ it 'either hits connection timeout (with :timeout option), or' do
31
+ expect { em do
32
+ Client.connect(:host => 'example.com', :timeout => 0.001)
33
+ done(2)
34
+ end
35
+ }.to raise_error AMQP::Error, /Could not connect to server example.com:5672/
36
+ end
37
+
38
+ it 'raises connection error (with unresolvable AMQP broker address), or' do
39
+ pending 'Need to rewrite to eliminate dependency on DNS loockup mechanism'
40
+ expect { em do
41
+ Client.connect(:host => 'impossible.')
42
+ done(2)
43
+ end
44
+ }.to raise_error EventMachine::ConnectionError, /unable to resolve server address/
45
+ end
46
+
47
+ it 'raises connection error (with wrong AMQP host), or' do
48
+ pending 'Need to rewrite to eliminate dependency on DNS loockup mechanism'
49
+ expect { em do
50
+ Client.connect(:host => 'example.com')
51
+ done(2)
52
+ end
53
+ }.to raise_error AMQP::Error, /Could not connect to server example.com:5672/
54
+ end
55
+
56
+ it 'raises connection error (with wrong AMQP port), or' do
57
+ expect { em do
58
+ Client.connect(:port => 131313)
59
+ done(2)
60
+ end
61
+ }.to raise_error AMQP::Error, /Could not connect to server 127.0.0.1:131313/
62
+ end
63
+
64
+ it 'returns client connecting to AMQP broker (with correct AMQP opts) - DELAY!' do
65
+ em do
66
+ client = Client.connect AMQP_OPTS
67
+ client.should_not be_connected
68
+ done(0.2) { client.should be_connected }
69
+ end
70
+ end
71
+
72
+ context 'when connection is attempted, initiated client:' do
73
+ include AMQP::EMSpec
74
+ em_before { @client = Client.connect AMQP_OPTS }
75
+
76
+ specify { @client.should_not be_connected; done }
77
+
78
+ context "after a delay, connection is established and the client:" do
79
+ it 'receives #connection_completed call from EM reactor' do
80
+ @client.should_receive(:connection_completed).with(no_args)
81
+ done(0.2)
82
+ end
83
+
84
+ it 'fires @connection_status block (if any) with :connected' do
85
+ # Set up status processing block first
86
+ @client.connection_status { |status| @status = status }
87
+ done(0.2) { @status.should == :connected }
88
+ end
89
+
90
+ it 'becomes connected' do
91
+ done(0.2) { @client.should be_connected }
92
+ end
93
+ end
94
+ end
95
+
96
+ end # context 'when .connect is called inside EM event loop:
97
+ end # creating new connection to AMQP broker using .connect
98
+
99
+ context 'given a client that is attempting to connect to AMQP broker using AMQP_OPTS' do
100
+ include AMQP::EMSpec
101
+ em_before { @client = Client.connect AMQP_OPTS }
102
+ subject { @client }
103
+
104
+ describe 'closing down' do
105
+ context 'calling #close' do
106
+ it 'just sets callback (to close connection) if @client is not yet connected' do
107
+ @client.should_not be_connected
108
+ @client.should_not_receive(:send)
109
+ @client.should_receive(:callback)
110
+ @client.close
111
+ done
112
+ end
113
+
114
+ context 'if @client is connected'
115
+ it 'closes all channels first if there are any' do
116
+ EM.add_timer(0.2) {
117
+ @client.should be_connected
118
+ channel1 = MQ.new(@client)
119
+ channel2 = MQ.new(@client)
120
+ channel1.should_receive(:close)
121
+ channel2.should_receive(:close)
122
+ @client.close
123
+ done }
124
+ end
125
+
126
+ it 'sends Protocol::Connection::Close method to broker' do
127
+ EM.add_timer(0.2) {
128
+ @client.should be_connected
129
+ @client.should_receive(:send) do |message|
130
+ message.should be_a Protocol::Connection::Close
131
+ end
132
+ @client.close
133
+ done }
134
+ end
135
+ end # 'calling #close'
136
+
137
+ context 'after broker responded to connection close request' do
138
+ it 'client receives Protocol::Connection::CloseOk method from broker' do
139
+ EM.add_timer(0.2) {
140
+ @client.should_receive(:process_frame) do |frame|
141
+ frame.payload.should be_a Protocol::Connection::CloseOk
142
+ end
143
+ @client.close
144
+ done(0.2) }
145
+ end
146
+
147
+ it 'calls block given to #close instead of pre-set callbacks' do
148
+ # stub it BEFORE #connection_completed fired!
149
+ @client.should_not_receive(:disconnected)
150
+ @client.should_not_receive(:reconnect)
151
+ EM.add_timer(0.2) {
152
+ @client.close { @on_disconnect_called = true }
153
+ done(0.2) { @on_disconnect_called.should be_true } }
154
+ end
155
+
156
+ context 'if no block was given to #close' do
157
+
158
+ it 'calls registered @on_disconnect hook' do
159
+ EM.add_timer(0.2) {
160
+ mock = mock('on_disconnect')
161
+ mock.should_receive(:call).with(no_args)
162
+
163
+ # subject_mock trips something and drops connection :(
164
+ @client.instance_exec { @on_disconnect = mock }
165
+ @client.close
166
+ done(0.2) }
167
+ end
168
+
169
+ it 'normally, @on_disconnect just calls private #disconnected method' do
170
+ # stub it BEFORE #connection_completed fired!
171
+ @client.should_receive(:disconnected)
172
+ EM.add_timer(0.2) { @client.close; done(0.2) }
173
+ end
174
+
175
+ it 'fires @connection_status block (if any) with :disconnected' do
176
+ EM.add_timer(0.2) {
177
+ # Set up status processing block first
178
+ @client.connection_status { |status| @status = status }
179
+ @client.close
180
+ done(0.2) { @status.should == :disconnected } }
181
+ end
182
+
183
+ it 'attempts to reconnect' do
184
+ @client.should_receive(:reconnect)
185
+ EM.add_timer(0.2) { @client.close; done(0.2) }
186
+ end
187
+ end # if no block was given to #close
188
+ end # after broker responded to connection close request
189
+
190
+ context '#unbind' do
191
+ it 'is a hook method called by EM when network connection is dropped' do
192
+ @client.should_receive(:unbind).with(no_args)
193
+ @client.close
194
+ done
195
+ end
196
+
197
+ it 'unsets @connected status' do
198
+ EM.add_timer(0.1) {
199
+ @client.should be_connected
200
+ @client.unbind
201
+ @client.should_not be_connected
202
+ done }
203
+ end
204
+
205
+ it 'seems that it implicitly calls #connection_completed' do
206
+ @client.should_receive(:connection_completed) do
207
+ @client.instance_exec do
208
+ # Avoid raising exception
209
+ @on_disconnect = method(:disconnected)
210
+ end
211
+ end
212
+ @client.unbind
213
+ done
214
+ end
215
+
216
+ it 'calls @on_disconnect hook' do
217
+ block_fired = false
218
+ EM.add_timer(0.1) {
219
+ @client.instance_exec do
220
+ @on_disconnect = proc { block_fired = true }
221
+ end
222
+ @client.unbind
223
+ done(0.1) { block_fired.should be_true } }
224
+ end
225
+ end
226
+ end # closing down
227
+
228
+ context 'reconnecting' do
229
+ include AMQP::EMSpec
230
+
231
+ it 're-initializes the client' do
232
+ @client.should_receive(:initialize)
233
+ @client.reconnect
234
+ done
235
+ end
236
+
237
+ it 'resets and clears existing channels, if any' do
238
+ channel1 = MQ.new(@client)
239
+ channel2 = MQ.new(@client)
240
+ EM.add_timer(0.2) {
241
+ channel1.should_receive(:reset)
242
+ channel2.should_receive(:reset)
243
+ @client.reconnect
244
+ @client.channels.should == {}
245
+ done
246
+ }
247
+ end
248
+
249
+ it 'reconnects to broker through EM.reconnect' do
250
+ EM.should_receive(:reconnect)
251
+ @client.reconnect
252
+ done
253
+ end
254
+
255
+ it 'delays reconnect attempt by 1 sec if reconnect is already in progress' do
256
+ EM.should_receive(:reconnect).exactly(1).times
257
+ EM.should_receive(:add_timer).with(1)
258
+ @client.reconnect
259
+ @client.reconnect
260
+ done
261
+ end
262
+
263
+ it 'attempts to automatically reconnect on #close' do
264
+ @client.should_receive(:reconnect)
265
+ EM.add_timer(0.2) { @client.close; done(0.2) }
266
+ end
267
+ end
268
+
269
+ context 'receiving data/processing frames' do
270
+ describe "#receive_data" do
271
+ it 'is a callback method that EM calls when it gets raw data from broker' do
272
+ @client.should_receive(:receive_data) do |data|
273
+ frame = Frame.parse(data)
274
+ frame.should be_an Frame::Method
275
+ frame.payload.should be_an Protocol::Connection::Start
276
+ done
277
+ end
278
+ end
279
+
280
+ it 'delegates actual frame processing to #process_frame' do
281
+ EM.add_timer(0.2) {
282
+ @client.should_receive(:process_frame).with(basic_header)
283
+ @client.receive_data(basic_header.to_s)
284
+ done }
285
+ end
286
+
287
+ it 'does not call #process_frame on incomplete frames' do
288
+ EM.add_timer(0.2) {
289
+ @client.should_not_receive(:process_frame)
290
+ @client.receive_data(basic_header.to_s[0..5])
291
+ done }
292
+ end
293
+
294
+ it 'calls #process_frame when frame is complete' do
295
+ EM.add_timer(0.2) {
296
+ @client.should_receive(:process_frame).with(basic_header)
297
+ @client.receive_data(basic_header.to_s[0..5])
298
+ @client.receive_data(basic_header.to_s[6..-1])
299
+ done }
300
+ end
301
+ end #receive_data
302
+
303
+ describe '#process_frame', '(client reaction to messages from broker)' do
304
+
305
+ it 'delegates frame processing to channel if the frame indicates channel' do
306
+ EM.add_timer(0.2) {
307
+ MQ.new(@client)
308
+ @client.channels[1].should_receive(:process_frame).
309
+ with(basic_header(:channel => 1))
310
+ @client.process_frame(basic_header(:channel => 1))
311
+ done }
312
+ end
313
+
314
+ describe ' Frame::Method', '(Method with args received)' do
315
+ describe 'Protocol::Connection::Start',
316
+ '(broker initiated start of AMQP connection)' do
317
+ let(:frame) { Frame::Method.new(Protocol::Connection::Start.new) }
318
+
319
+ it 'sends back to broker Protocol::Connection::StartOk with details' do
320
+ EM.add_timer(0.2) {
321
+ @client.should_receive(:send).
322
+ with(Protocol::Connection::StartOk.new(
323
+ {:platform => 'Ruby/EventMachine',
324
+ :product => 'AMQP',
325
+ :information => 'http://github.com/ruby-amqp/amqp',
326
+ :version => AMQP::VERSION}, 'AMQPLAIN',
327
+ {:LOGIN => AMQP.settings[:user],
328
+ :PASSWORD => AMQP.settings[:pass]}, 'en_US'))
329
+ @client.process_frame(frame)
330
+ done }
331
+ end
332
+ end # Protocol::Connection::Start
333
+
334
+ describe 'Protocol::Connection::Tune',
335
+ '(broker suggested connection parameters)' do
336
+ let(:frame) { Frame::Method.new(Protocol::Connection::Tune.new) }
337
+
338
+ it 'sends back to broker TuneOk with parameters and Open' do
339
+ EM.add_timer(0.2) {
340
+ @client.should_receive(:send) do |method|
341
+ if method.is_a? Protocol::Connection::TuneOk
342
+ method.should == Protocol::Connection::TuneOk.new(
343
+ :channel_max => 0,
344
+ :frame_max => 131072,
345
+ :heartbeat => 0)
346
+ else
347
+ method.should == Protocol::Connection::Open.new(
348
+ :virtual_host => AMQP.settings[:vhost],
349
+ :capabilities => '',
350
+ :insist => AMQP.settings[:insist])
351
+ end
352
+ end.exactly(2).times
353
+ @client.process_frame(frame)
354
+ done }
355
+ end
356
+ end # Protocol::Connection::Tune
357
+
358
+ describe 'Protocol::Connection::OpenOk',
359
+ '(broker confirmed connection opening)' do
360
+ let(:frame) { Frame::Method.new(AMQP::Protocol::Connection::OpenOk.new) }
361
+
362
+ it 'succeeds @client (as a deferrable)' do
363
+ @client.instance_variable_get(:@deferred_status).should == :unknown
364
+ @client.process_frame(frame)
365
+ done(0.1) {
366
+ @client.instance_variable_get(:@deferred_status).should == :succeeded }
367
+ end
368
+ end # Protocol::Connection::OpenOk
369
+
370
+ describe 'Protocol::Connection::Close',
371
+ '(unexpectedly received connection close from broker)' do
372
+ let(:frame) { Frame::Method.new(Protocol::Connection::Close.new(
373
+ :reply_code => 320,
374
+ :reply_text => "Nyanya",
375
+ :class_id => 10,
376
+ :method_id => 40)) }
377
+
378
+ it 'just prints debug info to STDERR' do
379
+ STDERR.should_receive(:puts).
380
+ with /Nyanya in AMQP::Protocol::Connection::Open/
381
+ @client.process_frame(frame)
382
+ done(0.1)
383
+ end
384
+
385
+ it 'should probably raise exception or something?'
386
+ end # Protocol::Connection::Close
387
+
388
+ describe 'Protocol::Connection::CloseOk',
389
+ '(broker confirmed our connection close request)' do
390
+ let(:frame) { Frame::Method.new(Protocol::Connection::CloseOk.new) }
391
+
392
+ it 'calls registered @on_disconnect hook' do
393
+ EM.add_timer(0.1) {
394
+ subject_mock(:@on_disconnect).should_receive(:call).with(no_args)
395
+ @client.process_frame(frame)
396
+ done }
397
+ end
398
+
399
+ end # Protocol::Connection::CloseOk
400
+ end # Frame::Method
401
+ end #process_frame
402
+ end # receiving data/processing frames
403
+
404
+ context 'channels' do
405
+ it 'starts with empty channels set' do
406
+ @client.channels.should be_empty
407
+ done
408
+ end
409
+
410
+ context '#add_channel' do
411
+ it 'adds given channel to channels set' do
412
+ mq = mock('mq')
413
+ @client.add_channel mq
414
+ @client.channels[1].should == mq
415
+ done
416
+ end
417
+ end
418
+ end
419
+
420
+ context '#send data' do
421
+ it 'formats data to be sent into Frame, setting channel to 0 by default' do
422
+ data = mock('data')
423
+ framed_data = mock('framed data')
424
+ data.should_receive(:to_frame).and_return(framed_data)
425
+ framed_data.should_receive(:channel=).with(0)
426
+ @client.send data
427
+ done
428
+ end
429
+
430
+ it 'sets channel number given in options' do
431
+ data = mock('data')
432
+ framed_data = mock('framed data')
433
+ data.should_receive(:to_frame).and_return(framed_data)
434
+ framed_data.should_receive(:channel=).with(1313)
435
+ @client.send data, :channel =>1313
436
+ done
437
+ end
438
+
439
+ it 'does not format data if it is already a Frame' do
440
+ data = basic_header
441
+ data.should_not_receive(:to_frame)
442
+ @client.send data
443
+ done
444
+ end
445
+
446
+ it 'then calls EM:Connection#send_data hook with framed data converted to String' do
447
+ EM.add_timer(0.1) {
448
+ data = basic_header
449
+ @client.should_receive(:send_data) do |data|
450
+ data.should be_a String
451
+ frame = Frame.parse(data)
452
+ frame.class.should == Frame::Header
453
+ frame.payload.class.should == Protocol::Header
454
+ frame.channel.should == 0
455
+ end
456
+ @client.send data
457
+ done }
458
+ end
459
+ end #send data
460
+
461
+ context '#connection_status' do
462
+ em_before { @called_with_statuses = [] }
463
+ it 'sets a block to be called on connection status changes' do
464
+ @client.connection_status { |status| @called_with_statuses << status }
465
+ @client.close
466
+ done(0.1) { @called_with_statuses.should == [:connected, :disconnected] }
467
+ end
468
+
469
+ end #connection_status
470
+ end #given a connected client
471
+ end # context with AMQP.client set to BasicClient (default)
472
+ end