mercury_amqp 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+ require 'mercury'
3
+
4
+ describe Mercury do
5
+ include MercuryFakeSpec
6
+
7
+ # These tests just cover the basics. Most of the testing is
8
+ # done in the Mercury::Monadic spec for convenience.
9
+
10
+ let!(:sent) { { 'a' => 1 } }
11
+ let!(:source) { 'test-exchange' }
12
+ let!(:queue) { 'test-queue' }
13
+
14
+ describe '::open' do
15
+ it 'opens a mercury instance' do
16
+ em do
17
+ Mercury.open do |m|
18
+ expect(m).to be_a Mercury
19
+ m.close do
20
+ done
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ describe '#close' do
28
+ itt 'closes the connection' do
29
+ em do
30
+ Mercury.open do |m|
31
+ m.close do
32
+ expect { m.publish(queue, {'a' => 1}) }.to raise_error /connection is closed/
33
+ done
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ describe '#start_listener' do
41
+ itt 'listens for messages' do
42
+ with_mercury do |m|
43
+ received = []
44
+ m.start_listener(source, received.method(:push)) do
45
+ m.publish(source, sent) do
46
+ em_wait_until(proc{received.any?}) do
47
+ expect(received.size).to eql 1
48
+ expect(received[0].content).to eql sent
49
+ m.close do
50
+ done
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ describe '#start_worker' do
60
+ itt 'listens for messages' do
61
+ with_mercury do |m|
62
+ received = []
63
+ m.start_worker(queue, source, received.method(:push)) do
64
+ m.publish(source, sent) do
65
+ em_wait_until(proc{received.any?}) do
66
+ expect(received.size).to eql 1
67
+ expect(received[0].content).to eql sent
68
+ m.close do
69
+ done
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def with_mercury(&block)
79
+ sources = [source]
80
+ queues = [queue]
81
+ em { delete_sources_and_queues_cps(sources, queues).run{done} }
82
+ em { Mercury.open(&block) }
83
+ em { delete_sources_and_queues_cps(sources, queues).run{done} }
84
+ end
85
+
86
+ end
87
+
@@ -0,0 +1,313 @@
1
+ require 'rspec'
2
+ require 'spec_helper'
3
+ require 'mercury'
4
+ require 'mercury/monadic'
5
+
6
+ describe Mercury::Monadic do
7
+ include Cps::Methods
8
+ include MercuryFakeSpec
9
+
10
+ let!(:source1) { 'test-exchange1' }
11
+ let!(:source2) { 'test-exchange2' }
12
+ let!(:source) { source1 }
13
+ let!(:queue1) { 'test-queue1' }
14
+ let!(:queue2) { 'test-queue2' }
15
+ let!(:queue) { queue1 }
16
+ let!(:worker) { queue }
17
+ let!(:tag1) { 'tag1' }
18
+ let!(:tag2) { 'tag2' }
19
+ let!(:tag) { tag1 }
20
+ let!(:msg1) { {'a' => 1} }
21
+ let!(:msg2) { {'b' => 2} }
22
+ let!(:msg3) { {'c' => 3} }
23
+ let!(:msg4) { {'d' => 4} }
24
+ let!(:msg) { msg1 }
25
+ let!(:long_enough_to_receive_any_messages) { 0.5 } # seconds
26
+
27
+ # Sending an receiving are complementary operations. You can't test
28
+ # one without testing the other. Consequently, these tests verify
29
+ # system behavior rather than method contracts.
30
+
31
+ itt 'sends and receives messages' do
32
+ test_with_mercury do |m|
33
+ msgs = []
34
+ seql do
35
+ and_then { m.start_listener(source1, &msgs.method(:push)) }
36
+ and_then { m.publish(source1, msg1) }
37
+ and_then { m.publish(source2, msg2) } # different source
38
+ and_then { m.publish(source1, msg3) }
39
+ and_then { wait_until { msgs.size == 2 } }
40
+ and_lift do
41
+ msgs.each { |msg| expect(msg).to be_a Mercury::ReceivedMessage }
42
+ expect(msgs[0].content).to eql(msg1)
43
+ expect(msgs[1].content).to eql(msg3)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ itt 'sends and receives tagged messages' do
50
+ test_with_mercury do |m|
51
+ msgs = []
52
+ seql do
53
+ and_then { m.start_listener(source, tag_filter: tag1, &msgs.method(:push)) }
54
+ and_then { m.publish(source, msg1, tag: tag1) }
55
+ and_then { m.publish(source, msg2, tag: tag2) } # different tag
56
+ and_then { m.publish(source, msg3, tag: tag1) }
57
+ and_then { wait_until { msgs.size == 2 } }
58
+ and_lift do
59
+ expect(msgs[0].content).to eql(msg1)
60
+ expect(msgs[0].tag).to eql(tag1)
61
+ expect(msgs[1].content).to eql(msg3)
62
+ expect(msgs[1].tag).to eql(tag1)
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ itt 'uses AMQP-style tag filters' do
69
+ test_with_mercury do |m|
70
+ successes = []
71
+ failures = []
72
+ bars = []
73
+ everything = []
74
+ seql do
75
+ and_then { m.start_listener(source, tag_filter: '*.success', &successes.method(:push)) }
76
+ and_then { m.start_listener(source, tag_filter: '*.failure', &failures.method(:push)) }
77
+ and_then { m.start_listener(source, tag_filter: 'bar.*', &bars.method(:push)) }
78
+ and_then { m.start_listener(source, tag_filter: '#', &everything.method(:push)) }
79
+ and_then { m.publish(source, msg1, tag: 'foo.success') }
80
+ and_then { m.publish(source, msg2, tag: 'foo.failure') }
81
+ and_then { m.publish(source, msg3, tag: 'bar.success') }
82
+ and_then { m.publish(source, msg4, tag: 'bar.failure') }
83
+ and_then { wait_until { successes.size == 2 && failures.size == 2 && bars.size == 2 && everything.size == 4 } }
84
+ and_lift do
85
+ expect(successes[0].content).to eql(msg1)
86
+ expect(successes[1].content).to eql(msg3)
87
+ expect(failures[0].content).to eql(msg2)
88
+ expect(failures[1].content).to eql(msg4)
89
+ expect(bars[0].content).to eql(msg3)
90
+ expect(bars[1].content).to eql(msg4)
91
+ expect(everything[0].content).to eql(msg1)
92
+ expect(everything[1].content).to eql(msg2)
93
+ expect(everything[2].content).to eql(msg3)
94
+ expect(everything[3].content).to eql(msg4)
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ itt 'workers share a queue' do
101
+ test_with_mercury do |m|
102
+ seql do
103
+ let(:m2) { Mercury::Monadic.open }
104
+ work1 = []
105
+ work2 = []
106
+ and_then { m.start_worker(worker, source, &push_and_ack(work1)) }
107
+ and_then { m2.start_worker(worker, source, &push_and_ack(work2)) }
108
+ and_then { m.publish(source, msg1) }
109
+ and_then { m.publish(source, msg2) }
110
+ and_then { wait_until { work1.size + work2.size == 2 } }
111
+ and_lift { expect((work1 + work2).map(&:content).uniq.size).to eql 2 }
112
+ and_then { m2.close }
113
+ end
114
+ end
115
+ end
116
+
117
+ itt 'workers can specify tag filters' do
118
+ test_with_mercury do |m|
119
+ seql do
120
+ let(:m2) { Mercury::Monadic.open }
121
+ work1 = []
122
+ work2 = []
123
+ and_then { m.start_worker(worker, source, tag_filter: 'success', &push_and_ack(work1)) }
124
+ and_then { m2.start_worker(worker, source, tag_filter: 'failure', &push_and_ack(work2)) }
125
+ and_then { m.publish(source, msg1, tag: 'success') }
126
+ and_then { m.publish(source, msg2, tag: 'failure') }
127
+ and_then { wait_until { work1.size == 1 && work2.size == 1 } }
128
+ and_lift do
129
+ expect(work1[0].content).to eql msg1
130
+ expect(work2[0].content).to eql msg2
131
+ end
132
+ and_then { m2.close }
133
+ end
134
+ end
135
+ end
136
+
137
+ def push_and_ack(array)
138
+ proc do |msg|
139
+ array.push(msg)
140
+ msg.ack
141
+ end
142
+ end
143
+
144
+ itt 'a worker must ack before receiving another message' do
145
+ test_with_mercury do |m|
146
+ msgs = []
147
+ seql do
148
+ and_then { m.start_worker(worker, source, &msgs.method(:push)) }
149
+ and_then { m.publish(source, msg1) }
150
+ and_then { m.publish(source, msg2) }
151
+ and_then { wait_for(long_enough_to_receive_any_messages) }
152
+ and_lift { expect(msgs.size).to eql 1 }
153
+ and_lift { msgs[0].ack }
154
+ and_then { wait_until { msgs.size == 2 } }
155
+ end
156
+ end
157
+ end
158
+
159
+ itt 'rejected messages are not requeued' do
160
+ test_with_mercury do |m|
161
+ msgs = []
162
+ seql do
163
+ and_then { m.start_worker(worker, source, &msgs.method(:push)) }
164
+ and_then { m.publish(source, msg) }
165
+ and_then { wait_until { msgs.size == 1 } }
166
+ and_lift { msgs[0].reject }
167
+ and_then { wait_for(long_enough_to_receive_any_messages) }
168
+ and_lift { expect(msgs.size).to eql 1}
169
+ end
170
+ end
171
+ end
172
+
173
+ itt 'nacked messages are requeued' do
174
+ test_with_mercury do |m|
175
+ msgs = []
176
+ seql do
177
+ and_then { m.start_worker(worker, source, &msgs.method(:push)) }
178
+ and_then { m.publish(source, msg) }
179
+ and_then { wait_until { msgs.size == 1 } }
180
+ and_lift { msgs[0].nack }
181
+ and_then { wait_until { msgs.size == 2} }
182
+ end
183
+ end
184
+ end
185
+
186
+ it 'unacked messages are requeued (client failure)' do
187
+ test_with_mercury do |m|
188
+ msgs = []
189
+ seql do
190
+ and_then { m.start_worker(worker, source, &msgs.method(:push)) }
191
+ and_then { m.publish(source, msg) }
192
+ and_then { wait_until { msgs.size == 1 } }
193
+ and_then { m.close }
194
+ let(:m2) { Mercury::Monadic.open }
195
+ and_then { m2.start_worker(worker, source, &msgs.method(:push)) }
196
+ and_then { wait_until { msgs.size == 2 } }
197
+ end
198
+ end
199
+ end
200
+
201
+ it 'raises when an error occurs' do
202
+ # verify it registers a handler
203
+ expect_any_instance_of(AMQP::Channel).to receive(:on_error) {|&b| @handler = b}
204
+
205
+ # verify the handler raises an error
206
+ expect do
207
+ em do
208
+ Mercury.open do
209
+ ch = double
210
+ info = double(reply_code: 'code', reply_text: 'text')
211
+ @handler.call(ch, info)
212
+ end
213
+ end
214
+ end.to raise_error 'An error occurred: code - text'
215
+ end
216
+
217
+ describe '#delete_source' do
218
+ itt 'deletes the source if it exists' do
219
+ test_with_mercury do |m|
220
+ seql do
221
+ and_then { m.start_listener(source) }
222
+ let(:r1) { m.source_exists?(source) }
223
+ and_lift { expect(r1).to be true }
224
+ and_then { m.delete_source(source) }
225
+ let(:r2) { m.source_exists?(source) }
226
+ and_lift { expect(r2).to be false }
227
+ end
228
+ end
229
+ end
230
+ itt 'does nothing if the source does not exist' do
231
+ test_with_mercury do |m|
232
+ seql do
233
+ and_then { m.delete_source(source) }
234
+ let(:r) { m.source_exists?(source) }
235
+ and_lift { expect(r).to be false }
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+ describe '#delete_work_queue' do
242
+ itt 'deletes the queue if it exists' do
243
+ test_with_mercury do |m|
244
+ seql do
245
+ and_then { m.start_worker(queue, source) }
246
+ let(:r1) { m.queue_exists?(queue) }
247
+ and_lift { expect(r1).to be true }
248
+ and_then { m.delete_work_queue(queue) }
249
+ let(:r2) { m.queue_exists?(queue) }
250
+ and_lift { expect(r2).to be false }
251
+ end
252
+ end
253
+ end
254
+ itt 'does nothing if the queue does not exist' do
255
+ test_with_mercury do |m|
256
+ seql do
257
+ and_then { m.delete_work_queue(queue) }
258
+ let(:r) { m.queue_exists?(queue) }
259
+ and_lift { expect(r).to be false }
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ describe '#source_exists?' do
266
+ itt 'returns false when the source does not exist' do
267
+ test_with_mercury do |m|
268
+ m.source_exists?('asdf').
269
+ and_lift { |result| expect(result).to be false }
270
+ end
271
+ end
272
+
273
+ it 'returns true when the source exists' do
274
+ test_with_mercury do |m|
275
+ m.source_exists?('amq.direct').
276
+ and_lift { |result| expect(result).to be true }
277
+ end
278
+ end
279
+ end
280
+
281
+ describe '#queue_exists?' do
282
+ itt 'returns false when the queue does not exist' do
283
+ test_with_mercury do |m|
284
+ m.queue_exists?('asdf').
285
+ and_lift { |result| expect(result).to be false }
286
+ end
287
+ end
288
+
289
+ itt 'returns true when the source exists' do
290
+ test_with_mercury do |m|
291
+ m.start_worker(queue1, source1, proc{}).
292
+ and_then { m.queue_exists?(queue1) }.
293
+ and_lift { |result| expect(result).to be true }
294
+ end
295
+ end
296
+ end
297
+
298
+ describe '#open' do
299
+ it 'relays args to Mercury.open' do
300
+ logger = double
301
+ expect(Mercury).to receive(:open).with(logger: logger, host: 'asdf')
302
+ Mercury::Monadic.open(logger: logger, host: 'asdf').run
303
+ end
304
+ end
305
+
306
+ # the block must return a Cps
307
+ def test_with_mercury(&block)
308
+ sources = [source1, source2]
309
+ queues = [queue1, queue2]
310
+ test_with_mercury_cps(sources, queues, &block)
311
+ end
312
+ end
313
+
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+ require 'mercury/sync'
3
+ require 'mercury/monadic'
4
+
5
+ describe Mercury::Sync do
6
+ include Cps::Methods
7
+ let!(:source) { 'test-exchange1' }
8
+ let!(:queue) { 'test-queue1' }
9
+ describe '::publish' do
10
+ it 'publishes synchronously' do
11
+ sent = {'a' => 1}
12
+ received = []
13
+ test_with_mercury do |m|
14
+ seql do
15
+ and_then { m.start_listener(source, received.method(:push)) }
16
+ and_lift { Mercury::Sync.publish(source, sent) }
17
+ and_then { wait_until { received.any? } }
18
+ and_lift do
19
+ expect(received.size).to eql 1
20
+ expect(received[0].content).to eql sent
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # the block must return a Cps
28
+ def test_with_mercury(&block)
29
+ sources = [source]
30
+ queues = [queue]
31
+ test_with_mercury_cps(sources, queues, &block)
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+ require 'mercury/utils'
3
+
4
+ describe Utils do
5
+ describe '::unsplat' do
6
+ it 'allows args to be provided in splatted form' do
7
+ expect(Utils.unsplat([])).to eql []
8
+ expect(Utils.unsplat([1])).to eql [1]
9
+ expect(Utils.unsplat([1, 2])).to eql [1, 2]
10
+ end
11
+ it 'allows args to be provided as an array' do
12
+ expect(Utils.unsplat([[]])).to eql []
13
+ expect(Utils.unsplat([[1]])).to eql [1]
14
+ expect(Utils.unsplat([[1, 2]])).to eql [1, 2]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+ require 'mercury/wire_serializer'
3
+
4
+ describe Mercury::WireSerializer do
5
+ subject {Mercury::WireSerializer.new}
6
+ describe '#write' do
7
+ it 'writes a string hash as JSON' do
8
+ expect(subject.write({'a' => 1})).to eql '{"a":1}'
9
+ end
10
+ it 'writes a symbol hash as JSON' do
11
+ expect(subject.write({a: 1})).to eql '{"a":1}'
12
+ end
13
+ it 'writes a struct as JSON' do
14
+ Foo = Struct.new(:a)
15
+ expect(subject.write(Foo.new(1))).to eql '{"a":1}'
16
+ end
17
+ it 'writes a string literally' do
18
+ expect(subject.write('asdf')).to eql 'asdf'
19
+ end
20
+ end
21
+ describe '#read' do
22
+ it 'reads JSON as a string hash' do
23
+ expect(subject.read('{"a":1}')).to eql('a' => 1)
24
+ end
25
+ it 'reads unparseable JSON as a string' do
26
+ expect(subject.read('asdf')).to eql 'asdf'
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,39 @@
1
+ require 'rspec'
2
+ require 'mercury/test_utils'
3
+ require 'mercury/fake'
4
+ include Mercury::TestUtils
5
+
6
+ # the block must return a Cps
7
+ def test_with_mercury_cps(sources, queues, &block)
8
+ em do
9
+ seql do
10
+ let(:m) { Mercury::Monadic.open }
11
+ and_then { delete_sources_and_queues_cps(sources, queues) }
12
+ and_then { block.call(m) }
13
+ and_then { delete_sources_and_queues_cps(sources, queues) }
14
+ and_then { m.close }
15
+ and_lift { done }
16
+ end.run
17
+ end
18
+ end
19
+
20
+ module MercuryFakeSpec
21
+ def self.included(base)
22
+ base.extend(ClassMethods)
23
+ end
24
+
25
+ module ClassMethods
26
+ # runs a test once with real mercury and once with Mercury::Fake
27
+ def itt(name, &block)
28
+ it(name, &block)
29
+ context 'with Mercury::Fake' do
30
+ before :each do
31
+ allow(Mercury).to receive(:open) do |&k|
32
+ EM.next_tick { k.call(Mercury::Fake.new) }
33
+ end
34
+ end
35
+ it(name, &block)
36
+ end
37
+ end
38
+ end
39
+ end