moqueue 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ = Contributors
2
+ * larrytheliquid (Larry Diehl) & Engine Yard http://github.com/larrytheliquid
3
+ * mattmatt (Mathias Meyer) & Peritor Consulting http://github.com/mattmatt
@@ -0,0 +1,55 @@
1
+ = Moqueue
2
+ Moqueue is a library for mocking the various objects that make up the ruby AMQP[http://github.com/tmm1/amqp] library. It allows you to use the AMQP library naturally and test your code easily without running an AMQP broker. If you want a higher level of control, you can use your favorite mocking and stubbing library to modify individual calls to MQ.queue and the like so that they return Moqueue's mock up versions. If you want to go all-in, you can tell Moqueue to overload the MQ and AMQP. This allows you to use MQ and AMQP as normal, while Moqueue works behind the scenes to wire everything together.
3
+
4
+ = Getting started
5
+
6
+ require "moqueue"
7
+ overload_amqp
8
+
9
+ mq = MQ.new
10
+ => #<MQ:0x1197ae8>
11
+
12
+ queue = mq.queue("mocktacular")
13
+ => #<Moqueue::MockQueue:0x1194550 @name="mocktacular">
14
+
15
+ topic = mq.topic("lolz")
16
+ => #<Moqueue::MockExchange:0x11913dc @topic="lolz">
17
+
18
+ queue.bind(topic, :key=> "cats.*")
19
+ => #<Moqueue::MockQueue:0x1194550 @name="mocktacular">
20
+
21
+ queue.subscribe {|header, msg| puts [header.routing_key, msg]}
22
+ => nil
23
+
24
+ topic.publish("eatin ur foodz", :key => "cats.inUrFridge")
25
+ # cats.inUrFridge
26
+ # eatin ur foodz
27
+
28
+ Note that in this example, we didn't have to deal with <tt>AMQP.start</tt> or <tt>EM.run</tt>. This should be ample evidence that you should run higher level tests without any mocks or stubs so you can be sure everything works with real MQ objects. With that said, <tt>#overload_amqp</tt> does overload the <tt>AMQP.start</tt> method, so you can use Moqueue for mid-level testing if desired. Have a look at the spec/examples directory to see Moqueue running some of AMQP's examples in overload mode for more demonstration of this.
29
+
30
+ = Custom Rspec Matchers
31
+ For Test::Unit users, Moqueue's default syntax should be a good fit with <tt>assert()</tt>:
32
+ assert(queue.received_message?("eatin ur foodz"))
33
+ Rspec users will probably want something a bit more natural language-y. You got it:
34
+ queue.should have_received("a message")
35
+ queue.should have_ack_for("a different message")
36
+
37
+ = What's Working? What's Not?
38
+ As you can tell from the example above, quite a bit is working. This includes direct exchanges where you call <tt>#publish</tt> and <tt>#subscribe</tt> on the same queue, acknowledgements, topic exchanges, and fanout exchanges.
39
+
40
+ What's not working:
41
+ * RPC exchanges.
42
+ * The routing key matching algorithm works for common cases, including "*" and "#" wildcards in the binding key. If you need anything more complicated than that, Moqueue is not guaranteed to do the right thing.
43
+ * Receiving acks when using topic exchanges works only if you subscribe before publishing.
44
+
45
+ There are some things that Moqueue may never be able to do. As one prominent example, for queues that are configured to expect acknowledgements (the <tt>:ack=>true</tt> option), the behavior on shutdown is not emulated correctly.
46
+
47
+ == Hacking
48
+ Moqueue is at a stage where it "works for me." That said, there may be methods or method signatures that aren't correct/ aren't supported. If this happens to you, fork me and send a pull request when you're done. Patches and feedback welcome.
49
+
50
+ == Moar
51
+ I wrote an introductory post on my blog, it's probably the best source of high-level discussion right now. Visit: http://kallistec.com/2009/06/21/introducing-moqueue/
52
+
53
+ If you prefer code over drivel, look at the specs under spec/examples. There you'll find some of the examples from the amqp library running completely Moqueue-ified; the basic_usage_spec.rb shows some lower-level use.
54
+
55
+ As always, you're invited to git yer fork on if you want to work on any of these. If you find a bug that you can't source or want to send love or hate mail, you can contact me directly at dan@kallistec.com
@@ -0,0 +1,63 @@
1
+ require "spec/rake/spectask"
2
+ require "rake/rdoctask"
3
+
4
+ task :default => :spec
5
+
6
+ desc "Run all of the specs"
7
+ Spec::Rake::SpecTask.new do |t|
8
+ t.spec_opts = ['--options', "\"spec/spec.opts\""]
9
+ t.fail_on_error = false
10
+ end
11
+
12
+ begin
13
+ require 'jeweler'
14
+ Jeweler::Tasks.new do |s|
15
+ s.name = "moqueue"
16
+ s.summary = "Mocktacular Companion to AMQP Library. Happy TATFTing!"
17
+ s.email = "dan@kallistec.com"
18
+ s.homepage = "http://github.com/danielsdeleo/moqueue"
19
+ s.description = "Mocktacular Companion to AMQP Library. Happy TATFTing!"
20
+ s.authors = ["Daniel DeLeo"]
21
+ s.files = FileList["[A-Za-z]*", "{lib,spec}/**/*"]
22
+ s.rubyforge_project = "moqueue"
23
+ s.add_dependency("amqp")
24
+ end
25
+ rescue LoadError
26
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
27
+ end
28
+
29
+ # These are new tasks
30
+ begin
31
+ require 'jeweler/rubyforge_tasks'
32
+ require 'rake/contrib/sshpublisher'
33
+
34
+ Jeweler::RubyforgeTasks.new
35
+
36
+ namespace :rubyforge do
37
+
38
+ desc "Release gem and RDoc documentation to RubyForge"
39
+ task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
40
+
41
+ namespace :release do
42
+ desc "Publish RDoc to RubyForge."
43
+ task :docs => [:rdoc] do
44
+ config = YAML.load(
45
+ File.read(File.expand_path('~/.rubyforge/user-config.yml'))
46
+ )
47
+
48
+ host = "#{config['username']}@rubyforge.org"
49
+ remote_dir = "/var/www/gforge-projects/moqueue/"
50
+ local_dir = 'rdoc'
51
+
52
+ Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
53
+ end
54
+ end
55
+ end
56
+ rescue LoadError
57
+ puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
58
+ end
59
+
60
+ Rake::RDocTask.new do |rd|
61
+ rd.main = "README.rdoc"
62
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
63
+ end
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 1
3
+ :patch: 4
4
+ :major: 0
@@ -0,0 +1,15 @@
1
+ unless defined?(MOQUEUE_ROOT)
2
+ MOQUEUE_ROOT = File.dirname(__FILE__) + "/"
3
+ end
4
+ require MOQUEUE_ROOT + "moqueue/fibers18"
5
+
6
+ require MOQUEUE_ROOT + "moqueue/mock_exchange"
7
+ require MOQUEUE_ROOT + "moqueue/mock_queue"
8
+ require MOQUEUE_ROOT + "moqueue/mock_headers"
9
+ require MOQUEUE_ROOT + "moqueue/mock_broker"
10
+
11
+ require MOQUEUE_ROOT + "moqueue/object_methods"
12
+ require MOQUEUE_ROOT + "moqueue/matchers"
13
+
14
+ module Moqueue
15
+ end
@@ -0,0 +1,57 @@
1
+ # Courtesy Aman Gupta's (original) em-spec library
2
+ unless defined? Fiber
3
+ require 'thread'
4
+
5
+ class FiberError < StandardError; end
6
+
7
+ class Fiber
8
+ def initialize
9
+ raise ArgumentError, 'new Fiber requires a block' unless block_given?
10
+
11
+ @yield = Queue.new
12
+ @resume = Queue.new
13
+
14
+ @thread = Thread.new{ @yield.push [ *yield(*@resume.pop) ] }
15
+ @thread.abort_on_exception = true
16
+ @thread[:fiber] = self
17
+ end
18
+ attr_reader :thread
19
+
20
+ def alive?
21
+ @thread.alive?
22
+ end
23
+
24
+ def resume *args
25
+ raise FiberError, 'dead fiber called' unless @thread.alive?
26
+ raise FiberError, 'double resume' if @thread == Thread.current
27
+ @resume.push(args)
28
+ result = @yield.pop
29
+ result.size > 1 ? result : result.first
30
+ end
31
+
32
+ def resume!
33
+ @resume.push []
34
+ end
35
+
36
+ def yield *args
37
+ @yield.push(args)
38
+ result = @resume.pop
39
+ result.size > 1 ? result : result.first
40
+ end
41
+
42
+ def self.yield *args
43
+ raise FiberError, "can't yield from root fiber" unless fiber = Thread.current[:fiber]
44
+ fiber.yield(*args)
45
+ end
46
+
47
+ def self.current
48
+ Thread.current[:fiber] or raise FiberError, 'not inside a fiber'
49
+ end
50
+
51
+ def inspect
52
+ "#<#{self.class}:0x#{self.object_id.to_s(16)}>"
53
+ end
54
+ end
55
+ else
56
+ require 'fiber' unless Fiber.respond_to?(:current)
57
+ end
@@ -0,0 +1,113 @@
1
+ module Moqueue
2
+ module Matchers
3
+
4
+ class HasReceived
5
+
6
+ def initialize(expected_msg)
7
+ @expected_msg = expected_msg
8
+ end
9
+
10
+ def matches?(queue)
11
+ if queue.respond_to?(:received_message?)
12
+ @queue = queue
13
+ @queue.received_message?(@expected_msg)
14
+ else
15
+ raise NoMethodError,
16
+ "Grrr. you can't use ``should have_received_message'' on #{queue.inspect} " +
17
+ "because it doesn't respond_to :received_message?"
18
+ end
19
+ end
20
+
21
+ def failure_message_for_should
22
+ "expected #{@queue.inspect} to have received message ``#{@expected_msg}''"
23
+ end
24
+
25
+ def failure_message_for_should_not
26
+ "expected #{@queue.inspect} to not have received message ``#{@expected_msg}''"
27
+ end
28
+
29
+ end
30
+
31
+ class HasAcked
32
+
33
+ def initialize(msg_expecting_ack)
34
+ @msg_expecting_ack = msg_expecting_ack
35
+ end
36
+
37
+ def matches?(queue_or_exchange)
38
+ if queue_or_exchange.respond_to?(:received_ack_for_message?)
39
+ @queue_or_exchange = queue_or_exchange
40
+ @queue_or_exchange.received_ack_for_message?(@msg_expecting_ack)
41
+ else
42
+ raise NoMethodError,
43
+ "Grrr. you can't use ``should have_received_ack_for'' on #{queue_or_exchange.inspect} " +
44
+ "because it doesn't respond_to :received_ack_for_message?"
45
+ end
46
+ end
47
+
48
+ def failure_message_for_should
49
+ "expected #{@queue_or_exchange.inspect} to have received an ack for the message ``#{@msg_expecting_ack}''"
50
+ end
51
+
52
+ def failure_message_for_should_not
53
+ "expected #{@queue_or_exchange.inspect} to not have received an ack for the message ``#{@msg_expecting_ack}''"
54
+ end
55
+ end
56
+
57
+ class HasExactRoutingKey
58
+
59
+ def initialize(expected_key)
60
+ @expected_key = expected_key
61
+ end
62
+
63
+ def matches?(queue)
64
+ if queue.respond_to?(:received_routing_key?)
65
+ @queue = queue
66
+ @queue.received_routing_key?(@expected_key)
67
+ else
68
+ raise NoMethodError,
69
+ "Grrr. you can't use ``should have_received_routing_key'' on #{queue.inspect} " +
70
+ "because it doesn't respond_to :received_routing_key?"
71
+ end
72
+ end
73
+
74
+ def failure_message_for_should
75
+ "expected #{@queue.inspect} to have received header with routing key ``#{@expected_msg}''"
76
+ end
77
+
78
+ def failure_message_for_should_not
79
+ "expected #{@queue.inspect} to not have received header with routing key ``#{@expected_msg}''"
80
+ end
81
+
82
+ end
83
+
84
+ def have_received_message(expected_msg)
85
+ HasReceived.new(expected_msg)
86
+ end
87
+
88
+ def have_received_ack_for(expected_msg)
89
+ HasAcked.new(expected_msg)
90
+ end
91
+
92
+ # Customer matcher for verifying a message was received with a specific routing key
93
+ # (matches exactly, no wildcards)
94
+ #
95
+ # queue.bind(exchange).subscribe {|msg| msg}
96
+ # exchange.publish msg, :key => 'foo.bar.baz'
97
+ # queue.should have_received_routing_key('foo.bar.baz')
98
+ #
99
+ def have_received_exact_routing_key(expected_key)
100
+ HasExactRoutingKey.new(expected_key)
101
+ end
102
+
103
+ alias_method :have_received, :have_received_message
104
+ alias_method :have_ack_for, :have_received_ack_for
105
+ alias_method :have_exact_routing_key, :have_received_exact_routing_key
106
+ end
107
+ end
108
+
109
+ if defined?(::Spec::Runner)
110
+ Spec::Runner.configure do |config|
111
+ config.include(::Moqueue::Matchers)
112
+ end
113
+ end
@@ -0,0 +1,53 @@
1
+ require "singleton"
2
+
3
+ module Moqueue
4
+ class MockBroker
5
+ include Singleton
6
+
7
+ attr_reader :registered_queues
8
+
9
+ def initialize
10
+ reset!
11
+ end
12
+
13
+ def reset!
14
+ @registered_queues = {}
15
+ @registered_direct_exchanges = {}
16
+ @registered_topic_exchanges = {}
17
+ @registered_fanout_exchanges = {}
18
+ end
19
+
20
+ def find_queue(name)
21
+ @registered_queues[name]
22
+ end
23
+
24
+ def register_queue(queue)
25
+ @registered_queues[queue.name] = queue
26
+ end
27
+
28
+ def register_direct_exchange(exchange)
29
+ @registered_direct_exchanges[exchange.direct] = exchange
30
+ end
31
+
32
+ def find_direct_exchange(name)
33
+ @registered_direct_exchanges[name]
34
+ end
35
+
36
+ def register_topic_exchange(exchange)
37
+ @registered_topic_exchanges[exchange.topic] = exchange
38
+ end
39
+
40
+ def find_topic_exchange(topic)
41
+ @registered_topic_exchanges[topic]
42
+ end
43
+
44
+ def register_fanout_exchange(exchange)
45
+ @registered_fanout_exchanges[exchange.fanout] = exchange
46
+ end
47
+
48
+ def find_fanout_exchange(fanout_name)
49
+ @registered_fanout_exchanges[fanout_name]
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,147 @@
1
+ module Moqueue
2
+
3
+ class MockExchange
4
+ attr_reader :topic, :fanout, :direct
5
+
6
+ class << self
7
+
8
+ def new(opts={})
9
+ if opts[:topic] && topic_exchange = MockBroker.instance.find_topic_exchange(opts[:topic])
10
+ return topic_exchange
11
+ end
12
+
13
+ if opts[:fanout] && fanout = MockBroker.instance.find_fanout_exchange(opts[:fanout])
14
+ return fanout
15
+ end
16
+
17
+ if opts[:direct] && direct = MockBroker.instance.find_direct_exchange(opts[:direct])
18
+ return direct
19
+ end
20
+
21
+ super
22
+ end
23
+
24
+ end
25
+
26
+ def initialize(opts={})
27
+ if @topic = opts[:topic]
28
+ MockBroker.instance.register_topic_exchange(self)
29
+ elsif @fanout = opts[:fanout]
30
+ MockBroker.instance.register_fanout_exchange(self)
31
+ elsif @direct = opts[:direct]
32
+ MockBroker.instance.register_direct_exchange(self)
33
+ end
34
+ end
35
+
36
+ def attached_queues
37
+ @attached_queues ||= []
38
+ end
39
+
40
+ def acked_messages
41
+ attached_queues.map do |q|
42
+ q = q.first if q.kind_of?(Array)
43
+ q.acked_messages
44
+ end.flatten
45
+ end
46
+
47
+ def attach_queue(queue, opts={})
48
+ if topic
49
+ attached_queues << [queue, TopicBindingKey.new(opts[:key])]
50
+ elsif direct
51
+ attached_queues << [queue, DirectBindingKey.new(opts[:key])]
52
+ else
53
+ attached_queues << queue
54
+ end
55
+ end
56
+
57
+ def publish(message, opts={})
58
+ require_routing_key(opts) if topic
59
+ matching_queues(opts).each do |q|
60
+ q.receive(message, prepare_header_opts(opts))
61
+ end
62
+ end
63
+
64
+ def received_ack_for_message?(message)
65
+ acked_messages.include?(message)
66
+ end
67
+
68
+ private
69
+
70
+ def routing_keys_match?(binding_key, message_key)
71
+ if topic
72
+ TopicBindingKey.new(binding_key).matches?(message_key)
73
+ elsif direct
74
+ DirectBindingKey.new(binding_key).matches?(message_key)
75
+ end
76
+ end
77
+
78
+ def matching_queues(opts={})
79
+ return attached_queues unless topic || direct
80
+ attached_queues.map {|q, binding| binding.matches?(opts[:key]) ? q : nil}.compact
81
+ end
82
+
83
+ def prepare_header_opts(opts={})
84
+ header_opts = opts.dup
85
+ if routing_key = header_opts.delete(:key)
86
+ header_opts[:routing_key] = routing_key
87
+ end
88
+ header_opts
89
+ end
90
+
91
+ def require_routing_key(opts={})
92
+ unless opts.has_key?(:key)
93
+ raise ArgumentError, "you must provide a key when publishing to a topic exchange"
94
+ end
95
+ end
96
+
97
+ public
98
+
99
+ module BaseKey
100
+ attr_reader :key
101
+
102
+ def ==(other)
103
+ other.respond_to?(:key) && other.key == @key
104
+ end
105
+ end
106
+
107
+ class TopicBindingKey
108
+ include BaseKey
109
+
110
+ def initialize(key_string)
111
+ @key = key_string.to_s.split(".")
112
+ end
113
+
114
+ def matches?(message_key)
115
+ message_key, binding_key = message_key.split("."), key.dup
116
+
117
+ match = true
118
+ while match
119
+ binding_token, message_token = binding_key.shift, message_key.shift
120
+ break if (binding_token.nil? && message_token.nil?) || (binding_token == "#")
121
+ match = ((binding_token == message_token) || (binding_token == '*') || (message_token == '*'))
122
+ end
123
+ match
124
+ end
125
+
126
+ end
127
+
128
+ # Requires an *exact* match
129
+ class DirectBindingKey
130
+ include BaseKey
131
+
132
+ def initialize(key_string)
133
+ @key = key_string.to_s
134
+ end
135
+
136
+ def matches?(message_key)
137
+ message_key, binding_key = message_key.to_s, key.dup
138
+
139
+ # looking for string equivalence
140
+ message_key == binding_key
141
+ end
142
+
143
+ end
144
+
145
+ end
146
+
147
+ end