ruby_ami 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,87 @@
1
+ %%{ #%
2
+
3
+ #########
4
+ ## This file is written with the Ragel programming language and parses the Asterisk Manager Interface protocol. It depends
5
+ ## upon Ragel actions which should be implemented in another Ragel-parsed file which includes this file.
6
+ ##
7
+ ## Ragel was used because the AMI protocol is extremely non-deterministic and, in the edge cases, requires something both
8
+ ## very robust and something which can recover from syntax errors.
9
+ ##
10
+ ## Note: This file is language agnostic. From this AMI parsers in many other languages can be generated.
11
+ #########
12
+
13
+ machine ami_protocol_parser_machine;
14
+
15
+ cr = "\r"; # A carriage return. Used before (almost) every newline character.
16
+ lf = "\n"; # Newline. Used (with cr) to separate key/value pairs and stanzas.
17
+ crlf = cr lf; # Means "carriage return and line feed". Used to separate key/value pairs and stanzas
18
+ loose_newline = cr? lf; # Used sometimes when the AMI protocol is nondeterministic about the delimiter
19
+
20
+ white = [\t ]; # Single whitespace character, either a tab or a space
21
+ colon = ":" [ ]**; # Separates keys from values. "A colon followed by any number of spaces"
22
+ stanza_break = crlf crlf; # The seperator between two stanzas.
23
+ rest_of_line = (any* -- crlf); # Match all characters until the next line seperator.
24
+
25
+ Prompt = "Asterisk Call Manager/" digit+ >version_starts "." digit+ %version_stops crlf;
26
+
27
+ Key = ((alnum | print) -- (cr | lf | ":"))+;
28
+ KeyValuePair = Key >key_starts %key_stops colon rest_of_line >value_starts %value_stops crlf;
29
+
30
+ FollowsDelimiter = loose_newline "--END COMMAND--";
31
+
32
+ Response = "Response"i colon;
33
+
34
+ Success = Response "Success"i %init_success crlf @{ fgoto success; };
35
+ Pong = Response "Pong"i %init_success crlf @{ fgoto success; };
36
+ Event = "Event"i colon %event_name_starts rest_of_line %event_name_stops crlf @{ fgoto success; };
37
+ Error = Response "Error"i %init_error crlf (("Message"i colon rest_of_line >error_reason_starts crlf >error_reason_stops) | KeyValuePair)+ crlf @error_received;
38
+ Follows = Response "Follows"i crlf @init_response_follows @{ fgoto response_follows; };
39
+
40
+ # For "Response: Follows"
41
+ FollowsBody = (any* -- FollowsDelimiter) >follows_text_starts FollowsDelimiter @follows_text_stops crlf;
42
+
43
+ ImmediateResponse = (any+ -- (loose_newline | ":")) >immediate_response_starts loose_newline @immediate_response_stops @{fret;};
44
+ SyntaxError = (any+ -- crlf) >syntax_error_starts crlf @syntax_error_stops;
45
+
46
+ irregularity := |*
47
+ ImmediateResponse; # Performs the fret in the ImmediateResponse FSM
48
+ SyntaxError => { fret; };
49
+ *|;
50
+
51
+ # When a new socket is established, Asterisk will send the version of the protocol per the Prompt machine. Because it's
52
+ # tedious for unit tests to always send this, we'll put some intelligence into this parser to support going straight into
53
+ # the protocol-parsing machine. It's also conceivable that a variant of AMI would not send this initial information.
54
+ main := |*
55
+ Prompt => { fgoto protocol; };
56
+ any => {
57
+ # If this scanner's look-ahead capability didn't match the prompt, let's ignore the need for a prompt
58
+ fhold;
59
+ fgoto protocol;
60
+ };
61
+ *|;
62
+
63
+ protocol := |*
64
+ Prompt;
65
+ Success;
66
+ Pong;
67
+ Event;
68
+ Error;
69
+ Follows crlf;
70
+ crlf => { fgoto protocol; }; # If we get a crlf out of place, let's just ignore it.
71
+ any => {
72
+ # If NONE of the above patterns match, we consider this a syntax error. The irregularity machine can recover gracefully.
73
+ fhold;
74
+ fcall irregularity;
75
+ };
76
+ *|;
77
+
78
+ success := KeyValuePair* crlf @message_received @{fgoto protocol;};
79
+
80
+ # For the "Response: Follows" protocol abnormality. What happens if there's a protocol irregularity in this state???
81
+ response_follows := |*
82
+ KeyValuePair+;
83
+ FollowsBody;
84
+ crlf @{ message_received @current_message; fgoto protocol; };
85
+ *|;
86
+
87
+ }%%
@@ -0,0 +1,17 @@
1
+ class Object
2
+ def metaclass
3
+ class << self
4
+ self
5
+ end
6
+ end
7
+
8
+ def meta_eval(&block)
9
+ metaclass.instance_eval &block
10
+ end
11
+
12
+ def meta_def(name, &block)
13
+ meta_eval do
14
+ define_method name, &block
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,44 @@
1
+ module RubyAMI
2
+ ##
3
+ # This is the object containing a response from Asterisk.
4
+ #
5
+ # Note: not all responses have an ActionID!
6
+ #
7
+ class Response
8
+ class << self
9
+ def from_immediate_response(text)
10
+ new.tap do |instance|
11
+ instance.text_body = text
12
+ end
13
+ end
14
+ end
15
+
16
+ attr_accessor :action,
17
+ :text_body # For "Response: Follows" sections
18
+ attr_reader :events
19
+
20
+ def initialize
21
+ @headers = HashWithIndifferentAccess.new
22
+ end
23
+
24
+ def has_text_body?
25
+ !!@text_body
26
+ end
27
+
28
+ def headers
29
+ @headers.clone
30
+ end
31
+
32
+ def [](arg)
33
+ @headers[arg]
34
+ end
35
+
36
+ def []=(key,value)
37
+ @headers[key] = value
38
+ end
39
+
40
+ def action_id
41
+ @headers['ActionID']
42
+ end
43
+ end
44
+ end # RubyAMI
@@ -0,0 +1,60 @@
1
+ module RubyAMI
2
+ class Stream < EventMachine::Connection
3
+ class ConnectionStatus
4
+ def eql?(other)
5
+ other.is_a? self.class
6
+ end
7
+
8
+ alias :== :eql?
9
+ end
10
+
11
+ Connected = Class.new ConnectionStatus
12
+ Disconnected = Class.new ConnectionStatus
13
+
14
+ def self.start(host, port, event_callback)
15
+ EM.connect host, port, self, event_callback
16
+ end
17
+
18
+ def initialize(event_callback)
19
+ super()
20
+ @event_callback = event_callback
21
+ @logger = Logger.new($stdout)
22
+ @logger.level = Logger::FATAL
23
+ @logger.debug "Starting up..."
24
+ @lexer = Lexer.new self
25
+ end
26
+
27
+ [:started, :stopped, :ready].each do |state|
28
+ define_method("#{state}?") { @state == state }
29
+ end
30
+
31
+ def post_init
32
+ @state = :started
33
+ @event_callback.call Connected.new
34
+ end
35
+
36
+ def send_action(action)
37
+ @logger.debug "[SEND] #{action.to_s}"
38
+ send_data action.to_s
39
+ end
40
+
41
+ def receive_data(data)
42
+ @logger.debug "[RECV] #{data}"
43
+ @lexer << data
44
+ end
45
+
46
+ def message_received(message)
47
+ @logger.debug "[RECV] #{message.inspect}"
48
+ @event_callback.call message
49
+ end
50
+
51
+ alias :error_received :message_received
52
+
53
+ # Called by EM when the connection is closed
54
+ # @private
55
+ def unbind
56
+ @state = :stopped
57
+ @event_callback.call Disconnected.new
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ module RubyAMI
2
+ VERSION = "0.1.1"
3
+ end
data/ruby_ami.gemspec ADDED
@@ -0,0 +1,40 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "ruby_ami/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "ruby_ami"
7
+ s.version = RubyAMI::VERSION
8
+ s.authors = ["Ben Langfeld"]
9
+ s.email = ["ben@langfeld.me"]
10
+ s.homepage = ""
11
+ s.summary = %q{Futzing with AMI so you don't have to}
12
+ s.description = %q{A Ruby client library for the Asterisk Management Interface build on eventmachine.}
13
+
14
+ s.rubyforge_project = "ruby_ami"
15
+
16
+ s.files = `git ls-files`.split("\n") << 'lib/ruby_ami/lexer.rb'
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_runtime_dependency %q<activesupport>, [">= 3.0.9"]
22
+ s.add_runtime_dependency %q<uuidtools>, [">= 0"]
23
+ s.add_runtime_dependency %q<eventmachine>, [">= 0"]
24
+ s.add_runtime_dependency %q<future-resource>, [">= 0"]
25
+ s.add_runtime_dependency %q<girl_friday>, [">= 0"]
26
+ s.add_runtime_dependency %q<countdownlatch>, [">= 1.0.0"]
27
+ s.add_runtime_dependency %q<i18n>, [">= 0"]
28
+
29
+ s.add_development_dependency %q<bundler>, ["~> 1.0.0"]
30
+ s.add_development_dependency %q<rspec>, [">= 2.5.0"]
31
+ s.add_development_dependency %q<cucumber>, [">= 0"]
32
+ s.add_development_dependency %q<ci_reporter>, [">= 1.6.3"]
33
+ s.add_development_dependency %q<yard>, ["~> 0.6.0"]
34
+ s.add_development_dependency %q<rcov>, [">= 0"]
35
+ s.add_development_dependency %q<rake>, [">= 0"]
36
+ s.add_development_dependency %q<mocha>, [">= 0"]
37
+ s.add_development_dependency %q<simplecov>, [">= 0"]
38
+ s.add_development_dependency %q<simplecov-rcov>, [">= 0"]
39
+ s.add_development_dependency %q<guard-rspec>
40
+ end
@@ -0,0 +1,163 @@
1
+ require 'spec_helper'
2
+
3
+ module RubyAMI
4
+ describe Action do
5
+ let(:name) { 'foobar' }
6
+ let(:headers) { {'foo' => 'bar'} }
7
+
8
+ subject do
9
+ Action.new name, headers do |response|
10
+ @foo = response
11
+ end
12
+ end
13
+
14
+ it { should be_new }
15
+
16
+ describe "SIPPeers actions" do
17
+ subject { Action.new('SIPPeers') }
18
+ its(:has_causal_events?) { should be true }
19
+ end
20
+
21
+ describe "Queues actions" do
22
+ subject { Action.new('Queues') }
23
+ its(:replies_with_action_id?) { should == false }
24
+ end
25
+
26
+ describe "IAXPeers actions" do
27
+ before { pending }
28
+ # FIXME: This test relies on the side effect that earlier tests have run
29
+ # and initialized the UnsupportedActionName::UNSUPPORTED_ACTION_NAMES
30
+ # constant for an "unknown" version of Asterisk. This should be fixed
31
+ # to be more specific about which version of Asterisk is under test.
32
+ # IAXPeers is supported (with Action IDs!) since Asterisk 1.8
33
+ subject { Action.new('IAXPeers') }
34
+ its(:replies_with_action_id?) { should == false }
35
+ end
36
+
37
+ describe "the ParkedCalls terminator event" do
38
+ subject { Action.new('ParkedCalls') }
39
+ its(:causal_event_terminator_name) { should == "parkedcallscomplete" }
40
+ end
41
+
42
+ it "should properly convert itself into a String when additional headers are given" do
43
+ string = Action.new("Hawtsawce", "Monkey" => "Zoo").to_s
44
+ string.should =~ /^Action: Hawtsawce\r\n/i
45
+ string.should =~ /[^\n]\r\n\r\n$/
46
+ string.should =~ /^(\w+:\s*[\w-]+\r\n){3}\r\n$/
47
+ end
48
+
49
+ it "should properly convert itself into a String when no additional headers are given" do
50
+ Action.new("Ping").to_s.should =~ /^Action: Ping\r\nActionID: [\w-]+\r\n\r\n$/i
51
+ Action.new("ParkedCalls").to_s.should =~ /^Action: ParkedCalls\r\nActionID: [\w-]+\r\n\r\n$/i
52
+ end
53
+
54
+ it 'should be able to be marked as sent' do
55
+ subject.state = :sent
56
+ subject.should be_sent
57
+ end
58
+
59
+ it 'should be able to be marked as complete' do
60
+ subject.state = :complete
61
+ subject.should be_complete
62
+ end
63
+
64
+ describe '#<<' do
65
+ describe 'for a non-causal action' do
66
+ context 'with a response' do
67
+ let(:response) { Response.new }
68
+
69
+ it 'should set the response' do
70
+ subject << response
71
+ subject.response.should be response
72
+ end
73
+ end
74
+
75
+ context 'with an error' do
76
+ let(:error) { Error.new }
77
+
78
+ it 'should set the response and raise the error when reading it' do
79
+ subject << error
80
+ lambda { subject.response }.should raise_error error
81
+ end
82
+ end
83
+
84
+ context 'with an event' do
85
+ it 'should raise an error' do
86
+ lambda { subject << Event.new('foo') }.should raise_error StandardError, /causal action/
87
+ end
88
+ end
89
+ end
90
+
91
+ describe 'for a causal action' do
92
+ let(:name) { 'Status' }
93
+
94
+ context 'with a response' do
95
+ let(:message) { Response.new }
96
+
97
+ before { subject << message }
98
+
99
+ it { should_not be_complete }
100
+ end
101
+
102
+ context 'with an event' do
103
+ let(:event) { Event.new 'foo' }
104
+
105
+ before { subject << event }
106
+
107
+ its(:events) { should == [event] }
108
+ end
109
+
110
+ context 'with a terminating event' do
111
+ let(:response) { Response.new }
112
+ let(:event) { Event.new 'StatusComplete' }
113
+
114
+ before do
115
+ subject << response
116
+ subject.should_not be_complete
117
+ subject << event
118
+ end
119
+
120
+ its(:events) { should == [event] }
121
+
122
+ it { should be_complete }
123
+
124
+ its(:response) { should be response }
125
+ end
126
+ end
127
+ end
128
+
129
+ describe 'setting the response' do
130
+ let(:response) { :bar }
131
+
132
+ before { subject.response = response }
133
+
134
+ it { should be_complete }
135
+ its(:response) { should == response }
136
+
137
+ it 'should call the response callback with the response' do
138
+ @foo.should == response
139
+ end
140
+ end
141
+
142
+ describe 'comparison' do
143
+ describe 'with another Action' do
144
+ context 'with identical name and headers' do
145
+ let(:other) { Action.new name, headers }
146
+ it { should == other }
147
+ end
148
+
149
+ context 'with identical name and different headers' do
150
+ let(:other) { Action.new name, 'boo' => 'baz' }
151
+ it { should_not == other }
152
+ end
153
+
154
+ context 'with different name and identical headers' do
155
+ let(:other) { Action.new 'BARBAZ', headers }
156
+ it { should_not == other }
157
+ end
158
+ end
159
+
160
+ it { should_not == :foo }
161
+ end
162
+ end # Action
163
+ end # RubyAMI
@@ -0,0 +1,324 @@
1
+ require 'spec_helper'
2
+
3
+ module RubyAMI
4
+ describe Client do
5
+ let(:event_handler) { [] }
6
+
7
+ let(:options) do
8
+ {
9
+ :host => '127.0.0.1',
10
+ :port => 50000 - rand(1000),
11
+ :username => 'username',
12
+ :password => 'password',
13
+ :event_handler => lambda { |event| event_handler << event }
14
+ }
15
+ end
16
+
17
+ subject { Client.new options }
18
+
19
+ it { should be_stopped }
20
+
21
+ its(:options) { should == options }
22
+
23
+ its(:action_queue) { should be_a GirlFriday::WorkQueue }
24
+
25
+ describe 'starting up' do
26
+ before do
27
+ MockServer.any_instance.stubs :receive_data
28
+ subject.start do
29
+ EM.start_server options[:host], options[:port], ServerMock
30
+ EM.add_timer(0.5) { EM.stop if EM.reactor_running? }
31
+ end
32
+ end
33
+
34
+ it { should be_started }
35
+
36
+ its(:events_stream) { should be_a Stream }
37
+ its(:actions_stream) { should be_a Stream }
38
+ end
39
+
40
+ describe 'logging in streams' do
41
+ context 'when the actions stream connects' do
42
+ let(:mock_actions_stream) { mock 'Actions Stream' }
43
+
44
+ let :expected_login_action do
45
+ Action.new 'Login',
46
+ 'Username' => 'username',
47
+ 'Secret' => 'password',
48
+ 'Events' => 'Off'
49
+ end
50
+
51
+ before do
52
+ Action.any_instance.stubs(:response).returns(true)
53
+ subject.stubs(:actions_stream).returns mock_actions_stream
54
+ end
55
+
56
+ it 'should log in' do
57
+ mock_actions_stream.expects(:send_action).with do |action|
58
+ action.to_s.should == expected_login_action.to_s
59
+ end
60
+
61
+ GirlFriday::WorkQueue.immediate!
62
+ subject.handle_message Stream::Connected.new
63
+ GirlFriday::WorkQueue.queue!
64
+ end
65
+ end
66
+
67
+ context 'when the events stream connects' do
68
+ let(:mock_events_stream) { mock 'Events Stream' }
69
+
70
+ let :expected_login_action do
71
+ Action.new 'Login',
72
+ 'Username' => 'username',
73
+ 'Secret' => 'password',
74
+ 'Events' => 'On'
75
+ end
76
+
77
+ before do
78
+ subject.stubs(:events_stream).returns mock_events_stream
79
+ end
80
+
81
+ it 'should log in' do
82
+ mock_events_stream.expects(:send_action).with do |action|
83
+ action.to_s.should == expected_login_action.to_s
84
+ end
85
+
86
+ subject.handle_event Stream::Connected.new
87
+ end
88
+ end
89
+ end
90
+
91
+ describe 'when the events stream disconnects' do
92
+ it 'should unbind' do
93
+ subject.expects(:unbind).once
94
+ subject.handle_event Stream::Disconnected.new
95
+ end
96
+ end
97
+
98
+ describe 'when the actions stream disconnects' do
99
+ before do
100
+ Action.any_instance.stubs(:response).returns(true)
101
+ end
102
+
103
+ it 'should prevent further actions being sent' do
104
+ subject.expects(:_send_action).once
105
+
106
+ GirlFriday::WorkQueue.immediate!
107
+ subject.handle_message Stream::Connected.new
108
+ GirlFriday::WorkQueue.queue!
109
+ subject.handle_message Stream::Disconnected.new
110
+
111
+ action = Action.new 'foo'
112
+ subject.send_action action
113
+
114
+ sleep 2
115
+
116
+ action.should be_new
117
+ end
118
+
119
+ it 'should unbind' do
120
+ subject.expects(:unbind).once
121
+ subject.handle_message Stream::Disconnected.new
122
+ end
123
+ end
124
+
125
+ describe 'when an event is received' do
126
+ let(:event) { Event.new 'foobar' }
127
+
128
+ it 'should call the event handler' do
129
+ subject.handle_event event
130
+ event_handler.should == [event]
131
+ end
132
+ end
133
+
134
+ describe 'sending actions' do
135
+ let(:action_name) { 'Login' }
136
+ let :headers do
137
+ {
138
+ 'Username' => 'username',
139
+ 'Secret' => 'password'
140
+ }
141
+ end
142
+ let(:expected_action) { Action.new action_name, headers }
143
+
144
+ let :expected_response do
145
+ Response.new.tap do |response|
146
+ response['ActionID'] = expected_action.action_id
147
+ response['Message'] = 'Action completed'
148
+ end
149
+ end
150
+
151
+ let(:mock_actions_stream) { mock 'Actions Stream' }
152
+
153
+ before do
154
+ subject.stubs(:actions_stream).returns mock_actions_stream
155
+ subject.stubs(:login_actions).returns nil
156
+ end
157
+
158
+ it 'should queue up actions to be sent' do
159
+ subject.handle_message Stream::Connected.new
160
+ subject.action_queue.expects(:<<).with expected_action
161
+ subject.send_action action_name, headers
162
+ end
163
+
164
+ describe 'forcibly for testing' do
165
+ before do
166
+ subject.actions_stream.expects(:send_action).with expected_action
167
+ subject._send_action expected_action
168
+ end
169
+
170
+ it 'should mark the action sent' do
171
+ expected_action.should be_sent
172
+ end
173
+
174
+ let(:receive_response) { subject.handle_message expected_response }
175
+
176
+ describe 'when a response is received' do
177
+ it 'should be sent to the action' do
178
+ expected_action.expects(:<<).once.with expected_response
179
+ receive_response
180
+ end
181
+
182
+ it 'should know its action' do
183
+ receive_response
184
+ expected_response.action.should be expected_action
185
+ end
186
+ end
187
+
188
+ describe 'when an error is received' do
189
+ let :expected_response do
190
+ Error.new.tap do |response|
191
+ response['ActionID'] = expected_action.action_id
192
+ response['Message'] = 'Action failed'
193
+ end
194
+ end
195
+
196
+ it 'should be sent to the action' do
197
+ expected_action.expects(:<<).once.with expected_response
198
+ receive_response
199
+ end
200
+
201
+ it 'should know its action' do
202
+ receive_response
203
+ expected_response.action.should be expected_action
204
+ end
205
+ end
206
+
207
+ describe 'when an event is received' do
208
+ let(:event) { Event.new 'foo' }
209
+
210
+ let(:receive_event) { subject.handle_message event }
211
+
212
+ context 'for a causal event' do
213
+ let(:expected_action) { Action.new 'Status' }
214
+
215
+ it 'should be sent to the action' do
216
+ expected_action.expects(:<<).once.with expected_response
217
+ expected_action.expects(:<<).once.with event
218
+ receive_response
219
+ receive_event
220
+ end
221
+
222
+ it 'should know its action' do
223
+ expected_action.stubs :<<
224
+ receive_response
225
+ receive_event
226
+ event.action.should be expected_action
227
+ end
228
+ end
229
+
230
+ context 'for a causal action which is complete' do
231
+ let(:expected_action) { Action.new 'Status' }
232
+
233
+ before do
234
+ expected_action.stubs(:complete?).returns true
235
+ end
236
+
237
+ it 'should raise an error' do
238
+ receive_response
239
+ receive_event
240
+ lambda { subject.handle_message Event.new('bar') }.should raise_error StandardError, /causal action/
241
+ end
242
+ end
243
+
244
+ context 'for a non-causal action' do
245
+ it 'should raise an error' do
246
+ lambda { receive_event }.should raise_error StandardError, /causal action/
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ describe 'from the queue' do
253
+ it 'should send actions to the stream and set their responses' do
254
+ subject.actions_stream.expects(:send_action).with expected_action
255
+ subject.handle_message Stream::Connected.new
256
+
257
+ Thread.new do
258
+ GirlFriday::WorkQueue.immediate!
259
+ subject.send_action expected_action
260
+ GirlFriday::WorkQueue.queue!
261
+ end
262
+
263
+ sleep 0.1
264
+
265
+ subject.handle_message expected_response
266
+ expected_action.response.should be expected_response
267
+ end
268
+
269
+ it 'should not send another action if the first action has not yet received a response' do
270
+ subject.actions_stream.expects(:send_action).once.with expected_action
271
+ subject.handle_message Stream::Connected.new
272
+ actions = []
273
+
274
+ 2.times do
275
+ action = Action.new action_name, headers
276
+ actions << action
277
+ subject.send_action action
278
+ end
279
+
280
+ sleep 2
281
+
282
+ actions.should have(2).actions
283
+ actions[0].should be_sent
284
+ actions[1].should be_new
285
+ end
286
+ end
287
+ end
288
+
289
+ describe '#stop' do
290
+ let(:mock_actions_stream) { mock 'Actions Stream' }
291
+ let(:mock_events_stream) { mock 'Events Stream' }
292
+
293
+ let(:streams) { [mock_actions_stream, mock_events_stream] }
294
+
295
+ before do
296
+ subject.stubs(:actions_stream).returns mock_actions_stream
297
+ subject.stubs(:events_stream).returns mock_events_stream
298
+ end
299
+
300
+ it 'should close both streams' do
301
+ streams.each { |s| s.expects :close_connection_after_writing }
302
+ subject.stop
303
+ end
304
+ end
305
+
306
+ describe '#unbind' do
307
+ context 'if EM is running' do
308
+ it 'shuts down EM' do
309
+ EM.expects(:reactor_running?).returns true
310
+ EM.expects(:stop)
311
+ subject.unbind
312
+ end
313
+ end
314
+
315
+ context 'if EM is not running' do
316
+ it 'does nothing' do
317
+ EM.expects(:reactor_running?).returns false
318
+ EM.expects(:stop).never
319
+ subject.unbind
320
+ end
321
+ end
322
+ end
323
+ end
324
+ end