tengine_event 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,981 @@
1
+ # -*- coding: utf-8 -*-
2
+ require_relative '../../spec_helper'
3
+
4
+ require_relative '../../../lib/tengine/mq/suite'
5
+
6
+ # ログを黙らせたり喋らせたりする
7
+ require 'amq/client'
8
+ if $DEBUG
9
+ require 'logger'
10
+ AMQP::Session.logger = Tengine.logger = Logger.new(STDERR)
11
+ else
12
+ AMQP::Session.logger = Tengine.logger = Tengine::NullLogger.new
13
+ end
14
+
15
+ require 'amq/client/callbacks'
16
+
17
+ describe Tengine::Mq::Suite do
18
+ shared_examples "Tengine::Mq::Suite" do
19
+ describe "#initialize" do
20
+ context "no args" do
21
+ subject { Tengine::Mq::Suite.new }
22
+ its(:config) { should == {
23
+ :sender => {
24
+ :keep_connection => false,
25
+ :retry_interval => 1,
26
+ :retry_count => 30,
27
+ },
28
+ :connection => {
29
+ :user => 'guest',
30
+ :pass => 'guest',
31
+ :vhost => '/',
32
+ :logging => false,
33
+ :insist => false,
34
+ :host => 'localhost',
35
+ :port => 5672,
36
+ :auto_reconnect_delay => 1,
37
+ },
38
+ :channel => {
39
+ :prefetch => 1,
40
+ :auto_recovery => true,
41
+ },
42
+ :exchange => {
43
+ :name => 'tengine_event_exchange',
44
+ :type => :direct,
45
+ :passive => false,
46
+ :durable => true,
47
+ :auto_delete => false,
48
+ :internal => false,
49
+ :nowait => false,
50
+ :publish => {
51
+ :content_type => "application/json",
52
+ :persistent => true,
53
+ },
54
+ },
55
+ :queue => {
56
+ :name => 'tengine_event_queue',
57
+ :passive => false,
58
+ :durable => true,
59
+ :auto_delete => false,
60
+ :exclusive => false,
61
+ :nowait => false,
62
+ :subscribe => {
63
+ :ack => true,
64
+ :nowait => false,
65
+ :confirm => nil,
66
+ },
67
+ },
68
+ }
69
+ }
70
+ end
71
+
72
+ context "hash arg" do
73
+ subject { Tengine::Mq::Suite.new :sender => { :keep_connection => false } }
74
+ its(:config) { should have_key(:sender) }
75
+ it "merges the argument" do
76
+ subject.config[:sender][:keep_connection].should be_false
77
+ subject.config[:sender].should have_key(:retry_interval)
78
+ end
79
+ end
80
+ end
81
+
82
+ describe "#config" do
83
+ subject { Tengine::Mq::Suite.new.config }
84
+ it { should be_kind_of(Hash) }
85
+ it { should be_frozen }
86
+ end
87
+
88
+ describe "#add_hook" do
89
+ subject { Tengine::Mq::Suite.new the_config }
90
+
91
+ context "no arg" do
92
+ it { expect { subject.add_hook }.to raise_error(ArgumentError) }
93
+ end
94
+
95
+ context "many arg" do
96
+ it { expect { subject.add_hook :foo, :bar }.to raise_error(ArgumentError) }
97
+ end
98
+
99
+ context "no block" do
100
+ it { expect { subject.add_hook :foo }.to raise_error(ArgumentError) }
101
+ end
102
+
103
+ context "one arg, one block" do
104
+ it { expect { subject.add_hook(:foo){ } }.to_not raise_error(ArgumentError) }
105
+ end
106
+
107
+ context "connection.on_closed" do
108
+ it "called" do
109
+ block_called = false
110
+ subject.add_hook "connection.on_closed" do
111
+ block_called = true
112
+ end
113
+
114
+ EM.run do
115
+ subject.subscribe {|x, y| }
116
+ EM.add_timer(0.1) { subject.stop }
117
+ end
118
+ block_called.should be_true
119
+ end
120
+ end
121
+
122
+ context "connection.on_tcp_connection_failure" do
123
+ subject {
124
+ port = the_config[:connection] ? the_config[:connection][:port] : 5672
125
+ Tengine::Mq::Suite.new :connection => { :port => port + rand(1024) }
126
+ }
127
+
128
+ it "https://www.pivotaltracker.com/story/show/18317933" do
129
+ block_called = false
130
+ subject.add_hook "connection.on_tcp_connection_failure" do
131
+ block_called = true
132
+ end
133
+ expect {
134
+ EM.run_block do
135
+ subject.subscribe {|x, y| }
136
+ EM.add_timer(0.2) { subject.stop }
137
+ end
138
+ }.to raise_exception
139
+ block_called.should be_true
140
+ end
141
+ end
142
+
143
+ # context "channel.on_error" ...
144
+ # context "connection.after_recovery" ...
145
+
146
+ it "hookを保持する" do
147
+ mq = nil
148
+ # 1st time
149
+ yielded = 0
150
+ EM.run do
151
+ mq = subject
152
+ mq.send :ensures, :connection do
153
+ mq.add_hook(:"connection.on_closed") do
154
+ yielded += 1
155
+ end
156
+ mq.stop
157
+ end
158
+ end
159
+
160
+ # 2nd time
161
+ yielded = 0
162
+ EM.run do
163
+ mq.send :ensures, :connection do
164
+ mq.stop
165
+ end
166
+ end
167
+ yielded.should == 1
168
+ end
169
+
170
+ it "hookを保持する #2" do
171
+ mq = nil
172
+
173
+ # 1st time
174
+ yielded = 0
175
+ EM.run do
176
+ mq = subject
177
+ mq.send :ensures, :channel do
178
+ mq.add_hook(:"channel.on_error") do
179
+ yielded += 1
180
+ end
181
+ mq.channel.exec_callback_once_yielding_self(:error, "channel close reason object")
182
+ mq.stop
183
+ end
184
+ end
185
+
186
+ # 2nd time
187
+ yielded = 0
188
+ EM.run do
189
+ mq.send :ensures, :channel do
190
+ mq.channel.exec_callback_once_yielding_self(:error, "channel close reason object")
191
+ mq.stop
192
+ end
193
+ end
194
+ yielded.should == 1
195
+ end
196
+ end
197
+
198
+ describe "#subscribe" do
199
+ subject { Tengine::Mq::Suite.new the_config }
200
+
201
+ context "no block" do
202
+ it { expect { subject.subscribe }.to raise_error(ArgumentError) }
203
+ end
204
+
205
+ context "no reactor, nowait: false" do
206
+ it "raises" do
207
+ block_called = false
208
+ expect {
209
+ subject.subscribe(:nowait => true) {
210
+ block_called = true
211
+ }
212
+ }.to raise_error(RuntimeError)
213
+ block_called.should be_false
214
+ end
215
+ end
216
+
217
+ context "no reactor, nowait: true" do
218
+ it "raises" do
219
+ block_called = false
220
+ expect {
221
+ subject.subscribe(:nowait=>false) {
222
+ block_called = true
223
+ }
224
+ }.to raise_error(RuntimeError)
225
+ block_called.should be_false
226
+ end
227
+ end
228
+
229
+ context "with reactor, with 1 message in queue" do
230
+ it "runs the block" do
231
+ block_called = false
232
+ body = nil
233
+ header = nil
234
+ expect {
235
+ EM.run do
236
+ subject.subscribe do |hdr, bdy|
237
+ block_called = true
238
+ body = bdy
239
+ header = hdr
240
+ hdr.ack
241
+ subject.stop
242
+ end
243
+
244
+ sender = Tengine::Event::Sender.new subject
245
+ ev = Tengine::Event.new :event_type_name => "foo"
246
+ subject.fire sender, ev, { :keep_connection => true }, nil
247
+ end
248
+ }.to_not raise_error(RuntimeError)
249
+ block_called.should be_true
250
+ header.should be_kind_of(AMQP::Header)
251
+ body.should be_kind_of(String)
252
+ body.should =~ /foo/
253
+ end
254
+ end
255
+
256
+ context "many messages in queue" do
257
+ before do
258
+ # キューにイベントがすでに溜まってるとおかしくなるので、吸い出しておく
259
+ EM.run do
260
+ i = 0
261
+ subject.subscribe do |hdr, bdy|
262
+ hdr.ack
263
+ i += 1
264
+ end
265
+ EM.add_periodic_timer(0.1) do
266
+ subject.stop if i.zero?
267
+ i = 0
268
+ end
269
+ end
270
+ end
271
+
272
+ it "runs the block every time" do
273
+ block_called = 0
274
+ expect {
275
+ EM.run do
276
+ subject.subscribe do |hdr, bdy|
277
+ block_called += 1
278
+ hdr.ack
279
+ end
280
+
281
+ sender = Tengine::Event::Sender.new subject
282
+ EM.add_timer(0.1) {
283
+ ev = Tengine::Event.new :event_type_name => "foo"
284
+ subject.fire sender, ev, { :keep_connection => true }, nil
285
+ EM.add_timer(0.1) {
286
+ ev = Tengine::Event.new :event_type_name => "foo"
287
+ subject.fire sender, ev, { :keep_connection => true }, nil
288
+ EM.add_timer(0.1) {
289
+ ev = Tengine::Event.new :event_type_name => "foo"
290
+ subject.fire sender, ev, { :keep_connection => true }, nil
291
+ EM.add_timer(0.3) {
292
+ subject.stop
293
+ }
294
+ }
295
+ }
296
+ }
297
+ end
298
+ }.to_not raise_error(RuntimeError)
299
+ block_called.should == 3
300
+ end
301
+ end
302
+ end
303
+
304
+ describe "#unsubscribe" do
305
+ subject { Tengine::Mq::Suite.new the_config }
306
+
307
+ context "no block" do
308
+ it { expect { subject.unsubscribe }.to raise_error(ArgumentError) }
309
+ end
310
+
311
+ context "no queue" do
312
+ it "runs the block" do
313
+ block_called = false
314
+ expect {
315
+ subject.unsubscribe {
316
+ block_called = true
317
+ subject.stop
318
+ }
319
+ }.to_not raise_error(RuntimeError)
320
+ block_called.should be_true
321
+ end
322
+ end
323
+
324
+ context "queue not subscribed" do
325
+ it "runs the block" do
326
+ block_called = false
327
+ expect {
328
+ EM.run do
329
+ subject.send :ensures, :queue do
330
+ subject.unsubscribe do
331
+ block_called = true
332
+ subject.stop
333
+ end
334
+ end
335
+ end
336
+ }.to_not raise_error(RuntimeError)
337
+ block_called.should be_true
338
+ end
339
+ end
340
+
341
+ context "subscribed" do
342
+ it "runs the block" do
343
+ block_called = false
344
+ expect {
345
+ EM.run do
346
+ subject.subscribe :nowait => false, :confirm => lambda {|x|
347
+ subject.unsubscribe {|y|
348
+ block_called = true
349
+ subject.stop
350
+ }
351
+ } do |hdr, bdy|
352
+ hdr.ack
353
+ end
354
+ end
355
+ }.to_not raise_error(RuntimeError)
356
+ block_called.should be_true
357
+ end
358
+ end
359
+
360
+ context "nowait:true" do
361
+ it "runs the block" do
362
+ block_called = false
363
+ expect {
364
+ EM.run do
365
+ subject.subscribe :nowait => false, :confirm => lambda {|x|
366
+ subject.unsubscribe({:nowait => true}) {|y|
367
+ block_called = true
368
+ subject.stop
369
+ }
370
+ } do |hdr, bdy|
371
+ hdr.ack
372
+ end
373
+ end
374
+ }.to_not raise_error(RuntimeError)
375
+ block_called.should be_true
376
+ end
377
+ end
378
+ end
379
+
380
+ describe "#fire" do
381
+ subject { Tengine::Mq::Suite.new the_config }
382
+ def sender; Tengine::Event::Sender.new subject end
383
+ def expected_event; Tengine::Event.new(:event_type_name => :foo, :key => "uniq_key") end
384
+
385
+ context "without reactor" do
386
+ it "raises" do
387
+ block_called = false
388
+ expect {
389
+ ev = Tengine::Event.new :event_type_name => "foo"
390
+ subject.fire sender, ev, { :keep_connection => true }, nil
391
+ }.to raise_error(RuntimeError)
392
+ block_called.should be_false
393
+ end
394
+ end
395
+
396
+ context "with reactor" do
397
+ after do
398
+ # キューにイベントがたまるのでてきとうに吸い出す
399
+ if @port
400
+ EM.run do
401
+ subject.send :ensures, :connection do
402
+ i = 0
403
+ subject.subscribe do |hdr, bdy|
404
+ hdr.ack
405
+ i += 1
406
+ end
407
+ @timer = EM.add_periodic_timer(0.1) do
408
+ if i.zero?
409
+ EM.cancel_timer @timer
410
+ subject.unsubscribe do
411
+ subject.stop
412
+ end
413
+ else
414
+ i = 0
415
+ end
416
+ end
417
+ end
418
+ end
419
+ end
420
+ end
421
+
422
+ it "JSON形式にserializeしてexchangeにpublishする" do
423
+ Thread.current[:expected_event] = expected_event
424
+ EM.run do
425
+ subject.send :ensures, :exchange do |xchg|
426
+ def xchg.publish json, hash
427
+ json.should == Thread.current[:expected_event].to_json
428
+ super
429
+ end
430
+ subject.fire sender, Thread.current[:expected_event], {:keep_connection => false}, nil
431
+ end
432
+ end
433
+ end
434
+
435
+ it "Tengine::Eventオブジェクトを直接指定する" do
436
+ # 上と同じ…過去には意味があった
437
+ Thread.current[:expected_event] = expected_event
438
+ EM.run do
439
+ subject.send :ensures, :exchange do |xchg|
440
+ def xchg.publish json, hash
441
+ json.should == Thread.current[:expected_event].to_json
442
+ super
443
+ end
444
+ subject.fire sender, Thread.current[:expected_event], {:keep_connection => false}, nil
445
+ end
446
+ end
447
+ end
448
+
449
+ context "publish後に特定の処理を行う" do
450
+ it "カスタム処理" do
451
+ block_called = false
452
+ EM.run do
453
+ sender.fire "foo" do
454
+ block_called = true
455
+ end
456
+ end
457
+ block_called.should be_true
458
+ end
459
+ end
460
+
461
+ context "keep_connection: true" do
462
+ it "do not stop the reactor" do
463
+ block_called = false
464
+ EM.run do
465
+ sender.fire "foo", :keep_connection => true
466
+ EM.add_timer(0.5) do
467
+ block_called = true
468
+ subject.stop
469
+ end
470
+ end
471
+ block_called.should be_true
472
+ end
473
+ end
474
+
475
+ context "keep_connection: false" do
476
+ it "stop the reactor" do
477
+ block_called = false
478
+ EM.run do
479
+ sender.fire "foo", :keep_connection => false
480
+ EM.add_timer(1) do
481
+ block_called = true
482
+ subject.stop
483
+ end
484
+ end
485
+ block_called.should be_false
486
+ end
487
+ end
488
+
489
+ context "AMQP::TCPConnectionFailed 以外のエラー" do
490
+ it "メッセージ送信ができなくてpublishに渡したブロックが呼び出されず、インターバルが過ぎて、EM.add_timeに渡したブロックが呼び出された場合" do
491
+ block_called = false
492
+ EM.run do
493
+ subject.send :ensures, :exchange do |xchg|
494
+ xchg.stub(:publish).with(expected_event.to_json, :content_type => "application/json", :persistent => true).exactly(31).times.and_raise(StandardError)
495
+ subject.fire sender, expected_event, {:keep_connection => false, :retry_interval => 0}, lambda { block_called = true }
496
+ subject.stop
497
+ end
498
+ end
499
+ block_called.should_not be_true
500
+ end
501
+
502
+ it "エラーが発生しても設定のリトライが行われる" do
503
+ EM.run do
504
+ subject.send :ensures, :exchange do |xchg|
505
+ # 正規のfireとリトライのfireなので、リトライ回数+1
506
+ xchg.stub(:publish).with(expected_event.to_json, :content_type => "application/json", :persistent => true).exactly(31).times.and_raise(StandardError)
507
+ subject.fire sender, expected_event, {:keep_connection => false, :retry_interval => 0}, nil
508
+ subject.stop
509
+ end
510
+ end
511
+ end
512
+
513
+ it "エラーが発生してもオプションで指定したリトライ回数分のリトライが行われる" do
514
+ EM.run do
515
+ subject.send :ensures, :exchange do |xchg|
516
+ # 正規のfireとリトライのfireなので、リトライ回数+1
517
+ xchg.stub(:publish).with(expected_event.to_json, :content_type => "application/json", :persistent => true).exactly(2).times.and_raise(StandardError)
518
+ subject.fire sender, expected_event, {:keep_connection => false, :retry_interval => 0, :retry_count => 1}, nil
519
+ subject.stop
520
+ end
521
+ end
522
+ end
523
+
524
+ it "エラーが発生してもオプションで指定したリトライ間隔でリトライが行われる" do
525
+ t0 = Time.now
526
+ EM.run do
527
+ subject.send :ensures, :exchange do |xchg|
528
+ # 正規のfireとリトライのfireなので、リトライ回数+1
529
+ x = sender
530
+ xchg.stub(:publish).with(expected_event.to_json, :content_type => "application/json", :persistent => true).exactly(3).times.and_raise(StandardError)
531
+ subject.fire x, expected_event, {:keep_connection => false, :retry_interval => 1, :retry_count => 2}, nil
532
+ subject.stop
533
+ end
534
+ end
535
+ t1 = Time.now
536
+ (t1 - t0).should be_within(1.0).of(2.0)
537
+ end
538
+
539
+ it "ちょうどretry_count回めのリトライして成功の場合は例外にならない" do
540
+ block_called = false
541
+ EM.run do
542
+ subject.send :ensures, :exchange do |xchg|
543
+ # 正規のfireとリトライのfireなので、リトライ回数+1
544
+ Thread.current[:expected_event] = expected_event
545
+ Thread.current[:x] = false
546
+ def xchg.publish str, hash
547
+ if Thread.current[:x] = !Thread.current[:x]
548
+ raise "foo"
549
+ else
550
+ super
551
+ end
552
+ end
553
+ subject.fire sender, expected_event, {:keep_connection => false, :retry_interval => 0, :retry_count => 2}, lambda { block_called = true }
554
+ subject.stop
555
+ end
556
+ end
557
+ block_called.should be_true
558
+ end
559
+ end
560
+ end
561
+
562
+ context "複数のEM event loopにまたがったfire" do
563
+ it "https://www.pivotaltracker.com/story/show/21252625" do
564
+ EM.run do sender.fire("foo") end
565
+ EM.run do sender.fire("foo") end
566
+ # ここまでくればOK
567
+ end
568
+ end
569
+
570
+ context "入り乱れたfireにおけるretryの回数" do
571
+ it "https://www.pivotaltracker.com/story/show/20236589" do
572
+ Thread.current[:n1] = 0
573
+ Thread.current[:n2] = 0
574
+ Thread.current[:ev1] = ev1 = Tengine::Event.new(:event_type_name => :foo, :key => "uniq_key")
575
+ Thread.current[:ev2] = ev2 = Tengine::Event.new(:event_type_name => :foo, :key => "another_uniq_key")
576
+ EM.run do
577
+ subject.send :ensures, :exchange do |xchg|
578
+ def xchg.publish json, hash
579
+ case json
580
+ when Thread.current[:ev1].to_json
581
+ Thread.current[:n1] += 1
582
+ raise "ev1"
583
+ when Thread.current[:ev2].to_json
584
+ Thread.current[:n2] += 1
585
+ raise "ev2"
586
+ end
587
+ end
588
+
589
+ subject.fire sender, ev1, {:keep_connection => true, :retry_interval => 0}, nil
590
+ subject.fire sender, ev2, {:keep_connection => true, :retry_interval => 0}, nil
591
+ subject.stop
592
+ end
593
+ end
594
+ n1 = Thread.current[:n1]
595
+ n2 = Thread.current[:n2]
596
+ if n1 == 31
597
+ n2.should <= 31
598
+ n2.should >= 2
599
+ elsif n2 == 31
600
+ n1.should <= 31
601
+ n1.should >= 2
602
+ else
603
+ raise "neither n1(#{n1}) nor n2(#{n2})"
604
+ end
605
+ end
606
+
607
+ it "無限にメモリを消費しない" do
608
+ n = 256 # 1024 # 4096
609
+ EM.run do
610
+ subject.send :ensures, :exchange do |xchg|
611
+ xchg.stub(:publish).with(an_instance_of(String), :content_type => "application/json", :persistent => true).exactly(31).times.and_raise(StandardError)
612
+ n.times do
613
+ EM.next_tick do
614
+ ev = Tengine::Event.new(:event_type_name => :foo, :key => "uniq_key")
615
+ subject.fire sender, ev, {:keep_connection => true, :retry_cont => 3, :retry_interval => 0}, nil
616
+ end
617
+ end
618
+ EM.next_tick do
619
+ subject.stop
620
+ end
621
+ end
622
+ end
623
+ GC.start
624
+ subject.pending_events{true}.size.should < n
625
+ end
626
+ end
627
+ end
628
+
629
+ describe "#stop" do
630
+ subject { Tengine::Mq::Suite.new the_config }
631
+ let(:sender) { Tengine::Event::Sender.new subject }
632
+ let(:expected_event) { Tengine::Event.new(:event_type_name => :foo, :key => "uniq_key") }
633
+
634
+ after do
635
+ # キューにイベントがたまるのでてきとうに吸い出す
636
+ if @port
637
+ EM.run do
638
+ subject.send :ensures, :connection do
639
+ i = 0
640
+ subject.subscribe do |hdr, bdy|
641
+ hdr.ack
642
+ i += 1
643
+ end
644
+ EM.add_periodic_timer(0.1) do
645
+ subject.stop if i.zero?
646
+ i = 0
647
+ end
648
+ end
649
+ end
650
+ end
651
+ end
652
+
653
+ it "EMのイベントループを抜ける" do
654
+ EM.run do
655
+ subject.stop
656
+ end
657
+ # ここに到達すればOK
658
+ end
659
+
660
+ it "ペンディングのイベントが送信されるまではEMのイベントループにとどまる" do
661
+ block_called = false
662
+ EM.run do
663
+ subject.send :ensures, :exchange do |xchg|
664
+ Thread.current[:expected_event] = expected_event
665
+ Thread.current[:x] = false
666
+ def xchg.publish str, hash
667
+ if Thread.current[:x] = !Thread.current[:x]
668
+ raise "foo"
669
+ else
670
+ super
671
+ end
672
+ end
673
+ subject.fire sender, expected_event, {:keep_connection => false, :retry_interval => 0, :retry_count => 2}, lambda { block_called = true }
674
+ subject.stop
675
+ end
676
+ end
677
+ block_called.should be_true
678
+ end
679
+ end
680
+
681
+ describe "#initiate_termination" do
682
+ subject { Tengine::Mq::Suite.new the_config }
683
+ after do
684
+ # キューにイベントがたまるのでてきとうに吸い出す
685
+ if @port
686
+ EM.run do
687
+ i = 0
688
+ j = false
689
+ subject.subscribe :confirm=>proc{j = true} do |hdr, bdy|
690
+ hdr.ack
691
+ i += 1
692
+ end
693
+ EM.add_periodic_timer(0.1) do
694
+ if j
695
+ subject.stop if i.zero?
696
+ i = 0
697
+ end
698
+ end
699
+ end
700
+ end
701
+ end
702
+
703
+ it "再接続しない" do
704
+ block_called = false
705
+ EM.run do
706
+ subject.add_hook("connection.after_recovery") { block_called = true }
707
+ subject.initiate_termination do
708
+ EM.defer(proc { finish; trigger@port },
709
+ proc { subject.stop })
710
+ end
711
+ end
712
+ block_called.should_not be_true
713
+ end
714
+ end
715
+
716
+ describe "#callback_entity" do
717
+ subject { Tengine::Mq::Suite.new the_config }
718
+ it "registers callbacks" do
719
+ EM.run do
720
+ subject.send :ensures, :queue do
721
+ { :queue => [
722
+ :before_recovery,
723
+ :after_recovery,
724
+ :after_connection_interruption,
725
+ ],
726
+ :exchange => [
727
+ :before_recovery,
728
+ :after_recovery,
729
+ :after_connection_interruption,
730
+ ],
731
+ :channel => [
732
+ :before_recovery,
733
+ :after_recovery,
734
+ :after_connection_interruption,
735
+ :error,
736
+ ],
737
+ :connection => [
738
+ :before_recovery,
739
+ :after_recovery,
740
+ :after_connection_interruption,
741
+ :error,
742
+ # :on_closed,
743
+ # :on_possible_authentication_failure,
744
+ # :on_tcp_connection_failure,
745
+ # :on_tcp_connection_loss,
746
+ ]
747
+ }.each_pair do |klass, mids|
748
+ obj = subject.send klass
749
+ mids.each do |mid|
750
+ case obj when AMQ::Client::Callbacks
751
+ obj.should be_has_callback(mid) # 英語おかしいがしょうがない
752
+ else
753
+ # mockかも
754
+ end
755
+ end
756
+ end
757
+ subject.stop
758
+ end
759
+ end
760
+ end
761
+ end
762
+ end
763
+
764
+ context "実際にMQに接続する試験" do
765
+ let(:rabbitmq) do
766
+ ret = nil
767
+ ENV["PATH"].split(/:/).find do |dir|
768
+ Dir.glob("#{dir}/rabbitmq-server") do |path|
769
+ if File.executable?(path)
770
+ ret = path
771
+ break
772
+ end
773
+ end
774
+ end
775
+
776
+ pending "these specs needs a rabbitmq installed" unless ret
777
+ ret
778
+ end
779
+
780
+ def trigger port = rand(32768)
781
+ raise "WRONG" if $_pid
782
+ require 'tmpdir'
783
+ $_dir = Dir.mktmpdir
784
+ # 指定したポートはもう使われているかもしれないので、その際は
785
+ # rabbitmqが起動に失敗するので、何回かポートを変えて試す。
786
+ n = 0
787
+ begin
788
+ envp = {
789
+ "RABBITMQ_NODENAME" => "rspec",
790
+ "RABBITMQ_NODE_PORT" => port.to_s,
791
+ "RABBITMQ_NODE_IP_ADDRESS" => "auto",
792
+ "RABBITMQ_MNESIA_BASE" => $_dir.to_s,
793
+ "RABBITMQ_LOG_BASE" => $_dir.to_s,
794
+ }
795
+ $_pid = Process.spawn(envp, rabbitmq, :chdir => $_dir, :in => :close)
796
+ x = Time.now
797
+ while Time.now < x + 16.0 do # まあこんくらい待てばいいでしょ
798
+ sleep 0.1
799
+ Process.waitpid2($_pid, Process::WNOHANG)
800
+ Process.kill 0, $_pid
801
+ # netstat -an は Linux / BSD ともに有効
802
+ # どちらかに限ればもう少し効率的な探し方はある。たとえば Linux 限定でよければ netstat -lnt ...
803
+ y = `netstat -an | fgrep LISTEN | fgrep #{port}`
804
+ if y.lines.to_a.size >= 1
805
+ @port = port
806
+ return
807
+ end
808
+ end
809
+ pending "failed to invoke rabbitmq in 16 secs."
810
+ rescue Errno::ECHILD, Errno::ESRCH
811
+ if (n += 1) > 10
812
+ pending "10 attempts to invoke rabbitmq failed."
813
+ else
814
+ port = rand(32768)
815
+ retry
816
+ end
817
+ end
818
+ end
819
+
820
+ def finish
821
+ if $_pid
822
+ begin
823
+ Process.kill "INT", $_pid
824
+ Process.waitpid $_pid
825
+ rescue Errno::ECHILD, Errno::ESRCH
826
+ ensure
827
+ require 'fileutils'
828
+ FileUtils.remove_entry_secure $_dir, :force
829
+ end
830
+ end
831
+ $_pid = nil
832
+ end
833
+
834
+ before :all do
835
+ pending "these specs needs a ruby 1.9.2" if RUBY_VERSION < "1.9.2"
836
+ trigger
837
+ end
838
+
839
+ after :all do
840
+ finish
841
+ end
842
+
843
+ let(:the_config) {
844
+ {
845
+ :connection => {
846
+ :port => @port,
847
+ },
848
+ }
849
+ }
850
+ it_should_behave_like "Tengine::Mq::Suite"
851
+ end
852
+
853
+ context "mock/stubによる試験" do
854
+ def trigger *;
855
+ end
856
+ def finish
857
+ end
858
+
859
+ class Mocker
860
+ class << self
861
+ alias_method :[], :new
862
+ end
863
+ def initialize inspect
864
+ @inspect = inspect
865
+ end
866
+ attr_reader :inspect
867
+ end
868
+
869
+ # for exchange
870
+ class RSpec::Mocks::Mock
871
+ def publish str, opt
872
+ $the_messages.push str
873
+ end
874
+ end
875
+
876
+ class RSpec::Mocks::MessageExpectation
877
+ def and_emyield *val
878
+ index = Object.new
879
+ @emvals ||= Hash.new
880
+ @emvals[index] = val
881
+ def self.invoke_with_yield(&block)
882
+ if block.nil?
883
+ @error_generator.raise_missing_block_error @args_to_yield
884
+ end
885
+ value = nil
886
+ @args_to_yield.each do |args_to_yield_this_time|
887
+ if Array === args_to_yield_this_time[0] and @emvals.key?(args_to_yield_this_time[0][0])
888
+ value = EM.next_tick do
889
+ block.yield(*args_to_yield_this_time[0][1])
890
+ end
891
+ else
892
+ if block.arity > -1 && args_to_yield_this_time.length != block.arity
893
+ @error_generator.raise_wrong_arity_error args_to_yield_this_time, block.arity
894
+ end
895
+ value = eval_block(*args_to_yield_this_time, &block)
896
+ end
897
+ end
898
+ value
899
+ end
900
+ and_yield([index, val])
901
+ end
902
+ end
903
+
904
+ before do
905
+ $the_messages = EM::Queue.new
906
+ @the_connection = mock(Mocker["connection"])
907
+ @the_channel_id = Numeric.new
908
+ @the_channel = mock(Mocker["channel"])
909
+ @the_exchange = mock(Mocker["exchange"])
910
+ @the_queue = mock(Mocker["queue"])
911
+ @callbacks = Hash.new do |h, k|
912
+ h[k] = Hash.new
913
+ end
914
+ AMQP.stub(:connect).with(an_instance_of(Hash)) do |h, block|
915
+ EM.next_tick do
916
+ block.yield @the_connection
917
+ if h[:port] != 5672
918
+ @callbacks[@the_connection][:on_tcp_connection_failure].yield @the_connection if @callbacks[@the_connection][:on_tcp_connection_failure]
919
+ raise AMQP::Error, "fake error."
920
+ end
921
+ end
922
+ end
923
+ AMQP::Channel.stub(:next_channel_id).and_return(@the_channel_id)
924
+ AMQP::Channel.stub(:new).with(@the_connection, @the_channel_id, an_instance_of(Hash)).and_emyield(@the_channel).and_return(@the_channel)
925
+ AMQP::Exchange.stub(:new).with(@the_channel, an_instance_of(Symbol), an_instance_of(String), an_instance_of(Hash)).and_emyield(@the_exchange).and_return(@the_exchange)
926
+ @the_connection.stub(:connected?).and_return(true)
927
+ @the_connection.stub(:disconnect) do |block|
928
+ EM.next_tick do
929
+ @callbacks[@the_connection][:on_closed].yield @the_connection if @callbacks[@the_connection][:on_closed]
930
+ block.yield
931
+ $the_messages = EM::Queue.new # reset
932
+ end
933
+ end
934
+ @the_connection.stub(:server_capabilities).and_return(Hash.new)
935
+ @the_connection.stub(:before_recovery)
936
+ @the_connection.stub(:after_recovery)
937
+ @the_connection.stub(:on_connection_interruption)
938
+ @the_connection.stub(:on_closed) do |block| @callbacks[@the_connection][:on_closed] = block end
939
+ @the_connection.stub(:on_possible_authentication_failure)
940
+ @the_connection.stub(:on_tcp_connection_failure) do |block| @callbacks[@the_connection][:on_tcp_connection_failure] = block end
941
+ @the_connection.stub(:on_tcp_connection_loss)
942
+ @the_channel.stub(:queue).with(an_instance_of(String), an_instance_of(Hash)).and_emyield(@the_queue).and_return(@the_queue)
943
+ @the_channel.stub(:close).and_emyield
944
+ @the_channel.stub(:before_recovery)
945
+ @the_channel.stub(:after_recovery)
946
+ @the_channel.stub(:on_connection_interruption)
947
+ @the_channel.stub(:on_error) do |block| @callbacks[@the_channel][:on_error] = block end
948
+ @the_channel.stub(:exec_callback_once_yielding_self).with(:error, "channel close reason object") do
949
+ EM.next_tick do
950
+ @callbacks[@the_channel][:on_error].yield @the_channel if @callbacks[@the_channel][:on_error]
951
+ end
952
+ end
953
+ @the_exchange.stub(:publish).with(an_instance_of(String), an_instance_of(Hash)) do |str, opt|
954
+ $the_messages.push str
955
+ end
956
+ @the_exchange.stub(:before_recovery)
957
+ @the_exchange.stub(:after_recovery)
958
+ @the_exchange.stub(:on_connection_interruption)
959
+ @the_queue.stub(:bind).with(@the_exchange, an_instance_of(Hash)).and_emyield
960
+ @the_queue.stub(:subscribe).with(an_instance_of(Hash)) do |h, block|
961
+ h[:confirm].call mock(Mocker["confirm-ok"]) if h[:confirm] and not h[:nowait]
962
+ cb = lambda do |ev|
963
+ header = AMQP::Header.new @the_channel, nil, Hash.new
964
+ header.stub(:ack)
965
+ block.yield header, ev
966
+ $the_messages.pop(&cb)
967
+ end
968
+ EM.next_tick do
969
+ $the_messages.pop(&cb)
970
+ end
971
+ end
972
+ @the_queue.stub(:before_recovery)
973
+ @the_queue.stub(:after_recovery)
974
+ @the_queue.stub(:on_connection_interruption)
975
+ @the_queue.stub(:default_consumer)
976
+ end
977
+
978
+ let(:the_config) { Hash.new }
979
+ it_should_behave_like "Tengine::Mq::Suite"
980
+ end
981
+ end