[Asterisk-code-review] SIP: Rewrite tcpauthlimit test in Python (testsuite[master])

Ashley Sanders asteriskteam at digium.com
Sun Aug 9 21:59:43 CDT 2015


Ashley Sanders has uploaded a new change for review.

  https://gerrit.asterisk.org/1055

Change subject: SIP: Rewrite tcpauthlimit test in Python
......................................................................

SIP: Rewrite tcpauthlimit test in Python

What was formerly a Lua test has been ported to a Python such that the
test can now be executed from the Jenkins build agents. Functionally,
the test is identical to its predecessor.

The test is composed of two types of scenarios: SIP Client and SIPp.

The SIP Client scenarios attempt to create n+1 TCP socket connections to
Asterisk, where n is the value of the tcpauthlimit property in sip.conf.
If the tcpauthlimit property is honored, the (n+1)th socket connection
will fail.

The SIPp scenarios attempt to create n*2 SIPp processes, where n is the
value of the tcpauthlimit property in sip.conf. Each SIPp scenario is
configured to connect to the same Asterisk host. If the tcpauthlimit
property is honored, only n of these scenarios will pass, while the
remaining n will fail.

ASTERISK-25225
Reported by Matt Jordan

Change-Id: Ica28ba0ca7ae92b3546da4cd23458f289c111d36
---
M lib/python/asterisk/sipp.py
A tests/channels/SIP/tcpauthlimit/configs/ast1/extensions.conf
A tests/channels/SIP/tcpauthlimit/configs/ast1/sip.conf
D tests/channels/SIP/tcpauthlimit/run-test
A tests/channels/SIP/tcpauthlimit/sip_client_scenario.py
A tests/channels/SIP/tcpauthlimit/sipp/inject_minion_bob.csv
A tests/channels/SIP/tcpauthlimit/sipp/inject_minion_dave.csv
A tests/channels/SIP/tcpauthlimit/sipp/inject_minion_jerry.csv
A tests/channels/SIP/tcpauthlimit/sipp/inject_minion_jon.csv
A tests/channels/SIP/tcpauthlimit/sipp/inject_minion_jorge.csv
A tests/channels/SIP/tcpauthlimit/sipp/inject_minion_kevin.csv
A tests/channels/SIP/tcpauthlimit/sipp/inject_minion_mark.csv
A tests/channels/SIP/tcpauthlimit/sipp/inject_minion_phil.csv
A tests/channels/SIP/tcpauthlimit/sipp/inject_minion_stuart.csv
A tests/channels/SIP/tcpauthlimit/sipp/inject_minion_tim.csv
A tests/channels/SIP/tcpauthlimit/sipp/uac.xml
A tests/channels/SIP/tcpauthlimit/sipp_scenario_wrapper.py
A tests/channels/SIP/tcpauthlimit/tcpauthlimit.py
M tests/channels/SIP/tcpauthlimit/test-config.yaml
D tests/channels/SIP/tcpauthlimit/test.lua
20 files changed, 1,170 insertions(+), 128 deletions(-)


  git pull ssh://gerrit.asterisk.org:29418/testsuite refs/changes/55/1055/1

diff --git a/lib/python/asterisk/sipp.py b/lib/python/asterisk/sipp.py
index ff959e0..4e157b6 100644
--- a/lib/python/asterisk/sipp.py
+++ b/lib/python/asterisk/sipp.py
@@ -484,7 +484,7 @@
             for msg in self.stderr:
                 LOGGER.warn(msg)
         else:
-            message = "SIPp scenario %s ended " % self._name
+            message = "SIPp scenario %s ended" % self._name
         try:
             if not self._stop_deferred.called:
                 self._stop_deferred.callback(self)
@@ -546,6 +546,7 @@
         self.sipp = test_suite_utils.which("sipp")
         self.passed = False
         self.exited = False
+        self.result = None
         self._process = None
         self.target = target
         self._our_exit_deferred = None
@@ -578,6 +579,7 @@
         def __scenario_callback(result):
             """Callback called when a scenario completes"""
             self.exited = True
+            self.result = result
             if (result.exitcode == 0):
                 self.passed = True
                 LOGGER.info("SIPp Scenario %s Exited" %
@@ -598,6 +600,7 @@
                 self._test_case.stop_reactor()
             return result
 
+        self.result = None
         sipp_args = [
             self.sipp, self.target,
             '-sf',
diff --git a/tests/channels/SIP/tcpauthlimit/configs/ast1/extensions.conf b/tests/channels/SIP/tcpauthlimit/configs/ast1/extensions.conf
new file mode 100644
index 0000000..bd65149
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/configs/ast1/extensions.conf
@@ -0,0 +1,7 @@
+[general]
+
+[default]
+exten => echo,1,NoOp()
+	same=> n,Answer()
+	same=> n,Echo()
+	same=> n,Hangup()
diff --git a/tests/channels/SIP/tcpauthlimit/configs/ast1/sip.conf b/tests/channels/SIP/tcpauthlimit/configs/ast1/sip.conf
new file mode 100644
index 0000000..27e026f
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/configs/ast1/sip.conf
@@ -0,0 +1,49 @@
+[general]
+tcpenable=yes
+tcpbindaddr=127.0.0.1:5060
+sipdebug=yes
+tcpauthlimit=5
+
+[minion-template](!)
+type=peer
+context=default
+
+[minion_bob](minion-template)
+host=127.0.0.2
+port=5062
+
+[minion_dave](minion-template)
+host=127.0.0.3
+port=5063
+
+[minion_jerry](minion-template)
+host=127.0.0.4
+port=5064
+
+[jon_minion](minion-template)
+host=127.0.0.5
+port=5065
+
+[minion_jorge](minion-template)
+host=127.0.0.6
+port=5066
+
+[minion_kevin](minion-template)
+host=127.0.0.7
+port=5067
+
+[minion_mark](minion-template)
+host=127.0.0.8
+port=5068
+
+[minion_phil](minion-template)
+host=127.0.0.9
+port=5069
+
+[minion_stuart](minion-template)
+host=127.0.0.10
+port=5070
+
+[minion_tim](minion-template)
+host=127.0.0.11
+port=5071
diff --git a/tests/channels/SIP/tcpauthlimit/run-test b/tests/channels/SIP/tcpauthlimit/run-test
deleted file mode 100755
index 9a12d98..0000000
--- a/tests/channels/SIP/tcpauthlimit/run-test
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env bash
-set -e
-. lib/sh/lua.sh
-asttest -a /$AST_TEST_ROOT -s `dirname $0` $@
diff --git a/tests/channels/SIP/tcpauthlimit/sip_client_scenario.py b/tests/channels/SIP/tcpauthlimit/sip_client_scenario.py
new file mode 100644
index 0000000..f41d5b8
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sip_client_scenario.py
@@ -0,0 +1,480 @@
+'''
+Copyright (C) 2015, Digium, Inc.
+Ashley Sanders <asanders at digium.com>
+
+This program is free software, distributed under the terms of
+the GNU General Public License Version 2.
+'''
+
+import sys
+import logging
+
+sys.path.append("lib/python/asterisk")
+sys.path.append("tests/channels/SIP")
+
+from twisted.internet import defer, reactor
+from twisted.internet.protocol import ClientFactory, Protocol
+
+LOGGER = logging.getLogger(__name__)
+
+
+class SipClient(Protocol):
+    """Client connection protocol."""
+
+    def __init__(self, addr, on_connect, client_id=None):
+        """Constructor.
+
+        Keyword Arguments:
+        addr                   -- The client address as a
+                                  twisted.internet.interfaces.IAddress.
+        on_connect             -- Callback to invoke when the client connection
+                                  is established.
+        """
+
+        self.__addr = addr
+        self.__on_connect = on_connect
+        self.__id = client_id
+
+    def connectionMade(self):
+        """Called when a connection is made.
+
+        Overrides twisted.internet.protocol.Protocol.connectionMade
+        """
+
+        msg = '{0} connection was successfully established.'.format(self)
+        LOGGER.debug(msg)
+
+        self.__on_connect()
+        Protocol.connectionMade(self)
+
+        msg = '%d: An apple a day keeps the doctor away\r\n'
+        for i in range(0, 1000):
+            self.transport.write(msg % i)
+
+    def disconnect(self):
+        """Disconnects the client connection."""
+
+        msg = '{0} disconnecting.'.format(self)
+        LOGGER.debug(msg)
+        self.transport.loseConnection()
+
+    def __format__(self, format_spec):
+        """Overrides default format handling for 'self'."""
+
+        return self.__class__.__name__  + ' [' + self.__id + ']:'
+
+class SipClientFactory(ClientFactory):
+    """Factory for building SIP webSocket clients."""
+
+    def __init__(self, on_connecting_changed, on_connection_lost, client_id):
+        """Constructor.
+
+        Keyword arguments:
+        on_connecting_changed  -- Callback to invoke when the client protocol
+                                  connection state changes.
+        on_connection_lost     -- Callback to invoke when the client protocol
+                                  destroys its transport connection.
+        client_id              -- The id to give to built clients.
+        """
+
+        self.__on_connecting_changed = on_connecting_changed
+        self.__on_connection_lost = on_connection_lost
+        self.__id = client_id
+
+        self.__protocol = None
+        self.__connected = None
+
+    def buildProtocol(self, addr):
+        """Twisted overload used to create the client connection.
+
+        Overrides twisted.internet.protocol.ClientFactory.buildProtocol
+
+        Keyword Arguments:
+        addr                   -- The client address as a
+                                  twisted.internet.interfaces.IAddress.
+        """
+
+        msg = '{0} Building a SipClient protocol.'.format(self)
+        LOGGER.debug(msg)
+
+        if self.__protocol is not None:
+            self.disconnect()
+
+        self.__protocol = SipClient(addr, self.__on_client_connection_made)
+        return self.__protocol
+
+    def clientConnectionFailed(self, connector, reason):
+        """Called when a client has failed to connect.
+
+        Overrides twisted.internet.protocol.ClientFactory.clientConnectionFailed
+
+        Keyword Arguments:
+        connector              -- The TCP connector.
+        reason                 -- The failure reason.
+        """
+
+        msg = '{0} Failed to establish a connection.'.format(self)
+        self.__on_client_state_change(False,
+                                      msg,
+                                      self.__on_connecting_changed,
+                                      reason)
+        ClientFactory.clientConnectionFailed(self, connector, reason)
+
+    def clientConnectionLost(self, connector, reason):
+        """Called when an established connection is lost.
+
+        Overrides twisted.internet.protocol.ClientFactory.clientConnectionLost
+
+        Keyword Arguments:
+        connector              -- The TCP connector.
+        reason                 -- The failure reason.
+        """
+
+        msg = '{0} Connection has been lost.'.format(self)
+        self.__on_client_state_change(False,
+                                      msg,
+                                      self.__on_connection_lost,
+                                      reason)
+        ClientFactory.clientConnectionLost(self, connector, reason)
+
+    def disconnect(self):
+        """Disconnects the client protocol."""
+
+        msg = '{0} Destroying transport connection.'.format(self)
+        LOGGER.debug(msg)
+        self.__protocol.disconnect()
+
+    def __format__(self, format_spec):
+        """Overrides default format handling for 'self'."""
+
+        return self.__class__.__name__ + ' [' + self.client_id + ']:'
+
+    def __on_client_connection_made(self):
+        """Handles the connectionMade event from the client protocol."""
+
+        msg = '{0} Connection has been established.'.format(self)
+        self.__on_client_state_change(True,
+                                      msg,
+                                      self.__on_connecting_changed,
+                                      self)
+
+    def __on_client_state_change(self, connected, message, callback, payload):
+        """Handles connection state changes.
+
+        Keyword Arguments:
+        connected              -- Whether or not the state change resulted in
+                                  the client protocol being connected.
+        message                -- The message to write to the log.
+        callback               -- The callback to invoke for notifing the
+                                  observer of the state change.
+        payload                -- The callback payload.
+        """
+
+        self.__connected = connected
+        LOGGER.debug(message)
+        callback(payload)
+
+    @property
+    def connecting(self):
+        """Whether or not the the client is trying to establish a connection.
+
+        Returns:
+        True if the client has finished connecting, False otherwise.
+        """
+
+        return self.__connected is None
+
+    @property
+    def connected(self):
+        """Whether or not the client protocol is connected.
+
+        Returns:
+        True if the client is connected. Otherwise, returns False.
+        """
+
+        return False if self.connecting else self.__connected
+
+    @property
+    def client_id(self):
+        """Returns the id for the client protocol."""
+
+        return self.__id
+
+
+class SipClientScenario(object):
+    """The test scenario for testing SIP socket creation.
+
+    This scenario confirms that Asterisk honors its tcpauthlimit property by
+    trying to create more SIP sockets than the configuration specifies as the
+    limit.
+    """
+
+    def __init__(self, scenario_id, skip=None, host='127.0.0.1', port=5060,
+                 allowed_connections=0):
+        """Constructor.
+
+        Keyword Arguments:
+        scenario_id            -- The id for this scenario. Used for logging.
+        skip                   -- A message to display if the scenario is to
+                                  be suspended from executing.
+                                  Optional. Default: None.
+        host                   -- The remote host address to use for a client
+                                  connection. Optional. Default: 127.0.0.1.
+        port                   -- The remote host port to use for a client
+                                  connection. Optional. Default: 5060.
+        allowed_connections    -- The number of clients allowed by chan_sip.
+                                  Optional. Default: 0.
+        """
+
+        self.__scenario_id = scenario_id
+        self.__status = skip
+
+        self.__host = host
+        self.__port = port
+        self.__allowed_connections = allowed_connections
+
+        self.__passed = None
+        self.__started = None
+        self.__stopping = None
+
+        self.__clients = None
+        self.__iterator = None
+
+        self.__on_started = None
+        self.__on_complete = None
+
+        self.__init_results()
+
+    def __disconnect_client(self, client):
+        """Disconnects the given client.
+
+        Keyword Arguments:
+        client             -- The client to disconnect.
+        """
+
+        if client is None and self.__stopping:
+            self.__stopping = False
+            return
+
+        msg = '{0} Disconnecting client[{1}]...'.format(self, client.client_id)
+        LOGGER.debug(msg)
+        client.disconnect()
+
+    def __evaluate_results(self, message):
+        """Evaluates the test scenario results.
+
+        Keyword Arguments:
+        message                -- The event payload.
+        """
+
+        msg = '{0} Evaluating scenario results...'.format(self)
+        LOGGER.debug(msg)
+
+        connected_clients = sum(client.connected for client in self.__clients)
+        self.passed = connected_clients == self.__allowed_connections
+
+        if not self.passed:
+            msg = '{0} Scenario failed.\n'
+            msg += '\tBased on the test configuration, I expected to receive:\n'
+            msg += '\t\t{1} client connections.\n'
+            msg += '\tHere is what I actually received:\n'
+            msg += '\t\t{2} client connections.'
+            LOGGER.error(msg.format(self,
+                                    self.__allowed_connections,
+                                    connected_clients))
+        else:
+            LOGGER.info('{0} Congrats! The scenario passed.'.format(self))
+
+        self.__on_complete.callback(self)
+        return message
+
+    def __format__(self, format_spec):
+        """Overrides default format handling for 'self'."""
+
+        return self.__class__.__name__ + ':'
+
+    def __get_next_client(self):
+        """Tries to get the next client from the list.
+
+        Returns:
+        A client if the iteration is not complete. Else returns None.
+        """
+
+        client = None
+        try:
+            client = self.__iterator.next()
+        except StopIteration:
+            if self.__stopping:
+                self.__clients = []
+        return client
+
+    def __init_results(self):
+        """Initialize the scenario results."""
+
+        self.__passed = None
+        self.__started = False
+        self.__stopping = False
+
+        self.__clients = self.__initialize_clients()
+        self.__iterator = iter(self.__clients)
+
+        self.__on_started = defer.Deferred()
+        self.__on_complete = defer.Deferred()
+
+    def __initialize_clients(self):
+        """Builds the SIP Client for this scenario.
+
+        Returns:
+        A list of n SipClientFactory instances, where n equals
+        self.__allowed_connections + 1.
+        """
+
+        clients = list()
+
+        for i in range(0, self.__allowed_connections + 1):
+            factory = SipClientFactory(self.__on_client_state_change,
+                                       self.__on_client_disconnect,
+                                       i)
+            clients.append(factory)
+
+        return clients
+
+    def __on_client_disconnect(self, sender):
+        """Handles a client disconnect event.
+
+        Keyword Arguments:
+        sender             -- The client that was disconnected.
+        """
+
+        client = sender.client_id
+        LOGGER.debug('{0} Client[{1}] is disconnected.'.format(self,
+                                                               client))
+
+        if self.__stopping:
+            self.__disconnect_client(self.__get_next_client())
+
+    def __on_client_state_change(self, sender):
+        """Handler for client connection state changes.
+
+        Keyword Arguments:
+        sender                 -- The client who sent the notification.
+        """
+
+        if self.__started:
+            return
+
+        msg = '{0} Received a state change notification from client [{1}].'
+        LOGGER.debug(msg.format(self, sender.client_id))
+
+        if len(self.__clients) < sender.client_id:
+            msg ='{0} Ruh-roh.'
+            msg += ' I received a message for a client that I didn\'t create.'
+            LOGGER.warning(msg.format(self))
+            return
+
+        if all(client.connecting is False for client in self.__clients):
+            LOGGER.debug('{0} All clients have started.'.format(self))
+            self.__started = True
+            self.__on_started.callback(self)
+
+    def __on_scenario_teardown(self, message):
+        """Disconnects the clients.
+
+        Keyword Arguments:
+        message                -- The event payload.
+        """
+
+        LOGGER.debug('{0} Tearing down the scenario.'.format(self))
+        self.__stopping = True
+
+        deferred = defer.Deferred()
+        deferred.addCallback(self.__disconnect_client)
+        deferred.callback(self.__get_next_client())
+
+        return message
+
+    def run(self):
+        """Runs the SIP client scenario.
+
+        Returns:
+        A twisted.internet.defer.Deferred instance that can be used to
+        determine when the scenario is complete.
+        """
+
+        if self.suspended:
+            LOGGER.debug('{0} Scenario suspended; nothing to do.'.format(self))
+            self.passed = False
+            return None
+
+        LOGGER.debug('{0} Starting scenario...'.format(self))
+
+        self.__init_results()
+        self.__on_started.addCallback(self.__evaluate_results)
+        self.__on_started.addCallback(self.__on_scenario_teardown)
+
+        self.__start_clients()
+        return self.__on_complete
+
+    def __start_clients(self):
+        """Starts connection process for this scenario's clients."""
+
+        msg = '{0} Attempting to connect {1} clients to Asterisk...'
+        LOGGER.debug(msg.format(self, len(self.__clients)))
+
+        for client in self.__clients:
+            reactor.connectTCP(self.__host, self.__port, client)
+
+    @property
+    def finished(self):
+        """Whether or not the this scenario has finished execution.
+
+        Returns:
+        True if the scenario has finished execution. False otherwise.
+        """
+
+        return self.__passed is not None
+
+    @property
+    def passed(self):
+        """The results of the scenario.
+
+        Returns:
+        False if the scenario has not finished execution. Else, True if the
+        scenario was successful. False otherwise.
+        """
+
+        if self.suspended:
+            return False
+
+        return False if not self.finished else self.__passed
+
+    @passed.setter
+    def passed(self, value):
+        """Safely set the pass/fail value for this scenario."""
+
+        if self.__passed is False:
+            return
+
+        self.__passed = value
+
+    @property
+    def scenario_id(self):
+        """Returns the id for this scenario."""
+
+        return self.__scenario_id
+
+    @property
+    def status(self):
+        """Returns a message indicating the suspended status of the scenario."""
+
+        return self.__status
+
+    @property
+    def suspended(self):
+        """Whether or not the scenario has been suspended from execution.
+
+        Returns:
+        True if the scenario is suspended, False otherwise.
+        """
+
+        return self.status is not None
diff --git a/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_bob.csv b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_bob.csv
new file mode 100644
index 0000000..028fc04
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_bob.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_bob
diff --git a/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_dave.csv b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_dave.csv
new file mode 100644
index 0000000..ed2f641
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_dave.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_dave
diff --git a/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_jerry.csv b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_jerry.csv
new file mode 100644
index 0000000..c23e60b
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_jerry.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_jerry
diff --git a/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_jon.csv b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_jon.csv
new file mode 100644
index 0000000..75dc2e8
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_jon.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+jon_minion
diff --git a/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_jorge.csv b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_jorge.csv
new file mode 100644
index 0000000..f0f006c
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_jorge.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_jorge
diff --git a/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_kevin.csv b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_kevin.csv
new file mode 100644
index 0000000..0420e06
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_kevin.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_kevin
diff --git a/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_mark.csv b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_mark.csv
new file mode 100644
index 0000000..173e106
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_mark.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_mark
diff --git a/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_phil.csv b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_phil.csv
new file mode 100644
index 0000000..d4ecb1e
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_phil.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_phil
diff --git a/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_stuart.csv b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_stuart.csv
new file mode 100644
index 0000000..8fbc75d
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_stuart.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_stuart
diff --git a/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_tim.csv b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_tim.csv
new file mode 100644
index 0000000..cad056a
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp/inject_minion_tim.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_tim
diff --git a/tests/channels/SIP/tcpauthlimit/sipp/uac.xml b/tests/channels/SIP/tcpauthlimit/sipp/uac.xml
new file mode 100644
index 0000000..dbccf07
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp/uac.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!DOCTYPE scenario SYSTEM "[field0].dtd">
+<scenario name="Basic Sipstone UAC">
+  <send retrans="500">
+    <![CDATA[
+
+      INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
+      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+      From: [field0] <sip:[field0]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
+      To: [service] <sip:[service]@[remote_ip]:[remote_port]>
+      Call-ID: [call_id]
+      CSeq: 1 INVITE
+      Contact: sip:[field0]@[local_ip]:[local_port]
+      Max-Forwards: 70
+      Subject: Performance Test
+      Content-Type: application/sdp
+      Content-Length: [len]
+
+      v=0
+      o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
+      s=-
+      c=IN IP[media_ip_type] [media_ip]
+      t=0 0
+      m=audio [media_port] RTP/AVP 0
+      a=rtpmap:0 PCMU/8000
+
+    ]]>
+  </send>
+
+  <recv response="100"
+        optional="true">
+  </recv>
+
+  <recv response="180" optional="true">
+  </recv>
+
+  <recv response="183" optional="true">
+  </recv>
+
+  <recv response="200" rtd="true">
+  </recv>
+
+  <send>
+    <![CDATA[
+
+      ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
+      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+      From: [field0] <sip:[field0]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
+      To: [service] <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
+      Call-ID: [call_id]
+      CSeq: 1 ACK
+      Contact: sip:[field0]@[local_ip]:[local_port]
+      Max-Forwards: 70
+      Subject: Performance Test
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <pause/>
+
+  <send retrans="500">
+    <![CDATA[
+
+      BYE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
+      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+      From: [field0] <sip:[field0]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
+      To: [service] <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
+      Call-ID: [call_id]
+      CSeq: 2 BYE
+      Contact: sip:[field0]@[local_ip]:[local_port]
+      Max-Forwards: 70
+      Subject: Performance Test
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv response="200" crlf="true">
+  </recv>
+</scenario>
+
diff --git a/tests/channels/SIP/tcpauthlimit/sipp_scenario_wrapper.py b/tests/channels/SIP/tcpauthlimit/sipp_scenario_wrapper.py
new file mode 100644
index 0000000..0f91d67
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_scenario_wrapper.py
@@ -0,0 +1,196 @@
+'''
+Copyright (C) 2015, Digium, Inc.
+Ashley Sanders <asanders at digium.com>
+
+This program is free software, distributed under the terms of
+the GNU General Public License Version 2.
+'''
+
+import sys
+import logging
+
+sys.path.append("lib/python/asterisk")
+sys.path.append("tests/channels/SIP")
+
+from sipp import SIPpScenario
+from twisted.internet import defer
+
+LOGGER = logging.getLogger(__name__)
+
+
+class SIPpScenarioWrapper(SIPpScenario):
+    """Wrapper class for a SIPpScenario.
+
+    This class provides the ability to skip running the SIPpScenario if
+    specified in the test module configuration.
+    """
+
+    def __init__(self, test_dir, scenario, positional_args=(),
+                 target='127.0.0.1', scenario_id=None, skip=None):
+        """Constructor.
+
+        Keyword Arguments:
+        test_dir               -- The path to the directory containing the test
+                                  module.
+        scenario               -- The SIPp scenario to execute (dictionary).
+        positional_args        -- SIPp non-standard parameters (those that can
+                                  be specified multiple times, or take multiple
+                                  arguments (iterable). Optional.
+                                  Default: Empty iterable.
+        target                 -- The address for the remote host. Optional.
+                                  Default: 127.0.0.1.
+        scenario_id            -- The scenario_id for this scenario. Used for
+                                  logging. If nothing is provided, the id will
+                                  be the value of scenario['scenario'].
+                                  Optional. Default: None.
+        skip                   -- A message to display if the scenario is to be
+                                  suspended from executing. Optional.
+                                  Default: None.
+        """
+
+        self.__scenario_id = scenario_id or scenario['scenario']
+        self.__status = skip
+
+        self.__running = False
+
+        self.__actual_result = None
+        self.__expected_result = None
+        self.__on_complete = None
+        self.__passed = None
+
+        self.__init_results()
+
+        SIPpScenario.__init__(self,
+                              test_dir,
+                              scenario,
+                              positional_args,
+                              target)
+
+    def adjust_result(self, expected_result=None):
+        """Adjusts the pass/fail status for the scenario.
+
+        Keyword Arguments:
+        expected_result        -- The expected SIPp exec result. If not
+                                  provided, the results will be analyzed as/is,
+                                  with no adjustments applied.
+                                  Optional. Default: None.
+        """
+
+        msg = '{0} Adjusting scenario results...'
+        LOGGER.debug(msg.format(self))
+
+        if self.__actual_result is None:
+            msg = '{0} Can\'t adjust results; {1}'
+            if self.__running:
+                msg.format(self, 'I am still running the SIPp scenario.')
+            else:
+                msg.format(self, 'I haven\'t run the SIPp scenario yet.')
+            LOGGER.debug(msg)
+            return
+
+        if expected_result is None:
+            self.__expected_result = self.__actual_result
+        else:
+            self.__expected_result = expected_result
+
+        if not self.passed:
+            msg = '{0} Scenario failed.\n'
+            msg += '\tBased on the test configuration,'
+            msg += ' I expected to receive:\n'
+            msg += '\t\tSIPp exit code:\t{1}\n'
+            msg += '\tHere is what I actually received:\n'
+            msg += '\t\tSIPp exit code:\t{2}\n'
+            LOGGER.error(msg.format(self,
+                                    self.__expected_result,
+                                    self.__actual_result))
+        else:
+            LOGGER.info('{0} Congrats! Scenario passed.'.format(self))
+
+    def __format__(self, format_spec):
+        """Overrides default format handling for 'self'."""
+
+        return self.__class__.__name__ + ' [' + self.__scenario_id + ']:'
+
+    def __init_results(self):
+        """Initialize the scenario results."""
+
+        self.__passed = False
+        self.__actual_result = None
+        self.__expected_result = None
+        self.__on_complete = defer.Deferred()
+
+    def run(self):
+        """Execute a SIPp scenario passed to this object.
+
+        Returns:
+        A twisted.internet.defer.Deferred instance that can be used to
+        determine when the SIPp Scenario has exited.
+        """
+
+        def __handle_results(result):
+            """Handles scenario results post-processing.
+
+            Keyword Arguments:
+            result             -- The SIPp exec result.
+            """
+
+            msg = '{0} SIPp execution complete.'
+            LOGGER.debug(msg.format(self))
+
+            self.__running = False
+            self.__actual_result = result.exitcode
+            self.__on_complete.callback(result)
+
+        self.__running = True
+        self.__init_results()
+
+        if self.suspended:
+            msg = '{0} I\'m suspended so I have nothing to do.'.format(self)
+            LOGGER.debug(msg)
+            return None
+
+        LOGGER.debug('{0} Starting SIPp execution'.format(self))
+        deferred = SIPpScenario.run(self)
+        deferred.addCallback(__handle_results)
+        return self.__on_complete
+
+    @property
+    def passed(self):
+        """Evaluates SIPp exit code for the pass/fail status.
+
+        Returns:
+        True if the SIPp exit code matched the expected result.
+        False otherwise.
+        """
+
+        if self.__actual_result is None or self.__expected_result is None:
+            return self.__passed
+        return self.__actual_result == self.__expected_result
+
+    @passed.setter
+    def passed(self, value):
+        """Overrides setting the pass/fail value for this scenario."""
+
+        self.__passed = value
+
+    @property
+    def scenario_id(self):
+        """Returns the scenario_id for this scenario."""
+
+        return self.__scenario_id
+
+    @property
+    def status(self):
+        """Returns a message indicating the suspended status of the scenario."""
+
+        return self.__status
+
+    @property
+    def suspended(self):
+        """Whether or not the scenario has been suspended from execution.
+
+        Returns:
+        True if the scenario is suspended, False otherwise.
+        """
+
+        return self.status is not None
diff --git a/tests/channels/SIP/tcpauthlimit/tcpauthlimit.py b/tests/channels/SIP/tcpauthlimit/tcpauthlimit.py
new file mode 100644
index 0000000..86bd91a
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/tcpauthlimit.py
@@ -0,0 +1,242 @@
+'''
+Copyright (C) 2015, Digium, Inc.
+Ashley Sanders <asanders at digium.com>
+
+This program is free software, distributed under the terms of
+the GNU General Public License Version 2.
+'''
+
+import sys
+import logging
+
+sys.path.append("lib/python")
+sys.path.append("tests/channels/SIP/tcpauthlimit")
+
+from sip_client_scenario import SipClientScenario
+from sipp_scenario_wrapper import SIPpScenarioWrapper
+from twisted.internet import defer
+
+LOGGER = logging.getLogger(__name__)
+
+
+class TcpAuthLimitTestModule(object):
+    """The test module.
+
+    This class serves as a harness for the test scenarios. It manages the
+    life-cycle of the the objects needed to execute the test plan.
+    """
+
+    def __init__(self, config, test_object):
+        """Constructor.
+
+        Keyword Arguments:
+        config                 -- The YAML configuration for this test.
+        test_object            -- The TestCaseModule instance for this test.
+        """
+
+        self.__test_object = test_object
+
+        self.__test_object.register_stop_observer(self.__on_asterisk_stop)
+        self.__test_object.register_ami_observer(self.__on_ami_connected)
+
+        self.__remote_host = config['remote-host'][0]
+        self.__tcpauthlimit = config['tcpauthlimit']
+
+        self.__scenarios = self.__build_scenarios(config['test-scenarios'])
+
+    def __build_scenarios(self, config_scenarios):
+        """Builds the scenarios.
+
+        Keyword Arguments:
+        config_scenarios       -- The test-scenarios section from the YAML
+                                  configuration.
+
+        Returns:
+        A list of scenarios on success. None on error.
+        """
+
+        scenarios = list()
+
+        msg = '{0} Building test scenarios.'
+        LOGGER.debug(msg.format(self))
+
+        remote_address = self.__remote_host['address']
+        remote_port = self.__remote_host['port']
+        tcpauthlimit = self.__tcpauthlimit
+
+        for config_scenario in config_scenarios:
+            scenario_type = config_scenario['type']
+            scenario_id = config_scenario.get('scenario-id') or None
+            skip = config_scenario.get('skip') or None
+
+            if scenario_type.lower() == 'sip-client':
+                scenario = SipClientScenario(scenario_id,
+                                             skip,
+                                             remote_address,
+                                             remote_port,
+                                             tcpauthlimit)
+            elif scenario_type.lower() == 'sipp-scenario':
+                key_args = config_scenario['key-args']
+                ordered_args = config_scenario.get('ordered-args') or []
+                target = config_scenario.get('target') or remote_address
+                scenario = SIPpScenarioWrapper(self.__test_object.test_name,
+                                               key_args,
+                                               ordered_args,
+                                               target,
+                                               scenario_id,
+                                               skip)
+            else:
+                msg = '{0} [{1}] is not a recognized scenario type.'
+                LOGGER.error(msg.format(self, scenario_type))
+                return None
+            scenarios.append(scenario)
+
+        if len(scenarios) == 0:
+            msg = '{0} Failing the test. No scenarios registered.'
+            LOGGER.error(msg.format(self))
+            self.__test_object.set_passed(False)
+            self.__test_object.stop_reactor()
+
+        return scenarios
+
+    def __evaluate_test_results(self):
+        """Evaluates the test results.
+
+        First, the method analyzes the SIPpScenarioWrapper instances to
+        determine if the number of those scenarios that passed equals the
+        tcpauthlimit (maximum number of connections permitted). Because more
+        scenarios are executed than the number of connections permitted, some
+        of these scenarios are expected to fail.
+
+        Finally, the remaining SIP client scenarios are queried for their
+        default pass/fail status.
+
+        Returns True on success, False otherwise.
+        """
+
+        def __get_scenarios(scenario_type):
+            """Creates a scenario generator for the given scenario type.
+
+            Keyword Arguments:
+            scenario_type      -- The type of scenario instance for the
+                                  generator to return.
+            """
+
+            for scenario in self.__scenarios:
+                if not scenario.suspended:
+                    if scenario.__class__.__name__ == scenario_type:
+                        yield scenario
+
+        LOGGER.debug('{0} Evaluating test results...'.format(self))
+
+        msg = '{0} Evaluating SIPp scenario results...'.format(self)
+        LOGGER.debug(msg)
+
+        scenario_count = sum(1 for s in __get_scenarios('SIPpScenarioWrapper'))
+        if scenario_count == 0:
+            msg = '{0} No SIPp scenario results to evaluate.'
+            LOGGER.debug(msg.format(self))
+        else:
+            actual = 0
+            expected = (
+                scenario_count if scenario_count < self.__tcpauthlimit
+                else self.__tcpauthlimit)
+            scenarios = __get_scenarios('SIPpScenarioWrapper')
+
+            for scenario in scenarios:
+                if scenario.passed:
+                    actual += 1
+
+                if actual <= expected:
+                    scenario.adjust_result()
+                else:
+                    scenario.adjust_result(255)
+
+            if actual != expected:
+                msg = '{0} One or more SIPp scenarios failed.'.format(self)
+                LOGGER.error(msg)
+                return False
+
+            msg = '{0} All SIPp scenarios passed.'.format(self)
+            LOGGER.debug(msg)
+
+        msg = '{0} Evaluating SIP client scenario results...'.format(self)
+        LOGGER.debug(msg)
+
+        if sum(1 for s in __get_scenarios('SipClientScenario')) == 0:
+            msg = '{0} No SIP client scenario results to evaluate.'
+            LOGGER.debug(msg.format(self))
+            return True
+
+        if all(s.passed for s in __get_scenarios('SipClientScenario')):
+            msg = '{0} All SIP client scenarios passed.'.format(self)
+            LOGGER.debug(msg)
+            return True
+
+        msg = '{0} One or more SIP client scenarios failed.'.format(self)
+        LOGGER.error(msg)
+        return False
+
+    def __format__(self, format_spec):
+        """Overrides default format handling for 'self'."""
+
+        return self.__class__.__name__ + ':'
+
+    def __on_ami_connected(self, ami):
+        """Handler for the AMI connect event.
+
+        Keyword Arguments:
+        ami                    -- The AMI instance that raised this event.
+        """
+
+        self.__run_scenarios()
+
+    def __on_asterisk_stop(self, result):
+        """Determines the overall pass/fail state for the test prior to
+        shutting down the reactor.
+
+        Keyword Arguments:
+        result                 -- A twisted deferred instance.
+
+        Returns:
+        A twisted deferred instance.
+        """
+
+        self.__test_object.set_passed(self.__evaluate_test_results())
+        msg = '{0} Test {1}.'
+        if self.__test_object.passed:
+            LOGGER.info(msg.format(self, 'passed'))
+        else:
+            LOGGER.error(msg.format(self, 'failed'))
+        return result
+
+    def __run_scenarios(self):
+        """Executes the scenarios."""
+
+        def __tear_down_test(message):
+            """Tears down the test.
+
+            Keyword Arguments:
+            message            -- The event payload.
+            """
+
+            LOGGER.debug('{0} Stopping reactor.'.format(self))
+            self.__test_object.stop_reactor()
+            return message
+
+        LOGGER.debug('{0} Running test scenarios.'.format(self))
+
+        deferreds = []
+        for scenario in self.__scenarios:
+            if scenario.suspended:
+                msg = 'skipped \'scenario %s\': %s'
+                console_msg = '--> %s ... ' % self.__test_object.test_name + msg
+                logger_msg = '{0} '.format(self) + msg.capitalize()
+                print console_msg % (scenario.scenario_id, scenario.status)
+                LOGGER.info(logger_msg.format(scenario.scenario_id,
+                                              scenario.status))
+            else:
+                deferred = scenario.run()
+                deferreds.append(deferred)
+
+        defer.DeferredList(deferreds).addCallback(__tear_down_test)
diff --git a/tests/channels/SIP/tcpauthlimit/test-config.yaml b/tests/channels/SIP/tcpauthlimit/test-config.yaml
index a5bb8e9..da8900d 100644
--- a/tests/channels/SIP/tcpauthlimit/test-config.yaml
+++ b/tests/channels/SIP/tcpauthlimit/test-config.yaml
@@ -1,19 +1,99 @@
 testinfo:
-    summary:     'Test the tcpauthlimit sip config option.'
+    summary: Test the tcpauthlimit sip config option.
     description: |
-        "This test ensures that chan_sip respects the tcpauthlimit config
-        option."
-    issues:
-        -mantis: 18996
-        -jira: SWP-3248
+        This test ensures that chan_sip respects the tcpauthlimit config
+        option by running two scenarios:
+            * The SipClientScenario: Attempts to create n+1 TCP socket
+        connections to Asterisk, where n is the value of the 'tcpauthlimit'
+        property in sip.conf. If the 'tcpauthlimit' property is honored, the
+        (n+1)th socket connection will fail to connect. After the scenario is
+        executed, the TCP connections are destroyed.
+            * The SIPpScenario: attempt to create n*2 SIPp processes, where n
+        is the value of the 'tcpauthlimit' property in sip.conf. Each SIPp
+        scenario is configured to connect to the same Asterisk host. If the
+        'tcpauthlimit' property is honored, only n of these scenarios will
+        pass, while the remaining n will fail.
 
 properties:
     minversion: '1.8.0.0'
     dependencies:
-        - app : 'bash'
-        - app : 'asttest'
-        - sipp :
-            version : 'v3.0'
+        - asterisk: 'chan_sip'
+        - python: 'autobahn.websocket'
+        - python: 'starpy'
+        - python: 'twisted'
+        - sipp:
+            version: 'v3.0'
     tags:
         - SIP
+    issues:
+        - mantis: '18996'
+        - jira: 'SWP-3248'
+        - jira: 'ASTERISK-25225'
 
+test-modules:
+    add-test-to-search-path: 'True'
+    add-relative-to-search-path: ['..']
+    test-object:
+        config-section: 'test-object-config'
+        typename: 'test_case.TestCaseModule'
+    modules:
+        -
+            config-section: 'tcpauthlimit_config'
+            typename: 'tcpauthlimit.TcpAuthLimitTestModule'
+
+test-object-config:
+    connect-ami: 'True'
+    reactor-timeout: 20
+    fail-on-any: 'False'
+
+tcpauthlimit_config:
+    tcpauthlimit: 5
+    remote-host:
+        -
+            address: '127.0.0.1'
+            port: 5060
+    test-scenarios:
+        -
+            type: 'sip-client'
+            scenario-id: 'scarlett-overkills'
+            skip: 'Skipping while TCP connection issues are debugged.'
+        -
+            type: 'sipp-scenario'
+            scenario-id: 'minion_bob'
+            key-args: {'scenario': 'uac.xml', '-i': '127.0.0.2', '-s': 'echo', '-d': '10000', '-p': '5062', '-t': 't1', '-inf':'inject_minion_bob.csv'}
+        -
+            type: 'sipp-scenario'
+            scenario-id: 'minion_dave'
+            key-args: {'scenario': 'uac.xml', '-i': '127.0.0.3', '-s': 'echo', '-d': '10000', '-p': '5063', '-t': 't1', '-inf':'inject_minion_dave.csv'}
+        -
+            type: 'sipp-scenario'
+            scenario-id: 'minion_jerry'
+            key-args: {'scenario': 'uac.xml', '-i': '127.0.0.4', '-s': 'echo', '-d': '10000', '-p': '5064', '-t': 't1', '-inf':'inject_minion_jerry.csv'}
+        -
+            type: 'sipp-scenario'
+            scenario-id: 'minion_jon'
+            key-args: {'scenario': 'uac.xml', '-i': '127.0.0.5', '-s': 'echo', '-d': '10000', '-p': '5065', '-t': 't1', '-inf':'inject_minion_jon.csv'}
+        -
+            type: 'sipp-scenario'
+            scenario-id: 'minion_jorge'
+            key-args: {'scenario': 'uac.xml', '-i': '127.0.0.6', '-s': 'echo', '-d': '10000', '-p': '5066', '-t': 't1', '-inf':'inject_minion_jorge.csv'}
+        -
+            type: 'sipp-scenario'
+            scenario-id: 'minion_kevin'
+            key-args: {'scenario': 'uac.xml', '-i': '127.0.0.7', '-s': 'echo', '-d': '10000', '-p': '5067', '-t': 't1', '-inf':'inject_minion_kevin.csv'}
+        -
+            type: 'sipp-scenario'
+            scenario-id: 'minion_mark'
+            key-args: {'scenario': 'uac.xml', '-i': '127.0.0.8', '-s': 'echo', '-d': '10000', '-p': '5068', '-t': 't1', '-inf':'inject_minion_mark.csv'}
+        -
+            type: 'sipp-scenario'
+            scenario-id: 'minion_phil'
+            key-args: {'scenario': 'uac.xml', '-i': '127.0.0.9', '-s': 'echo', '-d': '10000', '-p': '5069', '-t': 't1', '-inf':'inject_minion_phil.csv'}
+        -
+            type: 'sipp-scenario'
+            scenario-id: 'minion_stuart'
+            key-args: {'scenario': 'uac.xml', '-i': '127.0.0.10', '-s': 'echo', '-d': '10000', '-p': '5070', '-t': 't1', '-inf':'inject_minion_stuart.csv'}
+        -
+            type: 'sipp-scenario'
+            scenario-id: 'minion_tim'
+            key-args: {'scenario': 'uac.xml', '-i': '127.0.0.11', '-s': 'echo', '-d': '10000', '-p': '5071', '-t': 't1', '-inf':'inject_minion_tim.csv'}
diff --git a/tests/channels/SIP/tcpauthlimit/test.lua b/tests/channels/SIP/tcpauthlimit/test.lua
deleted file mode 100644
index 51fa788..0000000
--- a/tests/channels/SIP/tcpauthlimit/test.lua
+++ /dev/null
@@ -1,113 +0,0 @@
-have_error = false
-function print_error(err)
-	print(err)
-	have_error = true
-end
-
-function sipp_exec(to, from)
-	return proc.exec_io("sipp",
-	to,
-	"-m", "1",
-	"-t", "t1",
-	"-sn", "uac",
-	"-d", 10000,      -- keep calls up for 10 seconds
-	"-i", from,
-	"-p", 5060,
-	"-timeout", "60",
-	"-timeout_error"
-	)
-end
-
-function sipp_check_error(p, index)
-	local res, err = p:wait()
-
-	if not res then
-		print_error(err)
-		return res, err
-	end
-	if res ~= 0 then
-		print_error("error while connecting client " .. index .. " (sipp exited with status " .. res .. ")\n" .. p.stderr:read("*a"))
-	end
-
-	return res, err
-end
-
-function connect(addr)
-	local sock, err = socket.tcp()
-	if not sock then
-		return nil, err
-	end
-
-	local res, err = sock:connect(addr, 5060)
-	if not res then
-		sock:close()
-		return nil, err
-	end
-
-	-- select then read from the sock to see if it is sill up
-	local read, _, err = socket.select({sock}, nil, 0.1)
-	if read[1] ~= sock and err ~= "timeout" then
-		return nil, err
-	end
-
-	-- if we have data, then there is probably a problem because chan_sip
-	-- doesn't send anything to new sockets
-	if read[1] == sock then
-		res, err = sock:receive(1);
-		if not res then
-			return nil, err
-		end
-	end
-
-	return sock
-end
-
--- limit chan_sip to 5 connections
-limit = 5
-
-print("starting asterisk")
-a = ast.new()
-a["sip.conf"]["general"]["tcpauthlimit"] = limit
-a["sip.conf"]["general"]["tcpenable"] = "yes"
-sip_addr = "127.0.0." .. a.index
-a["sip.conf"]["general"]["tcpbindaddr"] = sip_addr
-
-a["extensions.conf"]["default"]["exten"] = "service,1,Answer"
-a["extensions.conf"]["default"]["exten"] = "service,n,Wait(60)"
-a:spawn()
-
-clients = {}
-
-print("connecting " .. limit .. " clients to asterisk")
-for i = 1, limit do
-	local sock = check("error connecting to chan_sip via TCP", connect(sip_addr))
-	table.insert(clients, sock)
-end
-
-print("attempting to connect one more, this should fail")
-fail_if(connect(sip_addr), "client " .. limit + 1 .. " successfully connected, this should have failed")
-
-for _, sock in ipairs(clients) do
-	sock:shutdown("both")
-	sock:close()
-end
-
-posix.sleep(3) -- let the connections shut down
-
-print("connecting and authenticating " .. limit * 2 .. " clients to asterisk")
-clients = {}
-for i = 1, limit * 2 do
-	table.insert(clients, sipp_exec(sip_addr, "127.0.0." .. a.index + i))
-
-	-- for some reason sipp opens two connections to asterisk when setting
-	-- up a call.  Pausing here gives time for one of them to go away.
-	posix.sleep(1)
-end
-
-print("checking for errors")
-for i, sipp in ipairs(clients) do
-	sipp_check_error(sipp, i)
-end
-
-fail_if(have_error, "one (or more) of our clients had a problem")
-

-- 
To view, visit https://gerrit.asterisk.org/1055
To unsubscribe, visit https://gerrit.asterisk.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ica28ba0ca7ae92b3546da4cd23458f289c111d36
Gerrit-PatchSet: 1
Gerrit-Project: testsuite
Gerrit-Branch: master
Gerrit-Owner: Ashley Sanders <asanders at digium.com>



More information about the asterisk-code-review mailing list