larrytheliquid-moqueue 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,57 @@
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
+ == TODOs
48
+ * Mocha-style expectation syntax, i.e. <tt>queue.expects_message("yessss").with_routing_key("awesome.yesItIs")</tt>
49
+ * Support for RPC exchanges
50
+ * Make it pretty on the inside.
51
+
52
+ == Moar
53
+ 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/
54
+
55
+ 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.
56
+
57
+ 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
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require "spec/rake/spectask"
2
+
3
+ task :default => :spec
4
+
5
+ desc "Run all of the specs"
6
+ Spec::Rake::SpecTask.new do |t|
7
+ t.spec_opts = ['--options', "\"spec/spec.opts\""]
8
+ t.fail_on_error = false
9
+ end
10
+
11
+ begin
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |s|
14
+ s.name = "moqueue"
15
+ s.summary = "Mocktacular Companion to AMQP Library. Happy TATFTing!"
16
+ s.email = "dan@kallistec.com"
17
+ s.homepage = "http://github.com/danielsdeleo/moqueue"
18
+ s.description = "Mocktacular Companion to AMQP Library. Happy TATFTing!"
19
+ s.authors = ["Daniel DeLeo"]
20
+ s.files = FileList["[A-Za-z]*", "{lib,spec}/**/*"]
21
+ end
22
+ rescue LoadError
23
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
24
+ end
25
+
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 2
@@ -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,74 @@
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
+ def have_received_message(expected_msg)
58
+ HasReceived.new(expected_msg)
59
+ end
60
+
61
+ def have_received_ack_for(expected_msg)
62
+ HasAcked.new(expected_msg)
63
+ end
64
+
65
+ alias_method :have_received, :have_received_message
66
+ alias_method :have_ack_for, :have_received_ack_for
67
+ end
68
+ end
69
+
70
+ if defined?(::Spec::Runner)
71
+ Spec::Runner.configure do |config|
72
+ config.include(::Moqueue::Matchers)
73
+ end
74
+ end
@@ -0,0 +1,44 @@
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_topic_exchanges = {}
16
+ @registered_fanout_exchanges = {}
17
+ end
18
+
19
+ def find_queue(name)
20
+ @registered_queues[name]
21
+ end
22
+
23
+ def register_queue(queue)
24
+ @registered_queues[queue.name] = queue
25
+ end
26
+
27
+ def register_topic_exchange(exchange)
28
+ @registered_topic_exchanges[exchange.topic] = exchange
29
+ end
30
+
31
+ def find_topic_exchange(topic)
32
+ @registered_topic_exchanges[topic]
33
+ end
34
+
35
+ def register_fanout_exchange(exchange)
36
+ @registered_fanout_exchanges[exchange.fanout] = exchange
37
+ end
38
+
39
+ def find_fanout_exchange(fanout_name)
40
+ @registered_fanout_exchanges[fanout_name]
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,113 @@
1
+ module Moqueue
2
+
3
+ class MockExchange
4
+ attr_reader :topic, :fanout
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
+ super
18
+ end
19
+
20
+ end
21
+
22
+ def initialize(opts={})
23
+ if @topic = opts[:topic]
24
+ MockBroker.instance.register_topic_exchange(self)
25
+ elsif @fanout = opts[:fanout]
26
+ MockBroker.instance.register_fanout_exchange(self)
27
+ end
28
+ end
29
+
30
+ def attached_queues
31
+ @attached_queues ||= []
32
+ end
33
+
34
+ def acked_messages
35
+ attached_queues.map do |q|
36
+ q = q.first if q.kind_of?(Array)
37
+ q.acked_messages
38
+ end.flatten
39
+ end
40
+
41
+ def attach_queue(queue, opts={})
42
+ if topic
43
+ attached_queues << [queue, BindingKey.new(opts[:key])]
44
+ else
45
+ attached_queues << queue
46
+ end
47
+ end
48
+
49
+ def publish(message, opts={})
50
+ require_routing_key(opts) if topic
51
+ matching_queues(opts).each do |q|
52
+ q.receive(message, prepare_header_opts(opts))
53
+ end
54
+ end
55
+
56
+ def received_ack_for_message?(message)
57
+ acked_messages.include?(message)
58
+ end
59
+
60
+ private
61
+
62
+ def routing_keys_match?(binding_key, message_key)
63
+ BindingKey.new(binding_key).matches?(message_key)
64
+ end
65
+
66
+ def matching_queues(opts={})
67
+ return attached_queues unless topic
68
+ attached_queues.map {|q, binding| binding.matches?(opts[:key]) ? q : nil}.compact
69
+ end
70
+
71
+ def prepare_header_opts(opts={})
72
+ header_opts = opts.dup
73
+ if routing_key = header_opts.delete(:key)
74
+ header_opts[:routing_key] = routing_key
75
+ end
76
+ header_opts
77
+ end
78
+
79
+ def require_routing_key(opts={})
80
+ unless opts.has_key?(:key)
81
+ raise ArgumentError, "you must provide a key when publishing to a topic exchange"
82
+ end
83
+ end
84
+
85
+ public
86
+
87
+ class BindingKey
88
+ attr_reader :key
89
+
90
+ def initialize(key_string)
91
+ @key = key_string.to_s.split(".")
92
+ end
93
+
94
+ def ==(other)
95
+ other.respond_to?(:key) && other.key == @key
96
+ end
97
+
98
+ def matches?(message_key)
99
+ message_key, binding_key = message_key.split("."), key.dup
100
+
101
+ match = true
102
+ while match
103
+ binding_token, message_token = binding_key.shift, message_key.shift
104
+ break if (binding_token.nil? && message_token.nil?) || (binding_token == "#")
105
+ match = ((binding_token == message_token) || (binding_token == '*') || (message_token == '*'))
106
+ end
107
+ match
108
+ end
109
+
110
+ end
111
+ end
112
+
113
+ end
@@ -0,0 +1,31 @@
1
+ module Moqueue
2
+
3
+ class MockHeaders
4
+ attr_accessor :size, :weight
5
+
6
+ def initialize(properties={})
7
+ @properties = properties
8
+ end
9
+
10
+ def ack
11
+ @received_ack = true
12
+ end
13
+
14
+ def received_ack?
15
+ @received_ack || false
16
+ end
17
+
18
+ def properties
19
+ @properties
20
+ end
21
+
22
+ def to_frame
23
+ nil
24
+ end
25
+
26
+ def method_missing method, *args, &blk
27
+ @properties.has_key?(method) ? @properties[method] : super
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,126 @@
1
+ module Moqueue
2
+
3
+ class DoubleSubscribeError < StandardError
4
+ end
5
+
6
+ class MockQueue
7
+ attr_reader :name
8
+
9
+ class << self
10
+
11
+ def new(name)
12
+ if existing_queue = MockBroker.instance.find_queue(name)
13
+ return existing_queue
14
+ end
15
+ super
16
+ end
17
+
18
+ end
19
+
20
+ def initialize(name)
21
+ @name = name
22
+ MockBroker.instance.register_queue(self)
23
+ end
24
+
25
+ def subscribe(opts={}, &block)
26
+ if @subscribe_block
27
+ raise DoubleSubscribeError, "you can't subscribe to the same queue twice"
28
+ end
29
+ @subscribe_block = block
30
+ @ack_msgs = opts[:ack] || false
31
+ process_unhandled_messages
32
+ end
33
+
34
+ def receive(message, header_opts={})
35
+ if callback = message_handler_callback
36
+ headers = MockHeaders.new(header_opts)
37
+ callback.call(*(callback.arity == 1 ? [message] : [headers, message]))
38
+ received_messages_and_headers << {:message => message, :headers => headers}
39
+ else
40
+ receive_message_later(message, header_opts)
41
+ end
42
+ end
43
+
44
+ def received_message?(message_content)
45
+ received_messages.include?(message_content)
46
+ end
47
+
48
+ def unsubscribe
49
+ true
50
+ end
51
+
52
+ def received_ack_for_message?(message_content)
53
+ acked_messages.include?(message_content)
54
+ end
55
+
56
+ def publish(message, opts = {})
57
+ if message_handler_callback
58
+ receive(message)
59
+ else
60
+ deferred_publishing_fibers << Fiber.new do
61
+ receive(message)
62
+ end
63
+ end
64
+ end
65
+
66
+ def bind(exchange, key=nil)
67
+ exchange.attach_queue(self, key)
68
+ self
69
+ end
70
+
71
+ def received_messages_and_headers
72
+ @received_messages_and_headers ||= []
73
+ end
74
+
75
+ def received_messages
76
+ received_messages_and_headers.map{|r| r[:message] }
77
+ end
78
+
79
+ def acked_messages
80
+ received_messages_and_headers.map do |r|
81
+ r[:message] if @ack_msgs && r[:headers].received_ack?
82
+ end
83
+ end
84
+
85
+ def run_callback(*args)
86
+ callback = message_handler_callback
87
+ callback.call(*(callback.arity == 1 ? [args.first] : args))
88
+ end
89
+
90
+ def callback_defined?
91
+ !!message_handler_callback
92
+ end
93
+
94
+ # configures a do-nothing subscribe block to force
95
+ # received messages to be processed and stored in
96
+ # #received_messages
97
+ def null_subscribe
98
+ subscribe {|msg| nil}
99
+ self
100
+ end
101
+
102
+ private
103
+
104
+ def receive_message_later(message, header_opts)
105
+ deferred_publishing_fibers << Fiber.new do
106
+ self.receive(message, header_opts)
107
+ end
108
+ end
109
+
110
+ def deferred_publishing_fibers
111
+ @deferred_publishing_fibers ||= []
112
+ end
113
+
114
+ def message_handler_callback
115
+ @subscribe_block || @pop_block || false
116
+ end
117
+
118
+ def process_unhandled_messages
119
+ while fiber = deferred_publishing_fibers.shift
120
+ fiber.resume
121
+ end
122
+ end
123
+
124
+ end
125
+
126
+ end
@@ -0,0 +1,31 @@
1
+ module Moqueue
2
+
3
+ module ObjectMethods
4
+ def mock_queue_and_exchange(name=nil)
5
+ queue = mock_queue(name)
6
+ exchange = mock_exchange
7
+ exchange.attached_queues << queue
8
+ [queue, exchange]
9
+ end
10
+
11
+ def mock_queue(name=nil)
12
+ MockQueue.new(name || "anonymous-#{rand(2**32).to_s(16)}")
13
+ end
14
+
15
+ def mock_exchange(opts={})
16
+ MockExchange.new(opts)
17
+ end
18
+
19
+ def overload_amqp
20
+ require MOQUEUE_ROOT + "moqueue/overloads"
21
+ end
22
+
23
+ def reset_broker
24
+ MockBroker.instance.reset!
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+
31
+ Object.send(:include, Moqueue::ObjectMethods)
@@ -0,0 +1,52 @@
1
+ require "eventmachine"
2
+
3
+ class MQ
4
+
5
+ class << self
6
+ def queue(name)
7
+ Moqueue::MockQueue.new(name)
8
+ end
9
+
10
+ def fanout(name, opts={})
11
+ Moqueue::MockExchange.new(opts.merge(:fanout=>name))
12
+ end
13
+
14
+ end
15
+
16
+ def initialize(*args)
17
+ end
18
+
19
+ def fanout(name, opts = {})
20
+ Moqueue::MockExchange.new(opts.merge(:fanout => name))
21
+ end
22
+
23
+ def queue(name, opts = {})
24
+ Moqueue::MockQueue.new(name)
25
+ end
26
+
27
+ def topic(topic_name)
28
+ Moqueue::MockExchange.new(:topic=>topic_name)
29
+ end
30
+
31
+ end
32
+
33
+ module AMQP
34
+
35
+ class << self
36
+ attr_reader :closing
37
+ alias :closing? :closing
38
+ end
39
+
40
+ def self.start(opts={},&block)
41
+ EM.run(&block)
42
+ end
43
+
44
+ def self.stop
45
+ @closing = true
46
+ yield if block_given?
47
+ @closing = false
48
+ end
49
+
50
+ def self.connect(*args)
51
+ end
52
+ end
data/lib/moqueue.rb ADDED
@@ -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
data/moqueue.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{moqueue}
5
+ s.version = "0.1.2"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Daniel DeLeo"]
9
+ s.date = %q{2009-07-24}
10
+ s.description = %q{Mocktacular Companion to AMQP Library. Happy TATFTing!}
11
+ s.email = %q{dan@kallistec.com}
12
+ s.extra_rdoc_files = ["README.rdoc"]
13
+ s.files = ["lib", "moqueue.gemspec", "Rakefile", "README.rdoc", "spec", "VERSION.yml", "lib/moqueue", "lib/moqueue/fibers18.rb", "lib/moqueue/matchers.rb", "lib/moqueue/mock_broker.rb", "lib/moqueue/mock_exchange.rb", "lib/moqueue/mock_headers.rb", "lib/moqueue/mock_queue.rb", "lib/moqueue/object_methods.rb", "lib/moqueue/overloads.rb", "lib/moqueue.rb", "spec/examples", "spec/examples/ack_spec.rb", "spec/examples/basic_usage_spec.rb", "spec/examples/example_helper.rb", "spec/examples/logger_spec.rb", "spec/examples/ping_pong_spec.rb", "spec/examples/stocks_spec.rb", "spec/spec.opts", "spec/spec_helper.rb", "spec/unit", "spec/unit/matchers_spec.rb", "spec/unit/mock_broker_spec.rb", "spec/unit/mock_exchange_spec.rb", "spec/unit/mock_headers_spec.rb", "spec/unit/mock_queue_spec.rb", "spec/unit/moqueue_spec.rb", "spec/unit/object_methods_spec.rb", "spec/unit/overloads_spec.rb"]
14
+ s.homepage = %q{http://github.com/danielsdeleo/moqueue}
15
+ s.rdoc_options = ["--inline-source", "--charset=UTF-8"]
16
+ s.require_paths = ["lib"]
17
+ s.rubygems_version = %q{1.3.3}
18
+ s.summary = %q{Mocktacular Companion to AMQP Library. Happy TATFTing!}
19
+
20
+ if s.respond_to? :specification_version then
21
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
22
+ s.specification_version = 3
23
+
24
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
25
+ else
26
+ end
27
+ else
28
+ end
29
+ end