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

Mark Michelson asteriskteam at digium.com
Fri Aug 28 14:45:22 CDT 2015


Mark Michelson has submitted this change and it was merged.

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
D tests/channels/SIP/tcpauthlimit/run-test
A tests/channels/SIP/tcpauthlimit/sipp_client_scenario/configs/ast1/extensions.conf
A tests/channels/SIP/tcpauthlimit/sipp_client_scenario/configs/ast1/sip.conf
A tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_bob.csv
A tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_dave.csv
A tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_jerry.csv
A tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_jon.csv
A tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_jorge.csv
A tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_kevin.csv
A tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_mark.csv
A tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_phil.csv
A tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_stuart.csv
A tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_tim.csv
A tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/uac.xml
A tests/channels/SIP/tcpauthlimit/sipp_client_scenario/test-config.yaml
A tests/channels/SIP/tcpauthlimit/sipp_scenario.py
A tests/channels/SIP/tcpauthlimit/tcp_client_scenario/configs/ast1/extensions.conf
A tests/channels/SIP/tcpauthlimit/tcp_client_scenario/configs/ast1/sip.conf
A tests/channels/SIP/tcpauthlimit/tcp_client_scenario/test-config.yaml
A tests/channels/SIP/tcpauthlimit/tcp_scenario.py
A tests/channels/SIP/tcpauthlimit/tcpauthlimit.py
D tests/channels/SIP/tcpauthlimit/test-config.yaml
D tests/channels/SIP/tcpauthlimit/test.lua
A tests/channels/SIP/tcpauthlimit/tests.yaml
M tests/channels/SIP/tests.yaml
26 files changed, 1,530 insertions(+), 138 deletions(-)

Approvals:
  Mark Michelson: Looks good to me, approved; Verified
  Jonathan Rose: Looks good to me, but someone else must approve



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/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/sipp_client_scenario/configs/ast1/extensions.conf b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/configs/ast1/extensions.conf
new file mode 100644
index 0000000..bd65149
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/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/sipp_client_scenario/configs/ast1/sip.conf b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/configs/ast1/sip.conf
new file mode 100644
index 0000000..e7d1176
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/configs/ast1/sip.conf
@@ -0,0 +1,43 @@
+[general]
+allowguest=no
+nat=no
+sipdebug=yes
+tcpauthlimit=5
+tcpbindaddr=127.0.0.1:5060
+tcpenable=yes
+
+[minion-template](!)
+type=friend
+context=default
+host=127.0.0.1
+secret=RErm9C
+
+[minion_bob](minion-template)
+port=5062
+
+[minion_dave](minion-template)
+port=5063
+
+[minion_jerry](minion-template)
+port=5064
+
+[minion_jon](minion-template)
+port=5065
+
+[minion_jorge](minion-template)
+port=5066
+
+[minion_kevin](minion-template)
+port=5067
+
+[minion_mark](minion-template)
+port=5068
+
+[minion_phil](minion-template)
+port=5069
+
+[minion_stuart](minion-template)
+port=5070
+
+[minion_tim](minion-template)
+port=5071
diff --git a/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_bob.csv b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_bob.csv
new file mode 100644
index 0000000..028fc04
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_bob.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_bob
diff --git a/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_dave.csv b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_dave.csv
new file mode 100644
index 0000000..ed2f641
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_dave.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_dave
diff --git a/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_jerry.csv b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_jerry.csv
new file mode 100644
index 0000000..c23e60b
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_jerry.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_jerry
diff --git a/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_jon.csv b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_jon.csv
new file mode 100644
index 0000000..c471782
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_jon.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_jon
diff --git a/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_jorge.csv b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_jorge.csv
new file mode 100644
index 0000000..f0f006c
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_jorge.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_jorge
diff --git a/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_kevin.csv b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_kevin.csv
new file mode 100644
index 0000000..0420e06
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_kevin.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_kevin
diff --git a/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_mark.csv b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_mark.csv
new file mode 100644
index 0000000..173e106
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_mark.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_mark
diff --git a/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_phil.csv b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_phil.csv
new file mode 100644
index 0000000..d4ecb1e
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_phil.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_phil
diff --git a/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_stuart.csv b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_stuart.csv
new file mode 100644
index 0000000..8fbc75d
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_stuart.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_stuart
diff --git a/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_tim.csv b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_tim.csv
new file mode 100644
index 0000000..cad056a
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/inject_minion_tim.csv
@@ -0,0 +1,2 @@
+SEQUENTIAL
+minion_tim
diff --git a/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/uac.xml b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/uac.xml
new file mode 100644
index 0000000..ba0dcf0
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/sipp/uac.xml
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!DOCTYPE scenario SYSTEM "[field0].dtd">
+<scenario name="Basic Sipstone UAC">
+
+    <!-- Send a the initial INVITE without authentication -->
+    <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: [cseq] 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>
+
+    <!-- Receive a 401 because no authentication was provided -->
+    <recv response="401" auth="true" rtd="true"/>
+
+    <!-- Wait a couple of seconds before proceeding -->
+    <pause milliseconds="2000"/>
+
+    <!-- Send a new INVITE with authentication this time -->
+    <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: [cseq] INVITE
+            Contact: sip:[field0]@[local_ip]:[local_port]
+            Max-Forwards: 70
+            Subject: Performance Test
+            [authentication]
+            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>
+
+    <!-- (Maybe) receive a TRYING response -->
+    <recv response="100" optional="true" />
+
+    <!-- (Maybe) receive a RINGING response -->
+    <recv response="180" optional="true"/>
+
+    <!-- (Maybe) receive a SESSION IN PROGRESS response -->
+    <recv response="183" optional="true"/>
+
+    <!-- Receive an OK response -->
+    <recv response="200" rtd="true"/>
+
+    <!-- Send an ACK -->
+    <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: [cseq] ACK
+            Contact: sip:[field0]@[local_ip]:[local_port]
+            Max-Forwards: 70
+            Subject: Performance Test
+            Content-Length: 0
+
+        ]]>
+    </send>
+
+    <!-- Wait a couple of seconds to give the server time to process the ACK -->
+    <pause milliseconds="2000"/>
+
+    <!-- Send a BYE to end the scenario  -->
+    <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: [cseq] BYE
+            Contact: sip:[field0]@[local_ip]:[local_port]
+            Max-Forwards: 70
+            Subject: Performance Test
+            Content-Length: 0
+
+        ]]>
+    </send>
+
+    <!-- Receive an OK response -->
+    <recv response="200" crlf="true" />
+
+</scenario>
diff --git a/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/test-config.yaml b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/test-config.yaml
new file mode 100644
index 0000000..43da056
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_client_scenario/test-config.yaml
@@ -0,0 +1,90 @@
+testinfo:
+    summary: Test the tcpauthlimit sip config option.
+    description: |
+        This test ensures that chan_sip respects the tcpauthlimit config
+        option by running the following scenario:
+            * 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 with an
+        unauthenticated session. 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:
+        - 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: 'sipp-scenario'
+            scenario-id: 'minion_bob'
+            key-args: {'scenario': 'uac.xml', '-i': '127.0.0.1', '-au': 'minion_bob', '-ap': 'RErm9C', '-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.1', '-au': 'minion_dave', '-ap': 'RErm9C', '-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.1', '-au': 'minion_jerry', '-ap': 'RErm9C', '-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.1', '-au': 'minion_jon', '-ap': 'RErm9C', '-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.1', '-au': 'minion_jorge', '-ap': 'RErm9C', '-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.1', '-au': 'minion_kevin', '-ap': 'RErm9C', '-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.1', '-au': 'minion_mark', '-ap': 'RErm9C', '-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.1', '-au': 'minion_phil', '-ap': 'RErm9C', '-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.1', '-au': 'minion_stuart', '-ap': 'RErm9C', '-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.1', '-au': 'minion_tim', '-ap': 'RErm9C', '-s': 'echo', '-d': '10000', '-p': '5071', '-t': 't1', '-inf':'inject_minion_tim.csv'}
diff --git a/tests/channels/SIP/tcpauthlimit/sipp_scenario.py b/tests/channels/SIP/tcpauthlimit/sipp_scenario.py
new file mode 100644
index 0000000..76a21aa
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/sipp_scenario.py
@@ -0,0 +1,165 @@
+#!/usr/bin/env python
+'''
+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 logging
+
+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 override the default results evaluation
+    mechanism of the SIPpScenario as specified in the test module
+    configuration.
+    """
+
+    def __init__(self, test_dir, scenario, positional_args=(),
+                 target='127.0.0.1', scenario_id=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.
+        """
+
+        self.__actual_result = None
+        self.__expected_result = None
+        self.__on_complete = None
+        self.__passed = None
+        self.__running = False
+        self.__scenario_id = scenario_id or scenario['scenario']
+
+        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 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 = self.result.exitcode
+            self.__on_complete.callback(result)
+
+        self.__running = True
+        self.__setup_state()
+
+        LOGGER.debug('{0} Starting SIPp execution'.format(self))
+        deferred = SIPpScenario.run(self)
+        deferred.addCallback(__handle_results)
+        return self.__on_complete
+
+    def __setup_state(self):
+        """Initialize the scenario state."""
+
+        self.__passed = False
+        self.__actual_result = None
+        self.__expected_result = None
+        self.__on_complete = defer.Deferred()
+
+    @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
diff --git a/tests/channels/SIP/tcpauthlimit/tcp_client_scenario/configs/ast1/extensions.conf b/tests/channels/SIP/tcpauthlimit/tcp_client_scenario/configs/ast1/extensions.conf
new file mode 100644
index 0000000..bd65149
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/tcp_client_scenario/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/tcp_client_scenario/configs/ast1/sip.conf b/tests/channels/SIP/tcpauthlimit/tcp_client_scenario/configs/ast1/sip.conf
new file mode 100644
index 0000000..e7d1176
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/tcp_client_scenario/configs/ast1/sip.conf
@@ -0,0 +1,43 @@
+[general]
+allowguest=no
+nat=no
+sipdebug=yes
+tcpauthlimit=5
+tcpbindaddr=127.0.0.1:5060
+tcpenable=yes
+
+[minion-template](!)
+type=friend
+context=default
+host=127.0.0.1
+secret=RErm9C
+
+[minion_bob](minion-template)
+port=5062
+
+[minion_dave](minion-template)
+port=5063
+
+[minion_jerry](minion-template)
+port=5064
+
+[minion_jon](minion-template)
+port=5065
+
+[minion_jorge](minion-template)
+port=5066
+
+[minion_kevin](minion-template)
+port=5067
+
+[minion_mark](minion-template)
+port=5068
+
+[minion_phil](minion-template)
+port=5069
+
+[minion_stuart](minion-template)
+port=5070
+
+[minion_tim](minion-template)
+port=5071
diff --git a/tests/channels/SIP/tcpauthlimit/tcp_client_scenario/test-config.yaml b/tests/channels/SIP/tcpauthlimit/tcp_client_scenario/test-config.yaml
new file mode 100644
index 0000000..0dc1fbe
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/tcp_client_scenario/test-config.yaml
@@ -0,0 +1,54 @@
+testinfo:
+    summary: Test the tcpauthlimit sip config option.
+    description: |
+        This test ensures that chan_sip respects the tcpauthlimit config
+        option by running the following scenario:
+            * The TcpClientScenario: Attempts to create n+1 TCP
+        unauthenticated 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.
+
+properties:
+    minversion: '1.8.0.0'
+    dependencies:
+        - 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: 25
+    fail-on-any: 'True'
+
+tcpauthlimit_config:
+    tcpauthlimit: 5
+    remote-host:
+        -
+            address: '127.0.0.1'
+            port: 5060
+    test-scenarios:
+        -
+            type: 'tcp-client'
+            scenario-id: 'felonius-gru'
diff --git a/tests/channels/SIP/tcpauthlimit/tcp_scenario.py b/tests/channels/SIP/tcpauthlimit/tcp_scenario.py
new file mode 100644
index 0000000..603ddc3
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/tcp_scenario.py
@@ -0,0 +1,704 @@
+#!/usr/bin/env python
+'''
+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 socket
+import logging
+
+from twisted.internet import defer, error, reactor
+from twisted.internet.protocol import ClientFactory
+from twisted.internet.tcp import EWOULDBLOCK
+from twisted.protocols.basic import LineReceiver
+from twisted.python import failure
+
+LOGGER = logging.getLogger(__name__)
+
+
+class ConnectionState(object):
+    """An enumeration to describe the connection state of a client."""
+
+    CONNECTING = 0
+    CONNECTED = 1
+    DONE = 2
+    LOST = 3
+    FAILED = 4
+
+    @staticmethod
+    def get_state_name(state):
+        """Gets the name of the given connection state.
+
+        Keyword Arguments:
+        state                  -- The connection state.
+        """
+
+        if state == ConnectionState.CONNECTING:
+            return 'CONNECTING'
+        elif state == ConnectionState.CONNECTED:
+            return 'CONNECTED'
+        elif state == ConnectionState.DONE:
+            return 'DONE'
+        elif state == ConnectionState.LOST:
+            return 'LOST'
+        elif state == ConnectionState.FAILED:
+            return 'FAILED'
+        return 'UNKNOWN'
+
+
+class TcpClient(LineReceiver):
+    """Client connection protocol."""
+
+    def __init__(self, client_id, hostaddr, clientaddr, on_connection_change):
+        """Constructor.
+
+        Keyword Arguments:
+        client_id              -- The id to use for this client.
+        hostaddr               -- The host address as a
+                                  twisted.internet.interfaces.IAddress.
+        clientaddr             -- A (host, port) tuple of the local address to
+                                  bind to the client.
+        on_connection_change   -- Callback to invoke when the client
+                                  connection is established.
+        """
+
+        self.__client = '%s:%d' % (clientaddr[0], clientaddr[1])
+        self.__connection_state = ConnectionState.CONNECTING
+        self.__host = '%s:%d' % (hostaddr.host, hostaddr.port)
+        self.__id = client_id
+        self.__lines = []
+        self.__on_connection_change = on_connection_change
+
+    def connectionLost(self, reason=failure.Failure(error.ConnectionDone())):
+        """Called when the connection is shut down.
+
+        Keyword Arguments:
+        reason                 -- The reason for the connection loss as a
+                                  twisted.python.failure.Failure instance.
+                                  Optional. Default: result of
+                                  failure.Failure(error.ConnectionDone()).
+        """
+
+        if (self.__connection_state == ConnectionState.CONNECTING or
+                self.__connection_state == ConnectionState.CONNECTED):
+            self.__connection_state = ConnectionState.LOST
+
+    def connectionMade(self):
+        """Called when a connection is made.
+
+        Overrides twisted.internet.protocols.basic.LineReceiver.connectionMade
+        """
+
+        msg = '{0} Initial connection to {1} established.'
+        LOGGER.debug(msg.format(self, self.host))
+
+        connection_state = self.__sync_socket()
+        if connection_state != ConnectionState.CONNECTED:
+            msg = '{0} Connection state to {1} : {2}'
+            state = ConnectionState.get_state_name(connection_state)
+            LOGGER.debug(msg.format(self, self.host, state))
+            self.disconnect()
+        else:
+            msg = '{0} Connection to {1} successful.'
+            LOGGER.debug(msg.format(self, self.host))
+
+        self.__connection_state = connection_state
+        self.__on_connection_change()
+
+    def disconnect(self):
+        """Disconnects the client connection."""
+
+        if self.connection_state == ConnectionState.CONNECTED:
+            return
+
+        msg = '{0} Disconnecting the transport...'.format(self)
+        LOGGER.debug(msg)
+        self.transport.loseConnection()
+
+    def fail_connection(self):
+        """Fails the connection.
+
+        In the event that the connector fails and we don't get the
+        notification, this method should be invoked from the factory to keep
+        us updated.
+        """
+
+        self.__connection_state = ConnectionState.FAILED
+
+    def __format__(self, format_spec):
+        """Overrides default format handling for 'self'."""
+
+        return '%s [%s]:' % (self.__class__.__name__, self.__client)
+
+    def lineReceived(self, line):
+        """Handler for receiving lines of data from the host.
+
+        Overrides twisted.internet.protocols.basic.LineReceiver.lineReceived
+
+        Keyword Arguments:
+        line                   -- The line of data received from the host.
+        """
+
+        pass
+
+    def rawDataReceived(self, data):
+        """Handler for receiving raw data from the host.
+
+        Overrides twisted.internet.protocols.basic.LineReceiver.rawDataReceived
+
+        Keyword Arguments:
+        data                   -- The blob of raw data received from the host.
+        """
+
+        pass
+
+    def __sync_socket(self):
+        """Attempts to syncronize the socket.
+
+        Sends a battery of Noop messages and then attempts to read from the
+        socket. The hope is that either we will catch up that the host has
+        rejected the connection or confirm that we are indeed connected to the
+        host.
+        """
+
+        msg = '{0} Syncronizing the connection state...'.format(self)
+        LOGGER.debug(msg)
+
+        msg = '%d: An apple a day keeps the doctor away\r\n'
+        for i in range(0, 150):
+            self.transport.write(msg % i)
+            if i % 10 == 0:
+                if self.__try_read() == ConnectionState.CONNECTED:
+                    return ConnectionState.CONNECTED
+        return self.__try_read()
+
+    def __try_read(self):
+        """Tries to read data from the socket."""
+
+        try:
+            self.transport.socket.recv(self.transport.bufferSize)
+        except socket.error, se:
+            # If we end up here, there is a good chance that Asterisk has
+            # pulled the rug out from underneath us. The only method we have
+            # for discerning this state, unfortunately, is to receive error
+            # code 11, or EWOULDBLOCK from the socket, which is
+            # "Resource Not Available". Therefore, flag this situation the
+            # same as the DONE state.
+            return (
+                ConnectionState.DONE if se.args[0] == EWOULDBLOCK
+                else ConnectionState.LOST)
+        return ConnectionState.CONNECTED
+
+    @property
+    def connection_state(self):
+        """Returns the connection state for the client.
+
+        Returns:
+        ConnectionState.CONNECTING  -- If the client is connecting.
+        ConnectionState.CONNECTED   -- If the client is connection to the host
+                                       was successful.
+        ConnectionState.DONE        -- If the client connection to the host
+                                       was rejected by the host.
+        ConnectionState.LOST        -- If the client connection to the host
+                                       was terminated (by either side).
+        """
+
+        return self.__connection_state
+
+    @property
+    def host(self):
+        """Returns the host used for the client connection."""
+
+        return self.__host
+
+
+class TcpClientFactory(ClientFactory):
+    """Factory for building SIP webSocket clients."""
+
+    def __init__(self, client_id, clientaddr, on_connection_lost):
+        """Constructor.
+
+        Keyword arguments:
+        client_id              -- The id to give to built clients.
+        clientaddr             -- A (host, port) tuple of the local address to
+                                  bind to the client.
+        on_connection_lost     -- Callback to invoke when the client protocol
+                                  destroys its transport connection.
+        """
+
+        self.__clientaddr = clientaddr
+        self.__done = None
+        self.__failed = False
+        self.__id = client_id
+        self.__on_connection_lost = on_connection_lost
+        self.__protocol = None
+
+    def buildProtocol(self, addr):
+        """Twisted overload used to create the client connection.
+
+        Overrides twisted.internet.protocol.ClientFactory.buildProtocol
+
+        Keyword Arguments:
+        addr                   -- The host address as a
+                                  twisted.internet.interfaces.IAddress.
+        """
+
+        msg = '{0} Building a TcpClient protocol...'.format(self)
+        LOGGER.debug(msg)
+
+        self.disconnect()
+        self.__protocol = TcpClient(self.client_id,
+                                    addr,
+                                    self.__clientaddr,
+                                    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 reason for the connection failure as a
+                                  twisted.python.failure.Failure instance.
+        """
+
+        msg = '{0} Failed to establish a connection. Reason: {1}'
+        LOGGER.debug(msg.format(self, reason.getErrorMessage()))
+        if self.__protocol:
+            self.__protocol.fail_connection()
+        self.__handle_client_state_change(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 reason for the connection loss as a
+                                  twisted.python.failure.Failure instance.
+        """
+
+        if self.connecting:
+            return
+
+        msg = '{0} Connection has been lost. Reason: {1}'
+        LOGGER.debug(msg.format(self, reason.getErrorMessage()))
+        self.__on_connection_lost(self, reason)
+
+    def connect(self, host='127.0.0.1', port=5060):
+        """Connects the client protocol.
+
+        Keyword Arguments:
+        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.
+        """
+
+        msg = '{0} Connecting to host \'{1}:{2}\'...'
+        LOGGER.debug(msg.format(self, host, port))
+
+        self.__done = defer.Deferred()
+        reactor.connectTCP(host, port, self, bindAddress=self.__clientaddr)
+        return self.__done
+
+    def disconnect(self):
+        """Disconnects the client protocol.
+
+        Returns:
+        True on success. Otherwise, returns False.
+        """
+
+        msg = '{0} Attempting to destroy client connection...'.format(self)
+        LOGGER.debug(msg)
+
+        if not self.__protocol:
+            msg = '{0} I don\'t have a client connection to destroy.'
+            LOGGER.debug(msg.format(self))
+            return False
+
+        self.__protocol.disconnect()
+        return True
+
+    def __format__(self, format_spec):
+        """Overrides default format handling for 'self'."""
+
+        return '%s [%s]:' % (self.__class__.__name__, self.clientaddr)
+
+    def __handle_client_state_change(self, reason):
+        """Handles connection state changes.
+
+        Keyword Arguments:
+        reason                 -- The reason for the state change as a
+                                  twisted.python.failure.Failure instance.
+        """
+
+        if self.__done is not None:
+            if reason is None:
+                self.__done.callback(self)
+            else:
+                self.__done.errback(self)
+            self.__done = None
+
+    def __on_client_connection_made(self):
+        """Handles the connectionMade event from the client protocol."""
+
+        msg = '{0} Client is done with connection attempt to host: \'{1}\''
+        LOGGER.debug(msg.format(self, self.__protocol.host))
+        self.__handle_client_state_change(None)
+
+    @property
+    def clientaddr(self):
+        """Returns the client address."""
+
+        return self.__clientaddr
+
+    @property
+    def client_id(self):
+        """Returns the id for the client."""
+
+        return self.__id
+
+    @property
+    def connecting(self):
+        """Whether or not the the client is trying to establish a connection.
+
+        Returns:
+        True if the client is connecting. Otherwise, returns False.
+        """
+
+        if self.__failed:
+            return False
+
+        if self.__protocol is None:
+            return True
+
+        return self.__protocol.connection_state == ConnectionState.CONNECTING
+
+    @property
+    def connected(self):
+        """Whether or not the client connection has been established.
+
+        Returns:
+        True if the client is connected. Otherwise, returns False.
+        """
+
+        if self.__protocol is None:
+            return False
+
+        return self.__protocol.connection_state == ConnectionState.CONNECTED
+
+
+class TcpClientScenario(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, host='127.0.0.1', port=5060,
+                 allowed_connections=0):
+        """Constructor.
+
+        Keyword Arguments:
+        scenario_id            -- The id for this scenario. Used for logging.
+        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.__allowed_connections = allowed_connections
+        self.__clients = None
+        self.__connected_clients = None
+        self.__host = host
+        self.__iterator = None
+        self.__port = port
+        self.__on_complete = None
+        self.__on_started = None
+        self.__scenario_id = scenario_id
+        self.__stopping = False
+
+    def __disconnect_client(self, client):
+        """Disconnects the given client.
+
+        Keyword Arguments:
+        client             -- The client to disconnect.
+
+        Returns:
+        The disconnected client.
+        """
+
+        if client is None and self.__stopping:
+            return None
+
+        msg = '{0} Disconnecting client [{1}]...'
+        LOGGER.debug(msg.format(self, client.clientaddr))
+
+        if not client.disconnect():
+            self.__on_client_disconnect(client, None)
+        return client
+
+    def __evaluate_results(self, payload):
+        """Evaluates the test scenario results.
+
+        Keyword Arguments:
+        payload                -- The event payload.
+        """
+
+        if self.finished:
+            return payload
+
+        msg = '{0} Evaluating scenario results...'.format(self)
+        LOGGER.debug(msg)
+
+        clients = sum(client.connected for client in self.__clients)
+        self.__connected_clients = clients
+        return payload
+
+    def __format__(self, format_spec):
+        """Overrides default format handling for 'self'."""
+
+        return self.__class__.__name__ + ' [' + self.__scenario_id + ']:'
+
+    def __get_next_client(self, client):
+        """Tries to get the next client from the list.
+
+        Keyword Arguments:
+        client                 -- The current client.
+
+        Returns:
+        A client if the iteration is not complete. Else returns None.
+        """
+
+        client = None
+        try:
+            client = self.__iterator.next()
+        except StopIteration:
+            pass
+        return client
+
+    def __handle_state_change(self, sender):
+        """Updates basic scenario state afer a client connection state change.
+
+        Keyword Arguments:
+        sender                 -- The client whose state has changed.
+
+        Returns:
+        True if the state was handled. Otherwise, returns False.
+        """
+
+        if sender is not None:
+            if len(self.__clients) < sender.client_id:
+                msg = '{0} Ruh-roh. I received an event for a client that I'
+                msg += ' did not create.\n\t\tClient ID:\t{1}'
+                LOGGER.warning(msg.format(self, sender.clientaddr))
+                return True
+
+        if self.__stopping:
+            client = self.__get_next_client(sender)
+            if client is None:
+                if all(c.connected is False for c in self.__clients):
+                    self.__report_results()
+                    self.__reset_state()
+            else:
+                self.__disconnect_client(client)
+            return True
+
+        if self.finished:
+            return True
+
+        if not self.started:
+            return False
+
+        msg = '{0} All clients have finished connecting.'.format(self)
+        LOGGER.debug(msg)
+        self.__on_started.callback(self)
+        return True
+
+    def __initialize_clients(self):
+        """Builds the SIP Client for this scenario.
+
+        Returns:
+        A list of n TcpClientFactory instances, where n equals
+        self.__allowed_connections + 1.
+        """
+
+        clients = list()
+
+        for i in range(0, self.__allowed_connections * 2):
+            user = i + 2
+            port = 5060 + user
+            factory = TcpClientFactory(i,
+                                       (self.__host, port),
+                                       self.__on_client_disconnect)
+            clients.append(factory)
+
+        return clients
+
+    def __on_client_connection_failure(self, reason):
+        """Handles a client disconnect event.
+
+        Keyword Arguments:
+        reason                 -- The reason for the connection failure as a
+                                  twisted.python.failure.Failure instance.
+        """
+
+        msg = '{0} Client connection failure: {1}.'
+        LOGGER.debug(msg.format(self, reason.getErrorMessage()))
+        self.__handle_state_change(None)
+
+    def __on_client_disconnect(self, sender, payload):
+        """Handles a client disconnect event.
+
+        Keyword Arguments:
+        sender                 -- The client that was disconnected.
+        payload                -- The event payload.
+        """
+
+        msg = '{0} Client [{1}] is disconnected.'
+        LOGGER.debug(msg.format(self, sender.clientaddr))
+        self.__handle_state_change(sender)
+
+    def __on_client_connecting_change(self, sender):
+        """Handler for client connection state changes.
+
+        Keyword Arguments:
+        sender                 -- The client who sent the notification.
+        payload                -- The event payload.
+        """
+
+        msg = '{0} Received a connection change notice from client [{1}].'
+        LOGGER.debug(msg.format(self, sender.clientaddr))
+        self.__handle_state_change(sender)
+
+    def __report_results(self):
+        """Logs the result of the scenario."""
+
+        if not self.passed:
+            msg = '{0} Scenario failed.\n'
+            msg += '\tBased on the test configuration, I expected to'
+            msg += ' receive:\n\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,
+                                    self.__connected_clients))
+        else:
+            LOGGER.info('{0} Congrats! The scenario passed.'.format(self))
+
+        self.__on_complete.callback(self)
+
+    def __reset_state(self):
+        """Resets the scenario state."""
+
+        self.__clients = None
+        self.__iterator = None
+        self.__on_complete = None
+        self.__on_started = None
+        self.__stopping = False
+
+    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.
+        """
+
+        LOGGER.debug('{0} Starting scenario...'.format(self))
+
+        self.__setup_state()
+        self.__start_clients()
+        return self.__on_complete
+
+    def __setup_state(self):
+        """Sets up the state needed for this scenario to run."""
+
+        self.__connected_clients = None
+        self.__clients = self.__initialize_clients()
+        self.__iterator = iter(self.__clients)
+
+        self.__on_complete = defer.Deferred()
+        self.__on_started = defer.Deferred()
+        self.__on_started.addCallback(self.__evaluate_results)
+        self.__on_started.addCallback(self.__teardown_state)
+
+    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)))
+
+        deferreds = []
+        for client in self.__clients:
+            deferred = client.connect(self.__host, self.__port)
+            if deferred is not None:
+                deferred.addCallback(self.__on_client_connecting_change)
+                deferred.addErrback(self.__on_client_connection_failure)
+                deferreds.append(deferred)
+
+    def __teardown_state(self, payload):
+        """Disconnects the clients and resets the state.
+
+        Keyword Arguments:
+        payload                -- The event payload.
+        """
+
+        self.__stopping = True
+
+        LOGGER.debug('{0} Tearing down the scenario...'.format(self))
+
+        deferred = defer.Deferred()
+        deferred.addCallback(self.__get_next_client)
+        deferred.addCallback(self.__disconnect_client)
+        deferred.callback(self)
+
+        return payload
+
+    @property
+    def finished(self):
+        """Whether or not the this scenario has finished execution.
+
+        Returns:
+        True if the scenario has finished execution. Otherwise, returns False.
+        """
+
+        return self.__connected_clients is not None
+
+    @property
+    def passed(self):
+        """The results of the scenario.
+
+        Returns:
+        True if the scenario was successful. Otherwise, returns False.
+        """
+
+        if not self.finished:
+            return False
+
+        return self.__connected_clients == self.__allowed_connections
+
+    @property
+    def scenario_id(self):
+        """Returns the id for this scenario."""
+
+        return self.__scenario_id
+
+    @property
+    def started(self):
+        """Returns a value indicating if this scenario has started."""
+
+        if self.__clients is None:
+            return False
+
+        return all(c.connecting is False for c in self.__clients)
diff --git a/tests/channels/SIP/tcpauthlimit/tcpauthlimit.py b/tests/channels/SIP/tcpauthlimit/tcpauthlimit.py
new file mode 100755
index 0000000..7241554
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/tcpauthlimit.py
@@ -0,0 +1,268 @@
+#!/usr/bin/env python
+'''
+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")
+
+from tcp_scenario import TcpClientScenario
+from sipp_scenario import SIPpScenarioWrapper
+from twisted.internet import defer
+
+LOGGER = logging.getLogger(__name__)
+
+
+def get_friendly_scenario_type(scenario_type):
+    """Returns the logger friendly name for the given scenario type.
+
+    Keyword Arguments:
+    scenario_type              -- The type of scenario.
+    """
+
+    if scenario_type == 'SIPpScenarioWrapper':
+        return 'SIPp scenario'
+
+    if scenario_type == 'TcpClientScenario':
+        return 'TCP client scenario'
+
+    return 'Unknown type scenario'
+
+
+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.__remote_host = config['remote-host'][0]
+        self.__tcpauthlimit = config['tcpauthlimit']
+        self.__scenarios = self.__build_scenarios(config['test-scenarios'])
+
+        self.__test_object.register_stop_observer(self.__on_asterisk_stop)
+        self.__test_object.register_ami_observer(self.__on_ami_connected)
+
+    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
+
+            if scenario_type.lower() == 'tcp-client':
+                scenario = TcpClientScenario(scenario_id,
+                                             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)
+            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_scenario_results(self, scenario_type):
+        """Evaluates the results for the given scenario type.
+
+        For SIPpScenarioWrapper scenario type evaluations, the scenarios are
+        polled 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.
+
+        For TcpClientScenario scenario type evaluations, the scenarios are
+        polled for their default pass/fail status without any adjustments being
+        applied.
+
+        Keyword Arguments:
+        scenario_type          -- The type of scenario instances to analyze.
+
+        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.
+
+            Returns a generator for the scenarios found matching the given
+            scenario type.
+            """
+
+            for scenario in self.__scenarios:
+                if scenario.__class__.__name__ == scenario_type:
+                    yield scenario
+
+        friendly_type = get_friendly_scenario_type(scenario_type)
+        scenario_count = sum(1 for s in __get_scenarios(scenario_type))
+
+        msg = '{0} Evaluating {1} results...'.format(self, friendly_type)
+        LOGGER.debug(msg)
+
+        if scenario_count == 0:
+            msg = '{0} No {1} results to evaluate.'
+            LOGGER.debug(msg.format(self, friendly_type))
+            return True
+        else:
+            actual = 0
+            msg = '{0} {1} \'{2}\' {3}.'
+
+            if scenario_type == 'SIPpScenarioWrapper':
+                expected = (
+                    scenario_count if scenario_count < self.__tcpauthlimit
+                    else self.__tcpauthlimit)
+            else:
+                expected = 1
+
+            for scenario in __get_scenarios(scenario_type):
+                if scenario.passed:
+                    actual += 1
+
+                if scenario_type == 'SIPpScenarioWrapper':
+                    if actual <= expected:
+                        scenario.adjust_result()
+                    else:
+                        scenario.adjust_result(255)
+
+                if scenario.passed:
+                    LOGGER.debug(msg.format(self,
+                                            friendly_type,
+                                            scenario.scenario_id,
+                                            'passed'))
+                else:
+                    LOGGER.debug(msg.format(self,
+                                            friendly_type,
+                                            scenario.scenario_id,
+                                            'failed'))
+
+            if actual != expected:
+                msg = '{0} One or more {1}s failed.'
+                LOGGER.error(msg.format(self, friendly_type))
+                return False
+
+            msg = '{0} All {1}s passed.'
+            LOGGER.debug(msg.format(self, friendly_type))
+            return True
+
+    def __evaluate_test_results(self):
+        """Evaluates the test results.
+
+        First, the method analyzes the SIPpScenarioWrapper instances (if any)
+        then analyzes the remaining TCP client scenarios (if any).
+
+        Returns True on success, False otherwise.
+        """
+
+        LOGGER.debug('{0} Evaluating test results...'.format(self))
+
+        if not self.__evaluate_scenario_results('SIPpScenarioWrapper'):
+            return False
+
+        return self.__evaluate_scenario_results('TcpClientScenario')
+
+    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:
+            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
deleted file mode 100644
index a5bb8e9..0000000
--- a/tests/channels/SIP/tcpauthlimit/test-config.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-testinfo:
-    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
-
-properties:
-    minversion: '1.8.0.0'
-    dependencies:
-        - app : 'bash'
-        - app : 'asttest'
-        - sipp :
-            version : 'v3.0'
-    tags:
-        - SIP
-
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")
-
diff --git a/tests/channels/SIP/tcpauthlimit/tests.yaml b/tests/channels/SIP/tcpauthlimit/tests.yaml
new file mode 100644
index 0000000..2dc5350
--- /dev/null
+++ b/tests/channels/SIP/tcpauthlimit/tests.yaml
@@ -0,0 +1,4 @@
+# Enter tests here in the order they should be considered for execution:
+tests:
+    - test: 'sipp_client_scenario'
+    - test: 'tcp_client_scenario'
diff --git a/tests/channels/SIP/tests.yaml b/tests/channels/SIP/tests.yaml
index f0c6ce6..377fa35 100644
--- a/tests/channels/SIP/tests.yaml
+++ b/tests/channels/SIP/tests.yaml
@@ -5,7 +5,7 @@
     - test: 'options'
     - test: 'refer_replaces_to_self'
     - test: 'info_dtmf'
-    - test: 'tcpauthlimit'
+    - dir: 'tcpauthlimit'
     - dir: 'tcpauthtimeout'
     - test: 'sip_outbound_address'
     - test: 'sip_attended_transfer'

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

Gerrit-MessageType: merged
Gerrit-Change-Id: Ica28ba0ca7ae92b3546da4cd23458f289c111d36
Gerrit-PatchSet: 7
Gerrit-Project: testsuite
Gerrit-Branch: master
Gerrit-Owner: Ashley Sanders <asanders at digium.com>
Gerrit-Reviewer: Ashley Sanders <asanders at digium.com>
Gerrit-Reviewer: Jonathan Rose <jrose at digium.com>
Gerrit-Reviewer: Joshua Colp <jcolp at digium.com>
Gerrit-Reviewer: Mark Michelson <mmichelson at digium.com>



More information about the asterisk-code-review mailing list