evented-spec 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. data/HISTORY +8 -1
  2. data/README.textile +51 -9
  3. data/Rakefile +3 -4
  4. data/VERSION +1 -1
  5. data/lib/evented-spec.rb +22 -1
  6. data/lib/evented-spec/amqp_spec.rb +27 -0
  7. data/lib/evented-spec/coolio_spec.rb +26 -0
  8. data/lib/evented-spec/em_spec.rb +26 -0
  9. data/lib/evented-spec/evented_example.rb +13 -5
  10. data/lib/evented-spec/evented_example/amqp_example.rb +8 -7
  11. data/lib/evented-spec/evented_example/coolio_example.rb +4 -18
  12. data/lib/evented-spec/evented_example/em_example.rb +6 -12
  13. data/lib/evented-spec/{amqp.rb → ext/amqp.rb} +31 -2
  14. data/lib/evented-spec/ext/coolio.rb +14 -0
  15. data/lib/evented-spec/spec_helper.rb +141 -0
  16. data/lib/evented-spec/spec_helper/amqp_helpers.rb +55 -0
  17. data/lib/evented-spec/spec_helper/coolio_helpers.rb +49 -0
  18. data/lib/evented-spec/spec_helper/event_machine_helpers.rb +52 -0
  19. data/spec/em_hooks_spec.rb +3 -3
  20. data/spec/evented-spec/adapters/adapter_seg.rb +156 -0
  21. data/spec/evented-spec/adapters/amqp_spec.rb +58 -0
  22. data/spec/evented-spec/adapters/cool_io_spec.rb +30 -0
  23. data/spec/evented-spec/adapters/em_spec.rb +29 -0
  24. data/spec/{em_defaults_spec.rb → evented-spec/defaults_options_spec.rb} +0 -0
  25. data/spec/{em_metadata_spec.rb → evented-spec/evented_spec_metadata_spec.rb} +0 -0
  26. data/spec/{util_spec.rb → evented-spec/util_spec.rb} +0 -0
  27. data/spec/{failing_rspec_spec.rb → integration/failing_rspec_spec.rb} +1 -1
  28. data/spec/spec_helper.rb +18 -9
  29. data/tasks/spec.rake +7 -2
  30. metadata +44 -24
  31. data/lib/evented-spec/rspec.rb +0 -267
  32. data/spec/cool_io_spec.rb +0 -72
  33. data/spec/rspec_amqp_spec.rb +0 -116
  34. data/spec/rspec_em_spec.rb +0 -53
  35. data/spec/shared_examples.rb +0 -194
@@ -0,0 +1,14 @@
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
@@ -0,0 +1,141 @@
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
+ if @evented_spec_metadata
46
+ @evented_spec_metadata
47
+ else
48
+ @evented_spec_metadata = superclass.evented_spec_metadata rescue {}
49
+ @evented_spec_metadata = EventedSpec::Util.deep_clone(@evented_spec_metadata)
50
+ end
51
+ end # evented_spec_metadata
52
+
53
+ # Sets/retrieves default timeout for running evented specs for this
54
+ # example group and its nested groups.
55
+ #
56
+ # @param [Float] desired timeout for the example group
57
+ # @return [Float]
58
+ def default_timeout(spec_timeout = nil)
59
+ if spec_timeout
60
+ default_options[:spec_timeout] = spec_timeout
61
+ else
62
+ default_options[:spec_timeout] || self.superclass.default_timeout
63
+ end
64
+ end
65
+
66
+ # Sets/retrieves default AMQP.start options for this example group
67
+ # and its nested groups.
68
+ #
69
+ # @param [Hash] context-specific options for helper methods like #amqp, #em, #coolio
70
+ # @return [Hash]
71
+ def default_options(opts = nil)
72
+ evented_spec_metadata[:default_options] ||= {}
73
+ if opts
74
+ evented_spec_metadata[:default_options].merge!(opts)
75
+ else
76
+ evented_spec_metadata[:default_options]
77
+ end
78
+ end
79
+
80
+ # Collection of evented hooks for current example group
81
+ #
82
+ # @return [Hash] hash with hooks
83
+ def evented_spec_hooks
84
+ evented_spec_metadata[:es_hooks] ||= Hash.new
85
+ end
86
+
87
+ # Collection of evented hooks of predefined type for current example group
88
+ #
89
+ # @param [Symbol] hook type
90
+ # @return [Array] hooks
91
+ def evented_spec_hooks_for(type)
92
+ evented_spec_hooks[type] ||= []
93
+ end # evented_spec_hooks_for
94
+ end
95
+
96
+ def self.included(example_group)
97
+ unless example_group.respond_to? :default_timeout
98
+ example_group.extend GroupMethods
99
+ end
100
+ end
101
+
102
+ # Retrieves default options passed in from enclosing example groups
103
+ #
104
+ # @return [Hash] default option for currently running example
105
+ def default_options
106
+ @default_options ||= self.class.default_options.dup rescue {}
107
+ end
108
+
109
+ # Executes an operation after certain delay
110
+ #
111
+ # @param [Float] time to wait before operation
112
+ def delayed(time, &block)
113
+ @evented_example.delayed(time) do
114
+ @example_group_instance.instance_eval(&block)
115
+ end
116
+ end # delayed
117
+
118
+ # Breaks the event loop and finishes the spec. This should be called after
119
+ # you are reasonably sure that your expectations succeeded.
120
+ # Done yields to any given block first, then stops EM event loop.
121
+ # For amqp specs, stops AMQP and cleans up AMQP state.
122
+ #
123
+ # You may pass delay (in seconds) to done. If you do so, please keep in mind
124
+ # that your (default or explicit) spec timeout may fire before your delayed done
125
+ # callback is due, leading to SpecTimeoutExceededError
126
+ #
127
+ # @param [Float] Delay before event loop is stopped
128
+ def done(*args, &block)
129
+ @evented_example.done *args, &block if @evented_example
130
+ end
131
+
132
+ # Manually sets timeout for currently running example. If spec doesn't call
133
+ # #done before timeout, it is marked as failed on timeout.
134
+ #
135
+ # @param [Float] Delay before event loop is stopped with error
136
+ def timeout(*args)
137
+ @evented_example.timeout *args if @evented_example
138
+ end
139
+
140
+ end # module SpecHelper
141
+ end # EventedSpec
@@ -0,0 +1,55 @@
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
@@ -0,0 +1,49 @@
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
@@ -0,0 +1,52 @@
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
@@ -43,7 +43,7 @@ shared_examples_for 'hooked em specs' do
43
43
  # Expectation is set in after{} hook
44
44
  em do
45
45
  expect { :this.should == :fail
46
- }.to raise_error RSPEC::Expectations::ExpectationNotMetError
46
+ }.to raise_error RSpec::Expectations::ExpectationNotMetError
47
47
  done
48
48
  end
49
49
  end
@@ -74,7 +74,7 @@ shared_examples_for 'hooked amqp specs' do
74
74
  # Expectation is set in after{} hook
75
75
  amqp do
76
76
  expect { :this.should == :fail
77
- }.to raise_error RSPEC::Expectations::ExpectationNotMetError
77
+ }.to raise_error RSpec::Expectations::ExpectationNotMetError
78
78
  done
79
79
  end
80
80
  end
@@ -108,7 +108,7 @@ describe EventedSpec::SpecHelper, ".em_before/.em_after" do
108
108
 
109
109
  it 'should execute em_after if RSpec expectation fails' do
110
110
  expect { :this.should == :fail
111
- }.to raise_error RSPEC::Expectations::ExpectationNotMetError
111
+ }.to raise_error RSpec::Expectations::ExpectationNotMetError
112
112
  end
113
113
  end # context 'for non-evented specs'
114
114
 
@@ -0,0 +1,156 @@
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