punchblock 0.8.4 → 0.9.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.
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - jruby-19mode # JRuby in 1.9 mode
6
+ - rbx-19mode # currently in active development, may or may not work for your project
7
+ - ruby-head
8
+ notifications:
9
+ irc: "irc.freenode.org#adhearsion"
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # develop
2
2
 
3
+ # v0.9.0 - 2012-01-30
4
+ * Bugfix: Remove the rest of the deprecated Tropo components (conference)
5
+ * Feature: Outbound dials on Asterisk now respect the dial timeout
6
+ * Bugfix: Registering stanza handlers on an XMPP connection now sets them in the correct order such that they do not override the internally defined handlers
7
+
3
8
  # v0.8.4 - 2012-01-19
4
9
  * Bugfix: End, Ringing & Answered events are allowed to have headers
5
10
  * Feature: Dial commands may have an optional timeout
data/Rakefile CHANGED
@@ -10,12 +10,6 @@ RSpec::Core::RakeTask.new(:spec) do |spec|
10
10
  spec.rspec_opts = '--color'
11
11
  end
12
12
 
13
- RSpec::Core::RakeTask.new(:rcov) do |spec|
14
- spec.pattern = 'spec/**/*_spec.rb'
15
- spec.rcov = true
16
- spec.rspec_opts = '--color'
17
- end
18
-
19
13
  task :default => :spec
20
14
  task :ci => ['ci:setup:rspec', :spec]
21
15
  task :hudson => :ci
data/lib/punchblock.rb CHANGED
@@ -44,7 +44,6 @@ module Punchblock
44
44
  TransportError = Class.new StandardError
45
45
 
46
46
  BASE_RAYO_NAMESPACE = 'urn:xmpp:rayo'
47
- BASE_TROPO_NAMESPACE = 'urn:xmpp:tropo'
48
47
  BASE_ASTERISK_NAMESPACE = 'urn:xmpp:rayo:asterisk'
49
48
  RAYO_VERSION = '1'
50
49
  RAYO_NAMESPACES = {:core => [BASE_RAYO_NAMESPACE, RAYO_VERSION].compact.join(':')}
@@ -54,11 +53,6 @@ module Punchblock
54
53
  RAYO_NAMESPACES[:"#{ns}_complete"] = [BASE_RAYO_NAMESPACE, ns.to_s, 'complete', RAYO_VERSION].compact.join(':')
55
54
  end
56
55
 
57
- [:conference].each do |ns|
58
- RAYO_NAMESPACES[ns] = [BASE_TROPO_NAMESPACE, ns.to_s, RAYO_VERSION].compact.join(':')
59
- RAYO_NAMESPACES[:"#{ns}_complete"] = [BASE_TROPO_NAMESPACE, ns.to_s, 'complete', RAYO_VERSION].compact.join(':')
60
- end
61
-
62
56
  [:agi, :ami].each do |ns|
63
57
  RAYO_NAMESPACES[ns] = [BASE_ASTERISK_NAMESPACE, ns.to_s, RAYO_VERSION].compact.join(':')
64
58
  RAYO_NAMESPACES[:"#{ns}_complete"] = [BASE_ASTERISK_NAMESPACE, ns.to_s, 'complete', RAYO_VERSION].compact.join(':')
@@ -11,6 +11,7 @@ module Punchblock
11
11
  # @param [Hash] options
12
12
  # @option options [String] :to destination to dial
13
13
  # @option options [String, Optional] :from what to set the Caller ID to
14
+ # @option options [Integer, Optional] :timeout in milliseconds
14
15
  # @option options [Array[Header], Hash, Optional] :headers SIP headers to attach to
15
16
  # the new call. Can be either a hash of key-value pairs, or an array of
16
17
  # Header objects.
@@ -38,6 +38,8 @@ module Punchblock
38
38
  Blather.logger = pb_logger
39
39
  Blather.default_log_level = :trace if Blather.respond_to? :default_log_level
40
40
 
41
+ register_handlers
42
+
41
43
  super()
42
44
  end
43
45
 
@@ -68,7 +70,6 @@ module Punchblock
68
70
  # Fire up the connection
69
71
  #
70
72
  def run
71
- register_handlers
72
73
  connect
73
74
  end
74
75
 
@@ -49,15 +49,17 @@ module Punchblock
49
49
 
50
50
  def dial(dial_command)
51
51
  @direction = :outbound
52
+ params = { :async => true,
53
+ :application => 'AGI',
54
+ :data => 'agi:async',
55
+ :channel => dial_command.to,
56
+ :callerid => dial_command.from,
57
+ :variable => "punchblock_call_id=#{id}"
58
+ }
59
+ params[:timeout] = dial_command.timeout unless dial_command.timeout.nil?
60
+
52
61
  originate_action = Punchblock::Component::Asterisk::AMI::Action.new :name => 'Originate',
53
- :params => {
54
- :async => true,
55
- :application => 'AGI',
56
- :data => 'agi:async',
57
- :channel => dial_command.to,
58
- :callerid => dial_command.from,
59
- :variable => "punchblock_call_id=#{id}"
60
- }
62
+ :params => params
61
63
  originate_action.request!
62
64
  translator.execute_global_command! originate_action
63
65
  dial_command.response = Ref.new :id => id
@@ -133,9 +135,9 @@ module Punchblock
133
135
  when Punchblock::Component::Asterisk::AGI::Command
134
136
  execute_component Component::Asterisk::AGICommand, command
135
137
  when Punchblock::Component::Output
136
- execute_component Component::Asterisk::Output, command
138
+ execute_component Component::Output, command
137
139
  when Punchblock::Component::Input
138
- execute_component Component::Asterisk::Input, command
140
+ execute_component Component::Input, command
139
141
  else
140
142
  command.response = ProtocolError.new 'command-not-acceptable', "Did not understand command for call #{id}", id
141
143
  end
@@ -5,6 +5,8 @@ module Punchblock
5
5
  extend ActiveSupport::Autoload
6
6
 
7
7
  autoload :Asterisk
8
+ autoload :Input
9
+ autoload :Output
8
10
 
9
11
  class Component
10
12
  include Celluloid
@@ -7,8 +7,6 @@ module Punchblock
7
7
 
8
8
  autoload :AGICommand
9
9
  autoload :AMIAction
10
- autoload :Input
11
- autoload :Output
12
10
  end
13
11
  end
14
12
  end
@@ -0,0 +1,114 @@
1
+ module Punchblock
2
+ module Translator
3
+ class Asterisk
4
+ module Component
5
+ class Input < Component
6
+
7
+ attr_reader :grammar, :buffer
8
+
9
+ def setup
10
+ @media_engine = call.translator.media_engine
11
+ @buffer = ""
12
+ end
13
+
14
+ def execute
15
+ initial_timeout = @component_node.initial_timeout || -1
16
+ @inter_digit_timeout = @component_node.inter_digit_timeout || -1
17
+
18
+ return with_error 'option error', 'A grammar document is required.' unless @component_node.grammar
19
+ return with_error 'option error', 'A mode value other than DTMF is unsupported on Asterisk.' unless @component_node.mode == :dtmf
20
+ return with_error 'option error', 'An initial timeout value that is negative (and not -1) is invalid.' unless initial_timeout >= -1
21
+ return with_error 'option error', 'An inter-digit timeout value that is negative (and not -1) is invalid.' unless @inter_digit_timeout >= -1
22
+
23
+ send_ref
24
+
25
+ case @media_engine
26
+ when :asterisk, nil
27
+ @grammar = @component_node.grammar.value.clone
28
+ grammar.inline!
29
+ grammar.tokenize!
30
+ grammar.normalize_whitespace
31
+
32
+ begin_initial_timer initial_timeout/1000 unless initial_timeout == -1
33
+
34
+ component = current_actor
35
+
36
+ @active = true
37
+
38
+ call.register_handler :ami, :name => 'DTMF' do |event|
39
+ component.process_dtmf! event['Digit'] if event['End'] == 'Yes'
40
+ end
41
+ end
42
+ end
43
+
44
+ def process_dtmf(digit)
45
+ return unless @active
46
+ pb_logger.trace "Processing incoming DTMF digit #{digit}"
47
+ buffer << digit
48
+ cancel_initial_timer
49
+ case (match = grammar.match buffer.dup)
50
+ when RubySpeech::GRXML::Match
51
+ pb_logger.trace "Found a match against buffer #{buffer}"
52
+ complete success_reason(match)
53
+ when RubySpeech::GRXML::NoMatch
54
+ pb_logger.trace "Buffer #{buffer} does not match grammar"
55
+ complete Punchblock::Component::Input::Complete::NoMatch.new
56
+ when RubySpeech::GRXML::PotentialMatch
57
+ pb_logger.trace "Buffer #{buffer} potentially matches grammar. Waiting..."
58
+ reset_inter_digit_timer
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def begin_initial_timer(timeout)
65
+ pb_logger.trace "Setting initial timer for #{timeout} seconds"
66
+ @initial_timer = after timeout do
67
+ pb_logger.trace "Initial timer expired."
68
+ complete Punchblock::Component::Input::Complete::NoInput.new
69
+ end
70
+ end
71
+
72
+ def cancel_initial_timer
73
+ return unless @initial_timer
74
+ @initial_timer.cancel
75
+ @initial_timer = nil
76
+ end
77
+
78
+ def reset_inter_digit_timer
79
+ return if @inter_digit_timeout == -1
80
+ @inter_digit_timer ||= begin
81
+ pb_logger.trace "Setting inter-digit timer for #{@inter_digit_timeout/1000} seconds"
82
+ after @inter_digit_timeout/1000 do
83
+ pb_logger.trace "Inter digit-timer expired."
84
+ complete Punchblock::Component::Input::Complete::NoMatch.new
85
+ end
86
+ end
87
+ pb_logger.trace "Resetting inter-digit timer"
88
+ @inter_digit_timer.reset
89
+ end
90
+
91
+ def cancel_inter_digit_timer
92
+ return unless @inter_digit_timer
93
+ @inter_digit_timer.cancel
94
+ @inter_digit_timer = nil
95
+ end
96
+
97
+ def success_reason(match)
98
+ Punchblock::Component::Input::Complete::Success.new :mode => match.mode,
99
+ :confidence => match.confidence,
100
+ :utterance => match.utterance,
101
+ :interpretation => match.interpretation
102
+ end
103
+
104
+ def complete(reason)
105
+ @active = false
106
+ cancel_initial_timer
107
+ cancel_inter_digit_timer
108
+ send_complete_event reason
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,92 @@
1
+ require 'active_support/core_ext/string/filters'
2
+
3
+ module Punchblock
4
+ module Translator
5
+ class Asterisk
6
+ module Component
7
+ class Output < Component
8
+
9
+ def setup
10
+ @media_engine = @call.translator.media_engine
11
+ end
12
+
13
+ def execute
14
+ return with_error 'option error', 'An SSML document is required.' unless @component_node.ssml
15
+
16
+ return with_error 'option error', 'An interrupt-on value of speech is unsupported.' if @component_node.interrupt_on == :speech
17
+
18
+ [:start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt|
19
+ return with_error 'option error', "A #{opt} value is unsupported on Asterisk." if @component_node.send opt
20
+ end
21
+
22
+ case @media_engine
23
+ when :asterisk, nil
24
+ return with_error 'option error', "A voice value is unsupported on Asterisk." if @component_node.voice
25
+
26
+ @execution_elements = @component_node.ssml.children.map do |node|
27
+ case node
28
+ when RubySpeech::SSML::Audio
29
+ lambda { current_actor.play_audio! node.src }
30
+ end
31
+ end.compact
32
+
33
+ @pending_actions = @execution_elements.count
34
+
35
+ send_ref
36
+
37
+ @interrupt_digits = '0123456789*#' if [:any, :dtmf].include? @component_node.interrupt_on
38
+
39
+ @execution_elements.each do |element|
40
+ element.call
41
+ wait :continue
42
+ process_playback_completion
43
+ end
44
+ when :unimrcp
45
+ doc = @component_node.ssml.to_s.squish.gsub(/["\\]/) { |m| "\\#{m}" }
46
+ send_ref
47
+ @call.send_agi_action! 'EXEC MRCPSynth', doc, mrcpsynth_options do |complete_event|
48
+ pb_logger.debug "MRCPSynth completed with #{complete_event}."
49
+ send_complete_event success_reason
50
+ end
51
+ end
52
+ end
53
+
54
+ def process_playback_completion
55
+ @pending_actions -= 1
56
+ pb_logger.debug "Received action completion. Now waiting on #{@pending_actions} actions."
57
+ if @pending_actions < 1
58
+ pb_logger.debug "Sending complete event"
59
+ send_complete_event success_reason
60
+ end
61
+ end
62
+
63
+ def continue(event = nil)
64
+ signal :continue, event
65
+ end
66
+
67
+ def play_audio(path)
68
+ pb_logger.debug "Playing an audio file (#{path}) via STREAM FILE"
69
+ op = current_actor
70
+ @call.send_agi_action! 'STREAM FILE', path, @interrupt_digits do |complete_event|
71
+ pb_logger.debug "STREAM FILE completed with #{complete_event}. Signalling to continue execution."
72
+ op.continue! complete_event
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def mrcpsynth_options
79
+ [].tap do |opts|
80
+ opts << 'i=any' if [:any, :dtmf].include? @component_node.interrupt_on
81
+ opts << "v=#{@component_node.voice}" if @component_node.voice
82
+ end.join '&'
83
+ end
84
+
85
+ def success_reason
86
+ Punchblock::Component::Output::Complete::Success.new
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -1,3 +1,3 @@
1
1
  module Punchblock
2
- VERSION = "0.8.4"
2
+ VERSION = "0.9.0"
3
3
  end
data/punchblock.gemspec CHANGED
@@ -36,7 +36,6 @@ Gem::Specification.new do |s|
36
36
  s.add_development_dependency %q<rspec>, ["~> 2.7.0"]
37
37
  s.add_development_dependency %q<ci_reporter>, [">= 1.6.3"]
38
38
  s.add_development_dependency %q<yard>, ["~> 0.6.0"]
39
- s.add_development_dependency %q<rcov>, [">= 0"]
40
39
  s.add_development_dependency %q<rake>, [">= 0"]
41
40
  s.add_development_dependency %q<mocha>, [">= 0"]
42
41
  s.add_development_dependency %q<i18n>, [">= 0"]
@@ -67,6 +67,7 @@ module Punchblock
67
67
  describe '#shutdown' do
68
68
  it 'should terminate the actor' do
69
69
  subject.shutdown
70
+ sleep 0.5
70
71
  subject.should_not be_alive
71
72
  end
72
73
  end
@@ -99,8 +100,10 @@ module Punchblock
99
100
  end
100
101
 
101
102
  describe '#dial' do
103
+ let(:dial_command_options) { {} }
104
+
102
105
  let :dial_command do
103
- Punchblock::Command::Dial.new :to => 'SIP/1234', :from => 'sip:foo@bar.com'
106
+ Punchblock::Command::Dial.new({:to => 'SIP/1234', :from => 'sip:foo@bar.com'}.merge(dial_command_options))
104
107
  end
105
108
 
106
109
  before { dial_command.request! }
@@ -120,6 +123,28 @@ module Punchblock
120
123
  subject.dial dial_command
121
124
  end
122
125
 
126
+ context 'with a timeout specified' do
127
+ let :dial_command_options do
128
+ { :timeout => 10000 }
129
+ end
130
+
131
+ it 'includes the timeout in the Originate AMI action' do
132
+ expected_action = Punchblock::Component::Asterisk::AMI::Action.new :name => 'Originate',
133
+ :params => {
134
+ :async => true,
135
+ :application => 'AGI',
136
+ :data => 'agi:async',
137
+ :channel => 'SIP/1234',
138
+ :callerid => 'sip:foo@bar.com',
139
+ :variable => "punchblock_call_id=#{subject.id}",
140
+ :timeout => 10000
141
+ }
142
+
143
+ translator.expects(:execute_global_command!).once.with expected_action
144
+ subject.dial dial_command
145
+ end
146
+ end
147
+
123
148
  it 'sends the call ID as a response to the Dial' do
124
149
  subject.dial dial_command
125
150
  dial_command.response
@@ -162,6 +187,7 @@ module Punchblock
162
187
  it "should cause the actor to be terminated" do
163
188
  translator.expects(:handle_pb_event!).once
164
189
  subject.process_ami_event ami_event
190
+ sleep 0.5
165
191
  subject.should_not be_alive
166
192
  end
167
193
 
@@ -442,7 +468,7 @@ module Punchblock
442
468
  let(:mock_action) { mock 'Component::Asterisk::Output', :id => 'foo' }
443
469
 
444
470
  it 'should create an AGI command component actor and execute it asynchronously' do
445
- Component::Asterisk::Output.expects(:new).once.with(command, subject).returns mock_action
471
+ Component::Output.expects(:new).once.with(command, subject).returns mock_action
446
472
  mock_action.expects(:internal=).never
447
473
  mock_action.expects(:execute!).once
448
474
  subject.execute_command command
@@ -457,7 +483,7 @@ module Punchblock
457
483
  let(:mock_action) { mock 'Component::Asterisk::Input', :id => 'foo' }
458
484
 
459
485
  it 'should create an AGI command component actor and execute it asynchronously' do
460
- Component::Asterisk::Input.expects(:new).once.with(command, subject).returns mock_action
486
+ Component::Input.expects(:new).once.with(command, subject).returns mock_action
461
487
  mock_action.expects(:internal=).never
462
488
  mock_action.expects(:execute!).once
463
489
  subject.execute_command command
@@ -0,0 +1,282 @@
1
+ require 'spec_helper'
2
+
3
+ module Punchblock
4
+ module Translator
5
+ class Asterisk
6
+ module Component
7
+ describe Input do
8
+ let(:connection) do
9
+ mock_connection_with_event_handler do |event|
10
+ command.add_event event
11
+ end
12
+ end
13
+ let(:media_engine) { nil }
14
+ let(:translator) { Punchblock::Translator::Asterisk.new mock('AMI'), connection, media_engine }
15
+ let(:call) { Punchblock::Translator::Asterisk::Call.new 'foo', translator }
16
+ let(:command_options) { {} }
17
+
18
+ let :command do
19
+ Punchblock::Component::Input.new command_options
20
+ end
21
+
22
+ let :grammar do
23
+ RubySpeech::GRXML.draw :mode => 'dtmf', :root => 'pin' do
24
+ rule id: 'digit' do
25
+ one_of do
26
+ 0.upto(9) { |d| item { d.to_s } }
27
+ end
28
+ end
29
+
30
+ rule id: 'pin', scope: 'public' do
31
+ item repeat: '2' do
32
+ ruleref uri: '#digit'
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ subject { Input.new command, call }
39
+
40
+ describe '#execute' do
41
+ before { command.request! }
42
+
43
+ context 'with a media engine of :unimrcp' do
44
+ pending
45
+ let(:media_engine) { :unimrcp }
46
+ end
47
+
48
+ context 'with a media engine of :asterisk' do
49
+ let(:media_engine) { :asterisk }
50
+
51
+ let(:command_opts) { {} }
52
+
53
+ let :command_options do
54
+ { :mode => :dtmf, :grammar => { :value => grammar } }.merge(command_opts)
55
+ end
56
+
57
+ def ami_event_for_dtmf(digit, position)
58
+ RubyAMI::Event.new('DTMF').tap do |e|
59
+ e['Digit'] = digit.to_s
60
+ e['Start'] = position == :start ? 'Yes' : 'No'
61
+ e['End'] = position == :end ? 'Yes' : 'No'
62
+ end
63
+ end
64
+
65
+ def send_ami_events_for_dtmf(digit)
66
+ call.process_ami_event ami_event_for_dtmf(digit, :start)
67
+ call.process_ami_event ami_event_for_dtmf(digit, :end)
68
+ end
69
+
70
+ let(:reason) { command.complete_event(5).reason }
71
+
72
+ describe "receiving DTMF events" do
73
+ before do
74
+ subject.execute
75
+ expected_event
76
+ end
77
+
78
+ context "when a match is found" do
79
+ before do
80
+ send_ami_events_for_dtmf 1
81
+ send_ami_events_for_dtmf 2
82
+ end
83
+
84
+ let :expected_event do
85
+ Punchblock::Component::Input::Complete::Success.new :mode => :dtmf,
86
+ :confidence => 1,
87
+ :utterance => '12',
88
+ :interpretation => 'dtmf-1 dtmf-2',
89
+ :component_id => subject.id,
90
+ :call_id => call.id
91
+ end
92
+
93
+ it "should send a success complete event with the relevant data" do
94
+ reason.should == expected_event
95
+ end
96
+ end
97
+
98
+ context "when the match is invalid" do
99
+ before do
100
+ send_ami_events_for_dtmf 1
101
+ send_ami_events_for_dtmf '#'
102
+ end
103
+
104
+ let :expected_event do
105
+ Punchblock::Component::Input::Complete::NoMatch.new :component_id => subject.id,
106
+ :call_id => call.id
107
+ end
108
+
109
+ it "should send a nomatch complete event" do
110
+ reason.should == expected_event
111
+ end
112
+ end
113
+ end
114
+
115
+ describe 'grammar' do
116
+ context 'unset' do
117
+ let(:command_opts) { { :grammar => nil } }
118
+ it "should return an error and not execute any actions" do
119
+ subject.execute
120
+ error = ProtocolError.new 'option error', 'A grammar document is required.'
121
+ command.response(0.1).should == error
122
+ end
123
+ end
124
+ end
125
+
126
+ describe 'mode' do
127
+ context 'unset' do
128
+ let(:command_opts) { { :mode => nil } }
129
+ it "should return an error and not execute any actions" do
130
+ subject.execute
131
+ error = ProtocolError.new 'option error', 'A mode value other than DTMF is unsupported on Asterisk.'
132
+ command.response(0.1).should == error
133
+ end
134
+ end
135
+
136
+ context 'any' do
137
+ let(:command_opts) { { :mode => :any } }
138
+ it "should return an error and not execute any actions" do
139
+ subject.execute
140
+ error = ProtocolError.new 'option error', 'A mode value other than DTMF is unsupported on Asterisk.'
141
+ command.response(0.1).should == error
142
+ end
143
+ end
144
+
145
+ context 'speech' do
146
+ let(:command_opts) { { :mode => :speech } }
147
+ it "should return an error and not execute any actions" do
148
+ subject.execute
149
+ error = ProtocolError.new 'option error', 'A mode value other than DTMF is unsupported on Asterisk.'
150
+ command.response(0.1).should == error
151
+ end
152
+ end
153
+ end
154
+
155
+ describe 'terminator' do
156
+ pending
157
+ end
158
+
159
+ describe 'recognizer' do
160
+ pending
161
+ end
162
+
163
+ describe 'initial-timeout' do
164
+ context 'a positive number' do
165
+ let(:command_opts) { { :initial_timeout => 1000 } }
166
+
167
+ it "should not cause a NoInput if first input is received in time" do
168
+ subject.execute
169
+ send_ami_events_for_dtmf 1
170
+ sleep 1.5
171
+ send_ami_events_for_dtmf 2
172
+ reason.should be_a Punchblock::Component::Input::Complete::Success
173
+ end
174
+
175
+ it "should cause a NoInput complete event to be sent after the timeout" do
176
+ subject.execute
177
+ sleep 1.5
178
+ send_ami_events_for_dtmf 1
179
+ send_ami_events_for_dtmf 2
180
+ reason.should be_a Punchblock::Component::Input::Complete::NoInput
181
+ end
182
+ end
183
+
184
+ context '-1' do
185
+ let(:command_opts) { { :initial_timeout => -1 } }
186
+
187
+ it "should not start a timer" do
188
+ subject.wrapped_object.expects(:begin_initial_timer).never
189
+ subject.execute
190
+ end
191
+ end
192
+
193
+ context 'unset' do
194
+ let(:command_opts) { { :initial_timeout => nil } }
195
+
196
+ it "should not start a timer" do
197
+ subject.wrapped_object.expects(:begin_initial_timer).never
198
+ subject.execute
199
+ end
200
+ end
201
+
202
+ context 'a negative number other than -1' do
203
+ let(:command_opts) { { :initial_timeout => -1000 } }
204
+
205
+ it "should return an error and not execute any actions" do
206
+ subject.execute
207
+ error = ProtocolError.new 'option error', 'An initial timeout value that is negative (and not -1) is invalid.'
208
+ command.response(0.1).should == error
209
+ end
210
+ end
211
+ end
212
+
213
+ describe 'inter-digit-timeout' do
214
+ context 'a positive number' do
215
+ let(:command_opts) { { :inter_digit_timeout => 1000 } }
216
+
217
+ it "should not prevent a Match if input is received in time" do
218
+ subject.execute
219
+ sleep 1.5
220
+ send_ami_events_for_dtmf 1
221
+ sleep 0.5
222
+ send_ami_events_for_dtmf 2
223
+ reason.should be_a Punchblock::Component::Input::Complete::Success
224
+ end
225
+
226
+ it "should cause a NoMatch complete event to be sent after the timeout" do
227
+ subject.execute
228
+ sleep 1.5
229
+ send_ami_events_for_dtmf 1
230
+ sleep 1.5
231
+ send_ami_events_for_dtmf 2
232
+ reason.should be_a Punchblock::Component::Input::Complete::NoMatch
233
+ end
234
+ end
235
+
236
+ context '-1' do
237
+ let(:command_opts) { { :inter_digit_timeout => -1 } }
238
+
239
+ it "should not start a timer" do
240
+ subject.wrapped_object.expects(:begin_inter_digit_timer).never
241
+ subject.execute
242
+ end
243
+ end
244
+
245
+ context 'unset' do
246
+ let(:command_opts) { { :inter_digit_timeout => nil } }
247
+
248
+ it "should not start a timer" do
249
+ subject.wrapped_object.expects(:begin_inter_digit_timer).never
250
+ subject.execute
251
+ end
252
+ end
253
+
254
+ context 'a negative number other than -1' do
255
+ let(:command_opts) { { :inter_digit_timeout => -1000 } }
256
+
257
+ it "should return an error and not execute any actions" do
258
+ subject.execute
259
+ error = ProtocolError.new 'option error', 'An inter-digit timeout value that is negative (and not -1) is invalid.'
260
+ command.response(0.1).should == error
261
+ end
262
+ end
263
+ end
264
+
265
+ describe 'sensitivity' do
266
+ pending
267
+ end
268
+
269
+ describe 'min-confidence' do
270
+ pending
271
+ end
272
+
273
+ describe 'max-silence' do
274
+ pending
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end