Tristan Matthews | 0a329cc | 2013-07-17 13:20:14 -0400 | [diff] [blame] | 1 | # $Id$ |
| 2 | |
| 3 | ## Automatic test module for SIPp. |
| 4 | ## |
| 5 | ## This module will need a test driver for each SIPp scenario: |
| 6 | ## - For simple scenario, i.e: make/receive call (including auth), this |
| 7 | ## test module can auto-generate a default test driver, i.e: make call |
| 8 | ## or apply auto answer. Just name the SIPp scenario using "uas" or |
| 9 | ## "uac" prefix accordingly. |
| 10 | ## - Custom test driver can be defined in a python script file containing |
| 11 | ## a list of the PJSUA instances and another list for PJSUA expects/ |
| 12 | ## commands. The custom test driver file must use the same filename as |
| 13 | ## the SIPp XML scenario. See samples of SIPp scenario + its driver |
| 14 | ## in tests/pjsua/scripts-sipp/ folder for detail. |
| 15 | ## |
| 16 | ## Here are defined macros that can be used in the custom driver: |
| 17 | ## - $SIPP_PORT : SIPp binding port |
| 18 | ## - $SIPP_URI : SIPp SIP URI |
| 19 | ## - $PJSUA_PORT[N] : binding port of PJSUA instance #N |
| 20 | ## - $PJSUA_URI[N] : SIP URI of PJSUA instance #N |
| 21 | |
| 22 | import ctypes |
| 23 | import time |
| 24 | import imp |
| 25 | import sys |
| 26 | import os |
| 27 | import re |
| 28 | import subprocess |
| 29 | from inc_cfg import * |
| 30 | import inc_const |
| 31 | |
| 32 | # flags that test is running in Unix |
| 33 | G_INUNIX = False |
| 34 | if sys.platform.lower().find("win32")!=-1 or sys.platform.lower().find("microsoft")!=-1: |
| 35 | G_INUNIX = False |
| 36 | else: |
| 37 | G_INUNIX = True |
| 38 | |
| 39 | # /dev/null handle, for redirecting output when SIPP is not in background mode |
| 40 | FDEVNULL = None |
| 41 | |
| 42 | # SIPp executable path and param |
| 43 | #SIPP_PATH = '"C:\\Program Files (x86)\\Sipp_3.2\\sipp.exe"' |
| 44 | SIPP_PATH = 'sipp' |
| 45 | SIPP_PORT = 6000 |
| 46 | SIPP_PARAM = "-m 1 -i 127.0.0.1 -p " + str(SIPP_PORT) |
| 47 | SIPP_TIMEOUT = 60 |
| 48 | # On BG mode, SIPp doesn't require special terminal |
| 49 | # On non-BG mode, on win, it needs env var: "TERMINFO=c:\cygwin\usr\share\terminfo" |
| 50 | # TODO: on unix with BG mode, waitpid() always fails, need to be fixed |
| 51 | SIPP_BG_MODE = False |
| 52 | #SIPP_BG_MODE = not G_INUNIX |
| 53 | |
| 54 | # Will be updated based on the test driver file (a .py file whose the same name as SIPp XML file) |
| 55 | PJSUA_INST_PARAM = [] |
| 56 | PJSUA_EXPECTS = [] |
| 57 | |
| 58 | # Default PJSUA param if test driver is not available: |
| 59 | # - no-tcp as SIPp is on UDP only |
| 60 | # - id, username, and realm: to allow PJSUA sending re-INVITE with auth after receiving 401/407 response |
| 61 | PJSUA_DEF_PARAM = "--null-audio --max-calls=1 --no-tcp --id=sip:a@localhost --username=a --realm=*" |
| 62 | |
| 63 | # Get SIPp scenario (XML file) |
| 64 | SIPP_SCEN_XML = "" |
| 65 | if ARGS[1].endswith('.xml'): |
| 66 | SIPP_SCEN_XML = ARGS[1] |
| 67 | else: |
| 68 | exit(-99) |
| 69 | |
| 70 | |
| 71 | # Functions for resolving macros in the test driver |
| 72 | def resolve_pjsua_port(mo): |
| 73 | return str(PJSUA_INST_PARAM[int(mo.group(1))].sip_port) |
| 74 | |
| 75 | def resolve_pjsua_uri(mo): |
| 76 | return PJSUA_INST_PARAM[int(mo.group(1))].uri[1:-1] |
| 77 | |
| 78 | def resolve_driver_macros(st): |
| 79 | st = re.sub("\$SIPP_PORT", str(SIPP_PORT), st) |
| 80 | st = re.sub("\$SIPP_URI", "sip:sipp@127.0.0.1:"+str(SIPP_PORT), st) |
| 81 | st = re.sub("\$PJSUA_PORT\[(\d+)\]", resolve_pjsua_port, st) |
| 82 | st = re.sub("\$PJSUA_URI\[(\d+)\]", resolve_pjsua_uri, st) |
| 83 | return st |
| 84 | |
| 85 | |
| 86 | # Init test driver |
| 87 | if os.access(SIPP_SCEN_XML[:-4]+".py", os.R_OK): |
| 88 | # Load test driver file (the corresponding .py file), if any |
| 89 | cfg_file = imp.load_source("cfg_file", SIPP_SCEN_XML[:-4]+".py") |
| 90 | for ua_idx, ua_param in enumerate(cfg_file.PJSUA): |
| 91 | ua_param = resolve_driver_macros(ua_param) |
| 92 | PJSUA_INST_PARAM.append(InstanceParam("pjsua"+str(ua_idx), ua_param)) |
| 93 | PJSUA_EXPECTS = cfg_file.PJSUA_EXPECTS |
| 94 | else: |
| 95 | # Generate default test driver |
| 96 | if os.path.basename(SIPP_SCEN_XML)[0:3] == "uas": |
| 97 | # auto make call when SIPp is as UAS |
| 98 | ua_param = PJSUA_DEF_PARAM + " sip:127.0.0.1:" + str(SIPP_PORT) |
| 99 | else: |
| 100 | # auto answer when SIPp is as UAC |
| 101 | ua_param = PJSUA_DEF_PARAM + " --auto-answer=200" |
| 102 | PJSUA_INST_PARAM.append(InstanceParam("pjsua", ua_param)) |
| 103 | |
| 104 | |
| 105 | # Start SIPp process, returning PID |
| 106 | def start_sipp(): |
| 107 | global SIPP_BG_MODE |
| 108 | sipp_proc = None |
| 109 | |
| 110 | sipp_param = SIPP_PARAM + " -sf " + SIPP_SCEN_XML |
| 111 | if SIPP_BG_MODE: |
| 112 | sipp_param = sipp_param + " -bg" |
| 113 | if SIPP_TIMEOUT: |
| 114 | sipp_param = sipp_param + " -timeout "+str(SIPP_TIMEOUT)+"s -timeout_error" + " -deadcall_wait "+str(SIPP_TIMEOUT)+"s" |
| 115 | |
| 116 | # add target param |
| 117 | sipp_param = sipp_param + " 127.0.0.1:" + str(PJSUA_INST_PARAM[0].sip_port) |
| 118 | |
| 119 | # run SIPp |
| 120 | fullcmd = os.path.normpath(SIPP_PATH) + " " + sipp_param |
| 121 | print "Running SIPP: " + fullcmd |
| 122 | if SIPP_BG_MODE: |
| 123 | sipp_proc = subprocess.Popen(fullcmd, bufsize=0, stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=G_INUNIX, universal_newlines=False) |
| 124 | else: |
| 125 | # redirect output to NULL |
| 126 | global FDEVNULL |
| 127 | #FDEVNULL = open(os.devnull, 'w') |
| 128 | FDEVNULL = open("logs/sipp_output.tmp", 'w') |
| 129 | sipp_proc = subprocess.Popen(fullcmd, shell=G_INUNIX, stdout=FDEVNULL, stderr=FDEVNULL) |
| 130 | |
| 131 | if not SIPP_BG_MODE: |
| 132 | if sipp_proc == None or sipp_proc.poll(): |
| 133 | return None |
| 134 | return sipp_proc |
| 135 | |
| 136 | else: |
| 137 | # get SIPp child process PID |
| 138 | pid = 0 |
| 139 | r = re.compile("PID=\[(\d+)\]", re.I) |
| 140 | |
| 141 | while True: |
| 142 | line = sipp_proc.stdout.readline() |
| 143 | pid_r = r.search(line) |
| 144 | if pid_r: |
| 145 | pid = int(pid_r.group(1)) |
| 146 | break |
| 147 | if not sipp_proc.poll(): |
| 148 | break |
| 149 | |
| 150 | if pid != 0: |
| 151 | # Win specific: get process handle from PID, as on win32, os.waitpid() takes process handle instead of pid |
| 152 | if (sys.platform == "win32"): |
| 153 | SYNCHRONIZE = 0x00100000 |
| 154 | PROCESS_QUERY_INFORMATION = 0x0400 |
| 155 | hnd = ctypes.windll.kernel32.OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION, False, pid) |
| 156 | pid = hnd |
| 157 | |
| 158 | return pid |
| 159 | |
| 160 | |
| 161 | # Wait SIPp process to exit, returning SIPp exit code |
| 162 | def wait_sipp(sipp): |
| 163 | if not SIPP_BG_MODE: |
| 164 | global FDEVNULL |
| 165 | sipp.wait() |
| 166 | FDEVNULL.close() |
| 167 | return sipp.returncode |
| 168 | |
| 169 | else: |
| 170 | print "Waiting SIPp (PID=" + str(sipp) + ") to exit.." |
| 171 | wait_cnt = 0 |
| 172 | while True: |
| 173 | try: |
| 174 | wait_cnt = wait_cnt + 1 |
| 175 | [pid_, ret_code] = os.waitpid(sipp, 0) |
| 176 | if sipp == pid_: |
| 177 | #print "SIPP returned ", ret_code |
| 178 | ret_code = ret_code >> 8 |
| 179 | |
| 180 | # Win specific: Close process handle |
| 181 | if (sys.platform == "win32"): |
| 182 | ctypes.windll.kernel32.CloseHandle(sipp) |
| 183 | |
| 184 | return ret_code |
| 185 | except os.error: |
| 186 | if wait_cnt <= 5: |
| 187 | print "Retry ("+str(wait_cnt)+") waiting SIPp.." |
| 188 | else: |
| 189 | return -99 |
| 190 | |
| 191 | |
| 192 | # Execute PJSUA flow |
| 193 | def exec_pjsua_expects(t, sipp): |
| 194 | # Get all PJSUA instances |
| 195 | ua = [] |
| 196 | for ua_idx in range(len(PJSUA_INST_PARAM)): |
| 197 | ua.append(t.process[ua_idx]) |
| 198 | |
| 199 | ua_err_st = "" |
| 200 | while len(PJSUA_EXPECTS): |
| 201 | expect = PJSUA_EXPECTS.pop(0) |
| 202 | ua_idx = expect[0] |
| 203 | expect_st = expect[1] |
| 204 | send_cmd = resolve_driver_macros(expect[2]) |
| 205 | # Handle exception in pjsua flow, to avoid zombie SIPp process |
| 206 | try: |
| 207 | if expect_st != "": |
| 208 | ua[ua_idx].expect(expect_st, raise_on_error = True) |
| 209 | if send_cmd != "": |
| 210 | ua[ua_idx].send(send_cmd) |
| 211 | except TestError, e: |
| 212 | ua_err_st = e.desc |
| 213 | break; |
| 214 | except: |
| 215 | ua_err_st = "Unknown error" |
| 216 | break; |
| 217 | |
| 218 | # Need to poll here for handling these cases: |
| 219 | # - If there is no PJSUA EXPECT scenario, we must keep polling the stdout, |
| 220 | # otherwise PJSUA process may stuck (due to stdout pipe buffer full?). |
| 221 | # - last PJSUA_EXPECT contains a pjsua command that needs time to |
| 222 | # finish, for example "v" (re-INVITE), the SIPp XML scenario may expect |
| 223 | # that re-INVITE transaction to be completed and without stdout poll |
| 224 | # PJSUA process may stuck. |
| 225 | # Ideally the poll should be done contiunously until SIPp process is |
| 226 | # terminated. |
| 227 | for ua_idx in range(len(ua)): |
| 228 | ua[ua_idx].expect(inc_const.STDOUT_REFRESH, raise_on_error = False) |
| 229 | |
| 230 | return ua_err_st |
| 231 | |
| 232 | |
| 233 | def sipp_err_to_str(err_code): |
| 234 | if err_code == 0: |
| 235 | return "All calls were successful" |
| 236 | elif err_code == 1: |
| 237 | return "At least one call failed" |
| 238 | elif err_code == 97: |
| 239 | return "exit on internal command. Calls may have been processed" |
| 240 | elif err_code == 99: |
| 241 | return "Normal exit without calls processed" |
| 242 | elif err_code == -1: |
| 243 | return "Fatal error (timeout)" |
| 244 | elif err_code == -2: |
| 245 | return "Fatal error binding a socket" |
| 246 | else: |
| 247 | return "Unknown error" |
| 248 | |
| 249 | |
| 250 | # Test body function |
| 251 | def TEST_FUNC(t): |
| 252 | |
| 253 | sipp_ret_code = 0 |
| 254 | ua_err_st = "" |
| 255 | |
| 256 | sipp = start_sipp() |
| 257 | if not sipp: |
| 258 | raise TestError("Failed starting SIPp") |
| 259 | |
| 260 | ua_err_st = exec_pjsua_expects(t, sipp) |
| 261 | |
| 262 | sipp_ret_code = wait_sipp(sipp) |
| 263 | |
| 264 | if ua_err_st != "": |
| 265 | raise TestError(ua_err_st) |
| 266 | |
| 267 | if sipp_ret_code: |
| 268 | rc = ctypes.c_byte(sipp_ret_code).value |
| 269 | raise TestError("SIPp returned error " + str(rc) + ": " + sipp_err_to_str(rc)) |
| 270 | |
| 271 | |
| 272 | # Here where it all comes together |
| 273 | test = TestParam(SIPP_SCEN_XML[:-4], |
| 274 | PJSUA_INST_PARAM, |
| 275 | TEST_FUNC) |