evented-spec 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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
data/HISTORY CHANGED
@@ -94,4 +94,11 @@
94
94
 
95
95
  * Forked into evented-spec.
96
96
  * cool.io support
97
- * Using amqp 0.8.* API instead of 0.7.*
97
+ * Using amqp 0.8.* API instead of 0.7.*
98
+
99
+ == 0.4.1 / 2011-04-29
100
+
101
+ * More cool.io goodies:
102
+ * EventedSpec::CoolioSpec module similar to EMSpec and AMQPSpec
103
+ * coolio_before / coolio_after hooks
104
+ * #delayed helper
@@ -4,7 +4,7 @@ Evented-spec is a set of helpers to help you test your asynchronous code.
4
4
 
5
5
  EventMachine/Cool.io-based code, including asynchronous "AMQP library":https://github.com/ruby-amqp/ruby-amqp is notoriously difficult to test. To the point that many people recommend using either "mocks":https://github.com/danielsdeleo/moqueue or "synchronous libraries":https://github.com/ruby-amqp/bunny instead of EM-based libraries in unit tests. This is not always an option, however -- sometimes your code just has to run inside the event loop, and you want to test a real thing, not just mocks.
6
6
 
7
- "em-spec":https://github.com/tmm1/em-spec gem made it easier to write evented specs, but it has several drawbacks. First, it is not easy to manage both EM.run and AMQP.start loops at the same time. Second, AMQP is not properly stopped and deactivated upon exceptions and timeouts, resulting in state leak between examples and multiple mystereous failures. amqp-spec, and, subsequently, evented-spec add more helpers to keep your specs from being bloated.
7
+ "em-spec":https://github.com/tmm1/em-spec gem made it easier to write evented specs, but it had several drawbacks. First, it is not easy to manage both EM.run and AMQP.start loops at the same time. Second, AMQP is not properly stopped and deactivated upon exceptions and timeouts, resulting in state leak between examples and multiple mystereous failures. amqp-spec, and, subsequently, evented-spec add more helpers to keep your specs from being bloated.
8
8
 
9
9
  h2. Usage
10
10
 
@@ -47,7 +47,7 @@ h3. EventedSpec::SpecHelper
47
47
 
48
48
  All three accept a hash of options. Look into method documentation to learn more.
49
49
 
50
- h3. EventedSpec::EMSpec, EventedSpec::AMQPSpec
50
+ h3. EventedSpec::EMSpec, EventedSpec::AMQPSpec, EventedSpec::CoolioSpec
51
51
 
52
52
  @EventedSpec::EMSpec@ wraps every example in em block, so it might save you a couple of lines per example. @EventedSpec::AMQPSpec@ wraps every example in amqp block.
53
53
 
@@ -83,17 +83,21 @@ end
83
83
 
84
84
  h2. Hooks
85
85
 
86
- There are 4 hooks available to evented specs:
86
+ There are 6 hooks available to evented specs:
87
87
 
88
88
  * @em_before@ -- launches after reactor started, before example runs
89
89
  * @em_after@ -- launches right before reactor is stopped, after example runs
90
90
  * @amqp_before@ -- launches after amqp connects, before example runs
91
91
  * @amqp_after@ -- launches before amqp disconnects, after example runs
92
+ * @coolio_before@ -- launches after Cool.io starts, before example runs
93
+ * @coolio_after@ -- launches before Cool.io stops, after example runs
92
94
 
93
- So, the order of hooks is as follows: @before(:all)@, @before(:each)@,
95
+ So, the order of hooks for an AMQP spec is as follows: @before(:all)@, @before(:each)@,
94
96
  @em_before@, @amqp_before@, example, @amqp_after@, @em_after@, @after(:each)@,
95
97
  @after(:all)@
96
98
 
99
+
100
+
97
101
  bc. describe "using amqp hooks" do
98
102
  include EventedSpec::AMQPSpec
99
103
  default_timeout 0.5
@@ -117,10 +121,6 @@ bc. describe "using amqp hooks" do
117
121
  end
118
122
  end
119
123
 
120
- h2. AMQP gem compatibility
121
-
122
- AMQP spec helpers are for newer version of AMQP gem, 0.8. If you need spec-helpers for AMQP gem 0.7, take a look at "amqp-spec":https://github.com/ruby-amqp/amqp-spec, API is mostly the same.
123
-
124
124
  h2. Words of warning on blocking the reactor
125
125
 
126
126
  Evented specs are currently run inside of reactor thread. What this effectively means is that you *should not block* during spec execution.
@@ -149,6 +149,48 @@ bc. describe "using amqp" do
149
149
  end
150
150
  end
151
151
 
152
+ You can also use #delayed helper method to maintain order of execution when callbacks are not an option.
153
+
154
+ bc. describe "using amqp" do
155
+ include EventedSpec::AMQPSpec
156
+ it "should do something useful" do
157
+ channel = AMQP::Channel.new
158
+ @stage = 0
159
+ delayed(0.2) {
160
+ channel.should be_open
161
+ @stage = 1
162
+ }
163
+ delayed(0.3) {
164
+ @stage.should == 1
165
+ done
166
+ }
167
+ delayed(0.4) {
168
+ # this block is never going to be executed
169
+ raise "Help me!"
170
+ }
171
+ end
172
+ end
173
+
174
+
175
+ h2. I have an existing reactor running in separate thread, amqp specs won't work for me what should I do?
176
+
177
+ Unfortunately, right now there aren't many remedies to your problem, besides stopping the event loop in before(:all) hook like this:
178
+
179
+ bc. describe "Example" do
180
+ before(:all) { EM.stop_event_loop; sleep(0.1) }
181
+ after(:all) { do_something_to_restart_the_eventmachine }
182
+ include EventedSpec::SpecHelper
183
+ it "should do something" do
184
+ em { done }
185
+ end
186
+ end
187
+
188
+ Reason is simple: if we don't restart event loop every spec example, all kinds of state leaks may occur: stale timers, delayed exceptions, weirdest errors and even segfaults. It isn't impossible but it certainly is very invasive.
189
+
190
+ h2. Compatibility
191
+
192
+ EventedSpec is tested with RSpec >= 2.5.0, Cool.io ~> 1.0.0, EventMachine >= 0.12.10, and AMQP >= 0.8.0. Running it with RSpec 1.3 and/or AMQP 0.7.0 is not unheard of, although not tested in all its entirety.
193
+
152
194
  h2. See also
153
195
 
154
196
  You can see evented-spec in use in spec suites for our amqp gems, "amq-client":https://github.com/ruby-amqp/amq-client/tree/master/spec and "amqp":https://github.com/ruby-amqp/amqp/tree/master/spec.
@@ -159,4 +201,4 @@ h2. Links
159
201
  * "amqp-spec":https://github.com/ruby-amqp/amqp-spec
160
202
  * "eventmachine":http://eventmachine.rubyforge.org/
161
203
  * "amqp":https://github.com/ruby-amqp/amqp
162
- * "amq-client":https://github.com/ruby-amqp/amq-client
204
+ * "amq-client":https://github.com/ruby-amqp/amq-client
data/Rakefile CHANGED
@@ -1,14 +1,13 @@
1
1
  require 'pathname'
2
- NAME = 'amqp-spec'
2
+ NAME = 'evented-spec'
3
3
  BASE_PATH = Pathname.new(__FILE__).dirname
4
4
  LIB_PATH = BASE_PATH + 'lib'
5
5
  PKG_PATH = BASE_PATH + 'pkg'
6
6
  DOC_PATH = BASE_PATH + 'rdoc'
7
7
 
8
8
  $LOAD_PATH.unshift LIB_PATH.to_s
9
- require 'version'
10
-
11
- CLASS_NAME = EventedSpec::AMQPSpec
9
+ require 'evented-spec'
10
+ CLASS_NAME = EventedSpec
12
11
  VERSION = CLASS_NAME::VERSION
13
12
 
14
13
  begin
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.0
1
+ 0.4.1
@@ -1,2 +1,23 @@
1
+ # There's little to no point in requiring only subset of evented-spec code yet,
2
+ # but in case you really want to, it won't stand in your way.
3
+
4
+ # These files are required
5
+ require 'evented-spec/util'
1
6
  require 'evented-spec/version'
2
- require "evented-spec/rspec"
7
+ require 'evented-spec/evented_example'
8
+ require 'evented-spec/spec_helper'
9
+
10
+ # As of now, amqp depends on em, em doesn't depend on anyone and
11
+ # neither does coolio.
12
+ # Pick the files accordingly.
13
+ require 'evented-spec/evented_example/em_example'
14
+ require 'evented-spec/evented_example/amqp_example'
15
+ require 'evented-spec/evented_example/coolio_example'
16
+
17
+ require 'evented-spec/spec_helper/event_machine_helpers'
18
+ require 'evented-spec/spec_helper/amqp_helpers'
19
+ require 'evented-spec/spec_helper/coolio_helpers'
20
+
21
+ require 'evented-spec/em_spec'
22
+ require 'evented-spec/amqp_spec'
23
+ require 'evented-spec/coolio_spec'
@@ -0,0 +1,27 @@
1
+ module EventedSpec
2
+ # If you include EventedSpec::AMQPSpec module into your example group, each example of this group
3
+ # will run inside AMQP.start loop without the need to explicitly call 'amqp'. In order
4
+ # to provide options to AMQP loop, default_options class method is defined. Remember,
5
+ # when using EventedSpec::Specs, you'll have a single set of AMQP.start options for all your
6
+ # examples.
7
+ #
8
+ module AMQPSpec
9
+ def self.included(example_group)
10
+ example_group.send(:include, SpecHelper)
11
+ example_group.extend(ClassMethods)
12
+ end
13
+
14
+ # @private
15
+ module ClassMethods
16
+ def it(*args, &block)
17
+ if block
18
+ new_block = Proc.new {|example_group_instance| (example_group_instance || self).instance_eval { amqp(&block) } }
19
+ super(*args, &new_block)
20
+ else
21
+ # pending example
22
+ super
23
+ end
24
+ end # it
25
+ end # ClassMethods
26
+ end # AMQPSpec
27
+ end # module EventedSpec
@@ -0,0 +1,26 @@
1
+ module EventedSpec
2
+ # Including EventedSpec::CoolioSpec module into your example group, each example of this group
3
+ # will run inside cool.io loop without the need to explicitly call 'coolio'.
4
+ #
5
+ module CoolioSpec
6
+ def self.included(example_group)
7
+ example_group.send(:include, SpecHelper)
8
+ example_group.extend ClassMethods
9
+ end
10
+
11
+ # @private
12
+ module ClassMethods
13
+ def it(*args, &block)
14
+ if block
15
+ # Shared example groups seem to pass example group instance
16
+ # to the actual example block
17
+ new_block = Proc.new {|example_group_instance| (example_group_instance || self).instance_eval { coolio(&block) } }
18
+ super(*args, &new_block)
19
+ else
20
+ # pending example
21
+ super
22
+ end
23
+ end # it
24
+ end # ClassMethods
25
+ end # EMSpec
26
+ end # module EventedSpec
@@ -0,0 +1,26 @@
1
+ module EventedSpec
2
+ # Including EventedSpec::EMSpec module into your example group, each example of this group
3
+ # will run inside EM.run loop without the need to explicitly call 'em'.
4
+ #
5
+ module EMSpec
6
+ def self.included(example_group)
7
+ example_group.send(:include, SpecHelper)
8
+ example_group.extend ClassMethods
9
+ end
10
+
11
+ # @private
12
+ module ClassMethods
13
+ def it(*args, &block)
14
+ if block
15
+ # Shared example groups seem to pass example group instance
16
+ # to the actual example block
17
+ new_block = Proc.new {|example_group_instance| (example_group_instance || self).instance_eval { em(&block) } }
18
+ super(*args, &new_block)
19
+ else
20
+ # pending example
21
+ super
22
+ end
23
+ end # it
24
+ end # ClassMethods
25
+ end # EMSpec
26
+ end # module EventedSpec
@@ -7,7 +7,7 @@ module EventedSpec
7
7
  class EventedExample
8
8
  # Default options to use with the examples
9
9
  DEFAULT_OPTIONS = {
10
- :spec_timeout => 1
10
+ :spec_timeout => 5
11
11
  }
12
12
 
13
13
  # Create new evented example
@@ -15,6 +15,16 @@ module EventedSpec
15
15
  @opts, @example_group_instance, @block = DEFAULT_OPTIONS.merge(opts), example_group_instance, block
16
16
  end
17
17
 
18
+ # Runs hooks of specified type (hopefully, inside event loop)
19
+ #
20
+ # @param hook type
21
+ def run_hooks(type)
22
+ @example_group_instance.class.evented_spec_hooks_for(type).each do |hook|
23
+ @example_group_instance.instance_eval(&hook)
24
+ end
25
+ end
26
+
27
+
18
28
  # Called from #run_event_loop when event loop is stopped,
19
29
  # but before the example returns.
20
30
  # Descendant classes may redefine to clean up type-specific state.
@@ -47,6 +57,8 @@ module EventedSpec
47
57
 
48
58
  # Override this method in your descendants
49
59
  #
60
+ # @note block should be evaluated in EventedExample descendant instance context
61
+ # (e.g. in EMExample instance)
50
62
  # @note delay may be nil, implying you need to execute the block immediately.
51
63
  # @abstract
52
64
  def delayed(delay = nil, &block)
@@ -55,7 +67,3 @@ module EventedSpec
55
67
  end # class EventedExample
56
68
  end # module SpecHelper
57
69
  end # module AMQP
58
-
59
- require 'evented-spec/evented_example/em_example'
60
- require 'evented-spec/evented_example/amqp_example'
61
- require 'evented-spec/evented_example/coolio_example'
@@ -1,3 +1,4 @@
1
+ require 'evented-spec/ext/amqp'
1
2
  module EventedSpec
2
3
  module SpecHelper
3
4
  # Represents spec running inside AMQP.start loop
@@ -7,8 +8,8 @@ module EventedSpec
7
8
  # See {EventedExample#run}
8
9
  def run
9
10
  run_em_loop do
10
- AMQP.start_connection(@opts) do
11
- run_em_hooks :amqp_before
11
+ ::AMQP.start_connection(@opts) do
12
+ run_hooks :amqp_before
12
13
  @example_group_instance.instance_eval(&@block)
13
14
  end
14
15
  end
@@ -22,9 +23,9 @@ module EventedSpec
22
23
  delayed(delay) do
23
24
  yield if block_given?
24
25
  EM.next_tick do
25
- run_em_hooks :amqp_after
26
- if AMQP.connection && !AMQP.closing?
27
- AMQP.stop_connection do
26
+ run_hooks :amqp_after
27
+ if ::AMQP.connection && !::AMQP.closing?
28
+ ::AMQP.stop_connection do
28
29
  # Cannot call finish_em_loop before connection is marked as closed
29
30
  # This callback is called before that happens.
30
31
  EM.next_tick { finish_em_loop }
@@ -32,7 +33,7 @@ module EventedSpec
32
33
  else
33
34
  # Need this branch because if AMQP couldn't connect,
34
35
  # the callback would never trigger
35
- AMQP.cleanup_state
36
+ ::AMQP.cleanup_state
36
37
  EM.next_tick { finish_em_loop }
37
38
  end
38
39
  end
@@ -44,7 +45,7 @@ module EventedSpec
44
45
  #
45
46
  # See {EventedExample#run}
46
47
  def finish_example
47
- AMQP.cleanup_state
48
+ ::AMQP.cleanup_state
48
49
  super
49
50
  end
50
51
  end # class AMQPExample < EventedExample
@@ -1,3 +1,4 @@
1
+ require 'evented-spec/ext/coolio'
1
2
  #
2
3
  # Cool.io loop is a little bit trickier to test, since it
3
4
  # doesn't go into a loop if there are no watchers.
@@ -14,6 +15,7 @@ module EventedSpec
14
15
  reset
15
16
  delayed(0) do
16
17
  begin
18
+ run_hooks :coolio_before
17
19
  @example_group_instance.instance_eval(&@block)
18
20
  rescue Exception => e
19
21
  @spec_exception ||= e
@@ -43,6 +45,7 @@ module EventedSpec
43
45
 
44
46
  # Stops the loop and finalizes the example
45
47
  def finish_loop
48
+ run_hooks :coolio_after
46
49
  default_loop.stop
47
50
  finish_example
48
51
  end
@@ -77,21 +80,4 @@ module EventedSpec
77
80
  end
78
81
  end # class CoolioExample
79
82
  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
83
+ end # module EventedSpec
@@ -3,13 +3,6 @@ module EventedSpec
3
3
  # Represents spec running inside EM.run loop.
4
4
  # See {EventedExample} for details and method descriptions.
5
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
6
  # Runs given block inside EM event loop.
14
7
  # Double-round exception handler needed because some of the exceptions bubble
15
8
  # outside of event loop due to asynchronous nature of evented examples
@@ -17,7 +10,7 @@ module EventedSpec
17
10
  def run_em_loop
18
11
  begin
19
12
  EM.run do
20
- run_em_hooks :em_before
13
+ run_hooks :em_before
21
14
 
22
15
  @spec_exception = nil
23
16
  timeout(@opts[:spec_timeout]) if @opts[:spec_timeout]
@@ -32,7 +25,7 @@ module EventedSpec
32
25
  rescue Exception => e
33
26
  @spec_exception ||= e
34
27
  # 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
28
+ run_hooks :em_after # Event loop broken, but we still need to run em_after hooks
36
29
  ensure
37
30
  finish_example
38
31
  end
@@ -41,7 +34,7 @@ module EventedSpec
41
34
  # Stops EM event loop. It is called from #done
42
35
  #
43
36
  def finish_em_loop
44
- run_em_hooks :em_after
37
+ run_hooks :em_after
45
38
  EM.stop_event_loop if EM.reactor_running?
46
39
  end
47
40
 
@@ -76,10 +69,11 @@ module EventedSpec
76
69
 
77
70
  # See {EventedExample#delayed}
78
71
  def delayed(delay, &block)
72
+ instance = self
79
73
  if delay
80
- EM.add_timer delay, &block
74
+ EM.add_timer delay, Proc.new { instance.instance_eval(&block) }
81
75
  else
82
- yield
76
+ instance.instance_eval(&block)
83
77
  end
84
78
  end # delayed
85
79
  end # class EMExample < EventedExample
@@ -1,8 +1,31 @@
1
1
  # Monkey patching some methods into AMQP to make it more testable
2
+ module EventedSpec
3
+ module AMQPBackports
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end # self.included
7
+
8
+ module ClassMethods
9
+ def connection
10
+ @conn
11
+ end # connection
12
+
13
+ def connection=(new_connection)
14
+ @conn = new_connection
15
+ end # connection=
16
+ end # module ClassMethods
17
+ end # module AMQPBackports
18
+ end # module EventedSpec
19
+
2
20
  module AMQP
3
21
  # Initializes new AMQP client/connection without starting another EM loop
4
22
  def self.start_connection(opts={}, &block)
5
- self.connection = connect opts, &block
23
+ if amqp_pre_08?
24
+ self.connection = connect opts
25
+ self.connection.callback &block
26
+ else
27
+ self.connection = connect opts, &block
28
+ end
6
29
  end
7
30
 
8
31
  # Closes AMQP connection gracefully
@@ -24,4 +47,10 @@ module AMQP
24
47
  self.connection = nil
25
48
  @closing = false
26
49
  end
27
- end
50
+
51
+ def self.amqp_pre_08?
52
+ AMQP::VERSION < "0.8"
53
+ end # self.amqp_pre_08?
54
+
55
+ include EventedSpec::AMQPBackports
56
+ end