evented-spec 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +30 -0
- data/HISTORY +97 -0
- data/LICENSE +20 -0
- data/README.textile +162 -0
- data/Rakefile +24 -0
- data/VERSION +1 -0
- data/lib/amqp-spec.rb +5 -0
- data/lib/evented-spec.rb +2 -0
- data/lib/evented-spec/amqp.rb +27 -0
- data/lib/evented-spec/evented_example.rb +61 -0
- data/lib/evented-spec/evented_example/amqp_example.rb +52 -0
- data/lib/evented-spec/evented_example/coolio_example.rb +97 -0
- data/lib/evented-spec/evented_example/em_example.rb +87 -0
- data/lib/evented-spec/rspec.rb +267 -0
- data/lib/evented-spec/util.rb +32 -0
- data/lib/evented-spec/version.rb +8 -0
- data/spec/cool_io_spec.rb +72 -0
- data/spec/em_defaults_spec.rb +132 -0
- data/spec/em_hooks_spec.rb +231 -0
- data/spec/em_metadata_spec.rb +43 -0
- data/spec/failing_rspec_spec.rb +63 -0
- data/spec/rspec_amqp_spec.rb +116 -0
- data/spec/rspec_em_spec.rb +53 -0
- data/spec/shared_examples.rb +194 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +54 -0
- data/spec/util_spec.rb +51 -0
- data/tasks/common.rake +18 -0
- data/tasks/doc.rake +14 -0
- data/tasks/gem.rake +40 -0
- data/tasks/git.rake +34 -0
- data/tasks/spec.rake +15 -0
- data/tasks/version.rake +71 -0
- metadata +149 -0
@@ -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
|