mercury_amqp 0.1.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.
@@ -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