evented-spec 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,97 @@
1
+ #
2
+ # Cool.io loop is a little bit trickier to test, since it
3
+ # doesn't go into a loop if there are no watchers.
4
+ #
5
+ # Basically, all we do is add a timeout watcher and run some callbacks
6
+ #
7
+ module EventedSpec
8
+ module SpecHelper
9
+ # Evented example which is run inside of cool.io loop.
10
+ # See {EventedExample} details and method descriptions.
11
+ class CoolioExample < EventedExample
12
+ # see {EventedExample#run}
13
+ def run
14
+ reset
15
+ delayed(0) do
16
+ begin
17
+ @example_group_instance.instance_eval(&@block)
18
+ rescue Exception => e
19
+ @spec_exception ||= e
20
+ done
21
+ end
22
+ end
23
+ timeout(@opts[:spec_timeout]) if @opts[:spec_timeout]
24
+ Coolio::DSL.run
25
+ end
26
+
27
+ # see {EventedExample#timeout}
28
+ def timeout(time = 1)
29
+ @spec_timer = delayed(time) do
30
+ @spec_exception ||= SpecTimeoutExceededError.new("timed out")
31
+ done
32
+ end
33
+ end
34
+
35
+ # see {EventedExample#done}
36
+ def done(delay = nil, &block)
37
+ @spec_timer.detach
38
+ delayed(delay) do
39
+ yield if block_given?
40
+ finish_loop
41
+ end
42
+ end
43
+
44
+ # Stops the loop and finalizes the example
45
+ def finish_loop
46
+ default_loop.stop
47
+ finish_example
48
+ end
49
+
50
+ # see {EventedExample#delayed}
51
+ def delayed(delay = nil, &block)
52
+ timer = Coolio::TimerWatcher.new(delay.to_f, false)
53
+ instance = self
54
+ timer.on_timer do
55
+ instance.instance_eval(&block)
56
+ end
57
+ timer.attach(default_loop)
58
+ timer
59
+ end
60
+
61
+ protected
62
+
63
+ def default_loop
64
+ Coolio::Loop.default
65
+ end
66
+
67
+ #
68
+ # Here is the drill:
69
+ # If you get an exception inside of Cool.io event loop, you probably can't
70
+ # do anything with it anytime later. You'll keep getting C-extension exceptions
71
+ # when trying to start up. Replacing the Coolio default event loop with a new
72
+ # one is relatively harmless.
73
+ #
74
+ # @private
75
+ def reset
76
+ Coolio::Loop.default_loop = Coolio::Loop.new
77
+ end
78
+ end # class CoolioExample
79
+ end # module SpecHelper
80
+ end # module EventedSpec
81
+
82
+ # Cool.io provides no means to change the default loop which makes sense in
83
+ # most situations, but not ours.
84
+ #
85
+ # @private
86
+ module Coolio
87
+ class Loop
88
+ # Sets cool.io default loop.
89
+ def self.default_loop=(event_loop)
90
+ if RUBY_VERSION >= "1.9.0"
91
+ Thread.current.instance_variable_set :@_coolio_loop, event_loop
92
+ else
93
+ @@_coolio_loop = event_loop
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,87 @@
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 hooks of specified type (hopefully, inside the event loop)
7
+ def run_em_hooks(type)
8
+ @example_group_instance.class.em_hooks[type].each do |hook|
9
+ @example_group_instance.instance_eval(&hook) #_with_rescue(&hook)
10
+ end
11
+ end
12
+
13
+ # Runs given block inside EM event loop.
14
+ # Double-round exception handler needed because some of the exceptions bubble
15
+ # outside of event loop due to asynchronous nature of evented examples
16
+ #
17
+ def run_em_loop
18
+ begin
19
+ EM.run do
20
+ run_em_hooks :em_before
21
+
22
+ @spec_exception = nil
23
+ timeout(@opts[:spec_timeout]) if @opts[:spec_timeout]
24
+ begin
25
+ yield
26
+ rescue Exception => e
27
+ @spec_exception ||= e
28
+ # p "Inside loop, caught #{@spec_exception.class.name}: #{@spec_exception}"
29
+ done # We need to properly terminate the event loop
30
+ end
31
+ end
32
+ rescue Exception => e
33
+ @spec_exception ||= e
34
+ # p "Outside loop, caught #{@spec_exception.class.name}: #{@spec_exception}"
35
+ run_em_hooks :em_after # Event loop broken, but we still need to run em_after hooks
36
+ ensure
37
+ finish_example
38
+ end
39
+ end
40
+
41
+ # Stops EM event loop. It is called from #done
42
+ #
43
+ def finish_em_loop
44
+ run_em_hooks :em_after
45
+ EM.stop_event_loop if EM.reactor_running?
46
+ end
47
+
48
+ # See {EventedExample#timeout}
49
+ def timeout(spec_timeout)
50
+ EM.cancel_timer(@spec_timer) if @spec_timer
51
+ @spec_timer = EM.add_timer(spec_timeout) do
52
+ @spec_exception = SpecTimeoutExceededError.new "Example timed out"
53
+ done
54
+ end
55
+ end
56
+
57
+ # see {EventedExample#run}
58
+ def run
59
+ run_em_loop do
60
+ @example_group_instance.instance_eval(&@block)
61
+ end
62
+ end
63
+
64
+ # Breaks the EM event loop and finishes the spec.
65
+ # Done yields to any given block first, then stops EM event loop.
66
+ #
67
+ # See {EventedExample#done}
68
+ def done(delay = nil)
69
+ delayed(delay) do
70
+ yield if block_given?
71
+ EM.next_tick do
72
+ finish_em_loop
73
+ end
74
+ end
75
+ end # done
76
+
77
+ # See {EventedExample#delayed}
78
+ def delayed(delay, &block)
79
+ if delay
80
+ EM.add_timer delay, &block
81
+ else
82
+ yield
83
+ end
84
+ end # delayed
85
+ end # class EMExample < EventedExample
86
+ end # module SpecHelper
87
+ end # module EventedSpec
@@ -0,0 +1,267 @@
1
+ require 'evented-spec/amqp'
2
+ require 'evented-spec/evented_example'
3
+ require 'evented-spec/util'
4
+
5
+ # You can include one of the following modules into your example groups:
6
+ # EventedSpec::SpecHelper,
7
+ # EventedSpec::AMQPSpec,
8
+ # EventedSpec::EMSpec.
9
+ #
10
+ # EventedSpec::SpecHelper module defines #ampq and #em methods that can be safely used inside
11
+ # your specs (examples) to test code running inside AMQP.start or EM.run loop
12
+ # respectively. Each example is running in a separate event loop,you can control
13
+ # for timeouts either with :spec_timeout option given to #amqp/#em method or setting
14
+ # a default timeout using default_timeout(timeout) macro inside describe/context block.
15
+ #
16
+ # If you include EventedSpec::Spec module into your example group, each example of this group
17
+ # will run inside AMQP.start loop without the need to explicitly call 'amqp'. In order to
18
+ # provide options to AMQP loop, default_options({opts}) macro is defined.
19
+ #
20
+ # Including EventedSpec::EMSpec module into your example group, each example of this group will
21
+ # run inside EM.run loop without the need to explicitly call 'em'.
22
+ #
23
+ # In order to stop AMQP/EM loop, you should call 'done' AFTER you are sure that your
24
+ # example is finished and your expectations executed. For example if you are using
25
+ # subscribe block that tests expectations on messages, 'done' should be probably called
26
+ # at the end of this block.
27
+ #
28
+ module EventedSpec
29
+
30
+ # EventedSpec::SpecHelper module defines #ampq and #em methods that can be safely used inside
31
+ # your specs (examples) to test code running inside AMQP.start or EM.run loop
32
+ # respectively. Each example is running in a separate event loop, you can control
33
+ # for timeouts either with :spec_timeout option given to #amqp/#em/#coolio method or setting
34
+ # a default timeout using default_timeout(timeout) macro inside describe/context block.
35
+ module SpecHelper
36
+ # Error which shows in RSpec log when example does not call #done inside
37
+ # of event loop.
38
+ SpecTimeoutExceededError = Class.new(RuntimeError)
39
+
40
+ # Class methods (macros) for any example groups that includes SpecHelper.
41
+ # You can use these methods as macros inside describe/context block.
42
+ module GroupMethods
43
+ # Returns evented-spec related metadata for particular example group.
44
+ # Metadata is cloned from parent to children, so that children inherit
45
+ # all the options and hooks set in parent example groups
46
+ #
47
+ # @return [Hash] hash with example group metadata
48
+ def evented_spec_metadata
49
+ if @evented_spec_metadata
50
+ @evented_spec_metadata
51
+ else
52
+ @evented_spec_metadata = superclass.evented_spec_metadata rescue {}
53
+ @evented_spec_metadata = EventedSpec::Util.deep_clone(@evented_spec_metadata)
54
+ end
55
+ end # evented_spec_metadata
56
+
57
+ # Sets/retrieves default timeout for running evented specs for this
58
+ # example group and its nested groups.
59
+ #
60
+ # @param [Float] desired timeout for the example group
61
+ # @return [Float]
62
+ def default_timeout(spec_timeout = nil)
63
+ if spec_timeout
64
+ default_options[:spec_timeout] = spec_timeout
65
+ else
66
+ default_options[:spec_timeout] || self.superclass.default_timeout
67
+ end
68
+ end
69
+
70
+ # Sets/retrieves default AMQP.start options for this example group
71
+ # and its nested groups.
72
+ #
73
+ # @param [Hash] context-specific options for helper methods like #amqp, #em, #coolio
74
+ # @return [Hash]
75
+ def default_options(opts = nil)
76
+ evented_spec_metadata[:default_options] ||= {}
77
+ if opts
78
+ evented_spec_metadata[:default_options].merge!(opts)
79
+ else
80
+ evented_spec_metadata[:default_options]
81
+ end
82
+ end
83
+
84
+ # Adds before hook that will run inside EM event loop before example starts.
85
+ #
86
+ # @param [Symbol] scope for hook (only :each is supported currently)
87
+ # @yield hook block
88
+ def em_before(scope = :each, &block)
89
+ raise ArgumentError, "em_before only supports :each scope" unless :each == scope
90
+ em_hooks[:em_before] << block
91
+ end
92
+
93
+ # Adds after hook that will run inside EM event loop after example finishes.
94
+ #
95
+ # @param [Symbol] scope for hook (only :each is supported currently)
96
+ # @yield hook block
97
+ def em_after(scope = :each, &block)
98
+ raise ArgumentError, "em_after only supports :each scope" unless :each == scope
99
+ em_hooks[:em_after].unshift block
100
+ end
101
+
102
+ # Adds before hook that will run inside AMQP connection (AMQP.start loop)
103
+ # before example starts
104
+ #
105
+ # @param [Symbol] scope for hook (only :each is supported currently)
106
+ # @yield hook block
107
+ def amqp_before(scope = :each, &block)
108
+ raise ArgumentError, "amqp_before only supports :each scope" unless :each == scope
109
+ em_hooks[:amqp_before] << block
110
+ end
111
+
112
+ # Adds after hook that will run inside AMQP connection (AMQP.start loop)
113
+ # after example finishes
114
+ #
115
+ # @param [Symbol] scope for hook (only :each is supported currently)
116
+ # @yield hook block
117
+ def amqp_after(scope = :each, &block)
118
+ raise ArgumentError, "amqp_after only supports :each scope" unless :each == scope
119
+ em_hooks[:amqp_after].unshift block
120
+ end
121
+
122
+ # Collection of evented hooks for current example group
123
+ #
124
+ # @return [Hash] hash with hooks
125
+ def em_hooks
126
+ evented_spec_metadata[:em_hooks] ||= {
127
+ :em_before => [],
128
+ :em_after => [],
129
+ :amqp_before => [],
130
+ :amqp_after => []
131
+ }
132
+ end
133
+ end
134
+
135
+ def self.included(example_group)
136
+ unless example_group.respond_to? :default_timeout
137
+ example_group.extend GroupMethods
138
+ end
139
+ end
140
+
141
+ # Retrieves default options passed in from enclosing example groups
142
+ #
143
+ # @return [Hash] default option for currently running example
144
+ def default_options
145
+ @em_default_options ||= self.class.default_options.dup rescue {}
146
+ end
147
+
148
+ # Yields to a given block inside EM.run and AMQP.start loops.
149
+ #
150
+ # @param [Hash] options for amqp connection initialization
151
+ # @option opts [String] :user ('guest') Username as defined by the AMQP server.
152
+ # @option opts [String] :pass ('guest') Password as defined by the AMQP server.
153
+ # @option opts [String] :vhost ('/') Virtual host as defined by the AMQP server.
154
+ # @option opts [Numeric] :timeout (nil) *Connection* timeout, measured in seconds.
155
+ # @option opts [Boolean] :logging (false) Toggle the extremely verbose AMQP logging.
156
+ # @option opts [Numeric] :spec_timeout (nil) Amount of time before spec is stopped by timeout
157
+ # @yield block to execute after amqp connects
158
+ def amqp(opts = {}, &block)
159
+ opts = default_options.merge opts
160
+ @evented_example = AMQPExample.new(opts, self, &block)
161
+ @evented_example.run
162
+ end
163
+
164
+ # Yields to block inside EM loop, :spec_timeout option (in seconds) is used to
165
+ # force spec to timeout if something goes wrong and EM/AMQP loop hangs for some
166
+ # reason.
167
+ #
168
+ # For compatibility with EM-Spec API, em method accepts either options Hash
169
+ # or numeric timeout in seconds.
170
+ #
171
+ # @param [Hash] options for eventmachine
172
+ # @param opts [Numeric] :spec_timeout (nil) Amount of time before spec is stopped by timeout
173
+ # @yield block to execute after eventmachine loop starts
174
+ def em(opts = {}, &block)
175
+ opts = default_options.merge(opts.is_a?(Hash) ? opts : { :spec_timeout => opts })
176
+ @evented_example = EMExample.new(opts, self, &block)
177
+ @evented_example.run
178
+ end
179
+
180
+ # Yields to block inside cool.io loop, :spec_timeout option (in seconds) is used to
181
+ # force spec to timeout if something goes wrong and EM/AMQP loop hangs for some
182
+ # reason.
183
+ #
184
+ # @param [Hash] options for cool.io
185
+ # @param opts [Numeric] :spec_timeout (nil) Amount of time before spec is stopped by timeout
186
+ # @yield block to execute after cool.io loop starts
187
+ def coolio(opts = {}, &block)
188
+ opts = default_options.merge opts
189
+ @evented_example = CoolioExample.new(opts, self, &block)
190
+ @evented_example.run
191
+ end
192
+
193
+ # Breaks the event loop and finishes the spec. This should be called after
194
+ # you are reasonably sure that your expectations succeeded.
195
+ # Done yields to any given block first, then stops EM event loop.
196
+ # For amqp specs, stops AMQP and cleans up AMQP state.
197
+ #
198
+ # You may pass delay (in seconds) to done. If you do so, please keep in mind
199
+ # that your (default or explicit) spec timeout may fire before your delayed done
200
+ # callback is due, leading to SpecTimeoutExceededError
201
+ #
202
+ # @param [Float] Delay before event loop is stopped
203
+ def done(*args, &block)
204
+ @evented_example.done *args, &block if @evented_example
205
+ end
206
+
207
+ # Manually sets timeout for currently running example. If spec doesn't call
208
+ # #done before timeout, it is marked as failed on timeout.
209
+ #
210
+ # @param [Float] Delay before event loop is stopped with error
211
+ def timeout(*args)
212
+ @evented_example.timeout *args if @evented_example
213
+ end
214
+
215
+ end # module SpecHelper
216
+
217
+ # If you include EventedSpec::AMQPSpec module into your example group, each example of this group
218
+ # will run inside AMQP.start loop without the need to explicitly call 'amqp'. In order
219
+ # to provide options to AMQP loop, default_options class method is defined. Remember,
220
+ # when using EventedSpec::Specs, you'll have a single set of AMQP.start options for all your
221
+ # examples.
222
+ #
223
+ module AMQPSpec
224
+ def self.included(example_group)
225
+ example_group.send(:include, SpecHelper)
226
+ example_group.extend(ClassMethods)
227
+ end
228
+
229
+ # @private
230
+ module ClassMethods
231
+ def it(*args, &block)
232
+ if block
233
+ new_block = Proc.new {|example_group_instance| (example_group_instance || self).instance_eval { amqp(&block) } }
234
+ super(*args, &new_block)
235
+ else
236
+ # pending example
237
+ super
238
+ end
239
+ end # it
240
+ end # ClassMethods
241
+ end # AMQPSpec
242
+
243
+ # Including EventedSpec::EMSpec module into your example group, each example of this group
244
+ # will run inside EM.run loop without the need to explicitly call 'em'.
245
+ #
246
+ module EMSpec
247
+ def self.included(example_group)
248
+ example_group.send(:include, SpecHelper)
249
+ example_group.extend ClassMethods
250
+ end
251
+
252
+ # @private
253
+ module ClassMethods
254
+ def it(*args, &block)
255
+ if block
256
+ # Shared example groups seem to pass example group instance
257
+ # to the actual example block
258
+ new_block = Proc.new {|example_group_instance| (example_group_instance || self).instance_eval { em(&block) } }
259
+ super(*args, &new_block)
260
+ else
261
+ # pending example
262
+ super
263
+ end
264
+ end # it
265
+ end # ClassMethods
266
+ end # EMSpec
267
+ end # EventedSpec