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,30 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ *.rbc
18
+ *.class
19
+ coverage
20
+ config
21
+ rdoc
22
+ pkg
23
+ log
24
+ .idea
25
+ .rvmrc
26
+ .bundle
27
+ Gemfile.lock
28
+
29
+ ## PROJECT::SPECIFIC
30
+ spec/amqp.yml
data/HISTORY ADDED
@@ -0,0 +1,97 @@
1
+ == 0.0.0 / 2010-10-13
2
+
3
+ * Birthday!
4
+
5
+ == 0.0.1 / 2010-10-13
6
+
7
+ * Initial code import from EM-Spec and AMQPHelper
8
+
9
+ == 0.0.2 / 2010-10-13
10
+
11
+ * Minimal functionality implemented
12
+
13
+ == 0.0.4 / 2010-10-14
14
+
15
+ * Problems with default_options resolved
16
+
17
+ == 0.1.0 / 2010-10-15
18
+
19
+ * Monkeypatched start/stop_connection directly into AMQP
20
+
21
+ == 0.1.2 / 2010-10-15
22
+
23
+ * Cleanup in AMQP.cleanup
24
+
25
+ == 0.1.3 / 2010-10-15
26
+
27
+ * Make sure Thread.current[:mq] state is cleaned after each example
28
+
29
+ == 0.1.7 / 2010-10-15
30
+
31
+ * em interface improved for timeout arg
32
+
33
+ == 0.1.11 / 2010-10-18
34
+
35
+ * Optional delay added to done
36
+
37
+ == 0.2.0 / 2010-10-28
38
+
39
+ * Rspec 2 support added
40
+
41
+ == 0.2.2 / 2010-10-31
42
+
43
+ * Metadata in Rspec 1
44
+
45
+ == 0.2.7 / 2010-11-04
46
+
47
+ * Make AMQP.cleanup_state more thorough
48
+
49
+ == 0.2.8 / 2010-11-15
50
+
51
+ * syncronize method added for wrapping async calls
52
+
53
+ == 0.2.9 / 2010-11-16
54
+
55
+ * Fallback to a separate default timeout
56
+
57
+ == 0.3.0 / 2010-11-16
58
+
59
+ * Hooks em_before/em_after implemented
60
+
61
+ == 0.3.1 / 2010-11-17
62
+
63
+ * Documentation cleanup
64
+
65
+ == 0.3.2 / 2010-11-23
66
+
67
+ * EM hooks now working as they should - bugs fixed
68
+
69
+ == 0.3.3 / 2010-11-24
70
+
71
+ * Spec timeout refactored
72
+
73
+ == 0.3.4 / 2010-11-26
74
+
75
+ * amqp_before/after hooks added
76
+
77
+ == 0.3.5 / 2011-01-07
78
+
79
+ * Drop-in support for legacy em-spec based examples added
80
+
81
+ == 0.3.6 / 2011-01-07
82
+
83
+ * Changed Gemfile to avoid circular dependency on AMQP
84
+
85
+ == 0.3.7 / 2011-01-07
86
+
87
+ * Changed Gemspec to avoid circular dependency on AMQP
88
+
89
+ == 0.3.8 / 2011-01-25
90
+
91
+ * MRI 1.8.7 support added
92
+
93
+ == 0.4.0 / 2011-03-13
94
+
95
+ * Forked into evented-spec.
96
+ * cool.io support
97
+ * Using amqp 0.8.* API instead of 0.7.*
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Arvicco
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,162 @@
1
+ h2. About evented-spec
2
+
3
+ Evented-spec is a set of helpers to help you test your asynchronous code.
4
+
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
+
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.
8
+
9
+ h2. Usage
10
+
11
+ To get started with evented-spec you need to include one of the helper modules in your example groups, e.g.:
12
+
13
+ bc. describe "eventmachine-based client" do
14
+ include EventedSpec::SpecHelper
15
+ it "should allow you to start a reactor" do
16
+ em do
17
+ EventMachine.reactor_running?.should be_true
18
+ done
19
+ end
20
+ end
21
+ context "nested contexts" do
22
+ it "don't require another include" do
23
+ em do
24
+ EventMachine.add_timer(0.1) { @timer_run = true }
25
+ done(0.3)
26
+ end
27
+ @timer_run.should be_true
28
+ end
29
+ end
30
+ end
31
+
32
+ Particular modules and methods are explained below.
33
+
34
+ h3. #done
35
+
36
+ We have no means to know when your work with reactor is finished, so whatever it is you need to call @done@ at some point. It optionally accepts a timeout and a block that is executed right before event reactor loop is stopped. If you don't call @done@, your specs are going to fail by timeout.
37
+
38
+ h3. EventedSpec::SpecHelper
39
+
40
+ @EventedSpec::SpecHelper@ is for semi-manual managing of reactor life-cycle. It includes three helpers: for EventMachine, Coolio and AMQP.
41
+
42
+ @em@ stands for EventMachine. It takes a block, which is run after reactor starts.
43
+
44
+ @amqp@ stands for AMQP. It takes a block, which is run after amqp connects with broker using given or default options.
45
+
46
+ @coolio@ stands for cool.io. It takes a block, which is run after reactor starts.
47
+
48
+ All three accept a hash of options. Look into method documentation to learn more.
49
+
50
+ h3. EventedSpec::EMSpec, EventedSpec::AMQPSpec
51
+
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
+
54
+ Also note that every example group including @EMSpec@ or @AMQPSpec@ automatically includes @SpecHelper@.
55
+
56
+ Example:
57
+
58
+ bc. describe "eventmachine specs" do
59
+ include EventedSpec::EMSpec
60
+ it "should run in a reactor" do
61
+ EventMachine.reactor_running?.should be_true
62
+ done # don't forget to finish your specs properly!
63
+ end
64
+ end
65
+
66
+
67
+ h3. default_options, default_timeout
68
+
69
+ You can also pass some default options to specs (like amqp settings), they're specific to domain you're using evented-spec in.
70
+
71
+ @default_timeout@ sets time (in seconds) for specs to time out
72
+
73
+ bc. describe "using default_timeout" do
74
+ include EventedSpec::SpecHelper
75
+ default_timeout 0.5
76
+ it "should prevent specs from hanging up" do
77
+ em do
78
+ 1.should == 1 # this spec is going to fail with timeout error because #done is not called
79
+ end
80
+ end
81
+ end
82
+
83
+
84
+ h2. Hooks
85
+
86
+ There are 4 hooks available to evented specs:
87
+
88
+ * @em_before@ -- launches after reactor started, before example runs
89
+ * @em_after@ -- launches right before reactor is stopped, after example runs
90
+ * @amqp_before@ -- launches after amqp connects, before example runs
91
+ * @amqp_after@ -- launches before amqp disconnects, after example runs
92
+
93
+ So, the order of hooks is as follows: @before(:all)@, @before(:each)@,
94
+ @em_before@, @amqp_before@, example, @amqp_after@, @em_after@, @after(:each)@,
95
+ @after(:all)@
96
+
97
+ bc. describe "using amqp hooks" do
98
+ include EventedSpec::AMQPSpec
99
+ default_timeout 0.5
100
+ amqp_before do
101
+ AMQP.connection.should_not be_nil
102
+ end
103
+ let(:data) { "Test string" }
104
+ it "should do something useful" do
105
+ AMQP::Channel.new do |channel, _|
106
+ exchange = channel.direct("amqp-test-exchange")
107
+ queue = channel.queue("amqp-test-queue").bind(exchange)
108
+ queue.subscribe do |hdr, msg|
109
+ hdr.should be_an AMQP::Header
110
+ msg.should == data
111
+ done { queue.unsubscribe; queue.delete }
112
+ end
113
+ EM.add_timer(0.2) do
114
+ exchange.publish data
115
+ end
116
+ end
117
+ end
118
+ end
119
+
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
+ h2. Words of warning on blocking the reactor
125
+
126
+ Evented specs are currently run inside of reactor thread. What this effectively means is that you *should not block* during spec execution.
127
+
128
+ For example, the following *will not* work:
129
+
130
+ bc. describe "using amqp" do
131
+ include EventedSpec::AMQPSpec
132
+ it "should do something useful" do
133
+ channel = AMQP::Channel.new
134
+ sleep 0.2 # voila, you're blocking the reactor
135
+ channel.should be_open # no, it should not
136
+ done
137
+ end
138
+ end
139
+
140
+ What you *should* do instead is use callbacks:
141
+
142
+ bc. describe "using amqp" do
143
+ include EventedSpec::AMQPSpec
144
+ it "should do something useful" do
145
+ AMQP::Channel.new do |channel, _|
146
+ channel.should be_open
147
+ done
148
+ end
149
+ end
150
+ end
151
+
152
+ h2. See also
153
+
154
+ 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.
155
+
156
+ h2. Links
157
+
158
+ * "cool.io":http://coolio.github.com/
159
+ * "amqp-spec":https://github.com/ruby-amqp/amqp-spec
160
+ * "eventmachine":http://eventmachine.rubyforge.org/
161
+ * "amqp":https://github.com/ruby-amqp/amqp
162
+ * "amq-client":https://github.com/ruby-amqp/amq-client
@@ -0,0 +1,24 @@
1
+ require 'pathname'
2
+ NAME = 'amqp-spec'
3
+ BASE_PATH = Pathname.new(__FILE__).dirname
4
+ LIB_PATH = BASE_PATH + 'lib'
5
+ PKG_PATH = BASE_PATH + 'pkg'
6
+ DOC_PATH = BASE_PATH + 'rdoc'
7
+
8
+ $LOAD_PATH.unshift LIB_PATH.to_s
9
+ require 'version'
10
+
11
+ CLASS_NAME = EventedSpec::AMQPSpec
12
+ VERSION = CLASS_NAME::VERSION
13
+
14
+ begin
15
+ require 'rake'
16
+ rescue LoadError
17
+ require 'rubygems'
18
+ gem 'rake', '~> 0.8.3.1'
19
+ require 'rake'
20
+ end
21
+
22
+ # Load rakefile tasks
23
+ Dir['tasks/*.rake'].sort.each { |file| load file }
24
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.4.0
@@ -0,0 +1,5 @@
1
+ puts <<-EOF
2
+ DEPRECATION WARNING:
3
+ Using require 'amqp-spec' is deprecated. Require 'evented-spec' instead.
4
+ EOF
5
+ require 'evented-spec'
@@ -0,0 +1,2 @@
1
+ require 'evented-spec/version'
2
+ require "evented-spec/rspec"
@@ -0,0 +1,27 @@
1
+ # Monkey patching some methods into AMQP to make it more testable
2
+ module AMQP
3
+ # Initializes new AMQP client/connection without starting another EM loop
4
+ def self.start_connection(opts={}, &block)
5
+ self.connection = connect opts, &block
6
+ end
7
+
8
+ # Closes AMQP connection gracefully
9
+ def self.stop_connection
10
+ if AMQP.connection and not AMQP.connection.closing?
11
+ @closing = true
12
+ self.connection.close {
13
+ yield if block_given?
14
+ self.connection = nil
15
+ cleanup_state
16
+ }
17
+ end
18
+ end
19
+
20
+ # Cleans up AMQP state after AMQP connection closes
21
+ def self.cleanup_state
22
+ Thread.list.each { |thread| thread[:mq] = nil }
23
+ Thread.list.each { |thread| thread[:mq_id] = nil }
24
+ self.connection = nil
25
+ @closing = false
26
+ end
27
+ end
@@ -0,0 +1,61 @@
1
+ module EventedSpec
2
+ module SpecHelper
3
+ # Represents example running inside some type of event loop.
4
+ # You are not going to use or interact with this class and its descendants directly.
5
+ #
6
+ # @abstract
7
+ class EventedExample
8
+ # Default options to use with the examples
9
+ DEFAULT_OPTIONS = {
10
+ :spec_timeout => 1
11
+ }
12
+
13
+ # Create new evented example
14
+ def initialize(opts, example_group_instance, &block)
15
+ @opts, @example_group_instance, @block = DEFAULT_OPTIONS.merge(opts), example_group_instance, block
16
+ end
17
+
18
+ # Called from #run_event_loop when event loop is stopped,
19
+ # but before the example returns.
20
+ # Descendant classes may redefine to clean up type-specific state.
21
+ #
22
+ # @abstract
23
+ def finish_example
24
+ raise @spec_exception if @spec_exception
25
+ end
26
+
27
+ # Run the example.
28
+ #
29
+ # @abstract
30
+ def run
31
+ raise NotImplementedError, "you should implement #run in #{self.class.name}"
32
+ end
33
+
34
+ # Sets timeout for currently running example
35
+ #
36
+ # @abstract
37
+ def timeout(spec_timeout)
38
+ raise NotImplementedError, "you should implement #timeout in #{self.class.name}"
39
+ end
40
+
41
+ # Breaks the event loop and finishes the spec.
42
+ #
43
+ # @abstract
44
+ def done(delay=nil, &block)
45
+ raise NotImplementedError, "you should implement #done method in #{self.class.name}"
46
+ end
47
+
48
+ # Override this method in your descendants
49
+ #
50
+ # @note delay may be nil, implying you need to execute the block immediately.
51
+ # @abstract
52
+ def delayed(delay = nil, &block)
53
+ raise NotImplementedError, "you should implement #delayed method in #{self.class.name}"
54
+ end # delayed(delay, &block)
55
+ end # class EventedExample
56
+ end # module SpecHelper
57
+ 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'
@@ -0,0 +1,52 @@
1
+ module EventedSpec
2
+ module SpecHelper
3
+ # Represents spec running inside AMQP.start loop
4
+ # See {EventedExample} for details and method descriptions.
5
+ class AMQPExample < EMExample
6
+ # Run @block inside the AMQP.start loop.
7
+ # See {EventedExample#run}
8
+ def run
9
+ run_em_loop do
10
+ AMQP.start_connection(@opts) do
11
+ run_em_hooks :amqp_before
12
+ @example_group_instance.instance_eval(&@block)
13
+ end
14
+ end
15
+ end
16
+
17
+ # Breaks the event loop and finishes the spec. It yields to any given block first,
18
+ # then stops AMQP, EM event loop and cleans up AMQP state.
19
+ #
20
+ # See {EventedExample#done}
21
+ def done(delay = nil)
22
+ delayed(delay) do
23
+ yield if block_given?
24
+ EM.next_tick do
25
+ run_em_hooks :amqp_after
26
+ if AMQP.connection && !AMQP.closing?
27
+ AMQP.stop_connection do
28
+ # Cannot call finish_em_loop before connection is marked as closed
29
+ # This callback is called before that happens.
30
+ EM.next_tick { finish_em_loop }
31
+ end
32
+ else
33
+ # Need this branch because if AMQP couldn't connect,
34
+ # the callback would never trigger
35
+ AMQP.cleanup_state
36
+ EM.next_tick { finish_em_loop }
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ # Called from run_event_loop when event loop is finished, before any exceptions
43
+ # is raised or example returns. We ensure AMQP state cleanup here.
44
+ #
45
+ # See {EventedExample#run}
46
+ def finish_example
47
+ AMQP.cleanup_state
48
+ super
49
+ end
50
+ end # class AMQPExample < EventedExample
51
+ end # module SpecHelper
52
+ end # module EventedExample