adhearsion 2.1.3 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/CHANGELOG.md +87 -0
  2. data/Guardfile +1 -1
  3. data/Rakefile +1 -2
  4. data/adhearsion.gemspec +1 -1
  5. data/features/cli_create.feature +1 -0
  6. data/features/step_definitions/cli_steps.rb +0 -10
  7. data/lib/adhearsion.rb +11 -0
  8. data/lib/adhearsion/call.rb +27 -12
  9. data/lib/adhearsion/call_controller.rb +1 -1
  10. data/lib/adhearsion/call_controller/dial.rb +3 -1
  11. data/lib/adhearsion/call_controller/input.rb +4 -4
  12. data/lib/adhearsion/call_controller/menu_dsl.rb +1 -0
  13. data/lib/adhearsion/call_controller/menu_dsl/array_match_calculator.rb +26 -0
  14. data/lib/adhearsion/call_controller/menu_dsl/fixnum_match_calculator.rb +2 -14
  15. data/lib/adhearsion/call_controller/menu_dsl/string_match_calculator.rb +6 -6
  16. data/lib/adhearsion/call_controller/output.rb +20 -14
  17. data/lib/adhearsion/call_controller/output/abstract_player.rb +5 -5
  18. data/lib/adhearsion/call_controller/output/formatter.rb +68 -68
  19. data/lib/adhearsion/call_controller/record.rb +91 -28
  20. data/lib/adhearsion/call_controller/utility.rb +12 -5
  21. data/lib/adhearsion/calls.rb +1 -1
  22. data/lib/adhearsion/configuration.rb +4 -0
  23. data/lib/adhearsion/events.rb +2 -1
  24. data/lib/adhearsion/generators/app/app_generator.rb +2 -2
  25. data/lib/adhearsion/generators/app/templates/Gemfile.erb +4 -0
  26. data/lib/adhearsion/generators/app/templates/Rakefile +5 -0
  27. data/lib/adhearsion/generators/app/templates/lib/simon_game.rb +3 -0
  28. data/lib/adhearsion/generators/app/templates/rspec +2 -0
  29. data/lib/adhearsion/generators/app/templates/spec/call_controllers/simon_game_spec.rb +142 -0
  30. data/lib/adhearsion/generators/controller/templates/lib/controller.rb +1 -1
  31. data/lib/adhearsion/generators/controller/templates/spec/controller_spec.rb +2 -0
  32. data/lib/adhearsion/initializer.rb +3 -2
  33. data/lib/adhearsion/outbound_call.rb +5 -2
  34. data/lib/adhearsion/router/route.rb +13 -2
  35. data/lib/adhearsion/rspec.rb +1 -1
  36. data/lib/adhearsion/statistics.rb +138 -0
  37. data/lib/adhearsion/tasks/environment.rb +1 -1
  38. data/lib/adhearsion/version.rb +1 -1
  39. data/spec/adhearsion/call_controller/dial_spec.rb +26 -0
  40. data/spec/adhearsion/call_controller/input_spec.rb +13 -1
  41. data/spec/adhearsion/call_controller/menu_dsl/array_match_calculator_spec.rb +76 -0
  42. data/spec/adhearsion/call_controller/menu_dsl/fixnum_match_calculator_spec.rb +8 -6
  43. data/spec/adhearsion/call_controller/menu_dsl/range_match_calculator_spec.rb +4 -4
  44. data/spec/adhearsion/call_controller/menu_dsl/string_match_calculator_spec.rb +6 -6
  45. data/spec/adhearsion/call_controller/output/formatter_spec.rb +3 -5
  46. data/spec/adhearsion/call_controller/output_spec.rb +59 -25
  47. data/spec/adhearsion/call_controller/record_spec.rb +123 -2
  48. data/spec/adhearsion/call_controller/utility_spec.rb +31 -11
  49. data/spec/adhearsion/call_spec.rb +77 -36
  50. data/spec/adhearsion/calls_spec.rb +13 -0
  51. data/spec/adhearsion/initializer_spec.rb +7 -0
  52. data/spec/adhearsion/outbound_call_spec.rb +14 -0
  53. data/spec/adhearsion/punchblock_plugin_spec.rb +5 -2
  54. data/spec/adhearsion/router/openended_route_spec.rb +2 -1
  55. data/spec/adhearsion/router/route_spec.rb +9 -1
  56. data/spec/adhearsion/router/unaccepting_route_spec.rb +2 -1
  57. data/spec/adhearsion/statistics/dump_spec.rb +38 -0
  58. data/spec/adhearsion/statistics_spec.rb +61 -0
  59. data/spec/adhearsion_spec.rb +21 -1
  60. data/spec/spec_helper.rb +2 -0
  61. metadata +16 -7
@@ -3,4 +3,4 @@
3
3
  class <%= @controller_name.camelcase %> < Adhearsion::CallController
4
4
  def run
5
5
  end
6
- end
6
+ end
@@ -1,5 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require 'spec_helper'
4
+
3
5
  describe <%= @controller_name.camelcase %> do
4
6
 
5
7
  let(:mock_call) { mock 'Call', :to => '1112223333', :from => "2223334444" }
@@ -38,9 +38,10 @@ module Adhearsion
38
38
  end
39
39
 
40
40
  def start
41
+ Adhearsion.statistics
41
42
  resolve_pid_file_path
42
43
  load_lib_folder
43
- load_config
44
+ load_config_file
44
45
  initialize_log_paths
45
46
  daemonize! if should_daemonize?
46
47
  start_logging
@@ -165,7 +166,7 @@ module Adhearsion
165
166
  true
166
167
  end
167
168
 
168
- def load_config
169
+ def load_config_file
169
170
  require "#{Adhearsion.config.root}/config/adhearsion.rb"
170
171
  end
171
172
 
@@ -15,6 +15,7 @@ module Adhearsion
15
15
  # @param [String] to the URI of the party to dial
16
16
  # @param [Hash] opts modifier options
17
17
  # @option opts [Class] :controller the controller to execute when the call is answered
18
+ # @option opts [Hash] :controller_metadata key-value pairs of metadata to set on the controller
18
19
  # @yield Call controller routine in block form
19
20
  #
20
21
  # @return [OutboundCall] the ringing call
@@ -23,7 +24,7 @@ module Adhearsion
23
24
  #
24
25
  def originate(to, opts = {}, &controller_block)
25
26
  new.tap do |call|
26
- call.execute_controller_or_router_on_answer opts.delete(:controller), &controller_block
27
+ call.execute_controller_or_router_on_answer opts.delete(:controller), opts.delete(:controller_metadata), &controller_block
27
28
  call.dial to, opts
28
29
  end
29
30
  end
@@ -69,6 +70,7 @@ module Adhearsion
69
70
  write_and_await_response(Punchblock::Command::Dial.new(options), wait_timeout).tap do |dial_command|
70
71
  @dial_command = dial_command
71
72
  Adhearsion.active_calls << current_actor
73
+ Adhearsion::Events.trigger_immediately :call_dialed, current_actor
72
74
  end
73
75
  end
74
76
 
@@ -93,9 +95,10 @@ module Adhearsion
93
95
  end
94
96
  end
95
97
 
96
- def execute_controller_or_router_on_answer(controller, &controller_block)
98
+ def execute_controller_or_router_on_answer(controller, metadata = {}, &controller_block)
97
99
  if controller || controller_block
98
100
  route = Router::Route.new 'inbound', controller, &controller_block
101
+ route.controller_metadata = metadata
99
102
  on_answer { route.dispatch current_actor }
100
103
  else
101
104
  run_router_on_answer
@@ -24,10 +24,12 @@ module Adhearsion
24
24
  end
25
25
 
26
26
  def dispatch(call, callback = nil)
27
+ Adhearsion::Events.trigger_immediately :call_routed, call: call, route: self
28
+
27
29
  controller = if target.respond_to?(:call)
28
- CallController.new call, &target
30
+ CallController.new call, controller_metadata, &target
29
31
  else
30
- target.new call
32
+ target.new call, controller_metadata
31
33
  end
32
34
 
33
35
  call.accept if accepting?
@@ -45,6 +47,15 @@ module Adhearsion
45
47
  }
46
48
  end
47
49
 
50
+ def controller_metadata=(metadata)
51
+ @controller_metadata = metadata
52
+ end
53
+
54
+ def controller_metadata
55
+ return {} unless instance_variable_defined?(:@controller_metadata)
56
+ @controller_metadata
57
+ end
58
+
48
59
  def evented?
49
60
  false
50
61
  end
@@ -4,4 +4,4 @@ require 'adhearsion'
4
4
 
5
5
  initializer = Adhearsion::Initializer.new
6
6
  initializer.load_lib_folder
7
- initializer.load_config
7
+ initializer.load_config_file
@@ -0,0 +1,138 @@
1
+ # encoding: utf-8
2
+
3
+ module Adhearsion
4
+ class Statistics
5
+ include Celluloid
6
+
7
+ exclusive
8
+
9
+ def initialize
10
+ @calls_dialed = @calls_offered = @calls_routed = @calls_rejected = 0
11
+ @calls_by_route = Hash.new { |h,k| h[k] = 0 }
12
+ end
13
+
14
+ #
15
+ # Create a point-time dump of process statistics
16
+ #
17
+ # @return [Adhearsion::Statistics::Dump]
18
+ def dump
19
+ Dump.new timestamp: Time.now, call_counts: dump_call_counts, calls_by_route: dump_calls_by_route
20
+ end
21
+
22
+ # @private
23
+ def register_call_dialed
24
+ @calls_dialed += 1
25
+ end
26
+
27
+ # @private
28
+ def register_call_offered
29
+ @calls_offered += 1
30
+ end
31
+
32
+ # @private
33
+ def register_call_routed(data)
34
+ @calls_routed += 1
35
+ @calls_by_route[data[:route].name] += 1
36
+ end
37
+
38
+ # @private
39
+ def register_call_rejected
40
+ @calls_rejected += 1
41
+ end
42
+
43
+ # @private
44
+ def setup_event_handlers
45
+ stats = current_actor
46
+
47
+ Events.punchblock(Punchblock::Event::Offer) do
48
+ begin
49
+ stats.register_call_offered
50
+ rescue Celluloid::DeadActorError
51
+ end
52
+ throw :pass
53
+ end
54
+
55
+ Events.call_dialed do
56
+ begin
57
+ stats.register_call_dialed
58
+ rescue Celluloid::DeadActorError
59
+ end
60
+ throw :pass
61
+ end
62
+
63
+ Events.call_rejected do
64
+ begin
65
+ stats.register_call_rejected
66
+ rescue Celluloid::DeadActorError
67
+ end
68
+ throw :pass
69
+ end
70
+
71
+ Events.call_routed do |data|
72
+ begin
73
+ stats.register_call_routed data
74
+ rescue Celluloid::DeadActorError
75
+ end
76
+ throw :pass
77
+ end
78
+ end
79
+
80
+ def to_s
81
+ "#<#{self.class} dump=#{dump}>"
82
+ end
83
+ alias :inspect :to_s
84
+
85
+ private
86
+
87
+ def dump_call_counts
88
+ {dialed: @calls_dialed, offered: @calls_offered, routed: @calls_routed, rejected: @calls_rejected, active: Adhearsion.active_calls.count}
89
+ end
90
+
91
+ def dump_calls_by_route
92
+ @calls_by_route.tap do |index|
93
+ Adhearsion.router.routes.each do |route|
94
+ index[route.name]
95
+ end
96
+ end
97
+ end
98
+
99
+ #
100
+ # A point-time dump of process statistics
101
+ class Dump
102
+ include Comparable
103
+
104
+ #
105
+ # @attribute
106
+ # @return [Time] the time at which this dump was generated
107
+ attr_reader :timestamp
108
+
109
+ #
110
+ # @attribute
111
+ # @return [Hash] hash of call counts during the lifetime of the process.
112
+ attr_reader :call_counts
113
+
114
+ #
115
+ # @attribute
116
+ # @return [Hash] hash of call counts during the lifetime of the process, indexed by the route they matched.
117
+ attr_reader :calls_by_route
118
+
119
+ def initialize(opts = {})
120
+ @timestamp = opts[:timestamp]
121
+ @call_counts = opts[:call_counts]
122
+ @calls_by_route = opts[:calls_by_route]
123
+ end
124
+
125
+ def <=>(other)
126
+ timestamp <=> other.timestamp
127
+ end
128
+
129
+ def to_s
130
+ attrs = [:timestamp, :call_counts, :calls_by_route].map do |attr|
131
+ "#{attr}=#{send(attr).inspect}"
132
+ end
133
+ "#<#{self.class} #{attrs.join ', '}>"
134
+ end
135
+ alias :inspect :to_s
136
+ end
137
+ end
138
+ end
@@ -7,7 +7,7 @@ task :environment do
7
7
  Adhearsion.config # load default config vlaues
8
8
  initializer = Adhearsion::Initializer.new
9
9
  initializer.load_lib_folder
10
- initializer.load_config
10
+ initializer.load_config_file
11
11
  rescue Exception => ex
12
12
  STDERR.puts "\nError while loading application configuration file: #{ex}"
13
13
  end
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Adhearsion
4
- VERSION = '2.1.3'
4
+ VERSION = '2.2.0'
5
5
  end
@@ -436,6 +436,10 @@ module Adhearsion
436
436
  @@confirmation_latch = latch
437
437
 
438
438
  def run
439
+ # Copy metadata onto call variables so we can assert it later. Ugly hack
440
+ metadata.each_pair do |key, value|
441
+ call[key] = value
442
+ end
439
443
  @@confirmation_latch.countdown!
440
444
  call['confirm'] || hangup
441
445
  end
@@ -451,6 +455,28 @@ module Adhearsion
451
455
  flexmock(OutboundCall).should_receive(:new).and_return other_mock_call
452
456
  end
453
457
 
458
+ context "with confirmation controller metadata specified" do
459
+ let(:options) { {:confirm => confirmation_controller, :confirm_metadata => {:foo => 'bar'}} }
460
+
461
+ it "should set the metadata on the controller" do
462
+ flexmock(other_mock_call).should_receive(:hangup).twice.and_return do
463
+ other_mock_call << mock_end
464
+ end
465
+ other_mock_call['confirm'] = false
466
+
467
+ dial_in_thread
468
+
469
+ latch.wait(0.1).should be_false
470
+
471
+ other_mock_call << mock_answered
472
+
473
+ confirmation_latch.wait(1).should be_true
474
+ latch.wait(2).should be_true
475
+
476
+ other_mock_call[:foo].should == 'bar'
477
+ end
478
+ end
479
+
454
480
  context "when an outbound call is answered" do
455
481
  it "should execute the specified confirmation controller" do
456
482
  flexmock(other_mock_call).should_receive(:hangup).twice.and_return do
@@ -63,9 +63,11 @@ module Adhearsion
63
63
  )
64
64
  }
65
65
 
66
+ let(:utterance) { 'dtmf-5' }
67
+
66
68
  def expect_component_complete_event
67
69
  complete_event = Punchblock::Event::Complete.new
68
- flexmock(complete_event).should_receive(:reason => flexmock(:interpretation => 'dtmf-5', :name => :input))
70
+ flexmock(complete_event).should_receive(:reason => flexmock(:utterance => utterance, :name => :input))
69
71
  flexmock(Punchblock::Component::Input).new_instances do |input|
70
72
  input.should_receive(:complete?).and_return(false)
71
73
  input.should_receive(:complete_event).and_return(complete_event)
@@ -84,6 +86,16 @@ module Adhearsion
84
86
  subject.wait_for_digit(timeout).should be == '5'
85
87
  end
86
88
 
89
+ context "when the utterance does not have dtmf- prefix" do
90
+ let(:utterance) { '5' }
91
+
92
+ it "returns the correct pressed digit" do
93
+ expect_component_complete_event
94
+ subject.should_receive(:execute_component_and_await_completion).once.with(Punchblock::Component::Input).and_return input_component
95
+ subject.wait_for_digit(timeout).should be == '5'
96
+ end
97
+ end
98
+
87
99
  context "with a nil timeout" do
88
100
  let(:timeout) { nil }
89
101
  let(:timeout_ms) { nil }
@@ -0,0 +1,76 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Adhearsion
6
+ class CallController
7
+ module MenuDSL
8
+ describe ArrayMatchCalculator do
9
+
10
+ let(:match_payload) { :doesnt_matter }
11
+
12
+ it "matching arrays with fixnums" do
13
+ calculator = ArrayMatchCalculator.new [11,5,14,115], match_payload
14
+ match_case = calculator.match '11'
15
+ match_case.should be_exact_match
16
+ match_case.should be_potential_match
17
+ match_case.exact_matches.should be == [11]
18
+ match_case.potential_matches.should be == [115]
19
+ end
20
+
21
+ it "matching arrays with strings with digits and special digits" do
22
+ calculator = ArrayMatchCalculator.new %w[*57 4 *54 115 ###], match_payload
23
+ match_case = calculator.match '*5'
24
+ match_case.should_not be_exact_match
25
+ match_case.should be_potential_match
26
+ match_case.potential_matches.should be == %w[*57 *54]
27
+
28
+ match_case = calculator.match '*57'
29
+ match_case.should be_exact_match
30
+ match_case.should_not be_potential_match
31
+ match_case.exact_matches.should be == %w[*57]
32
+ end
33
+
34
+ it "matching an array with a combination of Fixnums and Strings" do
35
+ calculator = ArrayMatchCalculator.new ['11',5,'14',115], match_payload
36
+ match_case = calculator.match '11'
37
+ match_case.should be_exact_match
38
+ match_case.should be_potential_match
39
+ match_case.exact_matches.should be == ['11']
40
+ match_case.potential_matches.should be == [115]
41
+ end
42
+
43
+ it "matching empty array should never match" do
44
+ calculator = ArrayMatchCalculator.new [], match_payload
45
+ match_case = calculator.match '98'
46
+ match_case.should_not be_exact_match
47
+ match_case.should_not be_potential_match
48
+ match_case.exact_matches.should be == []
49
+ match_case.potential_matches.should be == []
50
+
51
+ match_case = calculator.match '*2'
52
+ match_case.should_not be_exact_match
53
+ match_case.should_not be_potential_match
54
+ match_case.exact_matches.should be == []
55
+ match_case.potential_matches.should be == []
56
+ end
57
+
58
+ it "matching array with nil should skip nil field" do
59
+ pattern = [1,2,nil,5,10]
60
+ calculator = ArrayMatchCalculator.new pattern, match_payload
61
+ match_case = calculator.match '1'
62
+ match_case.should be_exact_match
63
+ match_case.should be_potential_match
64
+ match_case.exact_matches.should be == [1]
65
+ match_case.potential_matches.should be == [10]
66
+
67
+ match_case = calculator.match '99'
68
+ match_case.should_not be_exact_match
69
+ match_case.should_not be_potential_match
70
+
71
+ pattern.should == [1,2,nil,5,10]
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -10,26 +10,28 @@ module Adhearsion
10
10
 
11
11
  it "a potential match scenario" do
12
12
  calculator = FixnumMatchCalculator.new(444, match_payload)
13
- match = calculator.match 4
14
- match.potential_match?.should be true
15
- match.exact_match?.should_not be true
13
+ match = calculator.match '4'
14
+ match.should be_potential_match
15
+ match.should_not be_exact_match
16
16
  match.potential_matches.should be == [444]
17
17
  end
18
18
 
19
19
  it "a multi-digit exact match scenario" do
20
20
  calculator = FixnumMatchCalculator.new(5555, match_payload)
21
- calculator.match(5555).exact_match?.should be true
21
+ match = calculator.match '5555'
22
+ match.should be_exact_match
22
23
  end
23
24
 
24
25
  it "a single-digit exact match scenario" do
25
26
  calculator = FixnumMatchCalculator.new(1, match_payload)
26
- calculator.match(1).exact_match?.should be true
27
+ match = calculator.match '1'
28
+ match.should be_exact_match
27
29
  end
28
30
 
29
31
  it "the context name given to the calculator should be passed on the CalculatedMatch" do
30
32
  match_payload = :icanhascheezburger
31
33
  calculator = FixnumMatchCalculator.new(1337, match_payload)
32
- calculator.match(1337).match_payload.should be match_payload
34
+ calculator.match('1337').match_payload.should be match_payload
33
35
  end
34
36
  end
35
37
  end