Matt Jordan has submitted this change and it was merged. Change subject: Rewrite sip_attended_transfer test to stop failing. ......................................................................
Rewrite sip_attended_transfer test to stop failing. The sip_attended_transfer test has been bouncing for a while. There are two major fixes introduced here to prevent the bouncing. First, by converting to using the testsuite's PJSUA module, we no longer are using the native Python threading library. Instead, we are using a method that works better with the Twisted framework. Second, the test is more strict about when the transfer may be performed. The previous test would attempt the transfer when Asterisk reported that the call was bridged. The problem is that Asterisk may report the call as bridged before PJSUA has properly processed the 200 OK that Asterisk has sent to it. By waiting, we can be sure that all parties are prepared when the transfer is attempted. The test has also been rewritten to only work with Asterisk 12+. A new separate test will be written to work on Asterisk 11. This helps the code to be a little less cluttered. Change-Id: I1676801d90bcafc28ba25e8b6889f40ab08cc90e Address review feedback from Ashley * Two CallCallback classes have been combined into one * Bridge state has been factored into a minimal class * Unnecessary checks of test state have been removed. Change-Id: I1676801d90bcafc28ba25e8b6889f40ab08cc90e --- A tests/channels/SIP/sip_attended_transfer/attended_transfer.py M tests/channels/SIP/sip_attended_transfer/configs/ast1/extensions.conf M tests/channels/SIP/sip_attended_transfer/configs/ast1/sip.conf D tests/channels/SIP/sip_attended_transfer/run-test M tests/channels/SIP/sip_attended_transfer/test-config.yaml 5 files changed, 234 insertions(+), 216 deletions(-) Approvals: Matt Jordan: Looks good to me, approved; Verified Ashley Sanders: Looks good to me, but someone else must approve diff --git a/tests/channels/SIP/sip_attended_transfer/attended_transfer.py b/tests/channels/SIP/sip_attended_transfer/attended_transfer.py new file mode 100644 index 0000000..cb133f3 --- /dev/null +++ b/tests/channels/SIP/sip_attended_transfer/attended_transfer.py @@ -0,0 +1,183 @@ +""" +Copyright (C) 2015, Digium, Inc. +Mark Michelson <mmichel...@digium.com> + +This program is free software, distributed under the terms of +the GNU General Public License Version 2. +""" + +import logging +import pjsua as pj + +from twisted.internet import reactor + +LOGGER = logging.getLogger(__name__) + +INIT = 0 +BOB_CALLED = 1 +CAROL_CALLED = 2 +TRANSFERRED = 3 + + +class TransferAccountCallback(pj.AccountCallback): + '''Generic Account callback for Bob and Carol. + + The sole purpose of this callback is to auto-answer + incoming calls + ''' + + def __init__(self, account): + pj.AccountCallback.__init__(self, account) + + def on_incoming_call(self, call): + call.answer(200) + + +class CallCallback(pj.CallCallback): + '''Call Callback used for Calls made by Alice. + + Each call has some specific action it is expected + to take once the call has been confirmed to be + answered. The on_state method is overridden to + signal to the test that Alice is prepared for the + next step + ''' + + def __init__(self, call, on_answered): + pj.CallCallback.__init__(self, call) + self.on_answered = on_answered + + def on_state(self): + if self.call.info().state == pj.CallState.CONFIRMED: + reactor.callFromThread(self.on_answered) + + +class BridgeState(object): + '''Object for tracking state of a bridge + + The main data the test cares about is the bridge's unique id and whether two + channels have been bridged together by the bridge. + ''' + def __init__(self): + self.unique_id = None + self.bridged = False + + +class Transfer(object): + '''Controller for attended transfer test + + This contains all the methods for advancing the test, such as placing calls + and performing transfers. It also has several state variables that help to + determine the proper timing for performing actions. + ''' + + def __init__(self, test_object, accounts): + super(Transfer, self).__init__() + self.ami = test_object.ami[0] + self.ami.registerEvent('BridgeCreate', self.bridge_create) + self.ami.registerEvent('BridgeEnter', self.bridge_enter) + + # bridge1 bridges Alice and Bob + self.bridge1 = BridgeState() + # bridge2 bridges Alice and Carol + self.bridge2 = BridgeState() + + self.bob_call_answered = False + self.carol_call_answered = False + + bob = accounts.get('bob').account + bob.set_callback(TransferAccountCallback(bob)) + + carol = accounts.get('carol').account + carol.set_callback(TransferAccountCallback(carol)) + + self.alice = accounts.get('alice').account + self.call_to_bob = None + self.call_to_carol = None + + self.test_object = test_object + self.state = INIT + + def bridge_create(self, ami, event): + if not self.bridge1.unique_id: + self.bridge1.unique_id = event.get('bridgeuniqueid') + elif not self.bridge2.unique_id: + self.bridge2.unique_id = event.get('bridgeuniqueid') + else: + LOGGER.error("Unexpected third bridge created") + self.test_object.set_passed(False) + self.test_object.stop_reactor() + + def bridge_enter(self, ami, event): + if (event.get('bridgeuniqueid') == self.bridge1.unique_id and + event.get('bridgenumchannels') == '2'): + self.bridge1.bridged = True + if self.state == BOB_CALLED: + self.call_carol() + elif self.state == TRANSFERRED: + self.hangup_calls() + else: + LOGGER.error("Unexpected BridgeEnter event") + self.test_object.set_passed(False) + self.test_object.stop_reactor() + elif (event.get('bridgeuniqueid') == self.bridge2.unique_id and + event.get('bridgenumchannels') == '2'): + self.bridge2.bridged = True + if self.state == CAROL_CALLED: + self.transfer_call() + elif self.state == TRANSFERRED: + self.hangup_calls() + else: + LOGGER.error("Unexpected BridgeEnter event") + self.test_object.set_passed(False) + self.test_object.stop_reactor() + + def bob_call_confirmed(self): + self.bob_call_answered = True + self.call_carol() + + def carol_call_confirmed(self): + self.carol_call_answered = True + self.transfer_call() + + def call_bob(self): + self.call_to_bob = self.alice.make_call('sip:bob@127.0.0.1', + CallCallback(None, self.bob_call_confirmed)) + self.state = BOB_CALLED + + def call_carol(self): + if self.bridge1.bridged and self.bob_call_answered: + self.call_to_carol = self.alice.make_call('sip:carol@127.0.0.1', + CallCallback(None, self.carol_call_confirmed)) + self.state = CAROL_CALLED + + def transfer_call(self): + if self.bridge2.bridged and self.carol_call_answered: + self.call_to_bob.transfer_to_call(self.call_to_carol) + self.state = TRANSFERRED + + def hangup_calls(self): + bob_hangup = { + 'Action': 'Hangup', + 'Channel': '/SIP/bob-.*/', + } + carol_hangup = { + 'Action': 'Hangup', + 'Channel': '/SIP/carol-.*/', + } + self.ami.sendMessage(bob_hangup) + self.ami.sendMessage(carol_hangup) + self.test_object.set_passed(True) + self.test_object.stop_reactor() + + +def phones_registered(test_object, accounts): + '''Entry point for attended transfer test + + When the PJSUA module has detected that all phones have registered, this + method is called into. This initializes the test controller and sets the + test in motion by placing the first call of the test + ''' + + transfer = Transfer(test_object, accounts) + transfer.call_bob() diff --git a/tests/channels/SIP/sip_attended_transfer/configs/ast1/extensions.conf b/tests/channels/SIP/sip_attended_transfer/configs/ast1/extensions.conf index 7f1fc4e..45fcdb2 100644 --- a/tests/channels/SIP/sip_attended_transfer/configs/ast1/extensions.conf +++ b/tests/channels/SIP/sip_attended_transfer/configs/ast1/extensions.conf @@ -3,7 +3,7 @@ [globals] [transfertest] -exten => call_b,1,Dial(SIP/end_b) -exten => call_c,1,Dial(SIP/end_c) +exten => bob,1,Dial(SIP/bob) +exten => carol,1,Dial(SIP/carol) diff --git a/tests/channels/SIP/sip_attended_transfer/configs/ast1/sip.conf b/tests/channels/SIP/sip_attended_transfer/configs/ast1/sip.conf index 4148a46..3169e03 100644 --- a/tests/channels/SIP/sip_attended_transfer/configs/ast1/sip.conf +++ b/tests/channels/SIP/sip_attended_transfer/configs/ast1/sip.conf @@ -2,17 +2,18 @@ canreinvite=no sipdebug=yes -[end_a] +[alice] context=transfertest type=friend insecure=invite +host=dynamic -[end_b] +[bob] type=friend host=dynamic insecure=invite -[end_c] +[carol] type=friend host=dynamic insecure=invite diff --git a/tests/channels/SIP/sip_attended_transfer/run-test b/tests/channels/SIP/sip_attended_transfer/run-test deleted file mode 100755 index 6a4d304..0000000 --- a/tests/channels/SIP/sip_attended_transfer/run-test +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/env python -''' -Copyright (C) 2010, Digium, Inc. -David Vossel <dvos...@digium.com> - -This program is free software, distributed under the terms of -the GNU General Public License Version 2. -''' - -import sys -import os -from twisted.internet import reactor -import pjsua as pj -import threading - -sys.path.append("lib/python") -from asterisk.test_case import TestCase -import logging -from asterisk.version import AsteriskVersion - -LOGGER = logging.getLogger(__name__) - -class AutoAnswerCallback(pj.AccountCallback): - def __init__(self, account): - self.sem = threading.Semaphore(0) - pj.AccountCallback.__init__(self, account) - - def wait(self): - if self.sem: - self.sem.acquire() - - def on_reg_state(self): - if self.sem: - if self.account.info().reg_status >= 200: - self.sem.release() - self.sem = None - - def on_incoming_call(self, call): - call.answer(200) - -class AttTransferTest(TestCase): - def __init__(self): - TestCase.__init__(self) - #self.reactor_timeout = 60 - self.create_asterisk() - self.chans = [] - self.final_bridge = 0 - self.lib = None - self.ext_a = None - self.ext_b = None - self.ext_c = None - self.callToB = None - self.callToC = None - - def run(self): - TestCase.run(self) - self.create_ami_factory() - - def ami_connect(self, ami): - # start pjsua clients - self.lib = pj.Lib() - try: - self.lib.init() - self.lib.create_transport(pj.TransportType.UDP, pj.TransportConfig()) - self.lib.set_null_snd_dev() - self.lib.start() - - # we'll need this for later... - self.ext_a = self.lib.create_account(pj.AccountConfig("127.0.0.1", "end_a")) - self.ext_b = self.lib.create_account(pj.AccountConfig("127.0.0.1", "end_b")) - self.ext_c = self.lib.create_account(pj.AccountConfig("127.0.0.1", "end_c")) - # only legs B and C receive calls, so only those two need to register and autoanswer - ext_b_cb = AutoAnswerCallback(self.ext_b) - ext_c_cb = AutoAnswerCallback(self.ext_c) - self.ext_b.set_callback(ext_b_cb) - self.ext_c.set_callback(ext_c_cb) - # wait for registration - ext_b_cb.wait() - ext_c_cb.wait() - - except pj.Error, e: - LOGGER.error("Exception: " + str(e)) - self.doCleanup() - return - - # register callbacks required to handle call completion events - if AsteriskVersion() < AsteriskVersion('12'): - self.ami[0].registerEvent('Bridge', self.bridge_callback) - self.ami[0].registerEvent('VarSet', self.bridgepeer_callback) - else: - self.ami[0].registerEvent('BridgeEnter', - self.bridge_enter_callback) - self.ami[0].registerEvent('AttendedTransfer', - self.transfer_callback) - - # kick off first call from A to B - LOGGER.info("Kicking off A-to-B call") - self.callToB = self.ext_a.make_call("sip:call_b@127.0.0.1:5060") - - def bridge_callback(self, ami, event): - ''' - Pre-Asterisk 12 bridge callback. - - This callback stores the channels that end_a is connected to in - self.chans. Once self.chans has two channels in it, then end_a - transfers end_b to end_c. - ''' - - # We can get a few Link/Unlink events before the transfer kicks - # off; don't track channels twice - if event['channel2'] in self.chans: - return - - LOGGER.debug("Tracking channel %s" % event['channel2']) - self.chans.append(event['channel2']) - numchans = len(self.chans) - if numchans == 1: - # kick off second call from A to C - LOGGER.info("Kicking off A-to-C call") - self.callToC = self.ext_a.make_call("sip:call_c@127.0.0.1:5060") - elif numchans == 2: - # both channels are now up, so initiate the transfer - LOGGER.info("Kicking off transfer") - self.callToC.transfer_to_call(self.callToB) - - def bridgepeer_callback(self, ami, event): - ''' - Pre-Asterisk 12 bridge callback. - - This callback checks that after the transfer has completed, that the - BRIDGEPEER variable setting for the two channels is what is expected. - end_b should have end_c as its BRIDGEPEER, and end_c should have end_b - as its BRDIGEPEER. - ''' - - if event['variable'] != "BRIDGEPEER" or len(self.chans) < 2: - return - - LOGGER.info("Inspecting BRIDGEPEER VarSet") - - # we should get 2 bridgepeers with swapped channel and value headers indicating the bridged channels - if self.chans[:2] == [event['channel'], event['value']] or\ - self.chans[:2] == [event['value'], event['channel']]: - LOGGER.info("Got expected VarSet") - self.final_bridge += 1 - if self.final_bridge == 2: - LOGGER.info("Transfer successful!") - # success! - self.passed = True - self.doCleanup() - - def bridge_enter_callback(self, ami, event): - ''' - Asterisk 12+ bridge enter calback. - - We expect this callback to be called a total of five times. - 1. end_a enters bridge 1 - 2. end_b enters bridge 1 - 3. end_a enters bridge 2 - 4. end_c enters bridge 2 - 5. Either end_c enters bridge 1 or end_b enters bridge 2 - - After the end_a and end_b enter bridge 1, we initiate a call from end_a - to end_c. After end_a and end_c enter bridge2, we initiate an attended - transfer from end_a to transfer end_b to end_c. - ''' - LOGGER.info("Channel '%s' entered bridge '%s'" % (event.get('channel'), - event.get('bridgeuniqueid'))) - self.chans.append(event.get('channel')) - if (len(self.chans) == 2): - self.callToC = self.ext_a.make_call("sip:call_c@127.0.0.1:5060") - elif (len(self.chans) == 4): - self.callToC.transfer_to_call(self.callToB) - - def transfer_callback(self, ami, event): - ''' - Asterisk 12+ attended transfer callback. - - We expect this callback to be called exactly once. In this, we ensure - that the transfer was successful. - ''' - LOGGER.info("Got attended transfer callback") - if event.get('result') == 'Success': - self.passed = True - LOGGER.info("Successful transfer") - else: - LOGGER.error("Unsuccessful transfer: %s" % event.get('result')) - - self.doCleanup() - - def doCleanup(self): - #self.ami[0].hangup(self.chans[0]) - self.ast[0].cli_exec("core show locks") # get lock output in case of deadlock before tearing down. - self.ast[0].cli_exec("core show channels")# if channels are still up for some reason, we want to know that as well - self.lib.destroy() - self.lib = None - self.stop_reactor() - -def main(): - # Run Attended Transfer Test - test = AttTransferTest() - reactor.run() - if test.passed != True: - return 1 - return 0 - -if __name__ == "__main__": - sys.exit(main() or 0) - diff --git a/tests/channels/SIP/sip_attended_transfer/test-config.yaml b/tests/channels/SIP/sip_attended_transfer/test-config.yaml index 7275c22..7fe5c60 100644 --- a/tests/channels/SIP/sip_attended_transfer/test-config.yaml +++ b/tests/channels/SIP/sip_attended_transfer/test-config.yaml @@ -1,10 +1,53 @@ testinfo: summary: 'Test SIP Attended Transfer' description: | - 'This test verifies the SIP_REFER with Replaces attended transfer routine.' + 'This test sets up three PJSUA accounts: Alice, Bob, and Carol. The test commences + as follows: + * Alice places a call through Asterisk to Bob. + * Once Alice and Bob are bridged, Alice places a call through Asterisk to Carol. + * Once Alice and Carol are bridged, Alice performs an attended transfer. + * The test ensures that Bob and Carol are bridged and that Alice is hung up.' + +test-modules: + add-test-to-search-path: True + test-object: + config-section: test-object-config + typename: 'test_case.TestCaseModule' + modules: + - + config-section: 'pjsua-config' + typename: 'pjsua_mod.PJsua' + +test-object-config: + connect-ami: True + +pjsua-config: + callback_module: 'attended_transfer' + callback_method: 'phones_registered' + transports: + - + name: 'local-ipv4' + bind: '127.0.0.1' + bindport: '5061' + accounts: + - + name: 'alice' + username: 'alice' + domain: '127.0.0.1' + transport: 'local-ipv4' + - + name: 'bob' + username: 'bob' + domain: '127.0.0.1' + transport: 'local-ipv4' + - + name: 'carol' + username: 'carol' + domain: '127.0.0.1' + transport: 'local-ipv4' properties: - minversion: '1.8.0.0' + minversion: '12.0.0' dependencies: - python : 'twisted' - python : 'starpy' -- To view, visit https://gerrit.asterisk.org/19 To unsubscribe, visit https://gerrit.asterisk.org/settings Gerrit-MessageType: merged Gerrit-Change-Id: I1676801d90bcafc28ba25e8b6889f40ab08cc90e Gerrit-PatchSet: 3 Gerrit-Project: testsuite Gerrit-Branch: master Gerrit-Owner: Mark Michelson <mmichel...@digium.com> Gerrit-Reviewer: Ashley Sanders <asand...@digium.com> Gerrit-Reviewer: Mark Michelson <mmichel...@digium.com> Gerrit-Reviewer: Matt Jordan <mjor...@digium.com> -- _____________________________________________________________________ -- Bandwidth and Colocation Provided by http://www.api-digital.com -- asterisk-dev mailing list To UNSUBSCRIBE or update options visit: http://lists.digium.com/mailman/listinfo/asterisk-dev