punchblock 2.1.1 → 2.2.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.
- checksums.yaml +4 -4
- data/.travis.yml +2 -1
- data/CHANGELOG.md +7 -0
- data/lib/punchblock.rb +1 -1
- data/lib/punchblock/component.rb +2 -0
- data/lib/punchblock/component/input.rb +9 -1
- data/lib/punchblock/component/output.rb +1 -1
- data/lib/punchblock/component/receive_fax.rb +24 -0
- data/lib/punchblock/component/send_fax.rb +62 -0
- data/lib/punchblock/connection/asterisk.rb +1 -1
- data/lib/punchblock/event/complete.rb +15 -1
- data/lib/punchblock/translator/asterisk.rb +30 -15
- data/lib/punchblock/translator/asterisk/call.rb +13 -27
- data/lib/punchblock/translator/asterisk/component.rb +4 -7
- data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +1 -5
- data/lib/punchblock/translator/asterisk/component/composed_prompt.rb +8 -9
- data/lib/punchblock/translator/asterisk/component/input.rb +2 -3
- data/lib/punchblock/translator/asterisk/component/mrcp_prompt.rb +9 -9
- data/lib/punchblock/translator/asterisk/component/output.rb +134 -39
- data/lib/punchblock/translator/asterisk/component/record.rb +2 -3
- data/lib/punchblock/translator/asterisk/component/stop_by_redirect.rb +2 -3
- data/lib/punchblock/translator/dtmf_recognizer.rb +2 -4
- data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +6 -1
- data/lib/punchblock/translator/freeswitch/component/flite_output.rb +1 -1
- data/lib/punchblock/translator/freeswitch/component/output.rb +12 -10
- data/lib/punchblock/translator/freeswitch/component/tts_output.rb +1 -1
- data/lib/punchblock/version.rb +1 -1
- data/spec/punchblock/component/input_spec.rb +91 -0
- data/spec/punchblock/component/output_spec.rb +1 -2
- data/spec/punchblock/component/receive_fax_spec.rb +111 -0
- data/spec/punchblock/component/send_fax_spec.rb +110 -0
- data/spec/punchblock/connection/asterisk_spec.rb +1 -1
- data/spec/punchblock/translator/asterisk/call_spec.rb +53 -79
- data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +0 -3
- data/spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb +1 -1
- data/spec/punchblock/translator/asterisk/component/composed_prompt_spec.rb +2 -2
- data/spec/punchblock/translator/asterisk/component/input_spec.rb +6 -6
- data/spec/punchblock/translator/asterisk/component/mrcp_native_prompt_spec.rb +3 -3
- data/spec/punchblock/translator/asterisk/component/mrcp_prompt_spec.rb +9 -11
- data/spec/punchblock/translator/asterisk/component/output_spec.rb +902 -28
- data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +1 -1
- data/spec/punchblock/translator/asterisk/component_spec.rb +2 -9
- data/spec/punchblock/translator/asterisk_spec.rb +42 -94
- data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +5 -5
- data/spec/punchblock/translator/freeswitch/component/output_spec.rb +7 -3
- data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +17 -5
- metadata +67 -61
|
@@ -81,7 +81,6 @@ module Punchblock
|
|
|
81
81
|
it 'should send the component node false' do
|
|
82
82
|
subject.execute
|
|
83
83
|
original_command.response(1).should be_false
|
|
84
|
-
subject.should_not be_alive
|
|
85
84
|
end
|
|
86
85
|
|
|
87
86
|
context "which is 'No such channel'" do
|
|
@@ -90,7 +89,6 @@ module Punchblock
|
|
|
90
89
|
it "should return an :item_not_found error for the command" do
|
|
91
90
|
subject.execute
|
|
92
91
|
original_command.response(0.5).should be == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{mock_call.id}", mock_call.id)
|
|
93
|
-
subject.should_not be_alive
|
|
94
92
|
end
|
|
95
93
|
end
|
|
96
94
|
|
|
@@ -100,7 +98,6 @@ module Punchblock
|
|
|
100
98
|
it "should return an :item_not_found error for the command" do
|
|
101
99
|
subject.execute
|
|
102
100
|
original_command.response(0.5).should be == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{mock_call.id}", mock_call.id)
|
|
103
|
-
subject.should_not be_alive
|
|
104
101
|
end
|
|
105
102
|
end
|
|
106
103
|
end
|
|
@@ -58,7 +58,7 @@ module Punchblock
|
|
|
58
58
|
context 'for a non-causal action' do
|
|
59
59
|
it 'should send a complete event to the component node' do
|
|
60
60
|
ami_client.should_receive(:send_action).once.and_return response
|
|
61
|
-
subject.
|
|
61
|
+
subject.should_receive(:send_complete_event).once.with expected_complete_reason
|
|
62
62
|
subject.execute
|
|
63
63
|
end
|
|
64
64
|
end
|
|
@@ -207,7 +207,7 @@ module Punchblock
|
|
|
207
207
|
end
|
|
208
208
|
|
|
209
209
|
it "sets the command response to true" do
|
|
210
|
-
call.
|
|
210
|
+
call.should_receive(:redirect_back).once
|
|
211
211
|
subject.execute_command command
|
|
212
212
|
command.response(0.1).should be == true
|
|
213
213
|
end
|
|
@@ -219,7 +219,7 @@ module Punchblock
|
|
|
219
219
|
source_uri: subject.id,
|
|
220
220
|
target_call_id: call.id
|
|
221
221
|
|
|
222
|
-
call.
|
|
222
|
+
call.should_receive(:redirect_back)
|
|
223
223
|
subject.execute_command command
|
|
224
224
|
original_command.should_not be_complete
|
|
225
225
|
call.process_ami_event ami_event
|
|
@@ -96,7 +96,7 @@ module Punchblock
|
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
it "should not process further dtmf events" do
|
|
99
|
-
subject.
|
|
99
|
+
subject.should_receive(:process_dtmf).never
|
|
100
100
|
send_ami_events_for_dtmf 3
|
|
101
101
|
end
|
|
102
102
|
end
|
|
@@ -266,7 +266,7 @@ module Punchblock
|
|
|
266
266
|
end
|
|
267
267
|
|
|
268
268
|
it "should not process further dtmf events" do
|
|
269
|
-
subject.
|
|
269
|
+
subject.should_receive(:process_dtmf).never
|
|
270
270
|
send_ami_events_for_dtmf 3
|
|
271
271
|
end
|
|
272
272
|
end
|
|
@@ -331,7 +331,7 @@ module Punchblock
|
|
|
331
331
|
let(:original_command_opts) { { :initial_timeout => -1 } }
|
|
332
332
|
|
|
333
333
|
it "should not start a timer" do
|
|
334
|
-
subject.
|
|
334
|
+
subject.should_receive(:begin_initial_timer).never
|
|
335
335
|
subject.execute
|
|
336
336
|
end
|
|
337
337
|
end
|
|
@@ -340,7 +340,7 @@ module Punchblock
|
|
|
340
340
|
let(:original_command_opts) { { :initial_timeout => nil } }
|
|
341
341
|
|
|
342
342
|
it "should not start a timer" do
|
|
343
|
-
subject.
|
|
343
|
+
subject.should_receive(:begin_initial_timer).never
|
|
344
344
|
subject.execute
|
|
345
345
|
end
|
|
346
346
|
end
|
|
@@ -450,7 +450,7 @@ module Punchblock
|
|
|
450
450
|
let(:original_command_opts) { { :inter_digit_timeout => -1 } }
|
|
451
451
|
|
|
452
452
|
it "should not start a timer" do
|
|
453
|
-
subject.
|
|
453
|
+
subject.should_receive(:begin_inter_digit_timer).never
|
|
454
454
|
subject.execute
|
|
455
455
|
end
|
|
456
456
|
end
|
|
@@ -459,7 +459,7 @@ module Punchblock
|
|
|
459
459
|
let(:original_command_opts) { { :inter_digit_timeout => nil } }
|
|
460
460
|
|
|
461
461
|
it "should not start a timer" do
|
|
462
|
-
subject.
|
|
462
|
+
subject.should_receive(:begin_inter_digit_timer).never
|
|
463
463
|
subject.execute
|
|
464
464
|
end
|
|
465
465
|
end
|
|
@@ -623,13 +623,13 @@ module Punchblock
|
|
|
623
623
|
end
|
|
624
624
|
|
|
625
625
|
it "sets the command response to true" do
|
|
626
|
-
mock_call.
|
|
626
|
+
mock_call.should_receive(:redirect_back)
|
|
627
627
|
subject.execute_command command
|
|
628
628
|
command.response(0.1).should be == true
|
|
629
629
|
end
|
|
630
630
|
|
|
631
631
|
it "sends the correct complete event" do
|
|
632
|
-
mock_call.
|
|
632
|
+
mock_call.should_receive(:redirect_back)
|
|
633
633
|
subject.execute_command command
|
|
634
634
|
original_command.should_not be_complete
|
|
635
635
|
mock_call.process_ami_event ami_event
|
|
@@ -638,7 +638,7 @@ module Punchblock
|
|
|
638
638
|
end
|
|
639
639
|
|
|
640
640
|
it "redirects the call by unjoining it" do
|
|
641
|
-
mock_call.
|
|
641
|
+
mock_call.should_receive(:redirect_back)
|
|
642
642
|
subject.execute_command command
|
|
643
643
|
end
|
|
644
644
|
end
|
|
@@ -133,22 +133,20 @@ module Punchblock
|
|
|
133
133
|
context 'with multiple inline documents' do
|
|
134
134
|
let(:output_command_options) { { render_documents: [{value: ssml_doc}, {value: ssml_doc}] } }
|
|
135
135
|
|
|
136
|
-
it "should return
|
|
137
|
-
param = [[ssml_doc.to_doc.to_s, ssml_doc.to_doc.to_s].join(','), grammar.to_doc].map { |o| "\"#{o.to_s.squish.gsub('"', '\"')}\"" }.push('uer=1&b=1').join(',')
|
|
138
|
-
mock_call.should_receive(:execute_agi_command).once.with('EXEC SynthAndRecog', param).and_return code: 200, result: 1
|
|
136
|
+
it "should return an error and not execute any actions" do
|
|
139
137
|
subject.execute
|
|
140
|
-
|
|
138
|
+
error = ProtocolError.new.setup 'option error', 'Only one document is allowed.'
|
|
139
|
+
original_command.response(0.1).should be == error
|
|
141
140
|
end
|
|
142
141
|
end
|
|
143
142
|
|
|
144
143
|
context 'with multiple documents by URI' do
|
|
145
144
|
let(:output_command_options) { { render_documents: [{url: 'http://example.com/doc1.ssml'}, {url: 'http://example.com/doc2.ssml'}] } }
|
|
146
145
|
|
|
147
|
-
it "should return
|
|
148
|
-
param = [['http://example.com/doc1.ssml', 'http://example.com/doc2.ssml'].join(','), grammar.to_doc].map { |o| "\"#{o.to_s.squish.gsub('"', '\"')}\"" }.push('uer=1&b=1').join(',')
|
|
149
|
-
mock_call.should_receive(:execute_agi_command).once.with('EXEC SynthAndRecog', param).and_return code: 200, result: 1
|
|
146
|
+
it "should return an error and not execute any actions" do
|
|
150
147
|
subject.execute
|
|
151
|
-
|
|
148
|
+
error = ProtocolError.new.setup 'option error', 'Only one document is allowed.'
|
|
149
|
+
original_command.response(0.1).should be == error
|
|
152
150
|
end
|
|
153
151
|
end
|
|
154
152
|
|
|
@@ -617,13 +615,13 @@ module Punchblock
|
|
|
617
615
|
end
|
|
618
616
|
|
|
619
617
|
it "sets the command response to true" do
|
|
620
|
-
mock_call.
|
|
618
|
+
mock_call.should_receive(:redirect_back)
|
|
621
619
|
subject.execute_command command
|
|
622
620
|
command.response(0.1).should be == true
|
|
623
621
|
end
|
|
624
622
|
|
|
625
623
|
it "sends the correct complete event" do
|
|
626
|
-
mock_call.
|
|
624
|
+
mock_call.should_receive(:redirect_back)
|
|
627
625
|
subject.execute_command command
|
|
628
626
|
original_command.should_not be_complete
|
|
629
627
|
mock_call.process_ami_event ami_event
|
|
@@ -632,7 +630,7 @@ module Punchblock
|
|
|
632
630
|
end
|
|
633
631
|
|
|
634
632
|
it "redirects the call by unjoining it" do
|
|
635
|
-
mock_call.
|
|
633
|
+
mock_call.should_receive(:redirect_back)
|
|
636
634
|
subject.execute_command command
|
|
637
635
|
end
|
|
638
636
|
end
|
|
@@ -36,6 +36,13 @@ module Punchblock
|
|
|
36
36
|
mock_call.stub(:answered?).and_return(value)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
def expect_mrcpsynth_with_options(options)
|
|
40
|
+
mock_call.should_receive(:execute_agi_command).once.with do |*args|
|
|
41
|
+
args[0].should be == 'EXEC MRCPSynth'
|
|
42
|
+
args[1].should match options
|
|
43
|
+
end.and_return code: 200, result: 1
|
|
44
|
+
end
|
|
45
|
+
|
|
39
46
|
describe '#execute' do
|
|
40
47
|
before { original_command.request! }
|
|
41
48
|
|
|
@@ -167,6 +174,25 @@ module Punchblock
|
|
|
167
174
|
end
|
|
168
175
|
|
|
169
176
|
end
|
|
177
|
+
|
|
178
|
+
describe "with multiple documents" do
|
|
179
|
+
let :first_ssml_doc do
|
|
180
|
+
RubySpeech::SSML.draw do
|
|
181
|
+
audio :src => audio_filename
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
let :second_ssml_doc do
|
|
185
|
+
RubySpeech::SSML.draw do
|
|
186
|
+
say_as(:interpret_as => :cardinal) { 'FOO' }
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
let(:command_opts) { { render_documents: [{value: first_ssml_doc}, {value: second_ssml_doc}] } }
|
|
190
|
+
|
|
191
|
+
it "executes Swift with a concatenated version of the documents" do
|
|
192
|
+
mock_call.should_receive(:execute_agi_command).once.with 'EXEC Swift', ssml_with_options
|
|
193
|
+
subject.execute
|
|
194
|
+
end
|
|
195
|
+
end
|
|
170
196
|
end
|
|
171
197
|
|
|
172
198
|
context 'with a renderer of :unimrcp' do
|
|
@@ -190,13 +216,6 @@ module Punchblock
|
|
|
190
216
|
let(:synthstatus) { 'OK' }
|
|
191
217
|
before { mock_call.stub(:channel_var).with('SYNTHSTATUS').and_return synthstatus }
|
|
192
218
|
|
|
193
|
-
def expect_mrcpsynth_with_options(options)
|
|
194
|
-
mock_call.should_receive(:execute_agi_command).once.with do |*args|
|
|
195
|
-
args[0].should be == 'EXEC MRCPSynth'
|
|
196
|
-
args[1].should match options
|
|
197
|
-
end.and_return code: 200, result: 1
|
|
198
|
-
end
|
|
199
|
-
|
|
200
219
|
before { expect_answered }
|
|
201
220
|
|
|
202
221
|
it "should execute MRCPSynth" do
|
|
@@ -265,10 +284,30 @@ module Punchblock
|
|
|
265
284
|
|
|
266
285
|
context 'with multiple documents' do
|
|
267
286
|
let(:command_opts) { { :render_documents => [{:value => ssml_doc}, {:value => ssml_doc}] } }
|
|
268
|
-
|
|
287
|
+
|
|
288
|
+
it "should execute MRCPSynth once with each document" do
|
|
289
|
+
param = ["\"#{ssml_doc.to_s.squish.gsub('"', '\"')}\"", ''].join(',')
|
|
290
|
+
mock_call.should_receive(:execute_agi_command).once.with('EXEC MRCPSynth', param).and_return code: 200, result: 1
|
|
291
|
+
mock_call.should_receive(:execute_agi_command).once.with('EXEC MRCPSynth', param).and_return code: 200, result: 1
|
|
269
292
|
subject.execute
|
|
270
|
-
|
|
271
|
-
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
it 'should not execute further output after a stop command' do
|
|
296
|
+
mock_call.should_receive(:execute_agi_command).once.ordered.and_return do
|
|
297
|
+
sleep 0.5
|
|
298
|
+
end
|
|
299
|
+
latch = CountDownLatch.new 1
|
|
300
|
+
original_command.should_receive(:add_event).once.with do |e|
|
|
301
|
+
e.reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
302
|
+
latch.countdown!
|
|
303
|
+
end
|
|
304
|
+
Celluloid::Future.new { subject.execute }
|
|
305
|
+
sleep 0.2
|
|
306
|
+
mock_call.should_receive(:redirect_back).ordered
|
|
307
|
+
stop_command = Punchblock::Component::Stop.new
|
|
308
|
+
stop_command.request!
|
|
309
|
+
subject.execute_command stop_command
|
|
310
|
+
latch.wait(2).should be_true
|
|
272
311
|
end
|
|
273
312
|
end
|
|
274
313
|
end
|
|
@@ -341,10 +380,40 @@ module Punchblock
|
|
|
341
380
|
|
|
342
381
|
context 'set' do
|
|
343
382
|
let(:command_opts) { { :repeat_times => 2 } }
|
|
344
|
-
|
|
383
|
+
|
|
384
|
+
it "should render the specified number of times" do
|
|
385
|
+
2.times { expect_mrcpsynth_with_options(//) }
|
|
345
386
|
subject.execute
|
|
346
|
-
|
|
347
|
-
|
|
387
|
+
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
context 'to 0' do
|
|
391
|
+
let(:command_opts) { { :repeat_times => 0 } }
|
|
392
|
+
|
|
393
|
+
it "should render 10,000 the specified number of times" do
|
|
394
|
+
expect_answered
|
|
395
|
+
1000.times { expect_mrcpsynth_with_options(//) }
|
|
396
|
+
subject.execute
|
|
397
|
+
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
it 'should not execute further output after a stop command' do
|
|
402
|
+
mock_call.should_receive(:execute_agi_command).once.ordered.and_return do
|
|
403
|
+
sleep 0.2
|
|
404
|
+
end
|
|
405
|
+
latch = CountDownLatch.new 1
|
|
406
|
+
original_command.should_receive(:add_event).once.with do |e|
|
|
407
|
+
e.reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
408
|
+
latch.countdown!
|
|
409
|
+
end
|
|
410
|
+
Celluloid::Future.new { subject.execute }
|
|
411
|
+
sleep 0.1
|
|
412
|
+
mock_call.should_receive(:redirect_back).ordered
|
|
413
|
+
stop_command = Punchblock::Component::Stop.new
|
|
414
|
+
stop_command.request!
|
|
415
|
+
subject.execute_command stop_command
|
|
416
|
+
latch.wait(2).should be_true
|
|
348
417
|
end
|
|
349
418
|
end
|
|
350
419
|
end
|
|
@@ -655,6 +724,49 @@ module Punchblock
|
|
|
655
724
|
original_command.response(0.1).should be == error
|
|
656
725
|
end
|
|
657
726
|
end
|
|
727
|
+
|
|
728
|
+
context 'with multiple documents' do
|
|
729
|
+
let(:command_opts) { { render_documents: [{value: ssml_doc}, {value: ssml_doc}] } }
|
|
730
|
+
|
|
731
|
+
it "should render each document in turn using a Playback per document" do
|
|
732
|
+
expect_answered
|
|
733
|
+
2.times { expect_playback }
|
|
734
|
+
subject.execute
|
|
735
|
+
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
it 'should not execute further output after a stop command' do
|
|
739
|
+
expect_answered
|
|
740
|
+
mock_call.should_receive(:execute_agi_command).once.ordered.and_return do
|
|
741
|
+
sleep 0.2
|
|
742
|
+
end
|
|
743
|
+
latch = CountDownLatch.new 1
|
|
744
|
+
original_command.should_receive(:add_event).once.with do |e|
|
|
745
|
+
e.reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
746
|
+
latch.countdown!
|
|
747
|
+
end
|
|
748
|
+
Celluloid::Future.new { subject.execute }
|
|
749
|
+
sleep 0.1
|
|
750
|
+
mock_call.should_receive(:redirect_back).ordered
|
|
751
|
+
stop_command = Punchblock::Component::Stop.new
|
|
752
|
+
stop_command.request!
|
|
753
|
+
subject.execute_command stop_command
|
|
754
|
+
latch.wait(2).should be_true
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
context "when the PLAYBACKSTATUS variable is set to 'FAILED'" do
|
|
758
|
+
let(:playbackstatus) { 'FAILED' }
|
|
759
|
+
|
|
760
|
+
it "should terminate playback and send an error complete event" do
|
|
761
|
+
expect_answered
|
|
762
|
+
mock_call.should_receive(:execute_agi_command).once.and_return code: 200, result: 1
|
|
763
|
+
subject.execute
|
|
764
|
+
complete_reason = original_command.complete_event(0.1).reason
|
|
765
|
+
complete_reason.should be_a Punchblock::Event::Complete::Error
|
|
766
|
+
complete_reason.details.should == "Terminated due to playback error"
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
end
|
|
658
770
|
end
|
|
659
771
|
|
|
660
772
|
describe 'start-offset' do
|
|
@@ -729,10 +841,42 @@ module Punchblock
|
|
|
729
841
|
|
|
730
842
|
context 'set' do
|
|
731
843
|
let(:command_opts) { { :repeat_times => 2 } }
|
|
732
|
-
|
|
844
|
+
|
|
845
|
+
it "should render the specified number of times" do
|
|
846
|
+
expect_answered
|
|
847
|
+
2.times { expect_playback }
|
|
733
848
|
subject.execute
|
|
734
|
-
|
|
735
|
-
|
|
849
|
+
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
context 'to 0' do
|
|
853
|
+
let(:command_opts) { { :repeat_times => 0 } }
|
|
854
|
+
|
|
855
|
+
it "should render 10,000 the specified number of times" do
|
|
856
|
+
expect_answered
|
|
857
|
+
1000.times { expect_playback }
|
|
858
|
+
subject.execute
|
|
859
|
+
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
860
|
+
end
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
it 'should not execute further output after a stop command' do
|
|
864
|
+
expect_answered
|
|
865
|
+
mock_call.should_receive(:execute_agi_command).once.ordered.and_return do
|
|
866
|
+
sleep 0.2
|
|
867
|
+
end
|
|
868
|
+
latch = CountDownLatch.new 1
|
|
869
|
+
original_command.should_receive(:add_event).once.with do |e|
|
|
870
|
+
e.reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
871
|
+
latch.countdown!
|
|
872
|
+
end
|
|
873
|
+
Celluloid::Future.new { subject.execute }
|
|
874
|
+
sleep 0.1
|
|
875
|
+
mock_call.should_receive(:redirect_back).ordered
|
|
876
|
+
stop_command = Punchblock::Component::Stop.new
|
|
877
|
+
stop_command.request!
|
|
878
|
+
subject.execute_command stop_command
|
|
879
|
+
latch.wait(2).should be_true
|
|
736
880
|
end
|
|
737
881
|
end
|
|
738
882
|
end
|
|
@@ -804,7 +948,7 @@ module Punchblock
|
|
|
804
948
|
it "does not redirect the call" do
|
|
805
949
|
expect_answered
|
|
806
950
|
expect_playback
|
|
807
|
-
mock_call.
|
|
951
|
+
mock_call.should_receive(:redirect_back).never
|
|
808
952
|
subject.execute
|
|
809
953
|
original_command.response(0.1).should be_a Ref
|
|
810
954
|
send_ami_events_for_dtmf 1
|
|
@@ -817,24 +961,24 @@ module Punchblock
|
|
|
817
961
|
before do
|
|
818
962
|
expect_answered
|
|
819
963
|
mock_call.should_receive(:execute_agi_command).once.with('EXEC Playback', audio_filename)
|
|
820
|
-
subject.
|
|
964
|
+
subject.should_receive(:send_finish).and_return nil
|
|
821
965
|
end
|
|
822
966
|
|
|
823
967
|
context "when a DTMF digit is received" do
|
|
824
968
|
it "sends the correct complete event" do
|
|
825
|
-
mock_call.
|
|
969
|
+
mock_call.should_receive :redirect_back
|
|
826
970
|
subject.execute
|
|
827
971
|
original_command.response(0.1).should be_a Ref
|
|
828
972
|
original_command.should_not be_complete
|
|
829
973
|
send_ami_events_for_dtmf 1
|
|
830
|
-
mock_call.
|
|
974
|
+
mock_call.process_ami_event ami_event
|
|
831
975
|
sleep 0.2
|
|
832
976
|
original_command.should be_complete
|
|
833
977
|
reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
834
978
|
end
|
|
835
979
|
|
|
836
980
|
it "redirects the call back to async AGI" do
|
|
837
|
-
mock_call.
|
|
981
|
+
mock_call.should_receive(:redirect_back).once
|
|
838
982
|
subject.execute
|
|
839
983
|
original_command.response(0.1).should be_a Ref
|
|
840
984
|
send_ami_events_for_dtmf 1
|
|
@@ -848,24 +992,24 @@ module Punchblock
|
|
|
848
992
|
before do
|
|
849
993
|
expect_answered
|
|
850
994
|
mock_call.should_receive(:execute_agi_command).once.with('EXEC Playback', audio_filename)
|
|
851
|
-
subject.
|
|
995
|
+
subject.should_receive(:send_finish).and_return nil
|
|
852
996
|
end
|
|
853
997
|
|
|
854
998
|
context "when a DTMF digit is received" do
|
|
855
999
|
it "sends the correct complete event" do
|
|
856
|
-
mock_call.
|
|
1000
|
+
mock_call.should_receive :redirect_back
|
|
857
1001
|
subject.execute
|
|
858
1002
|
original_command.response(0.1).should be_a Ref
|
|
859
1003
|
original_command.should_not be_complete
|
|
860
1004
|
send_ami_events_for_dtmf 1
|
|
861
|
-
mock_call.
|
|
1005
|
+
mock_call.process_ami_event ami_event
|
|
862
1006
|
sleep 0.2
|
|
863
1007
|
original_command.should be_complete
|
|
864
1008
|
reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
865
1009
|
end
|
|
866
1010
|
|
|
867
1011
|
it "redirects the call back to async AGI" do
|
|
868
|
-
mock_call.
|
|
1012
|
+
mock_call.should_receive(:redirect_back).once
|
|
869
1013
|
subject.execute
|
|
870
1014
|
original_command.response(0.1).should be_a Ref
|
|
871
1015
|
send_ami_events_for_dtmf 1
|
|
@@ -884,6 +1028,736 @@ module Punchblock
|
|
|
884
1028
|
end
|
|
885
1029
|
end
|
|
886
1030
|
end
|
|
1031
|
+
|
|
1032
|
+
context "with a renderer of :native_or_unimrcp" do
|
|
1033
|
+
def expect_playback(filename = audio_filename)
|
|
1034
|
+
mock_call.should_receive(:execute_agi_command).ordered.once.with('EXEC Playback', filename).and_return code: 200
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
def expect_playback_noanswer
|
|
1038
|
+
mock_call.should_receive(:execute_agi_command).once.with('EXEC Playback', audio_filename + ',noanswer').and_return code: 200
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
def expect_mrcpsynth(doc = ssml_doc)
|
|
1042
|
+
mock_call.should_receive(:execute_agi_command).ordered.once.with('EXEC MRCPSynth', ["\"#{doc.to_s.squish.gsub('"', '\"')}\"", ''].join(',')).and_return code: 200, result: 1
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
let(:audio_filename) { 'tt-monkeys' }
|
|
1046
|
+
|
|
1047
|
+
let :ssml_doc do
|
|
1048
|
+
RubySpeech::SSML.draw do
|
|
1049
|
+
audio :src => audio_filename do
|
|
1050
|
+
string "Foobar"
|
|
1051
|
+
end
|
|
1052
|
+
end
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
let(:command_opts) { {} }
|
|
1056
|
+
|
|
1057
|
+
let :command_options do
|
|
1058
|
+
{ :render_document => {:value => ssml_doc}, renderer: :native_or_unimrcp }.merge(command_opts)
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
let :original_command do
|
|
1062
|
+
Punchblock::Component::Output.new command_options
|
|
1063
|
+
end
|
|
1064
|
+
|
|
1065
|
+
let(:playbackstatus) { 'SUCCESS' }
|
|
1066
|
+
before { mock_call.stub(:channel_var).with('PLAYBACKSTATUS').and_return playbackstatus }
|
|
1067
|
+
|
|
1068
|
+
describe 'ssml' do
|
|
1069
|
+
context 'unset' do
|
|
1070
|
+
let(:ssml_doc) { nil }
|
|
1071
|
+
it "should return an error and not execute any actions" do
|
|
1072
|
+
subject.execute
|
|
1073
|
+
error = ProtocolError.new.setup 'option error', 'An SSML document is required.'
|
|
1074
|
+
original_command.response(0.1).should be == error
|
|
1075
|
+
end
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
context 'with a single audio SSML node' do
|
|
1079
|
+
let(:audio_filename) { 'tt-monkeys' }
|
|
1080
|
+
let :ssml_doc do
|
|
1081
|
+
RubySpeech::SSML.draw language: 'pt-BR' do
|
|
1082
|
+
audio :src => audio_filename do
|
|
1083
|
+
voice name: 'frank' do
|
|
1084
|
+
string "Hello world"
|
|
1085
|
+
end
|
|
1086
|
+
end
|
|
1087
|
+
end
|
|
1088
|
+
end
|
|
1089
|
+
|
|
1090
|
+
it 'should playback the audio file using Playback' do
|
|
1091
|
+
expect_answered
|
|
1092
|
+
expect_playback
|
|
1093
|
+
subject.execute
|
|
1094
|
+
end
|
|
1095
|
+
|
|
1096
|
+
it 'should send a complete event when the file finishes playback' do
|
|
1097
|
+
def mock_call.answered?
|
|
1098
|
+
true
|
|
1099
|
+
end
|
|
1100
|
+
expect_playback
|
|
1101
|
+
subject.execute
|
|
1102
|
+
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1103
|
+
end
|
|
1104
|
+
|
|
1105
|
+
context "when the audio filename is prefixed by file://" do
|
|
1106
|
+
let(:audio_filename) { 'file://tt-monkeys' }
|
|
1107
|
+
|
|
1108
|
+
it 'should playback the audio file using Playback' do
|
|
1109
|
+
expect_answered
|
|
1110
|
+
expect_playback 'tt-monkeys'
|
|
1111
|
+
subject.execute
|
|
1112
|
+
end
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
context "when the audio filename has an extension" do
|
|
1116
|
+
let(:audio_filename) { 'tt-monkeys.wav' }
|
|
1117
|
+
|
|
1118
|
+
it 'should playback the audio file using Playback' do
|
|
1119
|
+
expect_answered
|
|
1120
|
+
expect_playback 'tt-monkeys'
|
|
1121
|
+
subject.execute
|
|
1122
|
+
end
|
|
1123
|
+
|
|
1124
|
+
context "when there are other dots in the filename" do
|
|
1125
|
+
let(:audio_filename) { 'blue.tt-monkeys.wav' }
|
|
1126
|
+
|
|
1127
|
+
it 'should playback the audio file using Playback' do
|
|
1128
|
+
expect_answered
|
|
1129
|
+
expect_playback 'blue.tt-monkeys'
|
|
1130
|
+
subject.execute
|
|
1131
|
+
end
|
|
1132
|
+
end
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
context "when we get a RubyAMI Error" do
|
|
1136
|
+
it "should send an error complete event" do
|
|
1137
|
+
expect_answered
|
|
1138
|
+
error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' }
|
|
1139
|
+
mock_call.should_receive(:execute_agi_command).and_raise error
|
|
1140
|
+
subject.execute
|
|
1141
|
+
complete_reason = original_command.complete_event(0.1).reason
|
|
1142
|
+
complete_reason.should be_a Punchblock::Event::Complete::Error
|
|
1143
|
+
complete_reason.details.should == "Terminated due to AMI error 'FooBar'"
|
|
1144
|
+
end
|
|
1145
|
+
end
|
|
1146
|
+
|
|
1147
|
+
context "when the channel is gone" do
|
|
1148
|
+
it "should send an error complete event" do
|
|
1149
|
+
expect_answered
|
|
1150
|
+
error = ChannelGoneError.new 'FooBar'
|
|
1151
|
+
mock_call.should_receive(:execute_agi_command).and_raise error
|
|
1152
|
+
subject.execute
|
|
1153
|
+
complete_reason = original_command.complete_event(0.1).reason
|
|
1154
|
+
complete_reason.should be_a Punchblock::Event::Complete::Hangup
|
|
1155
|
+
end
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
context "when the PLAYBACKSTATUS variable is set to 'FAILED'" do
|
|
1159
|
+
let(:playbackstatus) { 'FAILED' }
|
|
1160
|
+
|
|
1161
|
+
let(:synthstatus) { 'SUCCESS' }
|
|
1162
|
+
before { mock_call.stub(:channel_var).with('SYNTHSTATUS').and_return synthstatus }
|
|
1163
|
+
|
|
1164
|
+
let :fallback_doc do
|
|
1165
|
+
RubySpeech::SSML.draw language: 'pt-BR' do
|
|
1166
|
+
voice name: 'frank' do
|
|
1167
|
+
string "Hello world"
|
|
1168
|
+
end
|
|
1169
|
+
end
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
it "should attempt to render the children of the audio tag via MRCP and then send a complete event" do
|
|
1173
|
+
expect_answered
|
|
1174
|
+
expect_playback
|
|
1175
|
+
expect_mrcpsynth fallback_doc
|
|
1176
|
+
subject.execute
|
|
1177
|
+
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1178
|
+
end
|
|
1179
|
+
|
|
1180
|
+
context "and the SYNTHSTATUS variable is set to 'ERROR'" do
|
|
1181
|
+
let(:synthstatus) { 'ERROR' }
|
|
1182
|
+
|
|
1183
|
+
it "should send an error complete event" do
|
|
1184
|
+
expect_answered
|
|
1185
|
+
expect_playback
|
|
1186
|
+
expect_mrcpsynth fallback_doc
|
|
1187
|
+
subject.execute
|
|
1188
|
+
complete_reason = original_command.complete_event(0.1).reason
|
|
1189
|
+
complete_reason.should be_a Punchblock::Event::Complete::Error
|
|
1190
|
+
complete_reason.details.should == "Terminated due to UniMRCP error"
|
|
1191
|
+
end
|
|
1192
|
+
end
|
|
1193
|
+
end
|
|
1194
|
+
end
|
|
1195
|
+
|
|
1196
|
+
context 'with a single text node without spaces' do
|
|
1197
|
+
let(:audio_filename) { 'tt-monkeys' }
|
|
1198
|
+
let :ssml_doc do
|
|
1199
|
+
RubySpeech::SSML.draw { string audio_filename }
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
it 'should playback the audio file using Playback' do
|
|
1203
|
+
expect_answered
|
|
1204
|
+
expect_playback
|
|
1205
|
+
subject.execute
|
|
1206
|
+
end
|
|
1207
|
+
|
|
1208
|
+
it 'should send a complete event when the file finishes playback' do
|
|
1209
|
+
expect_answered
|
|
1210
|
+
expect_playback
|
|
1211
|
+
subject.execute
|
|
1212
|
+
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1213
|
+
end
|
|
1214
|
+
|
|
1215
|
+
context "when we get a RubyAMI Error" do
|
|
1216
|
+
it "should send an error complete event" do
|
|
1217
|
+
expect_answered
|
|
1218
|
+
error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' }
|
|
1219
|
+
mock_call.should_receive(:execute_agi_command).and_raise error
|
|
1220
|
+
subject.execute
|
|
1221
|
+
complete_reason = original_command.complete_event(0.1).reason
|
|
1222
|
+
complete_reason.should be_a Punchblock::Event::Complete::Error
|
|
1223
|
+
complete_reason.details.should == "Terminated due to AMI error 'FooBar'"
|
|
1224
|
+
end
|
|
1225
|
+
end
|
|
1226
|
+
|
|
1227
|
+
context "with early media playback" do
|
|
1228
|
+
it "should play the file with Playback" do
|
|
1229
|
+
expect_answered false
|
|
1230
|
+
expect_playback_noanswer
|
|
1231
|
+
mock_call.should_receive(:send_progress)
|
|
1232
|
+
subject.execute
|
|
1233
|
+
end
|
|
1234
|
+
|
|
1235
|
+
context "with interrupt_on set to something that is not nil" do
|
|
1236
|
+
let(:audio_filename) { 'tt-monkeys' }
|
|
1237
|
+
let :command_options do
|
|
1238
|
+
{
|
|
1239
|
+
:render_document => {
|
|
1240
|
+
:value => RubySpeech::SSML.draw { string audio_filename },
|
|
1241
|
+
},
|
|
1242
|
+
:interrupt_on => :any
|
|
1243
|
+
}
|
|
1244
|
+
end
|
|
1245
|
+
it "should return an error when the output is interruptible and it is early media" do
|
|
1246
|
+
expect_answered false
|
|
1247
|
+
error = ProtocolError.new.setup 'option error', 'Interrupt digits are not allowed with early media.'
|
|
1248
|
+
subject.execute
|
|
1249
|
+
original_command.response(0.1).should be == error
|
|
1250
|
+
end
|
|
1251
|
+
end
|
|
1252
|
+
end
|
|
1253
|
+
end
|
|
1254
|
+
|
|
1255
|
+
context 'with multiple audio SSML nodes' do
|
|
1256
|
+
let(:audio_filename1) { 'foo' }
|
|
1257
|
+
let(:audio_filename2) { 'bar' }
|
|
1258
|
+
let(:audio_filename3) { 'baz' }
|
|
1259
|
+
let :ssml_doc do
|
|
1260
|
+
RubySpeech::SSML.draw do
|
|
1261
|
+
audio :src => audio_filename1 do
|
|
1262
|
+
string "Fallback 1"
|
|
1263
|
+
end
|
|
1264
|
+
audio :src => audio_filename2 do
|
|
1265
|
+
string "Fallback 2"
|
|
1266
|
+
end
|
|
1267
|
+
audio :src => audio_filename3 do
|
|
1268
|
+
string "Fallback 3"
|
|
1269
|
+
end
|
|
1270
|
+
end
|
|
1271
|
+
end
|
|
1272
|
+
|
|
1273
|
+
it 'should playback all audio files using Playback' do
|
|
1274
|
+
latch = CountDownLatch.new 2
|
|
1275
|
+
expect_playback audio_filename1
|
|
1276
|
+
expect_playback audio_filename2
|
|
1277
|
+
expect_playback audio_filename3
|
|
1278
|
+
expect_answered
|
|
1279
|
+
subject.execute
|
|
1280
|
+
latch.wait 2
|
|
1281
|
+
sleep 2
|
|
1282
|
+
end
|
|
1283
|
+
|
|
1284
|
+
it 'should send a complete event after the final file has finished playback' do
|
|
1285
|
+
expect_answered
|
|
1286
|
+
expect_playback audio_filename1
|
|
1287
|
+
expect_playback audio_filename2
|
|
1288
|
+
expect_playback audio_filename3
|
|
1289
|
+
latch = CountDownLatch.new 1
|
|
1290
|
+
original_command.should_receive(:add_event).once.with do |e|
|
|
1291
|
+
e.reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1292
|
+
latch.countdown!
|
|
1293
|
+
end
|
|
1294
|
+
subject.execute
|
|
1295
|
+
latch.wait(2).should be_true
|
|
1296
|
+
end
|
|
1297
|
+
|
|
1298
|
+
it 'should not execute further output after a stop command' do
|
|
1299
|
+
expect_answered
|
|
1300
|
+
mock_call.should_receive(:execute_agi_command).once.ordered.and_return do
|
|
1301
|
+
sleep 0.2
|
|
1302
|
+
end
|
|
1303
|
+
latch = CountDownLatch.new 1
|
|
1304
|
+
original_command.should_receive(:add_event).once.with do |e|
|
|
1305
|
+
e.reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1306
|
+
latch.countdown!
|
|
1307
|
+
end
|
|
1308
|
+
Celluloid::Future.new { subject.execute }
|
|
1309
|
+
sleep 0.1
|
|
1310
|
+
mock_call.should_receive(:redirect_back).ordered
|
|
1311
|
+
stop_command = Punchblock::Component::Stop.new
|
|
1312
|
+
stop_command.request!
|
|
1313
|
+
subject.execute_command stop_command
|
|
1314
|
+
latch.wait(2).should be_true
|
|
1315
|
+
end
|
|
1316
|
+
|
|
1317
|
+
context "when the PLAYBACKSTATUS variable is set to 'FAILED'" do
|
|
1318
|
+
let(:synthstatus) { 'SUCCESS' }
|
|
1319
|
+
before { mock_call.stub(:channel_var).with('PLAYBACKSTATUS').and_return 'SUCCESS', 'FAILED', 'SUCCESS' }
|
|
1320
|
+
before { mock_call.stub(:channel_var).with('SYNTHSTATUS').and_return synthstatus }
|
|
1321
|
+
|
|
1322
|
+
let :fallback_doc do
|
|
1323
|
+
RubySpeech::SSML.draw do
|
|
1324
|
+
string "Fallback 2"
|
|
1325
|
+
end
|
|
1326
|
+
end
|
|
1327
|
+
|
|
1328
|
+
it "should attempt to render the document via MRCP and then send a complete event" do
|
|
1329
|
+
expect_answered
|
|
1330
|
+
expect_playback audio_filename1
|
|
1331
|
+
expect_playback audio_filename2
|
|
1332
|
+
expect_mrcpsynth fallback_doc
|
|
1333
|
+
expect_playback audio_filename3
|
|
1334
|
+
subject.execute
|
|
1335
|
+
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1336
|
+
end
|
|
1337
|
+
|
|
1338
|
+
context "and the SYNTHSTATUS variable is set to 'ERROR'" do
|
|
1339
|
+
let(:synthstatus) { 'ERROR' }
|
|
1340
|
+
|
|
1341
|
+
it "should terminate playback and send an error complete event" do
|
|
1342
|
+
expect_answered
|
|
1343
|
+
expect_playback audio_filename1
|
|
1344
|
+
expect_playback audio_filename2
|
|
1345
|
+
expect_mrcpsynth fallback_doc
|
|
1346
|
+
subject.execute
|
|
1347
|
+
complete_reason = original_command.complete_event(0.1).reason
|
|
1348
|
+
complete_reason.should be_a Punchblock::Event::Complete::Error
|
|
1349
|
+
complete_reason.details.should == "Terminated due to UniMRCP error"
|
|
1350
|
+
end
|
|
1351
|
+
end
|
|
1352
|
+
end
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
context "with an SSML document containing top-level elements other than <audio/>" do
|
|
1356
|
+
let :ssml_doc do
|
|
1357
|
+
RubySpeech::SSML.draw do
|
|
1358
|
+
voice name: 'Paul' do
|
|
1359
|
+
string "Foo Bar"
|
|
1360
|
+
end
|
|
1361
|
+
end
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1364
|
+
before { mock_call.stub(:channel_var).with('SYNTHSTATUS').and_return 'SUCCESS' }
|
|
1365
|
+
|
|
1366
|
+
it "should attempt to render the document via MRCP and then send a complete event" do
|
|
1367
|
+
expect_answered
|
|
1368
|
+
expect_mrcpsynth ssml_doc
|
|
1369
|
+
subject.execute
|
|
1370
|
+
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1371
|
+
end
|
|
1372
|
+
end
|
|
1373
|
+
|
|
1374
|
+
context "with mixed TTS and audio tags" do
|
|
1375
|
+
let :ssml_doc do
|
|
1376
|
+
RubySpeech::SSML.draw do
|
|
1377
|
+
voice name: 'Paul' do
|
|
1378
|
+
string "Foo Bar"
|
|
1379
|
+
end
|
|
1380
|
+
audio src: 'tt-monkeys'
|
|
1381
|
+
voice name: 'Frank' do
|
|
1382
|
+
string "Doo Dah"
|
|
1383
|
+
end
|
|
1384
|
+
string 'tt-weasels'
|
|
1385
|
+
end
|
|
1386
|
+
end
|
|
1387
|
+
|
|
1388
|
+
let :first_doc do
|
|
1389
|
+
RubySpeech::SSML.draw do
|
|
1390
|
+
voice name: 'Paul' do
|
|
1391
|
+
string "Foo Bar"
|
|
1392
|
+
end
|
|
1393
|
+
end
|
|
1394
|
+
end
|
|
1395
|
+
|
|
1396
|
+
let :second_doc do
|
|
1397
|
+
RubySpeech::SSML.draw do
|
|
1398
|
+
voice name: 'Frank' do
|
|
1399
|
+
string "Doo Dah"
|
|
1400
|
+
end
|
|
1401
|
+
end
|
|
1402
|
+
end
|
|
1403
|
+
|
|
1404
|
+
before { mock_call.stub(:channel_var).with('SYNTHSTATUS').and_return 'SUCCESS' }
|
|
1405
|
+
|
|
1406
|
+
it "should attempt to render the document via MRCP and then send a complete event" do
|
|
1407
|
+
expect_answered
|
|
1408
|
+
expect_mrcpsynth first_doc
|
|
1409
|
+
expect_playback 'tt-monkeys'
|
|
1410
|
+
expect_mrcpsynth second_doc
|
|
1411
|
+
expect_playback 'tt-weasels'
|
|
1412
|
+
subject.execute
|
|
1413
|
+
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1414
|
+
end
|
|
1415
|
+
end
|
|
1416
|
+
|
|
1417
|
+
context 'with multiple documents' do
|
|
1418
|
+
let :second_ssml_doc do
|
|
1419
|
+
RubySpeech::SSML.draw do
|
|
1420
|
+
audio :src => 'two.wav' do
|
|
1421
|
+
string "Bazzz"
|
|
1422
|
+
end
|
|
1423
|
+
end
|
|
1424
|
+
end
|
|
1425
|
+
|
|
1426
|
+
let :third_ssml_doc do
|
|
1427
|
+
RubySpeech::SSML.draw do
|
|
1428
|
+
audio :src => 'three.wav' do
|
|
1429
|
+
string "Barrrr"
|
|
1430
|
+
end
|
|
1431
|
+
end
|
|
1432
|
+
end
|
|
1433
|
+
|
|
1434
|
+
let(:command_opts) { { render_documents: [{value: ssml_doc}, {value: second_ssml_doc}, {value: third_ssml_doc}] } }
|
|
1435
|
+
|
|
1436
|
+
it "should render each document in turn using a Playback per document" do
|
|
1437
|
+
expect_answered
|
|
1438
|
+
expect_playback
|
|
1439
|
+
expect_playback 'two'
|
|
1440
|
+
expect_playback 'three'
|
|
1441
|
+
subject.execute
|
|
1442
|
+
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1443
|
+
end
|
|
1444
|
+
|
|
1445
|
+
it 'should not execute further output after a stop command' do
|
|
1446
|
+
expect_answered
|
|
1447
|
+
mock_call.should_receive(:execute_agi_command).once.ordered.and_return do
|
|
1448
|
+
sleep 0.2
|
|
1449
|
+
end
|
|
1450
|
+
latch = CountDownLatch.new 1
|
|
1451
|
+
original_command.should_receive(:add_event).once.with do |e|
|
|
1452
|
+
e.reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1453
|
+
latch.countdown!
|
|
1454
|
+
end
|
|
1455
|
+
Celluloid::Future.new { subject.execute }
|
|
1456
|
+
sleep 0.1
|
|
1457
|
+
mock_call.should_receive(:redirect_back).ordered
|
|
1458
|
+
stop_command = Punchblock::Component::Stop.new
|
|
1459
|
+
stop_command.request!
|
|
1460
|
+
subject.execute_command stop_command
|
|
1461
|
+
latch.wait(2).should be_true
|
|
1462
|
+
end
|
|
1463
|
+
|
|
1464
|
+
context "when the PLAYBACKSTATUS variable is set to 'FAILED'" do
|
|
1465
|
+
let(:synthstatus) { 'SUCCESS' }
|
|
1466
|
+
before { mock_call.stub(:channel_var).with('PLAYBACKSTATUS').and_return 'SUCCESS', 'FAILED', 'SUCCESS' }
|
|
1467
|
+
before { mock_call.stub(:channel_var).with('SYNTHSTATUS').and_return synthstatus }
|
|
1468
|
+
|
|
1469
|
+
let :fallback_doc do
|
|
1470
|
+
RubySpeech::SSML.draw do
|
|
1471
|
+
string "Bazzz"
|
|
1472
|
+
end
|
|
1473
|
+
end
|
|
1474
|
+
|
|
1475
|
+
it "should attempt to render the document via MRCP and then send a complete event" do
|
|
1476
|
+
expect_answered
|
|
1477
|
+
expect_playback
|
|
1478
|
+
expect_playback 'two'
|
|
1479
|
+
expect_mrcpsynth fallback_doc
|
|
1480
|
+
expect_playback 'three'
|
|
1481
|
+
subject.execute
|
|
1482
|
+
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1483
|
+
end
|
|
1484
|
+
|
|
1485
|
+
context "and the SYNTHSTATUS variable is set to 'ERROR'" do
|
|
1486
|
+
let(:synthstatus) { 'ERROR' }
|
|
1487
|
+
|
|
1488
|
+
it "should terminate playback and send an error complete event" do
|
|
1489
|
+
expect_answered
|
|
1490
|
+
expect_playback
|
|
1491
|
+
expect_playback 'two'
|
|
1492
|
+
expect_mrcpsynth fallback_doc
|
|
1493
|
+
subject.execute
|
|
1494
|
+
complete_reason = original_command.complete_event(0.1).reason
|
|
1495
|
+
complete_reason.should be_a Punchblock::Event::Complete::Error
|
|
1496
|
+
complete_reason.details.should == "Terminated due to UniMRCP error"
|
|
1497
|
+
end
|
|
1498
|
+
end
|
|
1499
|
+
end
|
|
1500
|
+
end
|
|
1501
|
+
end
|
|
1502
|
+
|
|
1503
|
+
describe 'start-offset' do
|
|
1504
|
+
context 'unset' do
|
|
1505
|
+
let(:command_opts) { { :start_offset => nil } }
|
|
1506
|
+
it 'should not pass any options to Playback' do
|
|
1507
|
+
expect_answered
|
|
1508
|
+
expect_playback
|
|
1509
|
+
subject.execute
|
|
1510
|
+
end
|
|
1511
|
+
end
|
|
1512
|
+
|
|
1513
|
+
context 'set' do
|
|
1514
|
+
let(:command_opts) { { :start_offset => 10 } }
|
|
1515
|
+
it "should return an error and not execute any actions" do
|
|
1516
|
+
subject.execute
|
|
1517
|
+
error = ProtocolError.new.setup 'option error', 'A start_offset value is unsupported on Asterisk.'
|
|
1518
|
+
original_command.response(0.1).should be == error
|
|
1519
|
+
end
|
|
1520
|
+
end
|
|
1521
|
+
end
|
|
1522
|
+
|
|
1523
|
+
describe 'start-paused' do
|
|
1524
|
+
context 'false' do
|
|
1525
|
+
let(:command_opts) { { :start_paused => false } }
|
|
1526
|
+
it 'should not pass any options to Playback' do
|
|
1527
|
+
expect_answered
|
|
1528
|
+
expect_playback
|
|
1529
|
+
subject.execute
|
|
1530
|
+
end
|
|
1531
|
+
end
|
|
1532
|
+
|
|
1533
|
+
context 'true' do
|
|
1534
|
+
let(:command_opts) { { :start_paused => true } }
|
|
1535
|
+
it "should return an error and not execute any actions" do
|
|
1536
|
+
subject.execute
|
|
1537
|
+
error = ProtocolError.new.setup 'option error', 'A start_paused value is unsupported on Asterisk.'
|
|
1538
|
+
original_command.response(0.1).should be == error
|
|
1539
|
+
end
|
|
1540
|
+
end
|
|
1541
|
+
end
|
|
1542
|
+
|
|
1543
|
+
describe 'repeat-interval' do
|
|
1544
|
+
context 'unset' do
|
|
1545
|
+
let(:command_opts) { { :repeat_interval => nil } }
|
|
1546
|
+
it 'should not pass any options to Playback' do
|
|
1547
|
+
expect_answered
|
|
1548
|
+
expect_playback
|
|
1549
|
+
subject.execute
|
|
1550
|
+
end
|
|
1551
|
+
end
|
|
1552
|
+
|
|
1553
|
+
context 'set' do
|
|
1554
|
+
let(:command_opts) { { :repeat_interval => 10 } }
|
|
1555
|
+
it "should return an error and not execute any actions" do
|
|
1556
|
+
subject.execute
|
|
1557
|
+
error = ProtocolError.new.setup 'option error', 'A repeat_interval value is unsupported on Asterisk.'
|
|
1558
|
+
original_command.response(0.1).should be == error
|
|
1559
|
+
end
|
|
1560
|
+
end
|
|
1561
|
+
end
|
|
1562
|
+
|
|
1563
|
+
describe 'repeat-times' do
|
|
1564
|
+
context 'unset' do
|
|
1565
|
+
let(:command_opts) { { :repeat_times => nil } }
|
|
1566
|
+
it 'should not pass any options to Playback' do
|
|
1567
|
+
expect_answered
|
|
1568
|
+
expect_playback
|
|
1569
|
+
subject.execute
|
|
1570
|
+
end
|
|
1571
|
+
end
|
|
1572
|
+
|
|
1573
|
+
context 'set' do
|
|
1574
|
+
let(:command_opts) { { :repeat_times => 2 } }
|
|
1575
|
+
|
|
1576
|
+
it "should render the specified number of times" do
|
|
1577
|
+
expect_answered
|
|
1578
|
+
2.times { expect_playback }
|
|
1579
|
+
subject.execute
|
|
1580
|
+
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1581
|
+
end
|
|
1582
|
+
|
|
1583
|
+
context 'to 0' do
|
|
1584
|
+
let(:command_opts) { { :repeat_times => 0 } }
|
|
1585
|
+
|
|
1586
|
+
it "should render 10,000 the specified number of times" do
|
|
1587
|
+
expect_answered
|
|
1588
|
+
1000.times { expect_playback }
|
|
1589
|
+
subject.execute
|
|
1590
|
+
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1591
|
+
end
|
|
1592
|
+
end
|
|
1593
|
+
|
|
1594
|
+
it 'should not execute further output after a stop command' do
|
|
1595
|
+
expect_answered
|
|
1596
|
+
mock_call.should_receive(:execute_agi_command).once.ordered.and_return do
|
|
1597
|
+
sleep 0.2
|
|
1598
|
+
end
|
|
1599
|
+
latch = CountDownLatch.new 1
|
|
1600
|
+
original_command.should_receive(:add_event).once.with do |e|
|
|
1601
|
+
e.reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1602
|
+
latch.countdown!
|
|
1603
|
+
end
|
|
1604
|
+
Celluloid::Future.new { subject.execute }
|
|
1605
|
+
sleep 0.1
|
|
1606
|
+
mock_call.should_receive(:redirect_back).ordered
|
|
1607
|
+
stop_command = Punchblock::Component::Stop.new
|
|
1608
|
+
stop_command.request!
|
|
1609
|
+
subject.execute_command stop_command
|
|
1610
|
+
latch.wait(2).should be_true
|
|
1611
|
+
end
|
|
1612
|
+
end
|
|
1613
|
+
end
|
|
1614
|
+
|
|
1615
|
+
describe 'max-time' do
|
|
1616
|
+
context 'unset' do
|
|
1617
|
+
let(:command_opts) { { :max_time => nil } }
|
|
1618
|
+
it 'should not pass any options to Playback' do
|
|
1619
|
+
expect_answered
|
|
1620
|
+
expect_playback
|
|
1621
|
+
subject.execute
|
|
1622
|
+
end
|
|
1623
|
+
end
|
|
1624
|
+
|
|
1625
|
+
context 'set' do
|
|
1626
|
+
let(:command_opts) { { :max_time => 30 } }
|
|
1627
|
+
it "should return an error and not execute any actions" do
|
|
1628
|
+
subject.execute
|
|
1629
|
+
error = ProtocolError.new.setup 'option error', 'A max_time value is unsupported on Asterisk.'
|
|
1630
|
+
original_command.response(0.1).should be == error
|
|
1631
|
+
end
|
|
1632
|
+
end
|
|
1633
|
+
end
|
|
1634
|
+
|
|
1635
|
+
describe 'voice' do
|
|
1636
|
+
context 'unset' do
|
|
1637
|
+
let(:command_opts) { { :voice => nil } }
|
|
1638
|
+
it 'should not pass the v option to Playback' do
|
|
1639
|
+
expect_answered
|
|
1640
|
+
expect_playback
|
|
1641
|
+
subject.execute
|
|
1642
|
+
end
|
|
1643
|
+
end
|
|
1644
|
+
|
|
1645
|
+
context 'set' do
|
|
1646
|
+
let(:command_opts) { { :voice => 'alison' } }
|
|
1647
|
+
it "should return an error and not execute any actions" do
|
|
1648
|
+
subject.execute
|
|
1649
|
+
error = ProtocolError.new.setup 'option error', 'A voice value is unsupported on Asterisk.'
|
|
1650
|
+
original_command.response(0.1).should be == error
|
|
1651
|
+
end
|
|
1652
|
+
end
|
|
1653
|
+
end
|
|
1654
|
+
|
|
1655
|
+
describe 'interrupt_on' do
|
|
1656
|
+
def ami_event_for_dtmf(digit, position)
|
|
1657
|
+
RubyAMI::Event.new 'DTMF',
|
|
1658
|
+
'Digit' => digit.to_s,
|
|
1659
|
+
'Start' => position == :start ? 'Yes' : 'No',
|
|
1660
|
+
'End' => position == :end ? 'Yes' : 'No'
|
|
1661
|
+
end
|
|
1662
|
+
|
|
1663
|
+
def send_ami_events_for_dtmf(digit)
|
|
1664
|
+
mock_call.process_ami_event ami_event_for_dtmf(digit, :start)
|
|
1665
|
+
mock_call.process_ami_event ami_event_for_dtmf(digit, :end)
|
|
1666
|
+
end
|
|
1667
|
+
|
|
1668
|
+
let(:reason) { original_command.complete_event(5).reason }
|
|
1669
|
+
let(:channel) { "SIP/1234-00000000" }
|
|
1670
|
+
let :ami_event do
|
|
1671
|
+
RubyAMI::Event.new 'AsyncAGI',
|
|
1672
|
+
'SubEvent' => "Start",
|
|
1673
|
+
'Channel' => channel,
|
|
1674
|
+
'Env' => "agi_request%3A%20async%0Aagi_channel%3A%20SIP%2F1234-00000000%0Aagi_language%3A%20en%0Aagi_type%3A%20SIP%0Aagi_uniqueid%3A%201320835995.0%0Aagi_version%3A%201.8.4.1%0Aagi_callerid%3A%205678%0Aagi_calleridname%3A%20Jane%20Smith%0Aagi_callingpres%3A%200%0Aagi_callingani2%3A%200%0Aagi_callington%3A%200%0Aagi_callingtns%3A%200%0Aagi_dnid%3A%201000%0Aagi_rdnis%3A%20unknown%0Aagi_context%3A%20default%0Aagi_extension%3A%201000%0Aagi_priority%3A%201%0Aagi_enhanced%3A%200.0%0Aagi_accountcode%3A%20%0Aagi_threadid%3A%204366221312%0A%0A"
|
|
1675
|
+
end
|
|
1676
|
+
|
|
1677
|
+
context "set to nil" do
|
|
1678
|
+
let(:command_opts) { { :interrupt_on => nil } }
|
|
1679
|
+
it "does not redirect the call" do
|
|
1680
|
+
expect_answered
|
|
1681
|
+
expect_playback
|
|
1682
|
+
mock_call.should_receive(:redirect_back).never
|
|
1683
|
+
subject.execute
|
|
1684
|
+
original_command.response(0.1).should be_a Ref
|
|
1685
|
+
send_ami_events_for_dtmf 1
|
|
1686
|
+
end
|
|
1687
|
+
end
|
|
1688
|
+
|
|
1689
|
+
context "set to :any" do
|
|
1690
|
+
let(:command_opts) { { :interrupt_on => :any } }
|
|
1691
|
+
|
|
1692
|
+
before do
|
|
1693
|
+
expect_answered
|
|
1694
|
+
mock_call.should_receive(:execute_agi_command).once.with('EXEC Playback', audio_filename)
|
|
1695
|
+
subject.should_receive(:send_finish).and_return nil
|
|
1696
|
+
end
|
|
1697
|
+
|
|
1698
|
+
context "when a DTMF digit is received" do
|
|
1699
|
+
it "sends the correct complete event" do
|
|
1700
|
+
mock_call.should_receive :redirect_back
|
|
1701
|
+
subject.execute
|
|
1702
|
+
original_command.response(0.1).should be_a Ref
|
|
1703
|
+
original_command.should_not be_complete
|
|
1704
|
+
send_ami_events_for_dtmf 1
|
|
1705
|
+
mock_call.process_ami_event ami_event
|
|
1706
|
+
sleep 0.2
|
|
1707
|
+
original_command.should be_complete
|
|
1708
|
+
reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1709
|
+
end
|
|
1710
|
+
|
|
1711
|
+
it "redirects the call back to async AGI" do
|
|
1712
|
+
mock_call.should_receive(:redirect_back).once
|
|
1713
|
+
subject.execute
|
|
1714
|
+
original_command.response(0.1).should be_a Ref
|
|
1715
|
+
send_ami_events_for_dtmf 1
|
|
1716
|
+
end
|
|
1717
|
+
end
|
|
1718
|
+
end
|
|
1719
|
+
|
|
1720
|
+
context "set to :dtmf" do
|
|
1721
|
+
let(:command_opts) { { :interrupt_on => :dtmf } }
|
|
1722
|
+
|
|
1723
|
+
before do
|
|
1724
|
+
expect_answered
|
|
1725
|
+
mock_call.should_receive(:execute_agi_command).once.with('EXEC Playback', audio_filename)
|
|
1726
|
+
subject.should_receive(:send_finish).and_return nil
|
|
1727
|
+
end
|
|
1728
|
+
|
|
1729
|
+
context "when a DTMF digit is received" do
|
|
1730
|
+
it "sends the correct complete event" do
|
|
1731
|
+
mock_call.should_receive :redirect_back
|
|
1732
|
+
subject.execute
|
|
1733
|
+
original_command.response(0.1).should be_a Ref
|
|
1734
|
+
original_command.should_not be_complete
|
|
1735
|
+
send_ami_events_for_dtmf 1
|
|
1736
|
+
mock_call.process_ami_event ami_event
|
|
1737
|
+
sleep 0.2
|
|
1738
|
+
original_command.should be_complete
|
|
1739
|
+
reason.should be_a Punchblock::Component::Output::Complete::Finish
|
|
1740
|
+
end
|
|
1741
|
+
|
|
1742
|
+
it "redirects the call back to async AGI" do
|
|
1743
|
+
mock_call.should_receive(:redirect_back).once
|
|
1744
|
+
subject.execute
|
|
1745
|
+
original_command.response(0.1).should be_a Ref
|
|
1746
|
+
send_ami_events_for_dtmf 1
|
|
1747
|
+
end
|
|
1748
|
+
end
|
|
1749
|
+
end
|
|
1750
|
+
|
|
1751
|
+
context "set to :voice" do
|
|
1752
|
+
let(:command_opts) { { :interrupt_on => :voice } }
|
|
1753
|
+
it "should return an error and not execute any actions" do
|
|
1754
|
+
subject.execute
|
|
1755
|
+
error = ProtocolError.new.setup 'option error', 'An interrupt-on value of speech is unsupported.'
|
|
1756
|
+
original_command.response(0.1).should be == error
|
|
1757
|
+
end
|
|
1758
|
+
end
|
|
1759
|
+
end
|
|
1760
|
+
end
|
|
887
1761
|
end
|
|
888
1762
|
|
|
889
1763
|
describe "#execute_command" do
|
|
@@ -915,13 +1789,13 @@ module Punchblock
|
|
|
915
1789
|
end
|
|
916
1790
|
|
|
917
1791
|
it "sets the command response to true" do
|
|
918
|
-
mock_call.
|
|
1792
|
+
mock_call.should_receive(:redirect_back)
|
|
919
1793
|
subject.execute_command command
|
|
920
1794
|
command.response(0.1).should be == true
|
|
921
1795
|
end
|
|
922
1796
|
|
|
923
1797
|
it "sends the correct complete event" do
|
|
924
|
-
mock_call.
|
|
1798
|
+
mock_call.should_receive(:redirect_back)
|
|
925
1799
|
subject.execute_command command
|
|
926
1800
|
original_command.should_not be_complete
|
|
927
1801
|
mock_call.process_ami_event ami_event
|
|
@@ -930,7 +1804,7 @@ module Punchblock
|
|
|
930
1804
|
end
|
|
931
1805
|
|
|
932
1806
|
it "redirects the call by unjoining it" do
|
|
933
|
-
mock_call.
|
|
1807
|
+
mock_call.should_receive(:redirect_back)
|
|
934
1808
|
subject.execute_command command
|
|
935
1809
|
end
|
|
936
1810
|
end
|