punchblock 0.8.4 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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