├── .gitignore ├── CONTRIBUTORS.rdoc ├── Gemfile ├── LICENSE ├── README.rdoc ├── Rakefile ├── VERSION.yml ├── lib ├── moqueue.rb └── moqueue │ ├── fibers18.rb │ ├── matchers.rb │ ├── mock_broker.rb │ ├── mock_exchange.rb │ ├── mock_headers.rb │ ├── mock_queue.rb │ ├── object_methods.rb │ └── overloads.rb ├── moqueue.gemspec └── spec ├── examples ├── ack_spec.rb ├── basic_usage_spec.rb ├── example_helper.rb ├── logger_spec.rb ├── ping_pong_spec.rb └── stocks_spec.rb ├── spec.opts ├── spec_helper.rb └── unit ├── matchers_spec.rb ├── mock_broker_spec.rb ├── mock_exchange_spec.rb ├── mock_headers_spec.rb ├── mock_queue_spec.rb ├── moqueue_spec.rb ├── object_methods_spec.rb └── overloads_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | rdoc/* 3 | Gemfile.lock 4 | -------------------------------------------------------------------------------- /CONTRIBUTORS.rdoc: -------------------------------------------------------------------------------- 1 | = Contributors 2 | * larrytheliquid (Larry Diehl) & Engine Yard http://github.com/larrytheliquid 3 | * mattmatt (Mathias Meyer) & Peritor Consulting http://github.com/mattmatt -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | source "https://rubygems.org" 3 | 4 | gemspec 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Daniel DeLeo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 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 | => # 11 | 12 | queue = mq.queue("mocktacular") 13 | => # 14 | 15 | topic = mq.topic("lolz") 16 | => # 17 | 18 | queue.bind(topic, :key=> "cats.*") 19 | => # 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 AMQP.start or EM.run. 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, #overload_amqp does overload the AMQP.start 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 assert(): 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 #publish and #subscribe 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 :ack=>true 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 -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 | rd.rdoc_dir = "rdoc" 64 | end -------------------------------------------------------------------------------- /VERSION.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :minor: 1 3 | :patch: 4 4 | :major: 0 5 | -------------------------------------------------------------------------------- /lib/moqueue.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/moqueue/fibers18.rb: -------------------------------------------------------------------------------- 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 58 | -------------------------------------------------------------------------------- /lib/moqueue/matchers.rb: -------------------------------------------------------------------------------- 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 | if Spec::Runner.respond_to?(:configure) 111 | Spec::Runner.configure do |config| 112 | config.include(::Moqueue::Matchers) 113 | end 114 | elsif defined?(::Rspec) && Rspec.respond_to?(:configure) 115 | Rspec.configure do |config| 116 | config.include(::Moqueue::Matchers) 117 | end 118 | end 119 | end -------------------------------------------------------------------------------- /lib/moqueue/mock_broker.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/moqueue/mock_exchange.rb: -------------------------------------------------------------------------------- 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 unless attached_queues.include?(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 148 | -------------------------------------------------------------------------------- /lib/moqueue/mock_headers.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/moqueue/mock_queue.rb: -------------------------------------------------------------------------------- 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 received_routing_key?(key) 49 | received_messages_and_headers.find { |r| r[:headers] && r[:headers].properties[:routing_key] == key } 50 | end 51 | 52 | def unsubscribe 53 | true 54 | end 55 | 56 | def prefetch(size) 57 | # noop 58 | end 59 | 60 | def received_ack_for_message?(message_content) 61 | acked_messages.include?(message_content) 62 | end 63 | 64 | def publish(message, opts = {}) 65 | if message_handler_callback 66 | receive(message) 67 | else 68 | deferred_publishing_fibers << Fiber.new do 69 | receive(message) 70 | end 71 | end 72 | end 73 | 74 | def bind(exchange, key=nil) 75 | exchange.attach_queue(self, key) 76 | self 77 | end 78 | 79 | def received_messages_and_headers 80 | @received_messages_and_headers ||= [] 81 | end 82 | 83 | def received_messages 84 | received_messages_and_headers.map{|r| r[:message] } 85 | end 86 | 87 | def received_headers 88 | received_messages_and_headers.map{ |r| r[:headers] } 89 | end 90 | 91 | def acked_messages 92 | received_messages_and_headers.map do |r| 93 | r[:message] if @ack_msgs && r[:headers].received_ack? 94 | end 95 | end 96 | 97 | def run_callback(*args) 98 | callback = message_handler_callback 99 | callback.call(*(callback.arity == 1 ? [args.first] : args)) 100 | end 101 | 102 | def callback_defined? 103 | !!message_handler_callback 104 | end 105 | 106 | # configures a do-nothing subscribe block to force 107 | # received messages to be processed and stored in 108 | # #received_messages 109 | def null_subscribe 110 | subscribe {|msg| nil} 111 | self 112 | end 113 | 114 | private 115 | 116 | def receive_message_later(message, header_opts) 117 | deferred_publishing_fibers << Fiber.new do 118 | self.receive(message, header_opts) 119 | end 120 | end 121 | 122 | def deferred_publishing_fibers 123 | @deferred_publishing_fibers ||= [] 124 | end 125 | 126 | def message_handler_callback 127 | @subscribe_block || @pop_block || false 128 | end 129 | 130 | def process_unhandled_messages 131 | while fiber = deferred_publishing_fibers.shift 132 | fiber.resume 133 | end 134 | end 135 | 136 | end 137 | 138 | end 139 | -------------------------------------------------------------------------------- /lib/moqueue/object_methods.rb: -------------------------------------------------------------------------------- 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 | # Takes a string name as a parameter. Each queue name may only be used 12 | # once. Multiple calls to #mock_queue with the same +name+ will return 13 | # the same object. 14 | def mock_queue(name=nil) 15 | MockQueue.new(name || "anonymous-#{rand(2**32).to_s(16)}") 16 | end 17 | 18 | # Takes a hash to specify the exchange type and its name. 19 | # 20 | # topic = mock_exchange(:topic => 'topic exchange') 21 | def mock_exchange(opts={}) 22 | MockExchange.new(opts) 23 | end 24 | 25 | # Overloads the class-level method calls typically used by AMQP code 26 | # such as MQ.direct, MQ.queue, MQ.topic, etc. 27 | def overload_amqp 28 | require MOQUEUE_ROOT + "moqueue/overloads" 29 | end 30 | 31 | # Deletes all exchanges and queues from the mock broker. As a consequence of 32 | # removing queues, all bindings and subscriptions are also deleted. 33 | def reset_broker 34 | MockBroker.instance.reset! 35 | end 36 | 37 | end 38 | 39 | end 40 | 41 | Object.send(:include, Moqueue::ObjectMethods) -------------------------------------------------------------------------------- /lib/moqueue/overloads.rb: -------------------------------------------------------------------------------- 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 direct(name, opts={}) 11 | Moqueue::MockExchange.new(opts.merge(:direct=>name)) 12 | end 13 | 14 | def fanout(name, opts={}) 15 | Moqueue::MockExchange.new(opts.merge(:fanout=>name)) 16 | end 17 | 18 | end 19 | 20 | def initialize(*args) 21 | end 22 | 23 | def direct(name, opts = {}) 24 | Moqueue::MockExchange.new(opts.merge(:direct => name)) 25 | end 26 | 27 | def fanout(name, opts = {}) 28 | Moqueue::MockExchange.new(opts.merge(:fanout => name)) 29 | end 30 | 31 | def queue(name, opts = {}) 32 | Moqueue::MockQueue.new(name) 33 | end 34 | 35 | def topic(topic_name) 36 | Moqueue::MockExchange.new(:topic=>topic_name) 37 | end 38 | 39 | end 40 | 41 | module AMQP 42 | 43 | class << self 44 | attr_reader :closing 45 | alias :closing? :closing 46 | end 47 | 48 | def self.start(opts={},&block) 49 | EM.run(&block) 50 | end 51 | 52 | def self.stop 53 | @closing = true 54 | yield if block_given? 55 | @closing = false 56 | end 57 | 58 | def self.connect(*args) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /moqueue.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec` 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{moqueue} 8 | s.version = "0.1.4" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Daniel DeLeo"] 12 | s.date = %q{2009-09-21} 13 | s.description = %q{Mocktacular Companion to AMQP Library. Happy TATFTing!} 14 | s.license = "MIT" 15 | s.email = %q{dan@kallistec.com} 16 | s.extra_rdoc_files = [ 17 | "README.rdoc" 18 | ] 19 | s.files = [ 20 | "CONTRIBUTORS.rdoc", 21 | "README.rdoc", 22 | "Rakefile", 23 | "VERSION.yml", 24 | "lib/moqueue.rb", 25 | "lib/moqueue/fibers18.rb", 26 | "lib/moqueue/matchers.rb", 27 | "lib/moqueue/mock_broker.rb", 28 | "lib/moqueue/mock_exchange.rb", 29 | "lib/moqueue/mock_headers.rb", 30 | "lib/moqueue/mock_queue.rb", 31 | "lib/moqueue/object_methods.rb", 32 | "lib/moqueue/overloads.rb", 33 | "moqueue.gemspec", 34 | "spec/examples/ack_spec.rb", 35 | "spec/examples/basic_usage_spec.rb", 36 | "spec/examples/example_helper.rb", 37 | "spec/examples/logger_spec.rb", 38 | "spec/examples/ping_pong_spec.rb", 39 | "spec/examples/stocks_spec.rb", 40 | "spec/spec.opts", 41 | "spec/spec_helper.rb", 42 | "spec/unit/matchers_spec.rb", 43 | "spec/unit/mock_broker_spec.rb", 44 | "spec/unit/mock_exchange_spec.rb", 45 | "spec/unit/mock_headers_spec.rb", 46 | "spec/unit/mock_queue_spec.rb", 47 | "spec/unit/moqueue_spec.rb", 48 | "spec/unit/object_methods_spec.rb", 49 | "spec/unit/overloads_spec.rb" 50 | ] 51 | s.homepage = %q{http://github.com/danielsdeleo/moqueue} 52 | s.rdoc_options = ["--charset=UTF-8"] 53 | s.require_paths = ["lib"] 54 | s.rubyforge_project = %q{moqueue} 55 | s.rubygems_version = %q{1.3.5} 56 | s.summary = %q{Mocktacular Companion to AMQP Library. Happy TATFTing!} 57 | s.test_files = [ 58 | "spec/examples/ack_spec.rb", 59 | "spec/examples/basic_usage_spec.rb", 60 | "spec/examples/example_helper.rb", 61 | "spec/examples/logger_spec.rb", 62 | "spec/examples/ping_pong_spec.rb", 63 | "spec/examples/stocks_spec.rb", 64 | "spec/spec_helper.rb", 65 | "spec/unit/matchers_spec.rb", 66 | "spec/unit/mock_broker_spec.rb", 67 | "spec/unit/mock_exchange_spec.rb", 68 | "spec/unit/mock_headers_spec.rb", 69 | "spec/unit/mock_queue_spec.rb", 70 | "spec/unit/moqueue_spec.rb", 71 | "spec/unit/object_methods_spec.rb", 72 | "spec/unit/overloads_spec.rb" 73 | ] 74 | 75 | if s.respond_to? :specification_version then 76 | s.specification_version = 3 77 | 78 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 79 | s.add_runtime_dependency(%q, [">= 0"]) 80 | else 81 | s.add_dependency(%q, [">= 0"]) 82 | end 83 | else 84 | s.add_dependency(%q, [">= 0"]) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/examples/ack_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/example_helper') 3 | 4 | # NOTE: moqueue currently does not mimic AMQP's behavior of: 5 | # 1) requiring graceful shutdown for acks to be delivered 6 | # 2) returning messages to the queue if not acked 7 | # 3) not processing messages when AMQP isn't "running" 8 | # 9 | # This causes the result of this test to differ from the actual result when run 10 | # with a real broker. The true behavior should be that the 3rd message 11 | # published should be unacknowledged and returned to the queue. In this test, 12 | # all messages get acknowleged 13 | describe Moqueue, "when running the ack example" do 14 | include ExampleHelper 15 | 16 | def run_ack_example(&perform_ack) 17 | AMQP.start(:host => 'localhost') do 18 | MQ.queue('awesome').publish('Totally rad 1') 19 | MQ.queue('awesome').publish('Totally rad 2') 20 | MQ.queue('awesome').publish('Totally rad 3') 21 | 22 | i = 0 23 | 24 | # Stopping after the second item was acked will keep the 3rd item in the queue 25 | MQ.queue('awesome').subscribe(:ack => true) do |h,m| 26 | if (i+=1) == 3 27 | #puts 'Shutting down...' 28 | AMQP.stop{ EM.stop } 29 | end 30 | 31 | if AMQP.closing? 32 | #puts "#{m} (ignored, redelivered later)" 33 | else 34 | #puts "received message: " + m 35 | perform_ack.call(h) 36 | end 37 | end 38 | end 39 | 40 | end 41 | 42 | before(:all) do 43 | overload_amqp 44 | end 45 | 46 | before(:each) do 47 | reset_broker 48 | reset! 49 | end 50 | 51 | it "should get the correct result without errors" do 52 | Timeout::timeout(2) do 53 | run_ack_example {|h| h.ack } 54 | end 55 | q = MQ.queue('awesome') 56 | q.should have(3).acked_messages 57 | q.received_ack_for_message?('Totally rad 1').should be_true 58 | q.received_ack_for_message?('Totally rad 2').should be_true 59 | q.received_ack_for_message?('Totally rad 3').should be_true 60 | end 61 | 62 | it "should be able to ack in an EM.next_tick" do 63 | Timeout::timeout(2) do 64 | run_ack_example do |h| 65 | EM.next_tick { h.ack } 66 | end 67 | end 68 | q = MQ.queue('awesome') 69 | q.should have(3).acked_messages 70 | q.received_ack_for_message?('Totally rad 1').should be_true 71 | q.received_ack_for_message?('Totally rad 2').should be_true 72 | q.received_ack_for_message?('Totally rad 3').should be_true 73 | end 74 | 75 | it "should be able to ack in an EM.defer callback" do 76 | Timeout::timeout(2) do 77 | run_ack_example do |h| 78 | EM.defer(proc { 79 | 1337 80 | }, 81 | proc { |result| 82 | result.should == 1337 83 | h.ack 84 | }) 85 | end 86 | end 87 | q = MQ.queue('awesome') 88 | q.should have(3).acked_messages 89 | q.received_ack_for_message?('Totally rad 1').should be_true 90 | q.received_ack_for_message?('Totally rad 2').should be_true 91 | q.received_ack_for_message?('Totally rad 3').should be_true 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/examples/basic_usage_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe "AMQP", "when mocked out by Moqueue" do 4 | 5 | before(:each) do 6 | reset_broker 7 | end 8 | 9 | it "should have direct exchanges" do 10 | queue = mock_queue("direct-exchanges") 11 | queue.publish("you are correct, sir!") 12 | queue.subscribe { |message| "do something with message" } 13 | queue.received_message?("you are correct, sir!").should be_true 14 | end 15 | 16 | it "should have direct exchanges with acks" do 17 | queue = mock_queue("direct-with-acks") 18 | queue.publish("yessir!") 19 | queue.subscribe(:ack => true) { |headers, message| headers.ack } 20 | queue.received_ack_for_message?("yessir!").should be_true 21 | end 22 | 23 | it "should have topic exchanges" do 24 | topic = mock_exchange(:topic => "TATFT") 25 | queue = mock_queue("rspec-fiend") 26 | queue.bind(topic, :key => "bdd.*").subscribe { |msg| "do something" } 27 | topic.publish("TATFT FTW", :key=> "bdd.4life") 28 | queue.received_message?("TATFT FTW").should be_true 29 | end 30 | 31 | it "should have topic exchanges with acks" do 32 | topic = mock_exchange(:topic => "animals") 33 | queue = mock_queue("cat-lover") 34 | queue.bind(topic, :key => "cats.#").subscribe(:ack => true) do |header, msg| 35 | header.ack 36 | "do something with message" 37 | end 38 | topic.publish("OMG kittehs!", :key => "cats.lolz.kittehs") 39 | topic.received_ack_for_message?("OMG kittehs!").should be_true 40 | end 41 | 42 | it "should have fanout exchanges with acks" do 43 | film = mock_exchange(:fanout => "Godfather") 44 | one_actor = mock_queue("Jack Woltz") 45 | other_actor = mock_queue("Captain McCluskey") 46 | one_actor.bind(film).subscribe(:ack =>true) { |h,msg| h.ack && "horse head" } 47 | other_actor.bind(film).subscribe(:ack => true) { |h,msg| h.ack && "dirty cops" } 48 | offer = "you can't refuse" 49 | film.publish(offer) 50 | one_actor.received_message?(offer).should be_true 51 | other_actor.received_message?(offer).should be_true 52 | film.should have(2).acked_messages 53 | end 54 | 55 | end 56 | 57 | describe Moqueue, "with syntax sugar" do 58 | 59 | before(:each) do 60 | reset_broker 61 | end 62 | 63 | it "counts received messages" do 64 | queue = mock_queue 65 | queue.subscribe { |msg| msg.should_not be_nil } 66 | 5.times {queue.publish("no moar beers kthxbye")} 67 | queue.should have(5).received_messages 68 | end 69 | 70 | it "counts acked messages" do 71 | queue = mock_queue 72 | queue.subscribe(:ack=>true) { |headers,msg| headers.ack } 73 | 5.times { queue.publish("time becomes a loop") } 74 | queue.should have(5).acked_messages 75 | end 76 | 77 | it "makes the callback (#subscribe) block testable" do 78 | emphasis = mock_queue 79 | emphasis.subscribe { |msg| @emphasized = "**" + msg + "**" } 80 | emphasis.run_callback("show emphasis").should == "**show emphasis**" 81 | end 82 | 83 | end 84 | 85 | describe Moqueue, "when using custom rspec matchers" do 86 | 87 | it "should accept syntax like queue.should have_received('a message')" do 88 | queue = mock_queue("sugary") 89 | queue.subscribe { |msg| "eat the message" } 90 | queue.publish("a message") 91 | queue.should have_received("a message") 92 | end 93 | 94 | it "should accept syntax like queue_or_exchange.should have_ack_for('a message')" do 95 | queue = mock_queue("more sugar") 96 | queue.subscribe(:ack => true) { |headers, msg| headers.ack } 97 | queue.publish("another message") 98 | queue.should have_ack_for("another message") 99 | end 100 | 101 | end -------------------------------------------------------------------------------- /spec/examples/example_helper.rb: -------------------------------------------------------------------------------- 1 | module ExampleHelper 2 | def capture_output(*args) 3 | @captured_output << args 4 | end 5 | 6 | def counter 7 | @counter += 1 8 | EM.stop {AMQP.stop} if @counter >= 2 9 | @counter 10 | end 11 | 12 | def reset! 13 | @counter, @captured_output = 0, [] 14 | end 15 | 16 | end -------------------------------------------------------------------------------- /spec/examples/logger_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe Moqueue, "when running the logger example" do 4 | 5 | class MyLoggerRulez 6 | def initialize *args, &block 7 | opts = args.pop if args.last.is_a? Hash 8 | opts ||= {} 9 | 10 | printer(block) if block 11 | 12 | @prop = opts 13 | @tags = ([:timestamp] + args).uniq 14 | end 15 | 16 | attr_reader :prop 17 | alias :base :prop 18 | 19 | def log severity, *args 20 | opts = args.pop if args.last.is_a? Hash and args.size != 1 21 | opts ||= {} 22 | opts = @prop.clone.update(opts) 23 | 24 | data = args.shift 25 | 26 | data = {:type => :exception, 27 | :name => data.class.to_s.intern, 28 | :backtrace => data.backtrace, 29 | :message => data.message} if data.is_a? Exception 30 | 31 | (@tags + args).each do |tag| 32 | tag = tag.to_sym 33 | case tag 34 | when :timestamp 35 | opts.update :timestamp => Time.now 36 | when :hostname 37 | @hostname ||= { :hostname => `hostname`.strip } 38 | opts.update @hostname 39 | when :process 40 | @process_id ||= { :process_id => Process.pid, 41 | :process_name => $0, 42 | :process_parent_id => Process.ppid, 43 | :thread_id => Thread.current.object_id } 44 | opts.update :process => @process_id 45 | else 46 | (opts[:tags] ||= []) << tag 47 | end 48 | end 49 | 50 | opts.update(:severity => severity, 51 | :msg => data) 52 | 53 | print(opts) 54 | unless MyLoggerRulez.disabled? 55 | MQ.fanout('logging', :durable => true).publish Marshal.dump(opts) 56 | end 57 | 58 | opts 59 | end 60 | alias :method_missing :log 61 | 62 | def print data = nil, &block 63 | if block 64 | @printer = block 65 | elsif data.is_a? Proc 66 | @printer = data 67 | elsif data 68 | (pr = @printer || self.class.printer) and pr.call(data) 69 | else 70 | @printer 71 | end 72 | end 73 | alias :printer :print 74 | 75 | def self.printer &block 76 | @printer = block if block 77 | @printer 78 | end 79 | 80 | def self.disabled? 81 | !!@disabled 82 | end 83 | 84 | def self.enable 85 | @disabled = false 86 | end 87 | 88 | def self.disable 89 | @disabled = true 90 | end 91 | end 92 | 93 | 94 | before(:all) do 95 | overload_amqp 96 | end 97 | 98 | 99 | def run_client 100 | AMQP.start do 101 | log = MyLoggerRulez.new 102 | log.debug 'its working!' 103 | 104 | log = MyLoggerRulez.new do |msg| 105 | #require 'pp' 106 | #pp msg 107 | #puts 108 | end 109 | 110 | log.info '123' 111 | log.debug [1,2,3] 112 | log.debug :one => 1, :two => 2 113 | log.error Exception.new('123') 114 | 115 | log.info '123', :process_id => Process.pid 116 | log.info '123', :process 117 | log.debug 'login', :session => 'abc', :user => 123 118 | 119 | log = MyLoggerRulez.new(:webserver, :timestamp, :hostname, &log.printer) 120 | log.info 'Request for /', :GET, :session => 'abc' 121 | 122 | #AMQP.stop{ EM.stop } 123 | end 124 | end 125 | 126 | def run_server 127 | AMQP.start(:host => 'localhost') do 128 | 129 | @server_queue = MQ.queue('logger') 130 | @server_queue.bind(MQ.fanout('logging', :durable => true)).subscribe do |msg| 131 | msg = Marshal.load(msg) 132 | end 133 | end 134 | end 135 | 136 | it "should get the expected results" do 137 | EM.run do 138 | threads = [] 139 | threads << Thread.new do 140 | run_server 141 | end 142 | threads << Thread.new do 143 | run_client 144 | end 145 | 146 | EM.add_timer(0.1) do 147 | @server_queue.should have(9).received_messages 148 | webserver_log = Marshal.load(@server_queue.received_messages.last) 149 | webserver_log[:tags].should == [:webserver, :GET] 150 | webserver_log[:msg].should == "Request for /" 151 | 152 | EM.stop 153 | threads.each { |t| t.join } 154 | end 155 | 156 | end 157 | end 158 | 159 | end 160 | -------------------------------------------------------------------------------- /spec/examples/ping_pong_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/example_helper') 3 | 4 | describe Moqueue, "when testing the ping pong example" do 5 | include ExampleHelper 6 | 7 | def ping_pong 8 | AMQP.start(:host => 'localhost') do 9 | 10 | # AMQP.logging = true 11 | 12 | amq = MQ.new 13 | EM.add_periodic_timer(0.1){ 14 | @counter_val = counter 15 | capture_output @counter_val, :sending, 'ping' 16 | amq.queue('one').publish('ping') 17 | } 18 | 19 | amq = MQ.new 20 | amq.queue('one').subscribe{ |msg| 21 | capture_output @counter_val, 'one', :received, msg, :sending, 'pong' 22 | amq.queue('two').publish('pong') 23 | } 24 | 25 | amq = MQ.new 26 | amq.queue('two').subscribe{ |msg| 27 | capture_output @counter_val, 'two', :received, msg 28 | } 29 | 30 | end 31 | 32 | end 33 | 34 | before(:all) do 35 | overload_amqp 36 | end 37 | 38 | before(:each) do 39 | reset! 40 | end 41 | 42 | it "should get the correct result without error" do 43 | Timeout::timeout(5) do 44 | ping_pong 45 | end 46 | expected = [[1, :sending, "ping"], [1, "one", :received, "ping", :sending, "pong"], [1, "two", :received, "pong"], 47 | [2, :sending, "ping"], [2, "one", :received, "ping", :sending, "pong"], [2, "two", :received, "pong"]] 48 | @captured_output.should == expected 49 | end 50 | end -------------------------------------------------------------------------------- /spec/examples/stocks_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/example_helper') 3 | 4 | describe Moqueue, "when running the stocks example" do 5 | include ExampleHelper 6 | 7 | def run_stocks 8 | AMQP.start(:host => 'localhost') do 9 | 10 | def log *args 11 | #p [ Time.now, *args ] 12 | end 13 | 14 | def publish_stock_prices 15 | mq = MQ.new 16 | counter = 0 17 | EM.add_periodic_timer(0.1){ 18 | counter += 1 19 | EM.stop if counter > 5 20 | 21 | {:appl => 170+rand(1000)/100.0, :msft => 22+rand(500)/100.0}.each do |stock, price| 22 | stock = "usd.#{stock}" 23 | 24 | log :publishing, stock, price 25 | mq.topic('stocks').publish(price, :key => stock) 26 | end 27 | } 28 | end 29 | 30 | def watch_appl_stock 31 | mq = MQ.new 32 | @apple_queue = mq.queue('apple stock') 33 | @apple_queue.bind(mq.topic('stocks'), :key => 'usd.appl').subscribe{ |price| 34 | log 'apple stock', price 35 | } 36 | end 37 | 38 | def watch_us_stocks 39 | mq = MQ.new 40 | @us_stocks = mq.queue('us stocks') 41 | @us_stocks.bind(mq.topic('stocks'), :key => 'usd.*').subscribe{ |info, price| 42 | log 'us stock', info.routing_key, price 43 | } 44 | end 45 | 46 | publish_stock_prices 47 | watch_appl_stock 48 | watch_us_stocks 49 | 50 | end 51 | end 52 | 53 | before(:each) do 54 | overload_amqp 55 | reset_broker 56 | end 57 | 58 | it "should get the correct results" do 59 | run_stocks 60 | @us_stocks.should have(12).received_messages 61 | @apple_queue.should have(6).received_messages 62 | end 63 | 64 | end -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | -c -f specdoc -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "spec" 3 | 4 | Spec::Runner.configure do |config| 5 | config.mock_with :mocha 6 | end 7 | 8 | require File.dirname(__FILE__) + "/../lib/moqueue" 9 | 10 | # Make sure tests fail if deferred blocks (for susbscribe and pop) don't get called 11 | def ensure_deferred_block_called(opts={:times=>1}) 12 | @poke_me = mock("poke_me") 13 | @poke_me.expects(:deferred_block_called).times(opts[:times]) 14 | end 15 | 16 | def deferred_block_called 17 | @poke_me.deferred_block_called 18 | true 19 | end 20 | 21 | def ensure_deferred_block_skipped 22 | @skip_me = mock("poke_me") 23 | @skip_me.expects(:deferred_block_called).times(0) 24 | end 25 | 26 | include Moqueue -------------------------------------------------------------------------------- /spec/unit/matchers_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe Matchers do 4 | class MatcherHarness 5 | include Moqueue::Matchers 6 | end 7 | 8 | before(:each) do 9 | @matchable = MatcherHarness.new 10 | @mock_moqueue = mock("mock Moqueue::MockQueue") 11 | @failure_exception = Spec::Expectations::ExpectationNotMetError 12 | end 13 | 14 | it "should include matchers in describe blocks automatically when using rspec" do 15 | self.class.include?(Moqueue::Matchers).should be_true 16 | end 17 | 18 | it "should implement Object#should have_received_message" do 19 | @mock_moqueue.expects(:received_message?).with("matchtacular").returns(true) 20 | @mock_moqueue.should have_received_message("matchtacular") 21 | end 22 | 23 | it "should implement Object#should_not have_received_message" do 24 | @mock_moqueue.expects(:received_message?).with("no match").returns(false) 25 | @mock_moqueue.should_not have_received_message("no match") 26 | end 27 | 28 | it "should implement Object#should have_exact_routing_key(key)" do 29 | @mock_moqueue.expects(:received_routing_key?).with("routing.key").returns(true) 30 | @mock_moqueue.should have_received_exact_routing_key("routing.key") 31 | end 32 | 33 | it "should implement Object#should_not have_exact_routing_key(key)" do 34 | @mock_moqueue.expects(:received_routing_key?).with("routing.key").returns(false) 35 | @mock_moqueue.should_not have_received_exact_routing_key("routing.key") 36 | end 37 | 38 | it "should have a useful failure message" do 39 | @mock_moqueue.expects(:received_message?).with("this fails").returns(false) 40 | failing_example = lambda {@mock_moqueue.should have_received_message("this fails")} 41 | error_message = "expected #{@mock_moqueue.inspect} to have received message ``this fails''" 42 | failing_example.should raise_error(@failure_exception, error_message) 43 | end 44 | 45 | it "should have a useful negative failure message" do 46 | @mock_moqueue.expects(:received_message?).with("FAIL").returns(true) 47 | failing_example = lambda{@mock_moqueue.should_not have_received_message("FAIL")} 48 | error_message = "expected #{@mock_moqueue.inspect} to not have received message ``FAIL''" 49 | failing_example.should raise_error(@failure_exception, error_message) 50 | end 51 | 52 | it "should fail gracefully if object being tested for #have_received doesn't respond to #received_message?" do 53 | begin 54 | Object.new.should have_received_message("foo") 55 | rescue => e 56 | end 57 | e.should be_a(NoMethodError) 58 | e.message.should match(/you can't use \`\`should have_received_message\'\' on #\ e 97 | end 98 | e.should be_a(NoMethodError) 99 | e.message.should match(/you can't use \`\`should have_received_message\'\' on #\ "thundercats") 24 | @broker.register_direct_exchange(exchange) 25 | @broker.find_direct_exchange("thundercats").should equal(exchange) 26 | end 27 | 28 | it "should keep a list of topic exchanges" do 29 | exchange = MockExchange.new(:topic => "lolcats") 30 | @broker.register_topic_exchange(exchange) 31 | @broker.find_topic_exchange("lolcats").should equal(exchange) 32 | end 33 | 34 | it "should keep a list of fanout queues" do 35 | exchange = MockExchange.new(:fanout => "joinTheNaniteBorg") 36 | @broker.register_fanout_exchange(exchange) 37 | @broker.find_fanout_exchange("joinTheNaniteBorg").should equal(exchange) 38 | end 39 | 40 | end -------------------------------------------------------------------------------- /spec/unit/mock_exchange_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe MockExchange do 4 | 5 | before(:each) do 6 | reset_broker 7 | @queue, @exchange = mock_queue_and_exchange 8 | end 9 | 10 | it "should manually attach queues" do 11 | ensure_deferred_block_called(:times => 2) 12 | exchange = mock_exchange 13 | one_queue, another_queue = mock_queue("one"), mock_queue("two") 14 | exchange.attach_queue(one_queue) 15 | exchange.attach_queue(another_queue) 16 | exchange.attached_queues.length.should == 2 17 | lambda { exchange.attach_queue(one_queue) }.should_not change(exchange.attached_queues, :length) 18 | one_queue.subscribe do |msg| 19 | deferred_block_called && msg.should == "mmm, smoothies" 20 | end 21 | another_queue.subscribe do |msg| 22 | deferred_block_called && msg.should == "mmm, smoothies" 23 | end 24 | exchange.publish("mmm, smoothies") 25 | end 26 | 27 | it "should accept options for the publish method" do 28 | lambda {@exchange.publish("whateva eva", :key=>"foo.bar")}.should_not raise_error(ArgumentError) 29 | end 30 | 31 | it "should emulate direct exchanges" do 32 | direct_exchange = MockExchange.new(:direct => "thundercats") 33 | direct_exchange.direct.should == "thundercats" 34 | end 35 | 36 | it "should register new direct exchanges with the mock broker" do 37 | MockBroker.instance.expects(:register_direct_exchange) 38 | MockExchange.new(:direct => "lolz") 39 | end 40 | 41 | it "should return a previously created direct exchange when asked to create a new one with the same name" do 42 | exchange = MockExchange.new(:direct => "smoochie") 43 | MockExchange.new(:direct => "smoochie").should equal(exchange) 44 | end 45 | 46 | it "should determine if routing keys match exactly on the direct exchange" do 47 | exchange = MockExchange.new(:direct => "lolz") 48 | key = MockExchange::DirectBindingKey 49 | key.new("cats").matches?("cats").should be_true 50 | key.new("cats").matches?("dogs").should be_false 51 | key.new("cats.*").matches?("cats.fridge").should be_false 52 | key.new("cats.evil").matches?("cats.fridge").should be_false 53 | key.new("cats.*").matches?("cats.fridge.in_urs").should be_false 54 | key.new("cats.#").matches?("cats.fridge.in_urs").should be_false 55 | end 56 | 57 | it "should forward messages to a queue only if the keys match exactly when emulating a direct exchange" do 58 | ensure_deferred_block_called 59 | exchange = MockExchange.new(:direct => "thundercats") 60 | queue = MockQueue.new("ho") 61 | queue.bind(exchange, :key=>"cats").subscribe do |msg| 62 | msg.should == "ohai" 63 | deferred_block_called 64 | end 65 | exchange.publish("ohai", :key => "cats") 66 | end 67 | 68 | it "should NOT forward messages to a queue if the keys mismatch when emulating a direct exchange" do 69 | ensure_deferred_block_skipped 70 | exchange = MockExchange.new(:direct => "thundercats") 71 | queue = MockQueue.new("ho") 72 | queue.bind(exchange, :key=>"cats").subscribe do |msg| 73 | msg.should == "ohai" 74 | deferred_block_called # should never execute! 75 | end 76 | exchange.publish("ohai", :key => "cats.attack") 77 | end 78 | 79 | it "should add the routing key to the headers' properties when publishing as a direct exchange" do 80 | ensure_deferred_block_called 81 | exchange = MockExchange.new(:direct => "thunderdogs") 82 | queue = MockQueue.new("dogzh8er").bind(exchange, :key=>"boxers") 83 | queue.subscribe do |headers, msg| 84 | deferred_block_called 85 | headers.routing_key.should == "boxers" 86 | msg.should == "Roxie" 87 | end 88 | exchange.publish("Roxie", :key=>"boxers") 89 | end 90 | 91 | it "should NOT raise an error when publishing to a direct exchange without specifying a key" do 92 | exchange = MockExchange.new(:direct => "spike") 93 | fail_msg = "you must provide a key when publishing to a topic exchange" 94 | lambda {exchange.publish("failtacular")}.should_not raise_error(ArgumentError, fail_msg) 95 | end 96 | 97 | it "should emulate topic exchanges" do 98 | #pending "support for storing and retrieving topic exchanges in MockBroker" 99 | topic_exchange = MockExchange.new(:topic => "lolcats") 100 | topic_exchange.topic.should == "lolcats" 101 | end 102 | 103 | it "should register new topic exchanges with the mock broker" do 104 | MockBroker.instance.expects(:register_topic_exchange) 105 | MockExchange.new(:topic => "lolz") 106 | end 107 | 108 | it "should return a previously created topic exchange when asked to create a new one with the same topic" do 109 | exchange = MockExchange.new(:topic => "fails") 110 | MockExchange.new(:topic => "fails").should equal(exchange) 111 | end 112 | 113 | it "should determine if routing keys match" do 114 | exchange = MockExchange.new(:topic => "lolz") 115 | key = MockExchange::TopicBindingKey 116 | key.new("cats").matches?("cats").should be_true 117 | key.new("cats").matches?("cats").should be_true 118 | key.new("cats").matches?("dogs").should be_false 119 | key.new("cats.*").matches?("cats.fridge").should be_true 120 | key.new("cats.evil").matches?("cats.fridge").should be_false 121 | key.new("cats.*").matches?("cats.fridge.in_urs").should be_false 122 | key.new("cats.#").matches?("cats.fridge.in_urs").should be_true 123 | end 124 | 125 | it "should forward messages to a queue only if the keys match when emulating a topic exchange" do 126 | ensure_deferred_block_called 127 | exchange = MockExchange.new(:topic => "lolz") 128 | queue = MockQueue.new("lolz-lover") 129 | queue.bind(exchange, :key=>"cats.*").subscribe do |msg| 130 | msg.should == "ohai" 131 | deferred_block_called 132 | end 133 | exchange.publish("ohai", :key => "cats.attack") 134 | end 135 | 136 | it "should add the routing key to the headers' properties when publishing as a topic exchange" do 137 | ensure_deferred_block_called 138 | exchange = MockExchange.new(:topic => "mehDogs") 139 | queue = MockQueue.new("dogzLover").bind(exchange, :key=>"boxers.*") 140 | queue.subscribe do |headers, msg| 141 | deferred_block_called 142 | headers.routing_key.should == "boxers.awesome" 143 | msg.should == "Roxie" 144 | end 145 | exchange.publish("Roxie", :key=>"boxers.awesome") 146 | end 147 | 148 | it "should raise an error when publishing to a topic exchange without specifying a key" do 149 | exchange = MockExchange.new(:topic=>"failz") 150 | fail_msg = "you must provide a key when publishing to a topic exchange" 151 | lambda {exchange.publish("failtacular")}.should raise_error(ArgumentError, fail_msg) 152 | end 153 | 154 | it "should allow the fanout exchange name to be queried" do 155 | exchange = MockExchange.new(:fanout => "hiMyNameIs") 156 | exchange.fanout.should == "hiMyNameIs" 157 | end 158 | 159 | it "should register new fanout exchanges with the MockBroker" do 160 | MockBroker.instance.expects(:register_fanout_exchange) 161 | MockExchange.new(:fanout => "nanite friendly") 162 | end 163 | 164 | it "should return the exact same fanout exchange if creating one with an identical name" do 165 | the_first_fanout = MockExchange.new(:fanout => "pseudo singleton") 166 | the_second_one = MockExchange.new(:fanout => "pseudo singleton") 167 | the_first_fanout.should equal(the_second_one) 168 | end 169 | 170 | end -------------------------------------------------------------------------------- /spec/unit/mock_headers_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe MockHeaders do 4 | 5 | it "should respond to the same methods as real AMQP::Protocol::Header" do 6 | headers = Moqueue::MockHeaders.new 7 | headers.should respond_to(:size) 8 | headers.should respond_to(:weight) 9 | headers.should respond_to(:properties) 10 | headers.should respond_to(:to_frame) 11 | end 12 | 13 | it "should add properties given to constructor" do 14 | headers = MockHeaders.new({:routing_key=>"lolz.cats.inKitchen"}) 15 | end 16 | 17 | it "should lookup unknown methods as keys in the hash" do 18 | headers = MockHeaders.new(:wtf_ftw_lolz_yo => "did I really write that?") 19 | headers.wtf_ftw_lolz_yo.should == "did I really write that?" 20 | end 21 | 22 | end -------------------------------------------------------------------------------- /spec/unit/mock_queue_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe MockQueue do 4 | 5 | before(:each) do 6 | reset_broker 7 | @queue, @exchange = mock_queue_and_exchange 8 | end 9 | 10 | it "should accept options for :ack=>(true|false) :nowait=>(true|false)" do 11 | lambda {@queue.subscribe(:ack=>true) { |message| p message}}.should_not raise_error(ArgumentError) 12 | end 13 | 14 | it "should pass mock headers to block when subscribe is given a block w/ 2 arity" do 15 | ensure_deferred_block_called 16 | @queue.subscribe do |headers, msg| 17 | headers.should be_kind_of(Moqueue::MockHeaders) 18 | msg.should == "the message" 19 | deferred_block_called 20 | end 21 | @exchange.publish("the message") 22 | end 23 | 24 | it "should allow retrieval of the headers for any published messages" do 25 | @queue.subscribe { |msg| msg } 26 | @exchange.publish("the message") 27 | @queue.received_headers.first.should be_kind_of(Moqueue::MockHeaders) 28 | end 29 | 30 | it "should create mock headers if pop is given a block w/ 2 arity" do 31 | pending 32 | end 33 | 34 | it "should process pending messages after a handler block is defined" do 35 | ensure_deferred_block_called 36 | @exchange.publish("found!") 37 | @queue.subscribe { |msg| deferred_block_called && msg.should == "found!" } 38 | end 39 | 40 | it "should not process pending messages twice" do 41 | ensure_deferred_block_called(:times=>2) 42 | @exchange.publish("take out the garbage") 43 | @queue.subscribe do |msg| 44 | deferred_block_called 45 | msg.should match(/take out (.*) garbage/) 46 | end 47 | @exchange.publish("take out more garbage") 48 | end 49 | 50 | it "should not ack messages when given or defaulting to :ack=>false" do 51 | ensure_deferred_block_called 52 | @queue.subscribe { |msg| deferred_block_called && msg.should == "hew-row" } 53 | @exchange.publish("hew-row") 54 | @exchange.received_ack_for_message?("hew-row").should_not be_true 55 | end 56 | 57 | it "should not ack messages when given :ack => true, but headers don't receive #ack" do 58 | ensure_deferred_block_called 59 | @queue.subscribe(:ack=>true) do |headers, msg| 60 | deferred_block_called 61 | msg.should == "10-4" 62 | end 63 | @exchange.publish("10-4") 64 | @exchange.received_ack_for_message?("10-4").should be_false 65 | end 66 | 67 | it "should ack messages when subscribe is given :ack=>true and headers are acked" do 68 | @queue.subscribe(:ack=>true) do |headers, msg| 69 | msg.should == "10-4" 70 | headers.ack 71 | end 72 | @exchange.publish("10-4") 73 | @exchange.received_ack_for_message?("10-4").should be_true 74 | end 75 | 76 | it "should provide ability to check for acks when direct exchange is used" do 77 | queue = MockQueue.new("direct-ack-check") 78 | queue.subscribe(:ack => true) do |headers, msg| 79 | msg.should == "ack me" 80 | headers.ack 81 | end 82 | queue.publish("ack me") 83 | queue.received_ack_for_message?("ack me").should be_true 84 | end 85 | 86 | it "should store received messages in the queues" do 87 | @queue.subscribe { |msg| msg.should == "save me!" } 88 | @exchange.publish("save me!") 89 | @queue.received_message?("save me!").should be_true 90 | end 91 | 92 | it "should #unsubscribe" do 93 | pending("should really remove the association with exchange") 94 | @queue.should respond_to(:unsubscribe) 95 | end 96 | 97 | it "should ignore #prefetch but at least raise an error" do 98 | lambda { @queue.prefetch(1337) }.should_not raise_error 99 | end 100 | 101 | it "should raise an error on double subscribe" do 102 | @queue.subscribe { |msg| "once" } 103 | second_subscribe = lambda { @queue.subscribe {|msg| "twice"} } 104 | second_subscribe.should raise_error(DoubleSubscribeError) 105 | end 106 | 107 | it "should emulate direct exchange publishing" do 108 | ensure_deferred_block_called 109 | @queue.subscribe { |msg|deferred_block_called && msg.should == "Dyrekt" } 110 | @queue.publish("Dyrekt") 111 | end 112 | 113 | it "should take an optional name argument" do 114 | lambda { MockQueue.new("say-my-name") }.should_not raise_error 115 | end 116 | 117 | it "should register itself with the mock broker if given a name" do 118 | MockBroker.instance.expects(:register_queue) 119 | queue = MockQueue.new("with-a-name") 120 | end 121 | 122 | it "should return the previously created queue when trying to create a queue with the same name" do 123 | queue = MockQueue.new("firsties") 124 | MockQueue.new("firsties").should equal(queue) 125 | end 126 | 127 | it "should support binding to a topic exchange" do 128 | queue = MockQueue.new("lolz lover") 129 | topic_exchange = MockExchange.new(:topic => "lolcats") 130 | topic_exchange.expects(:attach_queue).with(queue, :key=>"lolcats.fridges") 131 | queue.bind(topic_exchange, :key => "lolcats.fridges") #http://lolcatz.net/784/im-in-ur-fridge-eatin-ur-foodz/ 132 | end 133 | 134 | it "should make the callback (#subscribe block) available for direct use" do 135 | queue = MockQueue.new("inspect my guts, plz") 136 | queue.subscribe { |msg| msg + "yo" } 137 | queue.run_callback("hey-").should == "hey-yo" 138 | end 139 | 140 | it "should bind to a fanout exchange" do 141 | queue = MockQueue.new("fanouts are cool, too") 142 | lambda {queue.bind(MockExchange.new)}.should_not raise_error 143 | end 144 | 145 | it "should bind to a fanout exchange only once" do 146 | queue = MockQueue.new("fanouts are cool, too") 147 | exchange = MockExchange.new(:fanout => 'fanouts') 148 | queue.bind exchange 149 | queue.bind exchange # should be silently ignored 150 | exchange.publish "only get this once", {} 151 | end 152 | 153 | it "should provide a null subscribe that does nothing but allows messages to be received" do 154 | queue = MockQueue.new("nilly").null_subscribe 155 | queue.publish("I'm feelin this") 156 | queue.received_message?("I'm feelin this").should be_true 157 | end 158 | 159 | end 160 | -------------------------------------------------------------------------------- /spec/unit/moqueue_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe Moqueue do 4 | 5 | end -------------------------------------------------------------------------------- /spec/unit/object_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe ObjectMethods do 4 | 5 | before(:each) do 6 | reset_broker 7 | @queue, @exchange = mock_queue_and_exchange 8 | end 9 | 10 | it "should reset the MockBroker" do 11 | MockBroker.instance.expects(:reset!) 12 | reset_broker 13 | end 14 | 15 | it "should name the queue ``anonymous-RANDOM_GARBAGE'' if not given a name" do 16 | @queue.name.should match(/anonymous\-[0-9a-f]{0,8}/) 17 | end 18 | 19 | it "should name the queue with the name given" do 20 | q, exchange = mock_queue_and_exchange("wassup") 21 | q.name.should == "wassup" 22 | q2 = mock_queue("watup") 23 | q2.name.should == "watup" 24 | end 25 | 26 | it "should create a matched mock queue and mock exchange" do 27 | ensure_deferred_block_called 28 | @queue.subscribe do |message| 29 | deferred_block_called 30 | message.should == "FTW" 31 | end 32 | @exchange.publish("FTW") 33 | end 34 | 35 | it "should allow for overloading AMQP and MQ" do 36 | overload_amqp 37 | defined?(AMQP).should be_true 38 | defined?(MQ).should be_true 39 | end 40 | 41 | it "should provide a convenience method for creating mock queues" do 42 | mock_queue("Sugary").should be_kind_of(Moqueue::MockQueue) 43 | end 44 | 45 | it "should provide a convenience method for creating mock exchanges" do 46 | mock_exchange(:topic => "sweetSugar").should be_kind_of(Moqueue::MockExchange) 47 | end 48 | 49 | end -------------------------------------------------------------------------------- /spec/unit/overloads_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe "AMQP and MQ", "when overloaded by moqueue/overloads" do 4 | 5 | before(:all) do 6 | overload_amqp 7 | end 8 | 9 | it "should make AMQP.start take options and a block without connecting to AMQP broker" do 10 | ensure_deferred_block_called 11 | AMQP.start(:host => "localhost") do 12 | deferred_block_called 13 | EM.stop 14 | end 15 | end 16 | 17 | it "should run EM in AMQP.start" do 18 | EM.expects(:run) 19 | AMQP.start { EM.stop } 20 | end 21 | 22 | it "should provide a MQ.queue class method" do 23 | MQ.queue('FTW').should be_a(Moqueue::MockQueue) 24 | end 25 | 26 | it "should emulate the behavior of MQ.closing?" do 27 | ensure_deferred_block_called 28 | AMQP.stop do 29 | deferred_block_called 30 | AMQP.should be_closing 31 | end 32 | end 33 | 34 | it "should create direct exchanges" do 35 | MQ.new.direct("directamundo").should == MockExchange.new(:direct => "directamundo") 36 | end 37 | 38 | it "should create topic exchanges" do 39 | MQ.new.topic("lolzFTW").should == MockExchange.new(:topic => "lolzFTW") 40 | end 41 | 42 | it "should provide a MQ.direct class method" do 43 | MQ.direct("direct", :durable=>true).should be_a(Moqueue::MockExchange) 44 | end 45 | 46 | it "should provide a MQ.fanout class method" do 47 | MQ.fanout("fanout", :durable=>true).should be_a(Moqueue::MockExchange) 48 | end 49 | 50 | it "should create a named fanout queue via MQ.fanout" do 51 | fanout = MQ.fanout("SayMyNameSayMyName", :durable=>true) 52 | fanout.should be_a(Moqueue::MockExchange) 53 | fanout.fanout.should == "SayMyNameSayMyName" 54 | end 55 | 56 | end --------------------------------------------------------------------------------