├── VERSION ├── .rspec ├── spec ├── spec.opts ├── minitest │ ├── spec_helper.rb │ ├── em_integration_minispec.rb │ ├── basic_integration_minispec.rb │ ├── coolio_integration_minispec.rb │ └── amqp_integration_minispec.rb ├── amqp.yml ├── run.rb ├── evented-spec │ ├── adapters │ │ ├── em_spec.rb │ │ ├── cool_io_spec.rb │ │ ├── amqp_spec.rb │ │ └── adapter_seg.rb │ ├── evented_spec_metadata_spec.rb │ ├── util_spec.rb │ └── defaults_options_spec.rb ├── integration │ └── failing_rspec_spec.rb ├── spec_helper.rb └── em_hooks_spec.rb ├── .yardopts ├── lib ├── amqp-spec.rb ├── evented-spec │ ├── version.rb │ ├── ext │ │ ├── coolio.rb │ │ └── amqp.rb │ ├── em_spec.rb │ ├── coolio_spec.rb │ ├── util.rb │ ├── amqp_spec.rb │ ├── evented_example │ │ ├── amqp_example.rb │ │ ├── coolio_example.rb │ │ └── em_example.rb │ ├── spec_helper │ │ ├── coolio_helpers.rb │ │ ├── event_machine_helpers.rb │ │ └── amqp_helpers.rb │ ├── evented_example.rb │ └── spec_helper.rb └── evented-spec.rb ├── .travis.yml ├── .gitignore ├── tasks ├── common.rake ├── doc.rake ├── spec.rake ├── git.rake ├── gem.rake └── version.rake ├── Rakefile ├── LICENSE ├── Gemfile ├── evented-spec.gemspec ├── HISTORY └── README.textile /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.0.beta2 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format nested -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --color 2 | --format nested -------------------------------------------------------------------------------- /spec/minitest/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'evented-spec' 2 | -------------------------------------------------------------------------------- /spec/amqp.yml: -------------------------------------------------------------------------------- 1 | test: 2 | user: guest 3 | pass: guest 4 | host: localhost 5 | vhost: / -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --markup textile 3 | --exclude lib/evented-spec/ext 4 | lib/**/*.rb 5 | - 6 | LICENSE -------------------------------------------------------------------------------- /lib/amqp-spec.rb: -------------------------------------------------------------------------------- 1 | puts <<-EOF 2 | DEPRECATION WARNING: 3 | Using require 'amqp-spec' is deprecated. Require 'evented-spec' instead. 4 | EOF 5 | require 'evented-spec' -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.8.7 4 | - 1.9.2 5 | - 1.9.3 6 | - rbx-18mode 7 | - rbx-19mode 8 | - jruby-18mode 9 | - jruby-19mode 10 | - jruby-head 11 | script: "bundle exec rake spec:ci" -------------------------------------------------------------------------------- /lib/evented-spec/version.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | module EventedSpec 4 | # Path to version file 5 | VERSION_FILE = Pathname.new(__FILE__).dirname + '/../../VERSION' 6 | # Gem version 7 | VERSION = VERSION_FILE.exist? ? VERSION_FILE.read.strip : nil 8 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | *.rbc 18 | *.class 19 | coverage 20 | config 21 | rdoc 22 | pkg 23 | log 24 | .idea 25 | .rvmrc 26 | .bundle 27 | Gemfile.lock 28 | *.gem 29 | -------------------------------------------------------------------------------- /tasks/common.rake: -------------------------------------------------------------------------------- 1 | #task :default => 'test:run' 2 | #task 'gem:release' => 'test:run' 3 | 4 | task :notes do 5 | puts 'Output annotations (TBD)' 6 | end 7 | 8 | #Bundler not ready for prime time just yet 9 | #desc 'Bundle dependencies' 10 | #task :bundle do 11 | # output = `bundle check 2>&1` 12 | # 13 | # unless $?.to_i == 0 14 | # puts output 15 | # system "bundle install" 16 | # puts 17 | # end 18 | #end -------------------------------------------------------------------------------- /lib/evented-spec/ext/coolio.rb: -------------------------------------------------------------------------------- 1 | # Monkey patching some methods into Cool.io to make it more testable 2 | module Coolio 3 | class Loop 4 | # Cool.io provides no means to change the default loop which makes sense in 5 | # most situations, but not ours. 6 | def self.default_loop=(event_loop) 7 | if RUBY_VERSION >= "1.9.0" 8 | Thread.current.instance_variable_set :@_coolio_loop, event_loop 9 | else 10 | @@_coolio_loop = event_loop 11 | end 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /tasks/doc.rake: -------------------------------------------------------------------------------- 1 | desc 'Alias to doc:rdoc' 2 | task :doc => 'doc:rdoc' 3 | 4 | namespace :doc do 5 | require 'rake/rdoctask' 6 | Rake::RDocTask.new do |rdoc| 7 | # Rake::RDocTask.new(:rdoc => "rdoc", :clobber_rdoc => "clobber", :rerdoc => "rerdoc") do |rdoc| 8 | rdoc.rdoc_dir = DOC_PATH.basename.to_s 9 | rdoc.title = "#{NAME} #{VERSION} Documentation" 10 | rdoc.main = "README.doc" 11 | rdoc.rdoc_files.include('README*') 12 | rdoc.rdoc_files.include('lib/**/*.rb') 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | NAME = 'evented-spec' 3 | BASE_PATH = Pathname.new(__FILE__).dirname 4 | LIB_PATH = BASE_PATH + 'lib' 5 | PKG_PATH = BASE_PATH + 'pkg' 6 | DOC_PATH = BASE_PATH + 'rdoc' 7 | 8 | $LOAD_PATH.unshift LIB_PATH.to_s 9 | require 'evented-spec' 10 | CLASS_NAME = EventedSpec 11 | VERSION = CLASS_NAME::VERSION 12 | 13 | begin 14 | require 'rake' 15 | rescue LoadError 16 | require 'rubygems' 17 | gem 'rake', '~> 0.8.3.1' 18 | require 'rake' 19 | end 20 | 21 | # Load rakefile tasks 22 | Dir['tasks/*.rake'].sort.each { |file| load file } 23 | 24 | -------------------------------------------------------------------------------- /spec/run.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.setup 3 | Bundler.require :default, :test 4 | 5 | spec_dir = File.expand_path("..", __FILE__) 6 | lib_dir = File.expand_path("../../lib", __FILE__) 7 | if ARGV.delete('--minitest') 8 | require 'minitest/spec' 9 | require 'minitest/autorun' 10 | $LOAD_PATH.unshift "#{spec_dir}/minitest" 11 | $LOAD_PATH.unshift lib_dir 12 | Dir.glob("#{spec_dir}/**/*_minispec.rb").each {|spec| require spec } 13 | else 14 | require 'rspec' 15 | require 'rspec/autorun' 16 | Dir.glob("#{spec_dir}/**/*_spec.rb").each {|spec| require spec } 17 | end 18 | 19 | -------------------------------------------------------------------------------- /tasks/spec.rake: -------------------------------------------------------------------------------- 1 | desc 'Alias to spec:spec' 2 | task :spec => 'spec:all' 3 | 4 | namespace :spec do 5 | require 'rspec/core/rake_task' 6 | 7 | desc "Run all specs" 8 | RSpec::Core::RakeTask.new(:all){|task|} 9 | 10 | desc "Run all non-failing specs (for CI)" 11 | task(:ci) do |task| 12 | ENV["EXCLUDE_DELIBERATELY_FAILING_SPECS"] = "1" 13 | exec "ruby spec/run.rb && ruby spec/run.rb --minitest" 14 | end 15 | 16 | desc "Run specs with RCov" 17 | RSpec::Core::RakeTask.new(:rcov) do |t| 18 | t.rcov = true 19 | t.rcov_opts = ['--exclude', 'spec'] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/evented-spec/adapters/em_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe EventedSpec::SpecHelper, "EventMachine bindings" do 4 | include EventedSpec::SpecHelper 5 | default_timeout 0.5 6 | 7 | def em_running? 8 | EM.reactor_running? 9 | end # em_running? 10 | 11 | after(:each) { 12 | em_running?.should be_false 13 | } 14 | 15 | let(:method_name) { "em" } 16 | let(:prefix) { "em_" } 17 | 18 | it_should_behave_like "EventedSpec adapter" 19 | 20 | 21 | describe EventedSpec::EMSpec do 22 | include EventedSpec::EMSpec 23 | it "should run inside of em loop" do 24 | em_running?.should be_true 25 | done 26 | end 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /spec/evented-spec/adapters/cool_io_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe EventedSpec::SpecHelper, "Cool.io bindings", :nojruby => true do 4 | include EventedSpec::SpecHelper 5 | default_timeout 1 6 | 7 | def coolio_running? 8 | Coolio::Loop.default.instance_variable_get(:@running) 9 | end # coolio_running? 10 | 11 | after(:each) { 12 | coolio_running?.should be_false 13 | } 14 | 15 | let(:method_name) { "coolio" } 16 | let(:prefix) { "coolio_" } 17 | 18 | it_should_behave_like "EventedSpec adapter" 19 | 20 | 21 | describe EventedSpec::CoolioSpec do 22 | include EventedSpec::CoolioSpec 23 | it "should run inside of coolio loop" do 24 | coolio_running?.should be_true 25 | Coolio::Loop.default.has_active_watchers?.should be_true 26 | done 27 | end 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lib/evented-spec.rb: -------------------------------------------------------------------------------- 1 | # There's little to no point in requiring only subset of evented-spec code yet, 2 | # but in case you really want to, it won't stand in your way. 3 | 4 | # These files are required 5 | require 'evented-spec/util' 6 | require 'evented-spec/version' 7 | require 'evented-spec/evented_example' 8 | require 'evented-spec/spec_helper' 9 | 10 | # As of now, amqp depends on em, em doesn't depend on anyone and 11 | # neither does coolio. 12 | # Pick the files accordingly. 13 | require 'evented-spec/evented_example/em_example' 14 | require 'evented-spec/evented_example/amqp_example' 15 | require 'evented-spec/evented_example/coolio_example' 16 | 17 | require 'evented-spec/spec_helper/event_machine_helpers' 18 | require 'evented-spec/spec_helper/amqp_helpers' 19 | require 'evented-spec/spec_helper/coolio_helpers' 20 | 21 | require 'evented-spec/em_spec' 22 | require 'evented-spec/amqp_spec' 23 | require 'evented-spec/coolio_spec' -------------------------------------------------------------------------------- /lib/evented-spec/em_spec.rb: -------------------------------------------------------------------------------- 1 | module EventedSpec 2 | # Including EventedSpec::EMSpec module into your example group, each example of this group 3 | # will run inside EM.run loop without the need to explicitly call 'em'. 4 | # 5 | module EMSpec 6 | def self.included(example_group) 7 | example_group.send(:include, SpecHelper) 8 | example_group.extend ClassMethods 9 | end 10 | 11 | # @private 12 | module ClassMethods 13 | def it(*args, &block) 14 | if block 15 | # Shared example groups seem to pass example group instance 16 | # to the actual example block 17 | new_block = lambda do |*args_block| 18 | em(&block) 19 | end 20 | super(*args, &new_block) 21 | else 22 | # pending example 23 | super 24 | end 25 | end # it 26 | end # ClassMethods 27 | end # EMSpec 28 | end # module EventedSpec 29 | -------------------------------------------------------------------------------- /lib/evented-spec/coolio_spec.rb: -------------------------------------------------------------------------------- 1 | module EventedSpec 2 | # Including EventedSpec::CoolioSpec module into your example group, each example of this group 3 | # will run inside cool.io loop without the need to explicitly call 'coolio'. 4 | # 5 | module CoolioSpec 6 | def self.included(example_group) 7 | example_group.send(:include, SpecHelper) 8 | example_group.extend ClassMethods 9 | end 10 | 11 | # @private 12 | module ClassMethods 13 | def it(*args, &block) 14 | if block 15 | # Shared example groups seem to pass example group instance 16 | # to the actual example block 17 | new_block = lambda do |*args_block| 18 | coolio(&block) 19 | end 20 | 21 | super(*args, &new_block) 22 | else 23 | # pending example 24 | super 25 | end 26 | end # it 27 | end # ClassMethods 28 | end # EMSpec 29 | end # module EventedSpec 30 | -------------------------------------------------------------------------------- /lib/evented-spec/util.rb: -------------------------------------------------------------------------------- 1 | module EventedSpec 2 | # Miscellanous utility methods used throughout the code. 3 | module Util 4 | extend self 5 | 6 | # Creates a deep clone of an object. Different from normal Object#clone 7 | # method which is shallow clone (doesn't traverse hashes and arrays, 8 | # cloning their contents). 9 | # 10 | # @param Object to clone 11 | # @return Deep clone of the given object 12 | def deep_clone(value) 13 | case value 14 | when Hash 15 | value.inject({}) do |result, kv| 16 | result[kv[0]] = deep_clone(kv[1]) 17 | result 18 | end 19 | when Array 20 | value.inject([]) do |result, item| 21 | result << deep_clone(item) 22 | end 23 | else 24 | begin 25 | value.clone 26 | rescue TypeError 27 | value 28 | end 29 | end 30 | end # deep_clone 31 | end # module Util 32 | end # module EventedSpec -------------------------------------------------------------------------------- /tasks/git.rake: -------------------------------------------------------------------------------- 1 | desc "Alias to git:commit" 2 | task :git => 'git:commit' 3 | 4 | namespace :git do 5 | 6 | desc "Stage and commit your work [with message]" 7 | task :commit, [:message] do |t, args| 8 | puts "Staging new (unversioned) files" 9 | system "git add --all" 10 | if args.message 11 | puts "Committing with message: #{args.message}" 12 | system %Q[git commit -a -m "#{args.message}" --author arvicco] 13 | else 14 | puts "Committing" 15 | system %Q[git commit -a -m "No message" --author arvicco] 16 | end 17 | end 18 | 19 | desc "Push local changes to Github" 20 | task :push => :commit do 21 | puts "Pushing local changes to remote" 22 | system "git push" 23 | end 24 | 25 | desc "Create (release) tag on Github" 26 | task :tag => :push do 27 | tag = VERSION 28 | puts "Creating git tag: #{tag}" 29 | system %Q{git tag -a -m "Release tag #{tag}" #{tag}} 30 | puts "Pushing #{tag} to remote" 31 | system "git push origin #{tag}" 32 | end 33 | 34 | end -------------------------------------------------------------------------------- /lib/evented-spec/amqp_spec.rb: -------------------------------------------------------------------------------- 1 | module EventedSpec 2 | # If you include EventedSpec::AMQPSpec module into your example group, each example of this group 3 | # will run inside AMQP.start loop without the need to explicitly call 'amqp'. In order 4 | # to provide options to AMQP loop, default_options class method is defined. Remember, 5 | # when using EventedSpec::Specs, you'll have a single set of AMQP.start options for all your 6 | # examples. 7 | # 8 | module AMQPSpec 9 | def self.included(example_group) 10 | example_group.send(:include, SpecHelper) 11 | example_group.extend(ClassMethods) 12 | end 13 | 14 | # @private 15 | module ClassMethods 16 | def it(*args, &block) 17 | if block 18 | new_block = lambda do |*args_block| 19 | amqp(&block) 20 | end 21 | super(*args, &new_block) 22 | else 23 | # pending example 24 | super 25 | end 26 | end # it 27 | end # ClassMethods 28 | end # AMQPSpec 29 | end # module EventedSpec 30 | -------------------------------------------------------------------------------- /tasks/gem.rake: -------------------------------------------------------------------------------- 1 | desc "Alias to gem:release" 2 | task :release => 'gem:release' 3 | 4 | desc "Alias to gem:install" 5 | task :install => 'gem:install' 6 | 7 | desc "Alias to gem:build" 8 | task :gem => 'gem:build' 9 | 10 | namespace :gem do 11 | gem_file = "#{NAME}-#{VERSION}.gem" 12 | 13 | desc "(Re-)Build gem" 14 | task :build do 15 | puts "Remove existing gem package" 16 | rm_rf PKG_PATH 17 | puts "Build new gem package" 18 | system "gem build #{NAME}.gemspec" 19 | puts "Move built gem to package dir" 20 | mkdir_p PKG_PATH 21 | mv gem_file, PKG_PATH 22 | end 23 | 24 | desc "Cleanup already installed gem(s)" 25 | task :cleanup do 26 | puts "Cleaning up installed gem(s)" 27 | system "gem cleanup #{NAME}" 28 | end 29 | 30 | desc "Build and install gem" 31 | task :install => :build do 32 | system "gem install #{PKG_PATH}/#{gem_file}" 33 | end 34 | 35 | desc "Build and push gem to Gemcutter" 36 | task :release => [:build, 'git:tag'] do 37 | puts "Pushing gem to Gemcutter" 38 | system "gem push #{PKG_PATH}/#{gem_file}" 39 | end 40 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Arvicco 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | # Use local clones if possible. 4 | def custom_gem(name, options = Hash.new) 5 | local_path = File.expand_path("../vendor/#{name}", __FILE__) 6 | if File.exist?(local_path) 7 | gem name, options.merge(:path => local_path).delete_if { |key, _| [:git, :branch].include?(key) } 8 | else 9 | gem name, options 10 | end 11 | end 12 | 13 | group :development do 14 | gem "rake" 15 | gem "yard" 16 | gem "RedCloth", "~> 4.2.9" 17 | end 18 | 19 | group :test do 20 | # Should work for either RSpec1 or Rspec2, but you cannot have both at once. 21 | # Also, keep in mind that if you install Rspec 2 it prevents Rspec 1 from running normally. 22 | # Unless you use it like 'bundle exec spec spec', that is. 23 | 24 | if RUBY_PLATFORM =~ /mswin|windows|mingw/ 25 | # For color support on Windows (deprecated?) 26 | gem 'win32console' 27 | gem 'rspec', '~>1.3.0', :require => 'spec' 28 | else 29 | gem 'rspec', '~> 2.5.0', :require => nil 30 | end 31 | gem 'minitest', :require => nil 32 | 33 | gem "eventmachine" 34 | gem "cool.io", :platforms => :ruby 35 | custom_gem "amq-client", :git => "git://github.com/ruby-amqp/amq-client.git" 36 | custom_gem "amq-protocol", :git => "git://github.com/ruby-amqp/amq-protocol.git" 37 | custom_gem "amqp", :git => "git://github.com/ruby-amqp/amqp.git", :branch => "master" 38 | end 39 | -------------------------------------------------------------------------------- /spec/minitest/em_integration_minispec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "EventedSpec EventMachine bindings" do 4 | include EventedSpec::SpecHelper 5 | default_timeout 0.5 6 | 7 | it "can run inside of em loop" do 8 | EM.reactor_running?.must_equal false 9 | em do 10 | EM.reactor_running?.must_equal true 11 | done 12 | end 13 | EM.reactor_running?.must_equal false 14 | end 15 | 16 | describe "hooks" do 17 | def hooks 18 | @hooks ||= [] 19 | end 20 | 21 | before { hooks << :before } 22 | em_before { hooks << :em_before } 23 | em_after { hooks << :em_after } 24 | after { hooks << :after } 25 | 26 | it "execute in proper order" do 27 | hooks.must_equal [:before] 28 | em do 29 | hooks.must_equal [:before, :em_before] 30 | done 31 | end 32 | hooks.must_equal [:before, :em_before, :em_after] 33 | end 34 | end 35 | 36 | describe "#delayed" do 37 | default_timeout 0.7 38 | it "works as intended" do 39 | em do 40 | time = Time.now 41 | delayed(0.3) { Time.now.must_be_close_to time + 0.3, 0.1 } 42 | done(0.4) 43 | end 44 | end 45 | end 46 | 47 | describe EventedSpec::EMSpec do 48 | include EventedSpec::EMSpec 49 | it "wraps the whole example" do 50 | EM.reactor_running?.must_equal true 51 | done 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/minitest/basic_integration_minispec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe EventedSpec do 4 | describe "inclusion of helper modules" do 5 | include EventedSpec::SpecHelper 6 | 7 | it "creates reactor launchers" do 8 | [:em, :amqp, :coolio].each do |method| 9 | self.respond_to?(method).must_equal true 10 | end 11 | end 12 | 13 | it "adds various helpers" do 14 | [:done, :timeout, :delayed].each do |method| 15 | self.must_respond_to method 16 | end 17 | end 18 | 19 | it "creates hooks and other group helpers" do 20 | [:em_before, :em_after, :amqp_before, 21 | :amqp_after, :coolio_before, :coolio_after, 22 | :default_timeout, :default_options].each do |method| 23 | self.class.must_respond_to method 24 | end 25 | end 26 | 27 | describe "propagation to sub contexts" do 28 | it "should work" do 29 | [:em, :amqp, :coolio].each do |method| 30 | self.must_respond_to method 31 | end 32 | 33 | [:done, :timeout, :delayed].each do |method| 34 | self.must_respond_to method 35 | end 36 | 37 | [:em_before, :em_after, :amqp_before, 38 | :amqp_after, :coolio_before, :coolio_after, 39 | :default_timeout, :default_options].each do |method| 40 | self.class.must_respond_to method 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /evented-spec.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | Gem::Specification.new do |gem| 4 | gem.name = "evented-spec" 5 | gem.version = File.open('VERSION').read.strip 6 | gem.summary = %q{Simple API for writing asynchronous EventMachine/AMQP specs. Supports RSpec and RSpec2.} 7 | gem.description = %q{Simple API for writing asynchronous EventMachine and AMQP specs. Runs legacy EM-Spec based examples. Supports RSpec and RSpec2.} 8 | gem.authors = ["Arvicco", "Markiz"] 9 | gem.email = "arvitallian@gmail.com" 10 | gem.homepage = %q{http://github.com/ruby-amqp/evented-spec} 11 | gem.platform = Gem::Platform::RUBY 12 | gem.date = Time.now.strftime "%Y-%m-%d" 13 | 14 | # Files setup 15 | versioned = `git ls-files -z`.split("\0") 16 | gem.files = Dir['{bin,lib,man,spec,features,tasks}/**/*', 'Rakefile', 'README*', 17 | 'LICENSE*', 'VERSION*', 'HISTORY*', '.gitignore'] & versioned 18 | gem.test_files = Dir['spec/**/*'] & versioned 19 | gem.require_paths = ["lib"] 20 | 21 | # RDoc setup 22 | gem.rdoc_options.concat %W{--charset UTF-8 --main README.textile --title evented-spec} 23 | gem.extra_rdoc_files = ["LICENSE", "HISTORY", "README.textile"] 24 | 25 | # Dependencies 26 | gem.add_development_dependency("rspec", ["~> 2.5.0"]) 27 | gem.add_development_dependency("amqp", ["~> 0.8.0.rc1"]) 28 | gem.add_development_dependency("bundler", [">= 1.0.0"]) 29 | gem.add_development_dependency("RedCloth", ["~> 4.2.7"]) 30 | gem.add_development_dependency("yard") 31 | 32 | gem.rubyforge_project = "evented-spec" 33 | end 34 | -------------------------------------------------------------------------------- /lib/evented-spec/ext/amqp.rb: -------------------------------------------------------------------------------- 1 | # Monkey patching some methods into AMQP to make it more testable 2 | module EventedSpec 3 | module AMQPBackports 4 | def self.included(base) 5 | base.extend ClassMethods 6 | end # self.included 7 | 8 | module ClassMethods 9 | def connection 10 | @conn 11 | end # connection 12 | 13 | def connection=(new_connection) 14 | @conn = new_connection 15 | end # connection= 16 | end # module ClassMethods 17 | end # module AMQPBackports 18 | end # module EventedSpec 19 | 20 | module AMQP 21 | # Initializes new AMQP client/connection without starting another EM loop 22 | def self.start_connection(opts={}, &block) 23 | if amqp_pre_08? 24 | self.connection = connect opts 25 | self.connection.callback(&block) 26 | else 27 | self.connection = connect opts, &block 28 | end 29 | end 30 | 31 | # Closes AMQP connection gracefully 32 | def self.stop_connection 33 | if AMQP.connection and not AMQP.connection.closing? 34 | @closing = true 35 | self.connection.close { 36 | yield if block_given? 37 | self.connection = nil 38 | cleanup_state 39 | } 40 | end 41 | end 42 | 43 | # Cleans up AMQP state after AMQP connection closes 44 | def self.cleanup_state 45 | Thread.list.each { |thread| thread[:mq] = nil } 46 | Thread.list.each { |thread| thread[:mq_id] = nil } 47 | self.connection = nil 48 | @closing = false 49 | end 50 | 51 | def self.amqp_pre_08? 52 | AMQP::VERSION < "0.8" 53 | end # self.amqp_pre_08? 54 | 55 | include EventedSpec::AMQPBackports 56 | end 57 | -------------------------------------------------------------------------------- /spec/evented-spec/evented_spec_metadata_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Example Groups", ".evented_spec_metadata" do 4 | context "when EventedSpec::SpecHelper is included" do 5 | include EventedSpec::SpecHelper 6 | it "should assign some hash by default" do 7 | self.class.evented_spec_metadata.should == {} 8 | end 9 | 10 | context "in nested group" do 11 | evented_spec_metadata[:nested] = {} 12 | evented_spec_metadata[:other] = :value 13 | it "should merge metadata" do 14 | self.class.evented_spec_metadata.should == {:nested => {}, :other => :value} 15 | end 16 | 17 | context "in deeply nested group" do 18 | evented_spec_metadata[:nested][:deeply] = {} 19 | evented_spec_metadata[:other] = "hello" 20 | it "should merge metadata" do 21 | self.class.evented_spec_metadata[:nested][:deeply].should == {} 22 | end 23 | 24 | it "should allow to override merged metadata" do 25 | self.class.evented_spec_metadata[:other].should == "hello" 26 | end 27 | end 28 | 29 | context "in other deeply nested group" do 30 | evented_spec_metadata[:nested][:other] = {} 31 | it "should diverge without being tainted by neighbouring example groups" do 32 | self.class.evented_spec_metadata.should == {:nested => {:other => {}}, :other => :value} 33 | end 34 | end 35 | end 36 | end 37 | 38 | context "when EventedSpec::SpecHelper is not included" do 39 | it "should not be defined" do 40 | self.class.should_not respond_to(:evented_spec_metadata) 41 | end 42 | end 43 | end -------------------------------------------------------------------------------- /spec/evented-spec/adapters/amqp_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe EventedSpec::SpecHelper, "AMQP bindings" do 4 | include EventedSpec::SpecHelper 5 | default_timeout 0.5 6 | 7 | def amqp_running? 8 | EM.reactor_running? && !!AMQP.connection 9 | end # em_running? 10 | 11 | let(:method_name) { "amqp" } 12 | let(:prefix) { "amqp_" } 13 | 14 | it_should_behave_like "EventedSpec adapter" 15 | 16 | describe EventedSpec::AMQPSpec do 17 | include EventedSpec::AMQPSpec 18 | it "should run inside of amqp block" do 19 | amqp_running?.should be_true 20 | done 21 | end 22 | end 23 | 24 | describe "actual AMQP functionality" do 25 | include EventedSpec::SpecHelper 26 | default_options AMQP_OPTS if defined? AMQP_OPTS 27 | 28 | def publish_and_consume_once(queue_name="test_sink", data="data") 29 | amqp(:spec_timeout => 0.5) do 30 | AMQP::Channel.new do |channel, _| 31 | exchange = channel.direct(queue_name) 32 | queue = channel.queue(queue_name).bind(exchange) 33 | queue.subscribe do |hdr, msg| 34 | hdr.should be_an AMQP::Header 35 | msg.should == data 36 | done { queue.unsubscribe; queue.delete } 37 | end 38 | EM.add_timer(0.2) do 39 | exchange.publish data 40 | end 41 | end 42 | end 43 | end 44 | 45 | it 'sends data to the queue' do 46 | publish_and_consume_once 47 | end 48 | 49 | it 'does not hang sending data to the same queue, again' do 50 | publish_and_consume_once 51 | end 52 | 53 | it 'cleans Thread.current[:mq] after pubsub examples' do 54 | Thread.current[:mq].should be_nil 55 | end 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /spec/minitest/coolio_integration_minispec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | if !(RUBY_PLATFORM =~ /java/) 4 | describe "EventedSpec cool.io bindings" do 5 | def coolio_running? 6 | !!Coolio::Loop.default.instance_variable_get(:@running) 7 | end # coolio_running? 8 | 9 | include EventedSpec::SpecHelper 10 | default_timeout 0.5 11 | 12 | it "can run inside of em loop" do 13 | coolio_running?.must_equal false 14 | coolio do 15 | coolio_running?.must_equal true 16 | done 17 | end 18 | coolio_running?.must_equal false 19 | end 20 | 21 | describe "hooks" do 22 | describe "hooks" do 23 | def hooks 24 | @hooks ||= [] 25 | end 26 | 27 | before { hooks << :before } 28 | coolio_before { hooks << :coolio_before } 29 | coolio_after { hooks << :coolio_after } 30 | after { hooks << :after } 31 | 32 | it "execute in proper order" do 33 | hooks.must_equal [:before] 34 | coolio do 35 | hooks.must_equal [:before, :coolio_before] 36 | done 37 | end 38 | hooks.must_equal [:before, :coolio_before, :coolio_after] 39 | end 40 | end 41 | end 42 | 43 | 44 | describe "#delayed" do 45 | default_timeout 0.7 46 | it "works as intended" do 47 | coolio do 48 | time = Time.now 49 | delayed(0.3) { Time.now.must_be_close_to time + 0.3, 0.1 } 50 | done(0.4) 51 | end 52 | end 53 | end 54 | 55 | describe EventedSpec::CoolioSpec do 56 | include EventedSpec::CoolioSpec 57 | it "wraps the whole example" do 58 | coolio_running?.must_equal true 59 | done 60 | end 61 | end 62 | 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/integration/failing_rspec_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Following 9 examples should all be failing:', :deliberately_failing => true do 4 | describe EventMachine, " when running failing examples" do 5 | include EventedSpec::EMSpec 6 | 7 | it "should not bubble failures beyond rspec" do 8 | EM.add_timer(0.1) do 9 | :should_not_bubble.should == :failures 10 | done 11 | end 12 | end 13 | 14 | it "should not block on failure" do 15 | 1.should == 2 16 | end 17 | end 18 | 19 | describe EventMachine, " when testing with EventedSpec::EMSpec with a maximum execution time per test" do 20 | include EventedSpec::EMSpec 21 | 22 | default_timeout 1 23 | 24 | it 'should timeout before reaching done' do 25 | EM.add_timer(2) { done } 26 | end 27 | 28 | it 'should timeout before reaching done' do 29 | timeout(0.3) 30 | EM.add_timer(0.6) { done } 31 | end 32 | end 33 | 34 | describe AMQP, " when testing with EventedSpec::AMQPSpec with a maximum execution time per test" do 35 | 36 | include EventedSpec::AMQPSpec 37 | 38 | default_timeout 1 39 | 40 | it 'should timeout before reaching done' do 41 | EM.add_timer(2) { done } 42 | end 43 | 44 | it 'should timeout before reaching done' do 45 | timeout(0.2) 46 | EM.add_timer(0.5) { done } 47 | end 48 | 49 | it 'should fail due to timeout, not hang up' do 50 | timeout(0.2) 51 | end 52 | 53 | it 'should fail due to default timeout, not hang up' do 54 | end 55 | end 56 | 57 | describe AMQP, "when default timeout is not set" do 58 | include EventedSpec::AMQPSpec 59 | it "should fail by timeout anyway" do 60 | 1.should == 1 61 | end 62 | end 63 | end -------------------------------------------------------------------------------- /spec/evented-spec/util_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe EventedSpec::Util do 4 | describe ".deep_clone" do 5 | context "for non-clonables" do 6 | it "should return the argument" do 7 | described_class.deep_clone(nil).object_id.should == nil.object_id 8 | described_class.deep_clone(0).object_id.should == 0.object_id 9 | described_class.deep_clone(false).object_id.should == false.object_id 10 | end 11 | end 12 | 13 | context "for strings and other simple clonables" do 14 | let(:string) { "Hello!" } 15 | it "should return a clone" do 16 | clone = described_class.deep_clone(string) 17 | clone.should == string 18 | clone.object_id.should_not == string.object_id 19 | end 20 | end 21 | 22 | context "for arrays" do 23 | let(:array) { [child_hash, child_string] } 24 | let(:child_string) { "Hello!" } 25 | let(:child_hash) { {} } 26 | it "should return a deep clone" do 27 | clone = described_class.deep_clone(array) 28 | clone.should == array 29 | clone.object_id.should_not == array.object_id 30 | clone[0].object_id.should_not == child_hash.object_id 31 | clone[1].object_id.should_not == child_string.object_id 32 | end 33 | end 34 | 35 | context "for hash" do 36 | let(:hash) { 37 | {:child_hash => child_hash, :child_array => child_array} 38 | } 39 | let(:child_hash) { {:hello => "world"} } 40 | let(:child_array) { ["One"] } 41 | 42 | it "should return a deep clone" do 43 | clone = described_class.deep_clone(hash) 44 | clone.should == hash 45 | clone.object_id.should_not == hash.object_id 46 | clone[:child_hash].object_id.should_not == child_hash.object_id 47 | clone[:child_array].object_id.should_not == child_array.object_id 48 | end 49 | end 50 | end 51 | end -------------------------------------------------------------------------------- /lib/evented-spec/evented_example/amqp_example.rb: -------------------------------------------------------------------------------- 1 | require 'evented-spec/ext/amqp' 2 | module EventedSpec 3 | module SpecHelper 4 | # Represents spec running inside AMQP.start loop 5 | # See {EventedExample} for details and method descriptions. 6 | class AMQPExample < EMExample 7 | # Run @block inside the AMQP.start loop. 8 | # See {EventedExample#run} 9 | def run 10 | run_em_loop do 11 | ::AMQP.start_connection(@opts) do 12 | run_hooks :amqp_before 13 | @example_group_instance.instance_eval(&@block) 14 | end 15 | end 16 | end 17 | 18 | # Breaks the event loop and finishes the spec. It yields to any given block first, 19 | # then stops AMQP, EM event loop and cleans up AMQP state. 20 | # 21 | # See {EventedExample#done} 22 | def done(delay = nil) 23 | delayed(delay) do 24 | yield if block_given? 25 | EM.next_tick do 26 | run_hooks :amqp_after 27 | if ::AMQP.connection && !::AMQP.closing? 28 | ::AMQP.stop_connection do 29 | # Cannot call finish_em_loop before connection is marked as closed 30 | # This callback is called before that happens. 31 | EM.next_tick { finish_em_loop } 32 | end 33 | else 34 | # Need this branch because if AMQP couldn't connect, 35 | # the callback would never trigger 36 | ::AMQP.cleanup_state 37 | EM.next_tick { finish_em_loop } 38 | end 39 | end 40 | end 41 | end 42 | 43 | # Called from run_event_loop when event loop is finished, before any exceptions 44 | # is raised or example returns. We ensure AMQP state cleanup here. 45 | # 46 | # See {EventedExample#run} 47 | def finish_example 48 | ::AMQP.cleanup_state 49 | super 50 | end 51 | end # class AMQPExample < EventedExample 52 | end # module SpecHelper 53 | end # module EventedExample -------------------------------------------------------------------------------- /spec/minitest/amqp_integration_minispec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "EventedSpec AMQP bindings" do 4 | include EventedSpec::SpecHelper 5 | default_timeout 0.5 6 | 7 | def amqp_running? 8 | EM.reactor_running? && !!AMQP.connection 9 | end # amqp_running? 10 | 11 | 12 | it "runs after amqp is connected" do 13 | amqp_running?.must_equal false 14 | amqp do 15 | amqp_running?.must_equal true 16 | done 17 | end 18 | amqp_running?.must_equal false 19 | end 20 | 21 | describe "hooks" do 22 | def hooks 23 | @hooks ||= [] 24 | end 25 | 26 | before { hooks << :before } 27 | em_before { hooks << :em_before } 28 | amqp_before { hooks << :amqp_before } 29 | amqp_after { hooks << :amqp_after } 30 | em_after { hooks << :em_after } 31 | after { hooks << :after } 32 | 33 | it "execute in proper order" do 34 | hooks.must_equal [:before] 35 | amqp do 36 | hooks.must_equal [:before, :em_before, :amqp_before] 37 | done 38 | end 39 | hooks.must_equal [:before, :em_before, :amqp_before, 40 | :amqp_after, :em_after] 41 | end 42 | end 43 | 44 | describe EventedSpec::AMQPSpec do 45 | include EventedSpec::AMQPSpec 46 | 47 | it "runs after amqp is connected" do 48 | amqp_running?.must_equal true 49 | done 50 | end 51 | end 52 | 53 | describe "actual amqp functionality" do 54 | def publish_and_consume_once(queue_name="test_sink", data="data") 55 | AMQP::Channel.new do |channel, _| 56 | exchange = channel.direct(queue_name) 57 | queue = channel.queue(queue_name).bind(exchange) 58 | queue.subscribe do |hdr, msg| 59 | hdr.must_be_kind_of AMQP::Header 60 | msg.must_equal data 61 | done { queue.unsubscribe; queue.delete } 62 | end 63 | EM.add_timer(0.2) do 64 | exchange.publish data 65 | end 66 | end 67 | end 68 | 69 | it "can connect and publish something" do 70 | amqp do 71 | publish_and_consume_once 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /tasks/version.rake: -------------------------------------------------------------------------------- 1 | class Version 2 | attr_accessor :major, :minor, :patch, :build 3 | 4 | def initialize(version_string) 5 | raise "Invalid version #{version_string}" unless version_string =~ /^(\d+)\.(\d+)\.(\d+)(?:\.(.*?))?$/ 6 | @major = $1.to_i 7 | @minor = $2.to_i 8 | @patch = $3.to_i 9 | @build = $4 10 | end 11 | 12 | def bump_major(x) 13 | @major += x.to_i 14 | @minor = 0 15 | @patch = 0 16 | @build = nil 17 | end 18 | 19 | def bump_minor(x) 20 | @minor += x.to_i 21 | @patch = 0 22 | @build = nil 23 | end 24 | 25 | def bump_patch(x) 26 | @patch += x.to_i 27 | @build = nil 28 | end 29 | 30 | def update(major, minor, patch, build=nil) 31 | @major = major 32 | @minor = minor 33 | @patch = patch 34 | @build = build 35 | end 36 | 37 | def write(desc = nil) 38 | CLASS_NAME::VERSION_FILE.open('w') {|file| file.puts to_s } 39 | (BASE_PATH + 'HISTORY').open('a') do |file| 40 | file.puts "\n== #{to_s} / #{Time.now.strftime '%Y-%m-%d'}\n" 41 | file.puts "\n* #{desc}\n" if desc 42 | end 43 | end 44 | 45 | def to_s 46 | [major, minor, patch, build].compact.join('.') 47 | end 48 | end 49 | 50 | desc 'Set version: [x.y.z] - explicitly, [1/10/100] - bump major/minor/patch, [.build] - build' 51 | task :version, [:command, :desc] do |t, args| 52 | version = Version.new(VERSION) 53 | case args.command 54 | when /^(\d+)\.(\d+)\.(\d+)(?:\.(.*?))?$/ # Set version explicitly 55 | version.update($1, $2, $3, $4) 56 | when /^\.(.*?)$/ # Set build 57 | version.build = $1 58 | when /^(\d{1})$/ # Bump patch 59 | version.bump_patch $1 60 | when /^(\d{1})0$/ # Bump minor 61 | version.bump_minor $1 62 | when /^(\d{1})00$/ # Bump major 63 | version.bump_major $1 64 | else # Unknown command, just display VERSION 65 | puts "#{NAME} #{version}" 66 | next 67 | end 68 | 69 | puts "Writing version #{version} to VERSION file" 70 | version.write args.desc 71 | end 72 | -------------------------------------------------------------------------------- /lib/evented-spec/spec_helper/coolio_helpers.rb: -------------------------------------------------------------------------------- 1 | module EventedSpec 2 | module SpecHelper 3 | module CoolioHelpers 4 | module GroupHelpers 5 | # Adds before hook that will run inside coolio event loop before example starts. 6 | # 7 | # @param [Symbol] scope for hook (only :each is supported currently) 8 | # @yield hook block 9 | def coolio_before(scope = :each, &block) 10 | raise ArgumentError, "coolio_before only supports :each scope" unless :each == scope 11 | evented_spec_hooks_for(:coolio_before) << block 12 | end 13 | 14 | # Adds after hook that will run inside coolio event loop after example finishes. 15 | # 16 | # @param [Symbol] scope for hook (only :each is supported currently) 17 | # @yield hook block 18 | def coolio_after(scope = :each, &block) 19 | raise ArgumentError, "coolio_after only supports :each scope" unless :each == scope 20 | evented_spec_hooks_for(:coolio_after).unshift block 21 | end 22 | end # module GroupHelpers 23 | 24 | module ExampleHelpers 25 | # Yields to block inside cool.io loop, :spec_timeout option (in seconds) is used to 26 | # force spec to timeout if something goes wrong and EM/AMQP loop hangs for some 27 | # reason. 28 | # 29 | # @param [Hash] options for cool.io 30 | # @param opts [Numeric] :spec_timeout (nil) Amount of time before spec is stopped by timeout 31 | # @yield block to execute after cool.io loop starts 32 | def coolio(opts = {}, &block) 33 | opts = default_options.merge opts 34 | @evented_example = CoolioExample.new(opts, self, &block) 35 | @evented_example.run 36 | end 37 | end # module ExampleHelpers 38 | end # module CoolioHelpers 39 | end # module SpecHelper 40 | end # module EventedSpec 41 | 42 | module EventedSpec 43 | module SpecHelper 44 | module GroupMethods 45 | include EventedSpec::SpecHelper::CoolioHelpers::GroupHelpers 46 | end # module GroupHelpers 47 | include EventedSpec::SpecHelper::CoolioHelpers::ExampleHelpers 48 | end # module SpecHelper 49 | end # module EventedSpec 50 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | #$LOAD_PATH << "." unless $LOAD_PATH.include? "." # moronic 1.9.2 breaks things bad 2 | 3 | require 'bundler' 4 | Bundler.setup 5 | Bundler.require :default, :test 6 | 7 | require 'yaml' 8 | require 'evented-spec' 9 | require 'evented-spec/adapters/adapter_seg' 10 | 11 | 12 | require 'amqp' 13 | begin 14 | require 'cool.io' 15 | rescue LoadError => e 16 | if RUBY_PLATFORM =~ /java/ 17 | puts "Cool.io is unavailable for jruby" 18 | else 19 | # cause unknown, reraise 20 | raise e 21 | end 22 | end 23 | 24 | # Done is defined as noop to help share examples between evented and non-evented specs 25 | def done 26 | end 27 | 28 | RSpec.configure do |c| 29 | c.filter_run_excluding :nojruby => true if RUBY_PLATFORM =~ /java/ 30 | c.filter_run_excluding :deliberately_failing => true if ENV["EXCLUDE_DELIBERATELY_FAILING_SPECS"] 31 | end 32 | 33 | amqp_config = File.dirname(__FILE__) + '/amqp.yml' 34 | 35 | AMQP_OPTS = unless File.exists? amqp_config 36 | {:user => 'guest', 37 | :pass => 'guest', 38 | :host => 'localhost', 39 | :vhost => '/'} 40 | else 41 | class Hash 42 | def symbolize_keys 43 | self.inject({}) { |result, (key, value)| 44 | new_key = case key 45 | when String then 46 | key.to_sym 47 | else 48 | key 49 | end 50 | new_value = case value 51 | when Hash then 52 | value.symbolize_keys 53 | else 54 | value 55 | end 56 | result[new_key] = new_value 57 | result 58 | } 59 | end 60 | end 61 | 62 | YAML::load_file(amqp_config).symbolize_keys[:test] 63 | end -------------------------------------------------------------------------------- /lib/evented-spec/spec_helper/event_machine_helpers.rb: -------------------------------------------------------------------------------- 1 | module EventedSpec 2 | module SpecHelper 3 | module EventMachineHelpers 4 | module GroupMethods 5 | # Adds before hook that will run inside EM event loop before example starts. 6 | # 7 | # @param [Symbol] scope for hook (only :each is supported currently) 8 | # @yield hook block 9 | def em_before(scope = :each, &block) 10 | raise ArgumentError, "em_before only supports :each scope" unless :each == scope 11 | evented_spec_hooks_for(:em_before) << block 12 | end 13 | 14 | # Adds after hook that will run inside EM event loop after example finishes. 15 | # 16 | # @param [Symbol] scope for hook (only :each is supported currently) 17 | # @yield hook block 18 | def em_after(scope = :each, &block) 19 | raise ArgumentError, "em_after only supports :each scope" unless :each == scope 20 | evented_spec_hooks_for(:em_after).unshift block 21 | end 22 | end # module GroupMethods 23 | 24 | module ExampleMethods 25 | # Yields to block inside EM loop, :spec_timeout option (in seconds) is used to 26 | # force spec to timeout if something goes wrong and EM/AMQP loop hangs for some 27 | # reason. 28 | # 29 | # For compatibility with EM-Spec API, em method accepts either options Hash 30 | # or numeric timeout in seconds. 31 | # 32 | # @param [Hash] options for eventmachine 33 | # @param opts [Numeric] :spec_timeout (nil) Amount of time before spec is stopped by timeout 34 | # @yield block to execute after eventmachine loop starts 35 | def em(opts = {}, &block) 36 | opts = default_options.merge(opts.is_a?(Hash) ? opts : { :spec_timeout => opts }) 37 | @evented_example = EMExample.new(opts, self, &block) 38 | @evented_example.run 39 | end 40 | end # module ExampleMethods 41 | end # module EventMachine 42 | end # module SpecHelper 43 | end # module EventedSpec 44 | 45 | module EventedSpec 46 | module SpecHelper 47 | module GroupMethods 48 | include EventedSpec::SpecHelper::EventMachineHelpers::GroupMethods 49 | end # module GroupMethods 50 | include EventedSpec::SpecHelper::EventMachineHelpers::ExampleMethods 51 | end # module SpecHelper 52 | end # module EventedSpec -------------------------------------------------------------------------------- /HISTORY: -------------------------------------------------------------------------------- 1 | == 0.0.0 / 2010-10-13 2 | 3 | * Birthday! 4 | 5 | == 0.0.1 / 2010-10-13 6 | 7 | * Initial code import from EM-Spec and AMQPHelper 8 | 9 | == 0.0.2 / 2010-10-13 10 | 11 | * Minimal functionality implemented 12 | 13 | == 0.0.4 / 2010-10-14 14 | 15 | * Problems with default_options resolved 16 | 17 | == 0.1.0 / 2010-10-15 18 | 19 | * Monkeypatched start/stop_connection directly into AMQP 20 | 21 | == 0.1.2 / 2010-10-15 22 | 23 | * Cleanup in AMQP.cleanup 24 | 25 | == 0.1.3 / 2010-10-15 26 | 27 | * Make sure Thread.current[:mq] state is cleaned after each example 28 | 29 | == 0.1.7 / 2010-10-15 30 | 31 | * em interface improved for timeout arg 32 | 33 | == 0.1.11 / 2010-10-18 34 | 35 | * Optional delay added to done 36 | 37 | == 0.2.0 / 2010-10-28 38 | 39 | * Rspec 2 support added 40 | 41 | == 0.2.2 / 2010-10-31 42 | 43 | * Metadata in Rspec 1 44 | 45 | == 0.2.7 / 2010-11-04 46 | 47 | * Make AMQP.cleanup_state more thorough 48 | 49 | == 0.2.8 / 2010-11-15 50 | 51 | * syncronize method added for wrapping async calls 52 | 53 | == 0.2.9 / 2010-11-16 54 | 55 | * Fallback to a separate default timeout 56 | 57 | == 0.3.0 / 2010-11-16 58 | 59 | * Hooks em_before/em_after implemented 60 | 61 | == 0.3.1 / 2010-11-17 62 | 63 | * Documentation cleanup 64 | 65 | == 0.3.2 / 2010-11-23 66 | 67 | * EM hooks now working as they should - bugs fixed 68 | 69 | == 0.3.3 / 2010-11-24 70 | 71 | * Spec timeout refactored 72 | 73 | == 0.3.4 / 2010-11-26 74 | 75 | * amqp_before/after hooks added 76 | 77 | == 0.3.5 / 2011-01-07 78 | 79 | * Drop-in support for legacy em-spec based examples added 80 | 81 | == 0.3.6 / 2011-01-07 82 | 83 | * Changed Gemfile to avoid circular dependency on AMQP 84 | 85 | == 0.3.7 / 2011-01-07 86 | 87 | * Changed Gemspec to avoid circular dependency on AMQP 88 | 89 | == 0.3.8 / 2011-01-25 90 | 91 | * MRI 1.8.7 support added 92 | 93 | == 0.4.0 / 2011-03-13 94 | 95 | * Forked into evented-spec. 96 | * cool.io support 97 | * Using amqp 0.8.* API instead of 0.7.* 98 | 99 | == 0.4.1 / 2011-04-29 100 | 101 | * More cool.io goodies: 102 | * EventedSpec::CoolioSpec module similar to EMSpec and AMQPSpec 103 | * coolio_before / coolio_after hooks 104 | * #delayed helper 105 | 106 | == 0.4.1 ... 0.10.0.pre / 2011-04-29 ... 2012-02-17 107 | 108 | * API wasn't changed at all 109 | * Minitest support (as in 'minitest/spec') 110 | -------------------------------------------------------------------------------- /lib/evented-spec/evented_example.rb: -------------------------------------------------------------------------------- 1 | module EventedSpec 2 | module SpecHelper 3 | # Represents example running inside some type of event loop. 4 | # You are not going to use or interact with this class and its descendants directly. 5 | # 6 | # @abstract 7 | class EventedExample 8 | # Default options to use with the examples 9 | DEFAULT_OPTIONS = { 10 | :spec_timeout => 5 11 | } 12 | 13 | # Create new evented example 14 | def initialize(opts, example_group_instance, &block) 15 | @opts, @example_group_instance, @block = DEFAULT_OPTIONS.merge(opts), example_group_instance, block 16 | end 17 | 18 | # Runs hooks of specified type (hopefully, inside event loop) 19 | # 20 | # @param hook type 21 | def run_hooks(type) 22 | @example_group_instance.class.evented_spec_hooks_for(type).each do |hook| 23 | @example_group_instance.instance_eval(&hook) 24 | end 25 | end 26 | 27 | 28 | # Called from #run_event_loop when event loop is stopped, 29 | # but before the example returns. 30 | # Descendant classes may redefine to clean up type-specific state. 31 | # 32 | # @abstract 33 | def finish_example 34 | raise @spec_exception if @spec_exception 35 | end 36 | 37 | # Run the example. 38 | # 39 | # @abstract 40 | def run 41 | raise NotImplementedError, "you should implement #run in #{self.class.name}" 42 | end 43 | 44 | # Sets timeout for currently running example 45 | # 46 | # @abstract 47 | def timeout(spec_timeout) 48 | raise NotImplementedError, "you should implement #timeout in #{self.class.name}" 49 | end 50 | 51 | # Breaks the event loop and finishes the spec. 52 | # 53 | # @abstract 54 | def done(delay=nil, &block) 55 | raise NotImplementedError, "you should implement #done method in #{self.class.name}" 56 | end 57 | 58 | # Override this method in your descendants 59 | # 60 | # @note block should be evaluated in EventedExample descendant instance context 61 | # (e.g. in EMExample instance) 62 | # @note delay may be nil, implying you need to execute the block immediately. 63 | # @abstract 64 | def delayed(delay = nil, &block) 65 | raise NotImplementedError, "you should implement #delayed method in #{self.class.name}" 66 | end # delayed(delay, &block) 67 | end # class EventedExample 68 | end # module SpecHelper 69 | end # module AMQP 70 | -------------------------------------------------------------------------------- /lib/evented-spec/spec_helper/amqp_helpers.rb: -------------------------------------------------------------------------------- 1 | module EventedSpec 2 | module SpecHelper 3 | module AMQPHelpers # naming is v 4 | module GroupMethods 5 | # Adds before hook that will run inside AMQP connection (AMQP.start loop) 6 | # before example starts 7 | # 8 | # @param [Symbol] scope for hook (only :each is supported currently) 9 | # @yield hook block 10 | def amqp_before(scope = :each, &block) 11 | raise ArgumentError, "amqp_before only supports :each scope" unless :each == scope 12 | evented_spec_hooks_for(:amqp_before) << block 13 | end 14 | 15 | # Adds after hook that will run inside AMQP connection (AMQP.start loop) 16 | # after example finishes 17 | # 18 | # @param [Symbol] scope for hook (only :each is supported currently) 19 | # @yield hook block 20 | def amqp_after(scope = :each, &block) 21 | raise ArgumentError, "amqp_after only supports :each scope" unless :each == scope 22 | evented_spec_hooks_for(:amqp_after).unshift block 23 | end 24 | end # module GroupMethods 25 | 26 | module ExampleMethods 27 | # Yields to a given block inside EM.run and AMQP.start loops. 28 | # 29 | # @param [Hash] options for amqp connection initialization 30 | # @option opts [String] :user ('guest') Username as defined by the AMQP server. 31 | # @option opts [String] :pass ('guest') Password as defined by the AMQP server. 32 | # @option opts [String] :vhost ('/') Virtual host as defined by the AMQP server. 33 | # @option opts [Numeric] :timeout (nil) *Connection* timeout, measured in seconds. 34 | # @option opts [Boolean] :logging (false) Toggle the extremely verbose AMQP logging. 35 | # @option opts [Numeric] :spec_timeout (nil) Amount of time before spec is stopped by timeout 36 | # @yield block to execute after amqp connects 37 | def amqp(opts = {}, &block) 38 | opts = default_options.merge opts 39 | @evented_example = AMQPExample.new(opts, self, &block) 40 | @evented_example.run 41 | end 42 | end # module ExampleMethods 43 | end # module AMQP 44 | end # module SpecHelper 45 | end # module EventedSpec 46 | 47 | module EventedSpec 48 | module SpecHelper 49 | module GroupMethods 50 | include EventedSpec::SpecHelper::AMQPHelpers::GroupMethods 51 | end # module GroupMethods 52 | 53 | include EventedSpec::SpecHelper::AMQPHelpers::ExampleMethods 54 | end # module SpecHelper 55 | end # module EventedSpec -------------------------------------------------------------------------------- /lib/evented-spec/evented_example/coolio_example.rb: -------------------------------------------------------------------------------- 1 | require 'evented-spec/ext/coolio' 2 | # 3 | # Cool.io loop is a little bit trickier to test, since it 4 | # doesn't go into a loop if there are no watchers. 5 | # 6 | # Basically, all we do is add a timeout watcher and run some callbacks 7 | # 8 | module EventedSpec 9 | module SpecHelper 10 | # Evented example which is run inside of cool.io loop. 11 | # See {EventedExample} details and method descriptions. 12 | class CoolioExample < EventedExample 13 | # see {EventedExample#run} 14 | def run 15 | reset 16 | delayed(0) do 17 | begin 18 | run_hooks :coolio_before 19 | @example_group_instance.instance_eval(&@block) 20 | rescue Exception => e 21 | @spec_exception ||= e 22 | done 23 | end 24 | end 25 | timeout(@opts[:spec_timeout]) if @opts[:spec_timeout] 26 | Coolio::DSL.run 27 | end 28 | 29 | # see {EventedExample#timeout} 30 | def timeout(time = 1) 31 | @spec_timer = delayed(time) do 32 | @spec_exception ||= SpecTimeoutExceededError.new("timed out") 33 | done 34 | end 35 | end 36 | 37 | # see {EventedExample#done} 38 | def done(delay = nil, &block) 39 | @spec_timer.detach 40 | delayed(delay) do 41 | yield if block_given? 42 | finish_loop 43 | end 44 | end 45 | 46 | # Stops the loop and finalizes the example 47 | def finish_loop 48 | run_hooks :coolio_after 49 | default_loop.stop 50 | finish_example 51 | end 52 | 53 | # see {EventedExample#delayed} 54 | def delayed(delay = nil, &block) 55 | timer = Coolio::TimerWatcher.new(delay.to_f, false) 56 | instance = self 57 | timer.on_timer do 58 | instance.instance_eval(&block) 59 | end 60 | timer.attach(default_loop) 61 | timer 62 | end 63 | 64 | protected 65 | 66 | def default_loop 67 | Coolio::Loop.default 68 | end 69 | 70 | # 71 | # Here is the drill: 72 | # If you get an exception inside of Cool.io event loop, you probably can't 73 | # do anything with it anytime later. You'll keep getting C-extension exceptions 74 | # when trying to start up. Replacing the Coolio default event loop with a new 75 | # one is relatively harmless. 76 | # 77 | # @private 78 | def reset 79 | Coolio::Loop.default_loop = Coolio::Loop.new 80 | end 81 | end # class CoolioExample 82 | end # module SpecHelper 83 | end # module EventedSpec -------------------------------------------------------------------------------- /lib/evented-spec/evented_example/em_example.rb: -------------------------------------------------------------------------------- 1 | module EventedSpec 2 | module SpecHelper 3 | # Represents spec running inside EM.run loop. 4 | # See {EventedExample} for details and method descriptions. 5 | class EMExample < EventedExample 6 | # Runs given block inside EM event loop. 7 | # Double-round exception handler needed because some of the exceptions bubble 8 | # outside of event loop due to asynchronous nature of evented examples 9 | # 10 | def run_em_loop 11 | begin 12 | EM.run do 13 | run_hooks :em_before 14 | 15 | @spec_exception = nil 16 | timeout(@opts[:spec_timeout]) if @opts[:spec_timeout] 17 | begin 18 | yield 19 | rescue Exception => e 20 | @spec_exception ||= e 21 | # p "Inside loop, caught #{@spec_exception.class.name}: #{@spec_exception}" 22 | done # We need to properly terminate the event loop 23 | end 24 | end 25 | rescue Exception => e 26 | @spec_exception ||= e 27 | # p "Outside loop, caught #{@spec_exception.class.name}: #{@spec_exception}" 28 | run_hooks :em_after # Event loop broken, but we still need to run em_after hooks 29 | ensure 30 | finish_example 31 | end 32 | end 33 | 34 | # Stops EM event loop. It is called from #done 35 | # 36 | def finish_em_loop 37 | run_hooks :em_after 38 | EM.stop_event_loop if EM.reactor_running? 39 | end 40 | 41 | # See {EventedExample#timeout} 42 | def timeout(spec_timeout) 43 | @spec_timer ||= nil 44 | EM.cancel_timer(@spec_timer) if @spec_timer 45 | @spec_timer = EM.add_timer(spec_timeout) do 46 | @spec_exception = SpecTimeoutExceededError.new "Example timed out" 47 | done 48 | end 49 | end 50 | 51 | # see {EventedExample#run} 52 | def run 53 | run_em_loop do 54 | @example_group_instance.instance_eval(&@block) 55 | end 56 | end 57 | 58 | # Breaks the EM event loop and finishes the spec. 59 | # Done yields to any given block first, then stops EM event loop. 60 | # 61 | # See {EventedExample#done} 62 | def done(delay = nil) 63 | delayed(delay) do 64 | yield if block_given? 65 | EM.next_tick do 66 | finish_em_loop 67 | end 68 | end 69 | end # done 70 | 71 | # See {EventedExample#delayed} 72 | def delayed(delay, &block) 73 | instance = self 74 | if delay 75 | EM.add_timer delay, Proc.new { instance.instance_eval(&block) } 76 | else 77 | instance.instance_eval(&block) 78 | end 79 | end # delayed 80 | end # class EMExample < EventedExample 81 | end # module SpecHelper 82 | end # module EventedSpec -------------------------------------------------------------------------------- /spec/evented-spec/adapters/adapter_seg.rb: -------------------------------------------------------------------------------- 1 | # Our assumptions with this seg: 2 | # - let(:prefix) is defined (e.g. 'coolio_') 3 | # - let(:method_name) is defined (e.g. 'coolio') 4 | # - #{prefix}running? method is defined in example group, it is true inside 5 | # #{method_name} call block, and false outside of it 6 | # 7 | shared_examples_for "EventedSpec adapter" do 8 | # Unfortunately, I know no other way to extract variables from let(...) 9 | prefix = self.new.prefix 10 | method_name = self.new.method_name 11 | 12 | def loop_running? 13 | send("#{prefix}running?") 14 | end 15 | 16 | def loop(*args) 17 | send(method_name, *args) do 18 | yield 19 | end 20 | end 21 | 22 | before(:each) { loop_running?.should be_false } 23 | after(:each) { loop_running?.should be_false } 24 | 25 | describe "sanity check:" do 26 | it "we should not be in #{method_name} loop unless explicitly asked" do 27 | loop_running?.should be_false 28 | end 29 | end 30 | 31 | describe "#{method_name}" do 32 | it "should execute given block in the right scope" do 33 | @variable = 1 34 | loop do 35 | @variable.should == 1 36 | @variable = true 37 | done 38 | end 39 | @variable.should == true 40 | end 41 | 42 | it "should start default event loop and give control" do 43 | loop do 44 | loop_running?.should be_true 45 | done 46 | end 47 | end 48 | 49 | it "should stop the event loop afterwards" do 50 | loop do 51 | done 52 | end 53 | loop_running?.should be_false 54 | end 55 | 56 | it "should raise SpecTimeoutExceededError when #done is not issued" do 57 | expect { 58 | loop do 59 | end 60 | }.to raise_error(EventedSpec::SpecHelper::SpecTimeoutExceededError) 61 | end 62 | 63 | it "should propagate mismatched rspec expectations" do 64 | expect { 65 | loop do 66 | :fail.should == :win 67 | end 68 | }.to raise_error(RSpec::Expectations::ExpectationNotMetError) 69 | end 70 | end 71 | 72 | 73 | describe "#done" do 74 | it "should execute given block" do 75 | loop do 76 | done(0.05) do 77 | @variable = true 78 | end 79 | end 80 | @variable.should be_true 81 | end 82 | 83 | it "should cancel timeout" do 84 | expect { 85 | loop do 86 | done(0.2) 87 | end 88 | }.to_not raise_error 89 | end 90 | end 91 | 92 | describe "hooks" do 93 | context "before" do 94 | send("#{prefix}before") do 95 | @called_back = true 96 | loop_running?.should be_true 97 | end 98 | 99 | it "should run before example starts" do 100 | loop do 101 | @called_back.should be_true 102 | done 103 | end 104 | end 105 | end 106 | 107 | context "after" do 108 | send("#{prefix}after") do 109 | @called_back = true 110 | loop_running?.should be_true 111 | end 112 | 113 | it "should run after example finishes" do 114 | loop do 115 | @called_back.should be_false 116 | done 117 | end 118 | @called_back.should be_true 119 | end 120 | end 121 | end 122 | 123 | describe "#delayed" do 124 | it "should run an operation after certain amount of time" do 125 | loop(:spec_timeout => 3) do 126 | time = Time.now 127 | delayed(0.5) do 128 | (Time.now - time).should be_within(0.3).of(0.5) 129 | done 130 | end 131 | end 132 | end 133 | 134 | it "should preserve context" do 135 | loop(:spec_timeout => 3) do 136 | @instance_var = true 137 | delayed(0.1) do 138 | @instance_var.should be_true 139 | done 140 | end 141 | end 142 | end 143 | end 144 | 145 | 146 | describe "error handling" do 147 | it "bubbles failing expectations up to Rspec" do 148 | expect { 149 | loop do 150 | :this.should == :fail 151 | end 152 | }.to raise_error(RSpec::Expectations::ExpectationNotMetError) 153 | loop_running?.should be_false 154 | end 155 | end 156 | end -------------------------------------------------------------------------------- /spec/evented-spec/defaults_options_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe EventedSpec::SpecHelper, " .default_options" do 4 | include EventedSpec::SpecHelper 5 | root_default_options = {:root_key => 1} 6 | default_options root_default_options 7 | 8 | it 'subsequent and nested groups should not change root default options' do 9 | root_default_options.should == {:root_key => 1} 10 | end 11 | 12 | it 'example has access to default options' do 13 | default_options.should == root_default_options 14 | end 15 | 16 | it 'defaults can be changed inside example, diverging from example group defaults' do 17 | default_options[:example_key] = :example_value 18 | default_options.should have_key :example_key 19 | default_options.should_not == root_default_options 20 | end 21 | 22 | it 'changing example defaults has no effect on subsequent examples' do 23 | default_options.should_not have_key :example_key 24 | default_options.should == root_default_options 25 | end 26 | 27 | context 'inside nested example group 1' do 28 | nested_default_options = default_options 29 | 30 | it 'nested group defaults start as a copy of enclosing group default_options' do 31 | nested_default_options.should == root_default_options 32 | end 33 | 34 | it 'example has access to default options' do 35 | default_options.should == nested_default_options 36 | end 37 | 38 | it 'can be changed, thus diverging from example group default_options' do 39 | default_options[:example_key] = :example_value 40 | default_options.should have_key :example_key 41 | default_options.should_not == root_default_options 42 | end 43 | 44 | it 'changing example default_options has no effect on subsequent examples' do 45 | default_options.should_not have_key :example_key 46 | default_options.should == root_default_options 47 | end 48 | 49 | context 'inside deeply nested example group 1' do 50 | nested_default_options = default_options 51 | 52 | it 'nested group defaults start as a copy of enclosing group default_options' do 53 | nested_default_options.should == root_default_options 54 | end 55 | 56 | it 'example has access to default options' do 57 | default_options.should == nested_default_options 58 | end 59 | 60 | it 'can be changed in example, thus diverging from example group default_options' do 61 | default_options[:example_key] = :example_value 62 | default_options.should have_key :example_key 63 | default_options.should_not == nested_default_options 64 | end 65 | 66 | it 'changing example default_options has no effect on subsequent examples' do 67 | default_options.should_not have_key :example_key 68 | default_options.should == nested_default_options 69 | end 70 | end # inside deeply nested example group 1 71 | end # inside nested example group 1 72 | 73 | context 'inside nested example group 2' do 74 | default_options[:nested_key] = :nested_value 75 | nested_default_options = default_options 76 | 77 | it 'changing default options inside nested group works' do 78 | nested_default_options.should have_key :nested_key 79 | end 80 | 81 | it 'changing default_options in nested group affects example default_options' do 82 | default_options.should == nested_default_options 83 | default_options.should_not == root_default_options 84 | end 85 | 86 | it 'can be changed in example, thus diverging from example group default_options' do 87 | default_options[:example_key] = :example_value 88 | default_options.should have_key :example_key 89 | default_options.should have_key :nested_key 90 | default_options.should_not == nested_default_options 91 | default_options.should_not == root_default_options 92 | end 93 | 94 | it 'changing example default_options has no effect on subsequent examples' do 95 | default_options.should == nested_default_options 96 | end 97 | 98 | context 'inside deeply nested example group 2' do 99 | default_options[:deeply_nested_key] = :deeply_nested_value 100 | deeply_nested_default_options = default_options 101 | 102 | it 'inherits default options from enclosing group' do 103 | deeply_nested_default_options.should have_key :nested_key 104 | end 105 | 106 | it 'changing default options inside deeply nested group works' do 107 | deeply_nested_default_options.should have_key :deeply_nested_key 108 | end 109 | 110 | it 'changing default_options in nested group affects example group default_options' do 111 | default_options.should == deeply_nested_default_options 112 | default_options.should have_key :nested_key 113 | default_options.should have_key :deeply_nested_key 114 | default_options.should_not == nested_default_options 115 | default_options.should_not == root_default_options 116 | end 117 | 118 | it 'can be changed in example, thus diverging from example group default_options' do 119 | default_options[:example_key] = :example_value 120 | default_options.should have_key :example_key 121 | default_options.should have_key :nested_key 122 | default_options.should have_key :deeply_nested_key 123 | default_options.should_not == nested_default_options 124 | default_options.should_not == root_default_options 125 | end 126 | 127 | it 'changing example default_options has no effect on subsequent examples' do 128 | default_options.should == deeply_nested_default_options 129 | end 130 | end # inside deeply nested example group 2 131 | end # inside nested example group 2 132 | end # describe AMQP, "default_options" 133 | -------------------------------------------------------------------------------- /lib/evented-spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # You can include one of the following modules into your example groups: 2 | # EventedSpec::SpecHelper, 3 | # EventedSpec::AMQPSpec, 4 | # EventedSpec::EMSpec. 5 | # 6 | # EventedSpec::SpecHelper module defines #ampq and #em methods that can be safely used inside 7 | # your specs (examples) to test code running inside AMQP.start or EM.run loop 8 | # respectively. Each example is running in a separate event loop,you can control 9 | # for timeouts either with :spec_timeout option given to #amqp/#em method or setting 10 | # a default timeout using default_timeout(timeout) macro inside describe/context block. 11 | # 12 | # If you include EventedSpec::Spec module into your example group, each example of this group 13 | # will run inside AMQP.start loop without the need to explicitly call 'amqp'. In order to 14 | # provide options to AMQP loop, default_options({opts}) macro is defined. 15 | # 16 | # Including EventedSpec::EMSpec module into your example group, each example of this group will 17 | # run inside EM.run loop without the need to explicitly call 'em'. 18 | # 19 | # In order to stop AMQP/EM loop, you should call 'done' AFTER you are sure that your 20 | # example is finished and your expectations executed. For example if you are using 21 | # subscribe block that tests expectations on messages, 'done' should be probably called 22 | # at the end of this block. 23 | # 24 | module EventedSpec 25 | 26 | # EventedSpec::SpecHelper module defines #ampq and #em methods that can be safely used inside 27 | # your specs (examples) to test code running inside AMQP.start or EM.run loop 28 | # respectively. Each example is running in a separate event loop, you can control 29 | # for timeouts either with :spec_timeout option given to #amqp/#em/#coolio method or setting 30 | # a default timeout using default_timeout(timeout) macro inside describe/context block. 31 | module SpecHelper 32 | # Error which shows in RSpec log when example does not call #done inside 33 | # of event loop. 34 | SpecTimeoutExceededError = Class.new(RuntimeError) 35 | 36 | # Class methods (macros) for any example groups that includes SpecHelper. 37 | # You can use these methods as macros inside describe/context block. 38 | module GroupMethods 39 | # Returns evented-spec related metadata for particular example group. 40 | # Metadata is cloned from parent to children, so that children inherit 41 | # all the options and hooks set in parent example groups 42 | # 43 | # @return [Hash] hash with example group metadata 44 | def evented_spec_metadata 45 | @evented_spec_metadata ||= nil 46 | if @evented_spec_metadata 47 | @evented_spec_metadata 48 | else 49 | @evented_spec_metadata = superclass.evented_spec_metadata rescue {} 50 | @evented_spec_metadata = EventedSpec::Util.deep_clone(@evented_spec_metadata) 51 | end 52 | end # evented_spec_metadata 53 | 54 | # Sets/retrieves default timeout for running evented specs for this 55 | # example group and its nested groups. 56 | # 57 | # @param [Float] desired timeout for the example group 58 | # @return [Float] 59 | def default_timeout(spec_timeout = nil) 60 | if spec_timeout 61 | default_options[:spec_timeout] = spec_timeout 62 | else 63 | default_options[:spec_timeout] || self.superclass.default_timeout 64 | end 65 | end 66 | 67 | # Sets/retrieves default AMQP.start options for this example group 68 | # and its nested groups. 69 | # 70 | # @param [Hash] context-specific options for helper methods like #amqp, #em, #coolio 71 | # @return [Hash] 72 | def default_options(opts = nil) 73 | evented_spec_metadata[:default_options] ||= {} 74 | if opts 75 | evented_spec_metadata[:default_options].merge!(opts) 76 | else 77 | evented_spec_metadata[:default_options] 78 | end 79 | end 80 | 81 | # Collection of evented hooks for current example group 82 | # 83 | # @return [Hash] hash with hooks 84 | def evented_spec_hooks 85 | evented_spec_metadata[:es_hooks] ||= Hash.new 86 | end 87 | 88 | # Collection of evented hooks of predefined type for current example group 89 | # 90 | # @param [Symbol] hook type 91 | # @return [Array] hooks 92 | def evented_spec_hooks_for(type) 93 | evented_spec_hooks[type] ||= [] 94 | end # evented_spec_hooks_for 95 | end 96 | 97 | def self.included(example_group) 98 | unless example_group.respond_to? :default_timeout 99 | example_group.extend GroupMethods 100 | end 101 | end 102 | 103 | # Retrieves default options passed in from enclosing example groups 104 | # 105 | # @return [Hash] default option for currently running example 106 | def default_options 107 | @default_options ||= self.class.default_options.dup rescue {} 108 | end 109 | 110 | # Executes an operation after certain delay 111 | # 112 | # @param [Float] time to wait before operation 113 | def delayed(time, &block) 114 | @evented_example.delayed(time) do 115 | @example_group_instance.instance_eval(&block) 116 | end 117 | end # delayed 118 | 119 | # Breaks the event loop and finishes the spec. This should be called after 120 | # you are reasonably sure that your expectations succeeded. 121 | # Done yields to any given block first, then stops EM event loop. 122 | # For amqp specs, stops AMQP and cleans up AMQP state. 123 | # 124 | # You may pass delay (in seconds) to done. If you do so, please keep in mind 125 | # that your (default or explicit) spec timeout may fire before your delayed done 126 | # callback is due, leading to SpecTimeoutExceededError 127 | # 128 | # @param [Float] Delay before event loop is stopped 129 | def done(*args, &block) 130 | @evented_example.done(*args, &block) if @evented_example 131 | end 132 | 133 | # Manually sets timeout for currently running example. If spec doesn't call 134 | # #done before timeout, it is marked as failed on timeout. 135 | # 136 | # @param [Float] Delay before event loop is stopped with error 137 | def timeout(*args) 138 | @evented_example.timeout(*args) if @evented_example 139 | end 140 | 141 | end # module SpecHelper 142 | end # EventedSpec 143 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h2. About evented-spec 2 | 3 | Evented-spec is a set of helpers to help you test your asynchronous code. 4 | 5 | EventMachine/Cool.io-based code, including asynchronous "AMQP library":https://github.com/ruby-amqp/ruby-amqp is notoriously difficult to test. To the point that many people recommend using either "mocks":https://github.com/danielsdeleo/moqueue or "synchronous libraries":https://github.com/ruby-amqp/bunny instead of EM-based libraries in unit tests. This is not always an option, however -- sometimes your code just has to run inside the event loop, and you want to test a real thing, not just mocks. 6 | 7 | "em-spec":https://github.com/tmm1/em-spec gem made it easier to write evented specs, but it had several drawbacks. First, it is not easy to manage both EM.run and AMQP.start loops at the same time. Second, AMQP is not properly stopped and deactivated upon exceptions and timeouts, resulting in state leak between examples and multiple mystereous failures. amqp-spec, and, subsequently, evented-spec add more helpers to keep your specs from being bloated. 8 | 9 | h2. Usage 10 | 11 | To get started with evented-spec you need to include one of the helper modules in your example groups, e.g.: 12 | 13 | bc. describe "eventmachine-based client" do 14 | include EventedSpec::SpecHelper 15 | it "should allow you to start a reactor" do 16 | em do 17 | EventMachine.reactor_running?.should be_true 18 | done 19 | end 20 | end 21 | context "nested contexts" do 22 | it "don't require another include" do 23 | em do 24 | EventMachine.add_timer(0.1) { @timer_run = true } 25 | done(0.3) 26 | end 27 | @timer_run.should be_true 28 | end 29 | end 30 | end 31 | 32 | Particular modules and methods are explained below. 33 | 34 | h3. #done 35 | 36 | We have no means to know when your work with reactor is finished, so whatever it is you need to call @done@ at some point. It optionally accepts a timeout and a block that is executed right before event reactor loop is stopped. If you don't call @done@, your specs are going to fail by timeout. 37 | 38 | h3. EventedSpec::SpecHelper 39 | 40 | @EventedSpec::SpecHelper@ is for semi-manual managing of reactor life-cycle. It includes three helpers: for EventMachine, Coolio and AMQP. 41 | 42 | @em@ stands for EventMachine. It takes a block, which is run after reactor starts. 43 | 44 | @amqp@ stands for AMQP. It takes a block, which is run after amqp connects with broker using given or default options. 45 | 46 | @coolio@ stands for cool.io. It takes a block, which is run after reactor starts. 47 | 48 | All three accept a hash of options. Look into method documentation to learn more. 49 | 50 | h3. EventedSpec::EMSpec, EventedSpec::AMQPSpec, EventedSpec::CoolioSpec 51 | 52 | @EventedSpec::EMSpec@ wraps every example in em block, so it might save you a couple of lines per example. @EventedSpec::AMQPSpec@ wraps every example in amqp block. 53 | 54 | Also note that every example group including @EMSpec@ or @AMQPSpec@ automatically includes @SpecHelper@. 55 | 56 | Example: 57 | 58 | bc. describe "eventmachine specs" do 59 | include EventedSpec::EMSpec 60 | it "should run in a reactor" do 61 | EventMachine.reactor_running?.should be_true 62 | done # don't forget to finish your specs properly! 63 | end 64 | end 65 | 66 | 67 | h3. default_options, default_timeout 68 | 69 | You can also pass some default options to specs (like amqp settings), they're specific to domain you're using evented-spec in. 70 | 71 | @default_timeout@ sets time (in seconds) for specs to time out 72 | 73 | bc. describe "using default_timeout" do 74 | include EventedSpec::SpecHelper 75 | default_timeout 0.5 76 | it "should prevent specs from hanging up" do 77 | em do 78 | 1.should == 1 # this spec is going to fail with timeout error because #done is not called 79 | end 80 | end 81 | end 82 | 83 | h3. Specific timeout 84 | 85 | A specific timeout can be set as well 86 | 87 | describe "overriding default_timeout" do 88 | include EventedSpec::SpecHelper 89 | default_timeout 0.5 90 | it "should give it some more time" do 91 | em(5) do 92 | 1.should == 1 # this spec is going to fail with timeout error after 5 seconds 93 | end 94 | end 95 | end 96 | 97 | h2. Hooks 98 | 99 | There are 6 hooks available to evented specs: 100 | 101 | * @em_before@ -- launches after reactor started, before example runs 102 | * @em_after@ -- launches right before reactor is stopped, after example runs 103 | * @amqp_before@ -- launches after amqp connects, before example runs 104 | * @amqp_after@ -- launches before amqp disconnects, after example runs 105 | * @coolio_before@ -- launches after Cool.io starts, before example runs 106 | * @coolio_after@ -- launches before Cool.io stops, after example runs 107 | 108 | So, the order of hooks for an AMQP spec is as follows: @before(:all)@, @before(:each)@, 109 | @em_before@, @amqp_before@, example, @amqp_after@, @em_after@, @after(:each)@, 110 | @after(:all)@ 111 | 112 | 113 | 114 | bc. describe "using amqp hooks" do 115 | include EventedSpec::AMQPSpec 116 | default_timeout 0.5 117 | amqp_before do 118 | AMQP.connection.should_not be_nil 119 | end 120 | let(:data) { "Test string" } 121 | it "should do something useful" do 122 | AMQP::Channel.new do |channel, _| 123 | exchange = channel.direct("amqp-test-exchange") 124 | queue = channel.queue("amqp-test-queue").bind(exchange) 125 | queue.subscribe do |hdr, msg| 126 | hdr.should be_an AMQP::Header 127 | msg.should == data 128 | done { queue.unsubscribe; queue.delete } 129 | end 130 | EM.add_timer(0.2) do 131 | exchange.publish data 132 | end 133 | end 134 | end 135 | end 136 | 137 | h2. Words of warning on blocking the reactor 138 | 139 | Evented specs are currently run inside of reactor thread. What this effectively means is that you *should not block* during spec execution. 140 | 141 | For example, the following *will not* work: 142 | 143 | bc. describe "using amqp" do 144 | include EventedSpec::AMQPSpec 145 | it "should do something useful" do 146 | channel = AMQP::Channel.new 147 | sleep 0.2 # voila, you're blocking the reactor 148 | channel.should be_open # no, it should not 149 | done 150 | end 151 | end 152 | 153 | What you *should* do instead is use callbacks: 154 | 155 | bc. describe "using amqp" do 156 | include EventedSpec::AMQPSpec 157 | it "should do something useful" do 158 | AMQP::Channel.new do |channel, _| 159 | channel.should be_open 160 | done 161 | end 162 | end 163 | end 164 | 165 | You can also use #delayed helper method to maintain order of execution when callbacks are not an option. 166 | 167 | bc. describe "using amqp" do 168 | include EventedSpec::AMQPSpec 169 | it "should do something useful" do 170 | channel = AMQP::Channel.new 171 | @stage = 0 172 | delayed(0.2) { 173 | channel.should be_open 174 | @stage = 1 175 | } 176 | delayed(0.3) { 177 | @stage.should == 1 178 | done 179 | } 180 | delayed(0.4) { 181 | # this block is never going to be executed 182 | raise "Help me!" 183 | } 184 | end 185 | end 186 | 187 | 188 | h2. I have an existing reactor running in separate thread, amqp specs won't work for me what should I do? 189 | 190 | Unfortunately, right now there aren't many remedies to your problem, besides stopping the event loop in before(:all) hook like this: 191 | 192 | bc. describe "Example" do 193 | before(:all) { EM.stop_event_loop; sleep(0.1) } 194 | after(:all) { do_something_to_restart_the_eventmachine } 195 | include EventedSpec::SpecHelper 196 | it "should do something" do 197 | em { done } 198 | end 199 | end 200 | 201 | Reason is simple: if we don't restart event loop every spec example, all kinds of state leaks may occur: stale timers, delayed exceptions, weirdest errors and even segfaults. It isn't impossible but it certainly is very invasive. 202 | 203 | h2. Compatibility 204 | 205 | EventedSpec is tested with RSpec >= 2.5.0 / Minitest >= 2.11.2, Cool.io ~> 1.0.0, EventMachine >= 0.12.10, and AMQP >= 0.8.0. Running it with RSpec 1.3 and/or AMQP 0.7.0 is not unheard of, although not tested in all its entirety. 206 | 207 | h2. See also 208 | 209 | You can see evented-spec in use in spec suites for our amqp gems, "amq-client":https://github.com/ruby-amqp/amq-client/tree/master/spec and "amqp":https://github.com/ruby-amqp/amqp/tree/master/spec. 210 | 211 | h2. Links 212 | 213 | * "cool.io":http://coolio.github.com/ 214 | * "amqp-spec":https://github.com/ruby-amqp/amqp-spec 215 | * "eventmachine":http://eventmachine.rubyforge.org/ 216 | * "amqp":https://github.com/ruby-amqp/amqp 217 | * "amq-client":https://github.com/ruby-amqp/amq-client 218 | -------------------------------------------------------------------------------- /spec/em_hooks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # Registers specific hook type as being called, 4 | # sets expectations about EM reactor and AMQP connection state 5 | module HookHelper 6 | def hook(type, reactor, connection) 7 | @hooks_called << type.to_sym if type 8 | if :reactor_running == reactor 9 | EM.reactor_running?.should be_true 10 | else 11 | EM.reactor_running?.should be_false 12 | end 13 | if :amqp_connected == connection 14 | AMQP.connection.should be_connected 15 | else 16 | if AMQP.connection 17 | AMQP.connection.connected?.should be_false 18 | end 19 | end 20 | end 21 | end 22 | 23 | shared_examples_for 'hooked em specs' do 24 | it 'should execute em_before' do 25 | em do 26 | @hooks_called.should include :em_before 27 | @hooks_called.should_not include :em_after 28 | done 29 | end 30 | end 31 | 32 | it 'should execute em_after if business exception is raised' do 33 | # Expectation is set in after{} hook 34 | em do 35 | expect { 36 | raise StandardError 37 | }.to raise_error 38 | done 39 | end 40 | end 41 | 42 | it 'should execute em_after if RSpec expectation fails' do 43 | # Expectation is set in after{} hook 44 | em do 45 | expect { :this.should == :fail 46 | }.to raise_error RSpec::Expectations::ExpectationNotMetError 47 | done 48 | end 49 | end 50 | end 51 | 52 | shared_examples_for 'hooked amqp specs' do 53 | it 'should execute em_before' do 54 | amqp do 55 | @hooks_called.should include :em_before 56 | @hooks_called.should_not include :em_after 57 | @hooks_called.should include :amqp_before 58 | @hooks_called.should_not include :amqp_after 59 | done 60 | end 61 | end 62 | 63 | it 'should execute em_after if business exception is raised' do 64 | # Expectation is set in after{} hook 65 | amqp do 66 | expect { 67 | raise StandardError 68 | }.to raise_error 69 | done 70 | end 71 | end 72 | 73 | it 'should execute em_after if RSpec expectation fails' do 74 | # Expectation is set in after{} hook 75 | amqp do 76 | expect { :this.should == :fail 77 | }.to raise_error RSpec::Expectations::ExpectationNotMetError 78 | done 79 | end 80 | end 81 | end 82 | 83 | describe EventedSpec::SpecHelper, ".em_before/.em_after" do 84 | before { @hooks_called = [] } 85 | include HookHelper 86 | describe AMQP, " tested with EventedSpec::SpecHelper" do 87 | include EventedSpec::SpecHelper 88 | default_options AMQP_OPTS if defined? AMQP_OPTS 89 | 90 | before { hook :before, :reactor_not_running, :amqp_not_connected } 91 | em_before { hook :em_before, :reactor_running, :amqp_not_connected } 92 | em_after { hook(:em_after, :reactor_running, :amqp_not_connected) } 93 | 94 | context 'for non-evented specs' do 95 | after { 96 | @hooks_called.should == [:before] 97 | hook :after, :reactor_not_running, :amqp_not_connected } 98 | 99 | it 'should NOT execute em_before or em_after' do 100 | @hooks_called.should_not include :em_before 101 | @hooks_called.should_not include :em_after 102 | end 103 | 104 | it 'should NOT execute em_after if business exception is raised' do 105 | expect { raise StandardError 106 | }.to raise_error 107 | end 108 | 109 | it 'should execute em_after if RSpec expectation fails' do 110 | expect { :this.should == :fail 111 | }.to raise_error RSpec::Expectations::ExpectationNotMetError 112 | end 113 | end # context 'for non-evented specs' 114 | 115 | context 'for evented specs' do #, pending: true do 116 | after do 117 | @hooks_called.should include :before, :em_before, :em_after 118 | hook :after, :reactor_not_running, :amqp_not_connected 119 | end 120 | 121 | context 'with em block' do 122 | 123 | it_should_behave_like 'hooked em specs' 124 | 125 | it 'should not run nested em hooks' do 126 | em do 127 | @hooks_called.should_not include :context_em_before, :context_before 128 | done 129 | end 130 | end 131 | 132 | it 'should not run hooks from unrelated group' do 133 | em do 134 | @hooks_called.should_not include :amqp_context_em_before, 135 | :amqp_context_before, 136 | :amqp_before, 137 | :context_amqp_before 138 | done 139 | end 140 | end 141 | 142 | context 'inside nested example group' do 143 | before { hook :context_before, :reactor_not_running, :amqp_not_connected } 144 | em_before { hook :context_em_before, :reactor_running, :amqp_not_connected } 145 | em_after { hook :context_em_after, :reactor_running, :amqp_not_connected } 146 | 147 | after do 148 | @hooks_called.should include :before, 149 | :context_before, 150 | :em_before, 151 | :context_em_before, 152 | :context_em_after, 153 | :em_after 154 | hook :after, :reactor_not_running, :amqp_not_connected 155 | end 156 | 157 | it_should_behave_like 'hooked em specs' 158 | 159 | it 'should fire all nested :before hooks, but no :after hooks' do 160 | em do 161 | @hooks_called.should == [:before, 162 | :context_before, 163 | :em_before, 164 | :context_em_before] 165 | done 166 | end 167 | end 168 | 169 | end # context 'inside nested example group' 170 | end # context 'with em block' 171 | 172 | context 'with amqp block' do 173 | amqp_before { hook :amqp_before, :reactor_running, :amqp_connected } 174 | amqp_after { hook :amqp_after, :reactor_running, :amqp_connected } 175 | 176 | it_should_behave_like 'hooked amqp specs' 177 | 178 | it 'should not run nested em hooks' do 179 | amqp do 180 | @hooks_called.should_not include :amqp_context_before, 181 | :amqp_context_em_before, 182 | :context_amqp_before 183 | done 184 | end 185 | end 186 | 187 | it 'should not run hooks from unrelated group' do 188 | amqp do 189 | @hooks_called.should_not include :context_em_before, :context_before 190 | done 191 | end 192 | end 193 | 194 | context 'inside nested example group' do 195 | before { hook :amqp_context_before, :reactor_not_running, :amqp_not_connected } 196 | em_before { hook :amqp_context_em_before, :reactor_running, :amqp_not_connected } 197 | em_after { hook :amqp_context_em_after, :reactor_running, :amqp_not_connected } 198 | amqp_before { hook :context_amqp_before, :reactor_running, :amqp_connected } 199 | amqp_after { hook :context_amqp_after, :reactor_running, :amqp_connected } 200 | 201 | after { @hooks_called.should == [:before, 202 | :amqp_context_before, 203 | :em_before, 204 | :amqp_context_em_before, 205 | :amqp_before, 206 | :context_amqp_before, 207 | :context_amqp_after, 208 | :amqp_after, 209 | :amqp_context_em_after, 210 | :em_after] 211 | hook :after, :reactor_not_running, :amqp_not_connected } 212 | 213 | it_should_behave_like 'hooked amqp specs' 214 | 215 | it 'should fire all :before hooks in correct order' do 216 | amqp do 217 | @hooks_called.should == [:before, 218 | :amqp_context_before, 219 | :em_before, 220 | :amqp_context_em_before, 221 | :amqp_before, 222 | :context_amqp_before] 223 | done 224 | end 225 | end 226 | 227 | end # context 'inside nested example group' 228 | end # context 'with amqp block' 229 | end # context 'for evented specs' 230 | end # describe EventedSpec, " tested with EventedSpec::SpecHelper" 231 | end # describe EventedSpec, " with em_before/em_after" 232 | --------------------------------------------------------------------------------