ruby_ami 0.1.1

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