#
# Copyright (C) 2006 SIPfoundry Inc.
# Licensed by SIPfoundry under the LGPL license.
#
# Copyright (C) 2006 Pingtel Corp.
# Licensed to SIPfoundry under a Contributor Agreement.
#
##############################################################################
$SOURCE_DIR = File.dirname(__FILE__) # directory in which this file is located
# system requirements
require 'parsedate'
require File.join($SOURCE_DIR, '..', 'test_helper')
# application requirements
require File.join($SOURCE_DIR, '..', '..', 'call_resolver')
# :TODO: Make it easy to run all the unit tests, possibly via Rakefile, for build loop.
class CallResolverTest < Test::Unit::TestCase
fixtures :call_state_events, :cdrs
TEST_AOR = 'aor'
TEST_CONTACT = 'contact'
TEST_CALL_ID = 'call ID'
TEST_FROM_TAG = 'f'
TEST_TO_TAG = 't'
SECONDS_IN_A_DAY = 24 * 60 * 60
CALL_ID1 = 'call_id1'
CALL_ID2 = 'call_id2'
CALL_ID3 = 'call_id3'
TEST_DB1 = 'SIPXCSE_TEST1'
TEST_DB2 = 'SIPXCSE_TEST2'
LOCALHOST = 'localhost'
public
def setup
super
# Create the CallResolver, giving it the location of the test config file.
@resolver = CallResolver.new(File.join($SOURCE_DIR, 'data/callresolver-config'))
end
def test_connect_to_cdr_database
# Ensure that we are disconnected first
if ActiveRecord::Base.connected?
ActiveRecord::Base.remove_connection
end
assert(!ActiveRecord::Base.connected?, 'Must be disconnected from database')
# Connect to the database and verify that we are connected.
# Rails quirk: we can't assert "connected?" because the connection has been
# established but is not active yet, so "connected?" is not true. But we
# can check that the connection exists.
@resolver.send(:connect_to_cdr_database)
assert(ActiveRecord::Base.connection, 'Must be connected to database')
end
def test_load_distrib_events_in_time_window
# Create the two test databases if they don't already exist.
# Note: using existing databases to save time may fail if the schema changes
# in, which case you should manually delete the databases and run the test
# again.
DatabaseUtils.create_cdr_database(TEST_DB1)
DatabaseUtils.create_cdr_database(TEST_DB2)
cse_url1 = DatabaseUrl.new(TEST_DB1)
cse_url2 = DatabaseUrl.new(TEST_DB2)
# Pick an arbitrary base event time for all events
start_time = Time.now
# Test loading the events. Restore the CallStateEvent DB connection after
# the test if there was one. Restore the CSE database URLs.
conn = CallStateEvent.remove_connection
cse_database_urls = @resolver.send(:cse_database_urls)
begin
# Put events for the first call and part of the second call into the first
# test DB.
# Note: only the time values on the call that is split across two DBs
# matter, because only that call will be time-sorted when the events get
# merged.
CallStateEvent.establish_connection(DatabaseUrl.new(TEST_DB1).to_hash)
CallStateEvent.destroy_all
e1_1 = create_test_cse(CALL_ID1, start_time)
e1_2 = create_test_cse(CALL_ID1, start_time)
e2_1 = create_test_cse(CALL_ID2, start_time + 1)
e2_3 = create_test_cse(CALL_ID2, start_time + 3)
# Put events for the rest of the second call and the third call into the
# second test DB.
CallStateEvent.establish_connection(DatabaseUrl.new(TEST_DB2).to_hash)
CallStateEvent.destroy_all
e2_2 = create_test_cse(CALL_ID2, start_time + 2)
e2_4 = create_test_cse(CALL_ID2, start_time + 4)
e3_1 = create_test_cse(CALL_ID3, start_time)
call1 = [e1_1, e1_2]
call2_part1 = [e2_1, e2_3]
call2_part2 = [e2_2, e2_4]
call2 = (call2_part1 + call2_part2).sort!{|x, y| x.event_time <=> y.event_time}
call3 = [e3_1]
all_calls = [[call1, call2_part1], # call arrays for first DB
[call2_part2, call3]] # call arrays for second DB
end_time = start_time + 10
@resolver.config.send(:cse_database_urls=, [cse_url1, cse_url2])
call_map = @resolver.send(:load_distrib_events_in_time_window, start_time, end_time)
expected_call_map_entries = [call1, call2, call3]
check_call_map(all_calls, call_map, expected_call_map_entries)
ensure
# Restore the original connection, or at least clear the test connection
if conn
CallStateEvent.establish_connection(conn)
else
CallStateEvent.remove_connection
end
# Restore original DB URLs
@resolver.config.send(:cse_database_urls=, cse_database_urls)
end
end
# Create a test CSE. Fill in dummy values for fields we don't care about but
# have to be filled in because of not-null DB constraints
def create_test_cse(call_id, event_time)
CallStateEvent.create(:observer => 'observer',
:event_seq => 0,
:event_time => event_time,
:event_type => CallStateEvent::CALL_REQUEST_TYPE,
:cseq => 0,
:call_id => call_id,
:from_url => 'from_url',
:to_url => 'to_url')
end
def test_merge_events_for_call
e1 = CallStateEvent.new(:call_id => CALL_ID1)
e2 = CallStateEvent.new(:call_id => CALL_ID1)
time = Time.now;
e3 = CallStateEvent.new(:call_id => CALL_ID2, :event_time => time)
e4 = CallStateEvent.new(:call_id => CALL_ID2, :event_time => time + 1)
e5 = CallStateEvent.new(:call_id => CALL_ID2, :event_time => time + 2)
e6 = CallStateEvent.new(:call_id => CALL_ID2, :event_time => time + 3)
e7 = CallStateEvent.new(:call_id => CALL_ID3)
call1 = [e1, e2]
call2_part1 = [e3, e5]
call2_part2 = [e4, e6]
call2 = [e3, e4, e5, e6]
call3 = [e7]
all_calls = [[call1, call2_part1], # call arrays for first DB
[call2_part2, call3]] # call arrays for second DB
call_map = {}
@resolver.send(:merge_events_for_call, all_calls, call_map)
expected_call_map_entries = [call1, call2, call3]
check_call_map(all_calls, call_map, expected_call_map_entries)
end
def check_call_map(all_calls, call_map, expected_call_map_entries)
assert_equal(expected_call_map_entries.size, call_map.size)
[CALL_ID1, CALL_ID2, CALL_ID3].each_with_index do |call_id, i|
assert_equal(expected_call_map_entries[i], call_map[call_id])
end
end
def test_split_events_by_call
call1 = 'call1'
call2 = 'call2'
call3 = 'call3'
e1 = CallStateEvent.new(:call_id => call1)
e2 = CallStateEvent.new(:call_id => call2)
e3 = CallStateEvent.new(:call_id => call2)
e4 = CallStateEvent.new(:call_id => call3)
e5 = CallStateEvent.new(:call_id => call3)
e6 = CallStateEvent.new(:call_id => call3)
events = [e1, e2, e3, e4, e5, e6]
spl = @resolver.send(:split_events_by_call, events)
assert_equal([e1], spl[0])
assert_equal([e2, e3], spl[1])
assert_equal([e4, e5, e6], spl[2])
# check empty events array as input
assert_equal([], @resolver.send(:split_events_by_call, []))
end
def test_load_call_ids
start_time = Time.parse('1990-05-17T19:30:00.000Z')
end_time = Time.parse('1990-05-17T19:45:00.000Z')
# Load call IDs. Do a low level message send to bypass access control on
# this private method.
call_ids = @resolver.send(:load_call_ids, start_time, end_time)
# call IDs can come back in any order, so sort them to guarantee order
call_ids.sort!
# verify results
assert_equal(3, call_ids.length, 'Wrong number of call IDs')
assert_equal('testSimpleSuccess',
call_ids[0],
'Wrong first call ID')
assert_equal('testSimpleSuccessBogusCallInTimeWindow',
call_ids[1],
'Wrong second call ID')
end
def test_load_events_in_time_window
start_time = Time.parse('1990-05-17T19:30:00.000Z')
end_time = Time.parse('1990-05-17T19:45:00.000Z')
events = @resolver.send(:load_events_in_time_window, start_time, end_time)
assert_equal(6, events.length, 'Wrong number of events')
assert_equal('testSimpleSuccess', events[0].call_id, 'Wrong first call ID')
assert_equal('testSimpleSuccess_CalleeEnd',
events[5].call_id,
'Wrong last call ID')
assert_equal(1, events[0].cseq, 'Wrong first cseq')
assert_equal(2, events[1].cseq, 'Wrong second cseq')
assert_equal(3, events[2].cseq, 'Wrong third cseq')
end
def test_load_events_with_call_id
events = load_simple_success_events
assert_equal(3, events.length)
events.each_index do |index|
assert_equal(events[index].event_seq, index + 1, 'Events are not in the right order')
assert_equal('testSimpleSuccess', events[index].call_id)
end
end
def test_find_call_request
assert_nil(find_call_request([]),
"No events => must not find call request event")
# Create call request events. An original call request has no to_tag.
orig_req = new_call_request(Time.parse('2001-1-1'))
req1 = new_call_request(Time.parse('2002-1-1'), 'req1')
req2 = new_call_request(Time.parse('2003-1-1'), 'req2')
# Create setup and end events
setup = CallStateEvent.new(:event_type => CallStateEvent::CALL_SETUP_TYPE)
call_end = CallStateEvent.new(:event_type => CallStateEvent::CALL_END_TYPE)
assert_nil(find_call_request([setup, call_end]),
"No call request events => must not find call request event")
# When there are multiple call requests with no originals, we pick the first
# one in the array, under the assumption that the caller has already sorted
# the array by time.
req = find_call_request([setup, req1, call_end, req2])
assert_equal(req1, req,
'Multiple call requests with no originals => first one must win')
req = find_call_request([req1, orig_req, req2])
assert_equal(orig_req, req,
'Original call requests must be selected before call requests with to_tags')
orig_req2 = new_call_request(Time.parse('2000-1-1'))
req = find_call_request([call_end, orig_req2, orig_req, setup])
assert_equal(orig_req2, req,
'Multiple original call requests => first one must win')
end
def new_call_request(event_time, to_tag = nil)
params = {:event_type => CallStateEvent::CALL_REQUEST_TYPE,
:event_time => event_time}
params[:to_tag] = to_tag if to_tag
CallStateEvent.new(params)
end
def find_call_request(events)
@resolver.send(:find_call_request, events)
end
def test_start_cdr
cdr = @resolver.send(:start_cdr,
call_state_events('testSimpleSuccess_1'))
# verify the caller
assert_equal('sip:alice@example.com', cdr.caller_aor)
assert_equal('sip:alice@1.1.1.1', cdr.caller_contact)
# verify the callee: we have the AOR but not the contact
assert_equal('sip:bob@example.com', cdr.callee_aor)
assert_nil(cdr.callee_contact)
# verify the CDR
assert_equal('testSimpleSuccess', cdr.call_id)
assert_equal('f', cdr.from_tag)
assert_nil(cdr.to_tag) # don't have the to tag yet
assert_equal(Time.parse('1990-05-17T19:30:00.000Z'), cdr.start_time)
assert_equal(Cdr::CALL_REQUESTED_TERM, cdr.termination)
end
def test_best_call_leg
events = load_simple_success_events
# Pick the call leg with the best outcome and longest duration to be the
# basis for the CDR.
to_tag = @resolver.send(:best_call_leg, events)
assert_equal('t', to_tag, 'Wrong to_tag for best call leg')
# load events for the complicated case
call_id = 'testComplicatedSuccess'
events = @resolver.send(:load_events_with_call_id, call_id)
to_tag = @resolver.send(:best_call_leg, events)
assert_equal('t2', to_tag, 'Wrong to_tag for best call leg')
# try again, drop the final call_end event
to_tag = @resolver.send(:best_call_leg, events[0..4])
assert_equal('t1', to_tag, 'Wrong to_tag for best call leg')
# try again with three events
to_tag = @resolver.send(:best_call_leg, events[0..2])
assert_equal('t0', to_tag, 'Wrong to_tag for best call leg')
# try again with two events
to_tag = @resolver.send(:best_call_leg, events[0..1])
assert_equal('t0', to_tag, 'Wrong to_tag for best call leg')
# try again with just the call request
to_tag = @resolver.send(:best_call_leg, events[0..0])
assert_nil(to_tag, 'Wrong to_tag for best call leg')
end
def test_finish_cdr
events = load_simple_success_events
# fill in cdr_data with info from the events
to_tag = 't'
cdr = Cdr.new
status = @resolver.send(:finish_cdr, cdr, events, to_tag)
assert_equal(true, status)
# Check that the CDR is filled in as expected. It will only be partially
# filled in because we are testing just one part of the process.
assert_equal(to_tag, cdr.to_tag, 'Wrong to_tag')
assert_equal(Time.parse('1990-05-17T19:31:00.000Z'), cdr.connect_time,
'Wrong connect_time')
assert_equal(Time.parse('1990-05-17T19:40:00.000Z'), cdr.end_time,
'Wrong end_time')
assert_equal('sip:bob@2.2.2.2', cdr.callee_contact, 'Wrong callee contact')
assert_equal(Cdr::CALL_COMPLETED_TERM, cdr.termination, 'Wrong termination code')
assert_nil(cdr.failure_status)
assert_nil(cdr.failure_reason)
# Test a failed call. Check only that the failure info has been filled in
# properly. We've checked other info in the case above.
# This set of events has call request, call setup, call failed.
call_id = 'testFailed'
events = @resolver.send(:load_events_with_call_id, call_id)
check_failed_call(events, to_tag)
# Try again without the call setup event.
events.delete_if {|event| event.call_setup?}
check_failed_call(events, to_tag)
end
def test_finish_cdr_callee_hangs_up
events = load_simple_success_events_callee_hangs_up
# fill in cdr_data with info from the events
to_tag = 't'
cdr = Cdr.new
status = @resolver.send(:finish_cdr, cdr, events, to_tag)
assert_equal(true, status)
# Check that the CDR is filled in as expected. It will only be partially
# filled in because we are testing just one part of the process.
assert_equal(to_tag, cdr.to_tag, 'Wrong to_tag')
assert_equal(Time.parse('1990-05-17T19:41:00.000Z'), cdr.connect_time,
'Wrong connect_time')
assert_equal(Time.parse('1990-05-17T19:50:00.000Z'), cdr.end_time,
'Wrong end_time')
assert_equal('sip:bob@2.2.2.2', cdr.callee_contact, 'Wrong callee contact')
assert_equal(Cdr::CALL_COMPLETED_TERM, cdr.termination, 'Wrong termination code')
assert_nil(cdr.failure_status)
assert_nil(cdr.failure_reason)
end
# Helper method for test_finish_cdr. Check that failure info has been filled
# in properly.
def check_failed_call(events, to_tag)
cdr = Cdr.new
status = @resolver.send(:finish_cdr, cdr, events, to_tag)
assert_equal(true, status, 'Finishing the CDR failed')
assert_equal(Cdr::CALL_FAILED_TERM, cdr.termination, 'Wrong termination code')
assert_equal(499, cdr.failure_status)
assert_equal("You Can't Always Get What You Want", cdr.failure_reason)
end
def test_save_cdr
# For a clean test, make sure there is no preexisting CDR with the call
# ID that we are using.
Cdr.delete_all("call_id = '#{TEST_CALL_ID}'")
# Create a new complete CDR. Fill in mandatory fields so we don't get
# database integrity exceptions on save.
cdr = Cdr.new(:call_id => TEST_CALL_ID,
:from_tag => TEST_FROM_TAG,
:to_tag => TEST_TO_TAG,
:caller_aor => 'colbert@example.com',
:callee_aor => 'report@example.com',
:termination => Cdr::CALL_REQUESTED_TERM)
# Try to save it and confirm that it was saved. Use a clone so we don't
# modify the original and can reuse it.
saved_cdr = @resolver.send(:save_cdr, cdr.clone)
assert(saved_cdr.id, 'ID of object saved to database must be non-nil')
# Try to save another clone, marked as complete. Because the saved CDR
# was incomplete based on its termination, the save should succeed.
cdr2 = cdr.clone
cdr2.termination = Cdr::CALL_COMPLETED_TERM
save_again_cdr = @resolver.send(:save_cdr, cdr2)
assert_not_equal(saved_cdr.id, save_again_cdr.id)
assert_equal(Cdr::CALL_COMPLETED_TERM, save_again_cdr.termination)
# Try to save another clone. Should fail because the saved CDR is
# incomplete. Tweak the termination to help verify this.
cdr3 = cdr.clone
cdr3.termination = Cdr::CALL_IN_PROGRESS_TERM
saved_cdr = @resolver.send(:save_cdr, cdr3)
assert_equal(save_again_cdr.id, saved_cdr.id)
assert_equal(cdr2.termination, saved_cdr.termination)
end
def test_find_cdr
# Find a CDR we know is in the DB
cdr_to_find = Cdr.new(:call_id => 'call1')
cdr = @resolver.send(:find_cdr, cdr_to_find)
assert(cdr, "Couldn't find CDR")
# Try to find a CDR we know is not in the DB
cdr_to_find.call_id = 'extra_bogus_call_id'
cdr = @resolver.send(:find_cdr, cdr_to_find)
assert_nil(cdr, "Found a CDR that shouldn't exist")
# Trigger an ArgumentError
cdr_to_find.call_id = nil
assert_raise(ArgumentError) {cdr = @resolver.send(:find_cdr, cdr_to_find)}
end
def test_resolve_call
['testSimpleSuccess', 'testComplicatedSuccess', 'testFailed'].each do |call_id|
events = @resolver.send(:load_events_with_call_id, call_id)
@resolver.send(:resolve_call, events)
cdr = Cdr.find_by_call_id(call_id)
assert_not_nil(cdr, 'CDR was not created')
end
end
def test_resolve
Cdr.delete_all
start_time = Time.parse('1990-01-1T000:00:00.000Z')
end_time = Time.parse('2000-12-31T00:00.000Z')
@resolver.resolve(start_time, end_time)
assert_equal(4, Cdr.count, 'Wrong number of CDRs')
end
def test_get_start_time_from_cdr
Cdr.delete_all
start_time = @resolver.send(:get_daily_start_time)
# Give it one second difference in case the timing is off a bit
assert((Time.now() - (start_time+SECONDS_IN_A_DAY)) <= 1)
start_time = Time.parse('1990-01-1T000:00:00.000Z')
end_time = Time.parse('2000-12-31T00:00.000Z')
@resolver.resolve(start_time, end_time)
assert_equal(4, Cdr.count, 'Wrong number of CDRs')
start_time = @resolver.send(:get_daily_start_time)
assert_equal(start_time, Time.gm(2000,1,1,0,0,0))
end
#-----------------------------------------------------------------------------
# Helper methods
# load and return events for the simple case
def load_simple_success_events
call_id = 'testSimpleSuccess'
@resolver.send(:load_events_with_call_id, call_id)
end
# load and return events for the simple case
def load_simple_success_events_callee_hangs_up
call_id = 'testSimpleSuccess_CalleeEnd'
@resolver.send(:load_events_with_call_id, call_id)
end
end
syntax highlighted by Code2HTML, v. 0.9.1