evented-spec 0.4.0

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.
@@ -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