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