[asterisk-commits] mjordan: testsuite/asterisk/trunk r4065 - in /asterisk/trunk: lib/python/aste...

SVN commits to the Asterisk project asterisk-commits at lists.digium.com
Thu Aug 22 12:43:10 CDT 2013


Author: mjordan
Date: Thu Aug 22 12:43:07 2013
New Revision: 4065

URL: http://svnview.digium.com/svn/testsuite?view=rev&rev=4065
Log:
Give ARI its own test suite object

Testing ARI is a bit different than testing normal Asterisk tests. Typically, a
test in the Asterisk Test Suite uses the following model:

1. Create Asterisk Instances
2. Wait for instances to fully boot
3. Connect AMI
4. Wait for AMI connection
5. Spawn channels for test

Most of the test logic occurs in step number 5 - while it's possible to inject
logic into the first four steps without writing a new test object, it isn't
easy, particularly if what you need to do is defer the execution of one of
those four steps until something else happens. In particular, ARI tests need
to connect the websocket prior to step 4, as the AMI connection is what
typically causes channels to be created in the dialplan and test execution to
begin. If the websocket connection doesn't happen before that point, a channel
may enter into the Stasis application before the websocket connects and the
test will fail.

This patch gives ARI its own test object so that it can properly connect the
websocket prior to the AMI connection. This changes the startup routine to be:

1. Create Asterisk Instances
2. Wait for instances to fully boot
3. Connect ARI
4. Wait for ARI connection
5. Connection AMI
6. Wait for AMI connection
7. Spawn channels for test

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

Modified:
    asterisk/trunk/lib/python/asterisk/ari.py
    asterisk/trunk/sample-yaml/ari-config.yaml.sample
    asterisk/trunk/tests/rest_api/continue/configs/ast1/extensions.conf
    asterisk/trunk/tests/rest_api/continue/rest_continue.py
    asterisk/trunk/tests/rest_api/continue/test-config.yaml

Modified: asterisk/trunk/lib/python/asterisk/ari.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/lib/python/asterisk/ari.py?view=diff&rev=4065&r1=4064&r2=4065
==============================================================================
--- asterisk/trunk/lib/python/asterisk/ari.py (original)
+++ asterisk/trunk/lib/python/asterisk/ari.py Thu Aug 22 12:43:07 2013
@@ -11,22 +11,21 @@
 import logging
 import re
 import requests
-import time
 import traceback
 import urllib
 
+from TestCase import TestCase
 from twisted.internet import reactor
 from autobahn.websocket import WebSocketClientFactory, \
     WebSocketClientProtocol, connectWS
 
-logger = logging.getLogger(__name__)
+LOGGER = logging.getLogger(__name__)
 
 DEFAULT_PORT = 8088
-
 
 #: Default matcher to ensure we don't have any validation failures on the
 #  WebSocket
-ValidationMatcher = {
+VALIDATION_MATCHER = {
     'conditions': {
         'match': {
             'error': "^InvalidMessage$"
@@ -36,6 +35,143 @@
 }
 
 
+class AriTestObject(TestCase):
+    ''' Class that acts as a Test Object in the pluggable module framework '''
+
+    def __init__(self, test_path='', test_config=None):
+        ''' Constructor for a test object
+
+        Keyword Arguments:
+        test_path The full path to the test location
+        test_config The YAML test configuration
+        '''
+        super(AriTestObject, self).__init__(test_path, test_config)
+
+        if test_config is None:
+            # Meh, just use defaults
+            test_config = {}
+
+        self.apps = test_config.get('apps') or 'testsuite'
+        if isinstance(self.apps, list):
+            self.apps = ','.join(self.apps)
+        host = test_config.get('host') or '127.0.0.1'
+        port = test_config.get('port') or DEFAULT_PORT
+        userpass = (test_config.get('username') or 'testsuite',
+            test_config.get('password') or 'testsuite')
+
+        # Create the REST interface and the WebSocket Factory
+        self.ari = ARI(host, port=port, userpass=userpass)
+        self.ari_factory = AriClientFactory(receiver=self, host=host, port=port,
+                                        apps=self.apps,
+                                        userpass=userpass)
+        self.iterations = test_config.get('test-iterations')
+
+        self.test_iteration = 0
+        self.channels = []
+        self._ws_event_handlers = []
+
+        if self.iterations is None:
+            self.iterations = [{'channel': 'Local/s at default',
+                'application': 'Echo'}]
+
+        self.create_asterisk(count=1)
+
+    def run(self):
+        ''' Override of TestCase run
+
+        Called when reactor starts and after all instances of Asterisk have started
+        '''
+        super(AriTestObject, self).run()
+        self.ari_factory.connect()
+
+    def register_ws_event_handler(self, callback):
+        ''' Register a callback for when an event is received over the WS
+
+        :param callback The method to call when an event is received. This
+        will have a single parameter (the event) passed to it
+        '''
+        self._ws_event_handlers.append(callback)
+
+    def on_ws_event(self, message):
+        ''' Handler for WebSocket events
+
+        :param message The WS event payload
+        '''
+        for handler in self._ws_event_handlers:
+            handler(message)
+
+    def on_ws_open(self, protocol):
+        ''' Handler for WebSocket Client Protocol opened
+
+        :param protocol The WS Client protocol object
+        '''
+        reactor.callLater(0, self._create_ami_connection)
+
+    def on_ws_closed(self, protocol):
+        ''' Handler for WebSocket Client Protocol closed
+
+        :param protocol The WS Client protocol object
+        '''
+        LOGGER.debug('WebSocket connection closed...')
+
+    def _create_ami_connection(self):
+        ''' Create the AMI connection '''
+        self.create_ami_factory(count=1)
+
+    def ami_connect(self, ami):
+        ''' Override of TestCase ami_connect
+        Called when an AMI connection is made
+
+        :param ami The AMI factory
+        '''
+        ami.registerEvent('Newchannel', self._new_channel_handler)
+        ami.registerEvent('Hangup', self._hangup_handler)
+        self.execute_test()
+
+    def _new_channel_handler(self, ami, event):
+        ''' Handler for new channels
+
+        :param ami The AMI instance
+        :param event The Newchannl event
+        '''
+        LOGGER.debug('Tracking channel %s' % event['channel'])
+        self.channels.append(event['channel'])
+
+    def _hangup_handler(self, ami, event):
+        ''' Handler for channel hangup
+
+        :param ami The AMI instance
+        :param event Hangup event
+        '''
+        LOGGER.debug('Removing tracking for %s' % event['channel'])
+        self.channels.remove(event['channel'])
+        if len(self.channels) == 0:
+            self.test_iteration += 1
+            self.execute_test()
+
+    def execute_test(self):
+        ''' Execute the current iteration of the test '''
+
+        if (self.test_iteration == len(self.iterations)):
+            LOGGER.info('All iterations executed; stopping')
+            self.stop_reactor()
+            return
+
+        iteration = self.iterations[self.test_iteration]
+        if isinstance(iteration, list):
+            for channel in iteration:
+                self._spawn_channel(channel)
+        else:
+            self._spawn_channel(iteration)
+
+    def _spawn_channel(self, channel_def):
+        ''' Create a new channel '''
+
+        # There's only one Asterisk instance, so just use the first AMI factory
+        LOGGER.info('Creating channel %s' % channel_def['channel'])
+        self.ami[0].originate(**channel_def).addErrback(self.handleOriginateFailure)
+
+
 class WebSocketEventModule(object):
     '''Module for capturing events from the ARI WebSocket
     '''
@@ -46,35 +182,20 @@
         :param module_config: Configuration dict parse from test-config.yaml.
         :param test_object: Test control object.
         '''
-        logger.debug("WebSocketEventModule(%r)", test_object)
-        self.host = '127.0.0.1'
-        self.port = DEFAULT_PORT
-        self.test_object = test_object
-        username = module_config.get('username') or 'testsuite'
-        password = module_config.get('password') or 'testsuite'
-        userpass = (username, password)
-        #: ARI interface object
-        self.ari = ARI(self.host, port=self.port, userpass=userpass)
-        #: Matchers for incoming events
+        self.ari = test_object.ari
         self.event_matchers = [
             EventMatcher(self.ari, e, test_object)
             for e in module_config['events']]
-        self.event_matchers.append(EventMatcher(self.ari, ValidationMatcher,
+        self.event_matchers.append(EventMatcher(self.ari, VALIDATION_MATCHER,
                                                 test_object))
-        apps = module_config.get('apps') or 'testsuite'
-        if isinstance(apps, list):
-            apps = ','.join(apps)
-        #: Twisted protocol factory for ARI WebSockets
-        self.factory = AriClientFactory(host=self.host, port=self.port,
-                                        apps=apps, on_event=self.on_event,
-                                        userpass=userpass)
+        test_object.register_ws_event_handler(self.on_event)
 
     def on_event(self, event):
         '''Handle incoming events from the WebSocket.
 
         :param event: Dictionary parsed from incoming JSON event.
         '''
-        logger.error('%r' % event)
+        LOGGER.debug('Received event: %r' % event.get('type'))
         for matcher in self.event_matchers:
             matcher.on_event(event)
 
@@ -82,42 +203,39 @@
 class AriClientFactory(WebSocketClientFactory):
     '''Twisted protocol factory for building ARI WebSocket clients.
     '''
-    def __init__(self, host, apps, on_event, userpass, port=DEFAULT_PORT,
-                 timeout_secs=60):
+    def __init__(self, receiver, host, apps, userpass, port=DEFAULT_PORT,
+        timeout_secs=60):
         '''Constructor
 
+        :param receiver The object that will receive events from the protocol
         :param host: Hostname of Asterisk.
         :param apps: App names to subscribe to.
-        :param on_event: Callback to invoke for all received events.
         :param port: Port of Asterisk web server.
         :param timeout_secs: Maximum time to try to connect to Asterisk.
         '''
         url = "ws://%s:%d/ari/events?%s" % \
               (host, port,
                urllib.urlencode({'app': apps, 'api_key': '%s:%s' % userpass}))
-        logger.info("WebSocketClientFactory(url=%s)" % url)
-        WebSocketClientFactory.__init__(self, url, debug = True, protocols=['ari'])
-        self.on_event = on_event
+        LOGGER.info("WebSocketClientFactory(url=%s)" % url)
+        WebSocketClientFactory.__init__(self, url, debug = True,
+            protocols=['ari'])
         self.timeout_secs = timeout_secs
-        self.protocol = self.__build_protocol
         self.attempts = 0
         self.start = None
-
+        self.receiver = receiver
+
+    def buildProtocol(self, addr):
+        ''' Make the protocol '''
+        return AriClientProtocol(self.receiver, self)
+
+    def clientConnectionFailed(self, connector, reason):
+        ''' Doh, connection lost '''
+        LOGGER.debug('Connection lost; attempting again in 1 second')
+        reactor.callLater(1, self.reconnect)
+
+    def connect(self):
+        ''' Start the connection '''
         self.reconnect()
-
-    def __build_protocol(self):
-        '''Build a client protocol instance
-        '''
-        return AriClientProtocol(self.on_event)
-
-    def clientConnectionFailed(self, connector, reason):
-        '''Callback when client connection failed to connect.
-
-        :param connector: Twisted connector.
-        :param reason: Failure reason.
-        '''
-        logger.info("clientConnectionFailed(%s)" % (reason))
-        reactor.callLater(1, self.reconnect)
 
     def reconnect(self):
         '''Attempt to reconnect the ARI WebSocket.
@@ -125,13 +243,13 @@
         This call will give up after timeout_secs has been exceeded.
         '''
         self.attempts += 1
-        logger.debug("WebSocket attempt #%d" % self.attempts)
+        LOGGER.debug("WebSocket attempt #%d" % self.attempts)
         if not self.start:
             self.start = datetime.datetime.now()
         runtime = (datetime.datetime.now() - self.start).seconds
         if runtime >= self.timeout_secs:
-            logger.error("  Giving up after %d seconds" % self.timeout_secs)
-            return
+            LOGGER.error("  Giving up after %d seconds" % self.timeout_secs)
+            raise Exception("Failed to connect after %d seconds" % self.timeout_secs)
 
         connectWS(self)
 
@@ -139,31 +257,35 @@
 class AriClientProtocol(WebSocketClientProtocol):
     '''Twisted protocol for handling a ARI WebSocket connection.
     '''
-    def __init__(self, on_event):
+    def __init__(self, receiver, factory):
         '''Constructor.
 
-        :param on_event: Callback to invoke with each parsed event.
-        '''
-        self.on_event = on_event
+        :param receiver The event receiver
+        '''
+        LOGGER.debug('Made me a client protocol!')
+        self.receiver = receiver
+        self.factory = factory
 
     def onOpen(self):
         '''Called back when connection is open.
         '''
-        logger.debug("onOpen()")
+        LOGGER.debug('WebSocket Open')
+        self.receiver.on_ws_open(self)
 
     def onClose(self, wasClean, code, reason):
         '''Called back when connection is closed.
         '''
-        logger.debug("onClose(%r, %d, %s)" % (wasClean, code, reason))
-        reactor.callLater(1, self.factory.reconnect)
+        LOGGER.debug("WebSocket closed(%r, %d, %s)" % (wasClean, code, reason))
+        self.receiver.on_ws_closed(self)
 
     def onMessage(self, msg, binary):
         '''Called back when message is received.
 
         :param msg: Received text message.
         '''
-        logger.info("rxed: %s" % msg)
-        self.on_event(json.loads(msg))
+        LOGGER.debug("rxed: %s" % msg)
+        msg = json.loads(msg)
+        self.receiver.on_ws_event(msg)
 
 
 class ARI(object):
@@ -200,7 +322,7 @@
         :throws: requests.exceptions.HTTPError
         '''
         url = self.build_url(*args)
-        logger.info("GET %s %r" % (url, kwargs))
+        LOGGER.info("GET %s %r" % (url, kwargs))
         return raise_on_err(requests.get(url, params=kwargs,
                                          auth=self.userpass))
 
@@ -213,7 +335,7 @@
         :throws: requests.exceptions.HTTPError
         '''
         url = self.build_url(*args, **kwargs)
-        logger.info("POST %s %r" % (url, kwargs))
+        LOGGER.info("POST %s %r" % (url, kwargs))
         return raise_on_err(requests.post(url, params=kwargs,
                                           auth=self.userpass))
 
@@ -226,7 +348,7 @@
         :throws: requests.exceptions.HTTPError
         '''
         url = self.build_url(*args, **kwargs)
-        logger.info("DELETE %s %r" % (url, kwargs))
+        LOGGER.info("DELETE %s %r" % (url, kwargs))
         return raise_on_err(requests.delete(url, params=kwargs,
                                             auth=self.userpass))
 
@@ -273,11 +395,11 @@
             try:
                 res = self.callback(self.ari, message)
                 if not res:
-                    logger.error("Callback failed: %r" %
+                    LOGGER.error("Callback failed: %r" %
                                  self.instance_config)
                     self.passed = False
             except:
-                logger.error("Exception in callback: %s" %
+                LOGGER.error("Exception in callback: %s" %
                              traceback.format_exc())
                 self.passed = False
 
@@ -287,7 +409,7 @@
         :param args: Ignored arguments.
         '''
         if not self.count_range.contains(self.count):
-            logger.error("Expected %d <= count <= %d; was %d (%r)",
+            LOGGER.error("Expected %d <= count <= %d; was %d (%r)",
                          self.count_range.min, self.count_range.max,
                          self.count, self.conditions)
             self.passed = False
@@ -316,9 +438,9 @@
     :param message: Message to compare.
     :returns: True if message matches pattern; False otherwise.
     '''
-    #logger.debug("%r ?= %r" % (pattern, message))
-    #logger.debug("  %r" % type(pattern))
-    #logger.debug("  %r" % type(message))
+    #LOGGER.debug("%r ?= %r" % (pattern, message))
+    #LOGGER.debug("  %r" % type(pattern))
+    #LOGGER.debug("  %r" % type(message))
     if pattern is None:
         # Empty pattern always matches
         return True
@@ -345,7 +467,7 @@
         # Integers are literal matches
         return pattern == message
     else:
-        logger.error("Unhandled pattern type %s" % type(pattern)).__name__
+        LOGGER.error("Unhandled pattern type %s" % type(pattern)).__name__
 
 
 class Range(object):

Modified: asterisk/trunk/sample-yaml/ari-config.yaml.sample
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/sample-yaml/ari-config.yaml.sample?view=diff&rev=4065&r1=4064&r2=4065
==============================================================================
--- asterisk/trunk/sample-yaml/ari-config.yaml.sample (original)
+++ asterisk/trunk/sample-yaml/ari-config.yaml.sample Thu Aug 22 12:43:07 2013
@@ -1,10 +1,39 @@
 # -*- yaml -*-
 # Configuration sample for the Generic ARI module
 
-ari-config:
+# Configuration for ari.AriTestObject
+#
+# A test object that creates an ARI connection and an AMI connection. The test object
+# creates a number of channels and, when all channels have been created/hungup, stops
+# the test.
+test-object-config:
+    # The host to connect to. Default is 127.0.0.1
+    host: 127.0.0.1
+
+    # The port to connect to. Default is 8088.
+    port: 8088
+
+    # The username to use for the connection. Default is testsuite
+    username: testsuite
+
+    # The password to use for the connection. Default is testsuite
+    password: testsuite
+
     # ARI always attempts to connect to the /ari/events WebSocket.
     # This is a comma seperated list of applications to connect for.
+    # If not provided, default is 'testsuite'
     apps: foo-test
+
+    # Define the channels to create for the test. If not specified, a single Local
+    # channel will be created to s at default, tied to the Echo application. Channels
+    # can be created in parallel for a single iteration if a list is provided.
+    test-iterations:
+        - { channel: Local/foo at bar, context: default, exten: yackity, priority: 1}
+        - [ { channel: Local/foo at bar, application: Echo, async: True},
+            { channel: Local/yackity at schmackity, application: Echo, async: True} ]
+
+# Pluggable module that listens for events over the ARI WebSocket
+ari-config:
     # List of events to monitor for. Every event received on the WebSocket
     # is compared against the 'conditions' clause. If the conditions match,
     # then the further specified processing is evaluated.
@@ -34,7 +63,6 @@
                         # block sequences can also be used for arrays
                         args:
                             - 'barman.*'
-
 
             # The above example would match the following:
             #  { 'application': 'foo-test', 'args': [ 'bar' ] }

Modified: asterisk/trunk/tests/rest_api/continue/configs/ast1/extensions.conf
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/rest_api/continue/configs/ast1/extensions.conf?view=diff&rev=4065&r1=4064&r2=4065
==============================================================================
--- asterisk/trunk/tests/rest_api/continue/configs/ast1/extensions.conf (original)
+++ asterisk/trunk/tests/rest_api/continue/configs/ast1/extensions.conf Thu Aug 22 12:43:07 2013
@@ -2,6 +2,6 @@
 
 exten => s,1,NoOp()
 	same => n,Answer()
-	same => n,Stasis(continue-test)
-	same => n,Stasis(continue-test,fin)
+	same => n,Stasis(testsuite)
+	same => n,Stasis(testsuite,fin)
 	same => n,Hangup()

Modified: asterisk/trunk/tests/rest_api/continue/rest_continue.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/rest_api/continue/rest_continue.py?view=diff&rev=4065&r1=4064&r2=4065
==============================================================================
--- asterisk/trunk/tests/rest_api/continue/rest_continue.py (original)
+++ asterisk/trunk/tests/rest_api/continue/rest_continue.py Thu Aug 22 12:43:07 2013
@@ -16,7 +16,7 @@
 def on_start(ari, event):
     logger.debug("on_start(%r)" % event)
     global id
-    id = event['channel']['uniqueid']
+    id = event['channel']['id']
     ari.post('channels', id, 'continue')
     return True
 
@@ -24,11 +24,11 @@
 def on_end(ari, event):
     global id
     logger.debug("on_end(%r)" % event)
-    return id == event['channel']['uniqueid']
+    return id == event['channel']['id']
 
 
 def on_second_start(ari, event):
     global id
     logger.debug("on_second_start(%r)" % event)
     ari.delete('channels', id)
-    return id == event['channel']['uniqueid']
+    return id == event['channel']['id']

Modified: asterisk/trunk/tests/rest_api/continue/test-config.yaml
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/rest_api/continue/test-config.yaml?view=diff&rev=4065&r1=4064&r2=4065
==============================================================================
--- asterisk/trunk/tests/rest_api/continue/test-config.yaml (original)
+++ asterisk/trunk/tests/rest_api/continue/test-config.yaml Thu Aug 22 12:43:07 2013
@@ -8,24 +8,20 @@
     add-test-to-search-path: True
     test-object:
         config-section: test-object-config
-        typename: SimpleTestCase.SimpleTestCase
+        typename: ari.AriTestObject
     modules:
         -   config-section: ari-config
             typename: ari.WebSocketEventModule
 
 test-object-config:
-    spawn-after-hangup: True
-    test-iterations:
-        -   channel: Local/s at default
-            application: Echo
+
 
 ari-config:
-    apps: continue-test
     events:
         -   conditions:
                 match:
                     type: StasisStart
-                    application: continue-test
+                    application: testsuite
                     args: []
             count: 1
             callback:
@@ -34,7 +30,7 @@
         -   conditions:
                 match:
                     type: StasisEnd
-                    application: continue-test
+                    application: testsuite
             count: 2
             callback:
                 module: rest_continue
@@ -42,7 +38,7 @@
         -   conditions:
                 match:
                     type: StasisStart
-                    application: continue-test
+                    application: testsuite
                     args: [fin]
             count: 1
             callback:




More information about the asterisk-commits mailing list