[asterisk-commits] mjordan: testsuite/asterisk/trunk r2092 - in /asterisk/trunk: configs/ lib/py...

SVN commits to the Asterisk project asterisk-commits at lists.digium.com
Thu Sep 8 06:28:39 CDT 2011


Author: mjordan
Date: Thu Sep  8 06:28:32 2011
New Revision: 2092

URL: http://svnview.digium.com/svn/testsuite?view=rev&rev=2092
Log:
Added additional voicemail / TestEvent support to python libraries

Voicemail tests that conduct 'leave message operations' required
additional library support.  This patch adds that, as well as general
support libraries for the Asterisk AMI TestEvent.

Review: https://reviewboard.asterisk.org/r/1360/

Added:
    asterisk/trunk/lib/python/asterisk/TestState.py
      - copied unchanged from r2091, asterisk/team/mjordan/voicemail_tests_082811/lib/python/asterisk/TestState.py
Modified:
    asterisk/trunk/configs/manager.conf
    asterisk/trunk/lib/python/asterisk/TestCase.py
    asterisk/trunk/lib/python/asterisk/voicemail.py

Modified: asterisk/trunk/configs/manager.conf
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/configs/manager.conf?view=diff&rev=2092&r1=2091&r2=2092
==============================================================================
--- asterisk/trunk/configs/manager.conf (original)
+++ asterisk/trunk/configs/manager.conf Thu Sep  8 06:28:32 2011
@@ -7,7 +7,7 @@
 
 [user]
 secret = mysecret
-read = system,call,log,verbose,agent,user,config,dtmf,reporting,cdr,dialplan
+read = system,call,log,verbose,agent,user,config,dtmf,reporting,cdr,dialplan,test
 write = system,call,agent,user,config,command,reporting,originate
 
 #include "manager.users.conf.inc"

Modified: asterisk/trunk/lib/python/asterisk/TestCase.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/lib/python/asterisk/TestCase.py?view=diff&rev=2092&r1=2091&r2=2092
==============================================================================
--- asterisk/trunk/lib/python/asterisk/TestCase.py (original)
+++ asterisk/trunk/lib/python/asterisk/TestCase.py Thu Sep  8 06:28:32 2011
@@ -8,7 +8,7 @@
 '''
 
 import sys
-import logging, logging.config
+import logging
 import logging.config
 import os
 from twisted.internet import reactor
@@ -128,3 +128,9 @@
         logger.info("AMI Connect instance %s" % (ami.id + 1))
         self.ami[ami.id] = ami
 
+    def handleOriginateFailure(self, reason):
+        """ Convenience callback handler for twisted deferred errors for an AMI originate call """
+        logger.error("Error sending originate:")
+        logger.error(reason.getTraceback())
+        self.stop_reactor()
+        return reason

Modified: asterisk/trunk/lib/python/asterisk/voicemail.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/lib/python/asterisk/voicemail.py?view=diff&rev=2092&r1=2091&r2=2092
==============================================================================
--- asterisk/trunk/lib/python/asterisk/voicemail.py (original)
+++ asterisk/trunk/lib/python/asterisk/voicemail.py Thu Sep  8 06:28:32 2011
@@ -12,12 +12,255 @@
 import os
 import glob
 import shutil
+import logging
 
 from asterisk import Asterisk
 from config import Category
 from config import ConfigFile
+from TestCase import TestCase
+from TestState import TestState
+from TestState import TestStateController
 
 sys.path.append("lib/python")
+
+logger = logging.getLogger(__name__)
+
+
+"""
+Class that holds the state of some test condition, and allows a callback function to be used to evaluate
+whether or not that test condition has passed
+"""
+class TestCondition(object):
+
+    """
+    evaluateFunc    function used to evaluate the condition
+    testConditionData    Some piece of data that will be passed to the evaluateFunc
+
+    Note that evaluteFunc should return True or False, and take in two parameters - a value being evaluated
+    and the member data testConditionData
+    """
+    def __init__(self, evaluateFunc = None, testConditionData = None):
+        self.__evaluateFunc = evaluateFunc
+        self.testConditionData = testConditionData
+        self.currentState = False
+
+    """
+    Evaluate the test condition
+
+    value    The value to evaluate
+    """
+    def evaluate(self, value):
+        if self.__evaluateFunc != None:
+            self.currentState = self.__evaluateFunc(value, self)
+        else:
+            logger.warn("WARNING: no evaluate function defined, setting currenState to value")
+            self.currentState = value
+        return
+
+
+"""
+Base class for voice mail tests that use the TestCase AMI event and the TestStateController
+"""
+class VoiceMailTest(TestCase):
+
+    """
+    The formats a message can be left in
+    """
+    formats = ["ulaw","wav","WAV"]
+
+    """
+    The default expected channel to be used to send info to the voicemail server
+    """
+    defaultSenderChannel = "SIP/ast1-00000000"
+
+    def __init__(self):
+        TestCase.__init__(self)
+
+        self.amiReceiver = None
+        self.amiSender = None
+        self.astSender = None
+        self.__testConditions = {}
+        self.senderChannel = VoiceMailTest.defaultSenderChannel
+
+    """
+    Create the test controller.  Should be called once amiReceiver and amiSender have both been set
+    """
+    def createTestController(self):
+        if (self.amiReceiver != None and self.amiSender != None):
+            self.testStateController = TestStateController(self, self.amiReceiver)
+
+    def __handleRedirectFailure__(self, reason):
+        logger.warn("Error sending redirect - test may or may not fail:")
+        logger.warn(reason.getTraceback())
+        return reason
+
+    """
+    Hangs up the current call
+    """
+    def hangup(self):
+        if self.astSender == None:
+            logger.error("Attempting to send hangup to non-existant Asterisk instance")
+            TestCase.testStateController.changeState(FailureTestState(self.controller))
+            return
+
+        df = self.amiSender.redirect(self.senderChannel, "voicemailCaller", "hangup", 1)
+        df.addErrback(self.__handleRedirectFailure__)
+
+    """
+    Send a DTMF signal to the voicemail server
+    dtmfToSend    The DTMF code to send
+    """
+    def sendDTMF(self, dtmfToSend):
+        logger.info("Attempting to send DTMF " + dtmfToSend)
+        if self.amiSender == None:
+            logger.error("Attempting to send DTMF to non-connected caller AMI")
+            TestCase.testStateController.changeState(FailureTestState(self.controller))
+            return
+
+        if self.astSender == None:
+            logger.error("Attempting to send DTMF to non-existant Asterisk instance")
+            TestCase.testStateController.changeState(FailureTestState(self.controller))
+            return
+
+        self.astSender.cli_exec("dialplan set global DTMF_TO_SEND " + dtmfToSend)
+
+        """
+        Redirect to the DTMF extension - note that we assume that we only have one channel to
+        the other asterisk instance
+        """
+        df = self.amiSender.redirect(self.senderChannel, "voicemailCaller", "sendDTMF", 1)
+        df.addErrback(self.__handleRedirectFailure__)
+
+    """
+    Send a sound file to the voicemail server
+    audioFile    The local path to the file to stream
+    """
+    def sendSoundFile(self, audioFile):
+        if self.amiSender == None:
+            logger.error("Attempting to send sound file to non-connected caller AMI")
+            TestCase.testStateController.changeState(FailureTestState(self.controller))
+            return
+
+        if self.astSender == None:
+            logger.error("Attempting to send sound file to non-existant Asterisk instance")
+            TestCase.testStateController.changeState(FailureTestState(self.controller))
+            return
+
+        self.astSender.cli_exec("dialplan set global TALK_AUDIO " + audioFile)
+
+        """
+        Redirect to the send sound file extension - note that we assume that we only have one channel to
+        the other asterisk instance
+        """
+        df = self.amiSender.redirect(self.senderChannel, "voicemailCaller", "sendAudio", 1)
+        df.addErrback(self.__handleRedirectFailure__)
+
+    """
+    Send a sound file to the voicemail server, then send a DTMF signal
+    audioFile    The local path to the file to stream
+    dtmfToSend   The DTMF signal to send
+
+    Note that this is necessary so that when the audio file is finished, we close the audio recording cleanly;
+    otherwise, Asterisk will detect the end of file as a hangup
+    """
+    def sendSoundFileWithDTMF(self, audioFile, dtmfToSend):
+        if self.amiSender == None:
+            logger.error("Attempting to send sound file / DTMF to non-connected caller AMI")
+            TestCase.testStateController.changeState(FailureTestState(self.controller))
+            return
+
+        if self.astSender == None:
+            logger.error("Attempting to send sound file / DTMF to non-existant Asterisk instance")
+            TestCase.testStateController.changeState(FailureTestState(self.controller))
+            return
+
+        self.astSender.cli_exec("dialplan set global TALK_AUDIO " + audioFile)
+        self.astSender.cli_exec("dialplan set global DTMF_TO_SEND " + dtmfToSend)
+
+        """
+        Redirect to the send sound file extension - note that we assume that we only have one channel to
+        the other asterisk instance
+        """
+        df = self.amiSender.redirect(self.senderChannel, "voicemailCaller", "sendAudioWithDTMF", 1)
+        df.addErrback(self.__handleRedirectFailure__)
+
+    """
+    Add a new test condition to track
+
+    conditionName    The unique name of the condition
+    condition        The TestCondition object
+    """
+    def addTestCondition(self, conditionName, condition):
+        self.__testConditions[conditionName] = condition
+
+    """
+    Set a test condition to the specified value, and evalute whether or not it has passed
+
+    conditionName    The unique name of the condition
+    value            The value to pass to the evaluation checker
+    """
+    def setTestCondition(self, conditionName, value):
+        if conditionName in self.__testConditions.keys():
+            self.__testConditions[conditionName].evaluate(value)
+
+    """
+    Get the current state of a test condition
+
+    conditionName    The unique name of the condition
+    returns True if the condition has passed; False otherwise
+    """
+    def getTestCondition(self, conditionName):
+        if conditionName in self.__testConditions.keys():
+            return self.__testConditions[conditionName].currentState
+        return False
+
+    """
+    Check all test conditions
+
+    returns True if all have passed; False if any have not
+    """
+    def checkTestConditions(self):
+        retVal = True
+        for k, v in self.__testConditions.items():
+            if not v.currentState:
+                logger.warn("Test Condition [" + k + "] has not passed") 
+                retVal = False
+
+        return retVal
+
+"""
+Base class for VoiceMail TestEvent state machine handling
+
+Note - this class exists mostly to share the VoiceMailTest object across the concrete class
+implementations
+"""
+class VoiceMailState(TestState):
+
+    """
+    controller        The TestStateController managing the test
+    voiceMailTest     The main test object
+    """
+    def __init__(self, controller, voiceMailTest):
+        TestState.__init__(self, controller)
+        self.voiceMailTest = voiceMailTest
+        if self.voiceMailTest == None:
+            logger.error("Failed to set voicemail test object")
+            raise RuntimeError('Failed to set voicemail test object')
+
+        logger.debug(" Entering state [" + self.getStateName() + "]")
+
+    """
+    Should be overriden by derived classes and return the name of the current state
+    """
+    def getStateName(self):
+        pass
+
+    """
+    Can be called by derived classes to output a state that is being ignored
+    """
+    def handleDefaultState(self, event):
+        logger.debug(" State [" + self.getStateName() + "] - ignoring state change " + event['state'])
+
 
 """
 Class that manages creation of, verification of, and teardown of Asterisk mailboxes on the local filesystem
@@ -71,7 +314,6 @@
     def __init__(self, ast):
         self.__ast = ast
         self.voicemailDirectory = self.__ast.directories['astspooldir'] + '/voicemail'
-
 
     """
     Creates the basic set of folders needed for a mailbox on the file system
@@ -116,7 +358,7 @@
 
         except IOError as e:
             if e.errno == errno.EACCESS:
-                print "You do not have sufficient permissions to perform the necessary directory manipulations"
+                logger.error( "You do not have sufficient permissions to perform the necessary directory manipulations")
                 return False
 
         return True
@@ -154,14 +396,15 @@
         f.write('rdnis=unknown\n')
         f.write('priority=2\n')
         f.write('callerchan=SIP/ast1-00000000\n')
-        f.write('callerid=\"Anonymous\"<ast1>\n')
+        f.write('callerid=\"Anonymous\"<555-5555>\n')
         f.write('origdate=Tue Aug  9 10:05:13 PM UTC 2011\n')
         f.write('origtime=1312927513\n')
         if (folder == self.urgentFolderName):
             f.write('flag=Urgent\n')
         else:
             f.write('flag=\n')
-        f.write('duration=1\n')
+        f.write('category=tt-monkeys\n')
+        f.write('duration=6\n')
         f.close()
 
         for format in formats:
@@ -215,6 +458,24 @@
         """
         fileName = msgName + ".txt"
         retVal = retVal & self.checkVoiceFileExists(context, mailbox, fileName, folder)
+
+        return retVal
+
+    """
+    Check if a voicemail greeting exists on the filesystem
+    context    The context of the mailbox
+    mailbox    The mailbox
+    msgname    The name of the greeting to find
+    lstFormats The formats we expect to be recorded for us
+
+    true if the greeting exists, false otherwise
+    """
+    def checkGreetingExists(self, context, mailbox, msgname, lstFormats):
+        retVal = True
+
+        for format in lstFormats:
+            fileName = msgname + "." + format
+            retVal = retVal & self.checkVoiceFileExists(context, mailbox, fileName, "")
 
         return retVal
 
@@ -248,6 +509,53 @@
         return False
 
     """
+    An object that holds voicemail user information
+    """
+    class UserObject(object):
+        def __init__(self):
+            self.password = ""
+            self.fullname = ""
+            self.emailaddress = ""
+            self.pageraddress = ""
+
+    """
+    Gets user information from the voicemail configuration file
+
+    context    The context of the mailbox
+    mailbox    The mailbox
+    sourceFile    The file containing the user information to pull from.  Defaults
+        to voicemail.conf
+
+    returns A VoiceMailMailboxManagement.UserObject object, populated with the user's values,
+        or an empty object
+    """
+    def getUserObject(self, context, mailbox, sourceFile="voicemail.conf"):
+
+        filePath = self.__ast.baseDirectory + self.__ast.directories['astetcdir'] + "/" + sourceFile
+
+        configFile = ConfigFile(filePath)
+        userObject = VoiceMailMailboxManagement.UserObject()
+        for cat in configFile.categories:
+            if cat.name == context:
+                for kvp in cat.options:
+                    if kvp[0] == mailbox:
+                        tokens = kvp[1].split(',')
+                        i = 0
+                        for token in tokens:
+                            if i == 0:
+                                userObject.password = token
+                            elif i == 1:
+                                userObject.fullname = token
+                            elif i == 2:
+                                userObject.emailaddress = token
+                            elif i == 3:
+                                userObject.pageraddress = token
+                            i += 1
+                        return userObject
+
+        return userObject
+
+    """
     Checks if a file exists under the voicemail file structure
     context    The context of the mailbox
     mailbox    The mailbox
@@ -266,7 +574,6 @@
             return True
         else:
             return False
-
 
     def __removeItemsFromFolder__(self, mailboxPath, folder):
         folderPath = os.path.join(self.__ast.baseDirectory, "%(mp)s/%(f)s" % {'mp':mailboxPath, 'f':folder})




More information about the asterisk-commits mailing list