## Automatic test module for SIPp. ## ## This module will need a test driver for each SIPp scenario: ## - For simple scenario, i.e: make/receive call (including auth), this ## test module can auto-generate a default test driver, i.e: make call ## or apply auto answer. Just name the SIPp scenario using "uas" or ## "uac" prefix accordingly. ## - Custom test driver can be defined in a python script file containing ## a list of the PJSUA instances and another list for PJSUA expects/ ## commands. The custom test driver file must use the same filename as ## the SIPp XML scenario. See samples of SIPp scenario + its driver ## in tests/pjsua/scripts-sipp/ folder for detail. ## ## Here are defined macros that can be used in the custom driver: ## - $SIPP_PORT : SIPp binding port ## - $SIPP_URI : SIPp SIP URI ## - $PJSUA_PORT[N] : binding port of PJSUA instance #N ## - $PJSUA_URI[N] : SIP URI of PJSUA instance #N import ctypes import time import sys import os import re import subprocess from inc_cfg import * import inc_const import inc_util as util # flags that test is running in Unix G_INUNIX = False if sys.platform.lower().find("win32")!=-1 or sys.platform.lower().find("microsoft")!=-1: G_INUNIX = False else: G_INUNIX = True # /dev/null handle, for redirecting output when SIPP is not in background mode FDEVNULL = None # SIPp executable path and param #SIPP_PATH = '"C:\\devs\\bin\\Sipp_3.2\\sipp.exe"' SIPP_PATH = 'sipp' SIPP_PORT = 50070 SIPP_PARAM = "-m 1 -i 127.0.0.1 -p " + str(SIPP_PORT) SIPP_TIMEOUT = 60 # On BG mode, SIPp doesn't require special terminal # On non-BG mode, on win, it needs env var: "TERMINFO=c:\cygwin\usr\share\terminfo" # TODO: on unix with BG mode, waitpid() always fails, need to be fixed SIPP_BG_MODE = False #SIPP_BG_MODE = not G_INUNIX # Will be updated based on the test driver file (a .py file whose the same name as SIPp XML file) PJSUA_INST_PARAM = [] PJSUA_EXPECTS = [] # Default PJSUA param if test driver is not available: # - no-tcp as SIPp is on UDP only # - id, username, and realm: to allow PJSUA sending re-INVITE with auth after receiving 401/407 response PJSUA_DEF_PARAM = "--null-audio --max-calls=1 --no-tcp --id=sip:a@localhost --username=a --realm=*" # Get SIPp scenario (XML file) SIPP_SCEN_XML = "" if ARGS[1].endswith('.xml'): SIPP_SCEN_XML = ARGS[1] else: exit(-99) # Functions for resolving macros in the test driver def resolve_pjsua_port(mo): return str(PJSUA_INST_PARAM[int(mo.group(1))].sip_port) def resolve_pjsua_uri(mo): return PJSUA_INST_PARAM[int(mo.group(1))].uri[1:-1] def resolve_driver_macros(st): st = re.sub("\$SIPP_PORT", str(SIPP_PORT), st) st = re.sub("\$SIPP_URI", "sip:sipp@127.0.0.1:"+str(SIPP_PORT), st) st = re.sub("\$PJSUA_PORT\[(\d+)\]", resolve_pjsua_port, st) st = re.sub("\$PJSUA_URI\[(\d+)\]", resolve_pjsua_uri, st) return st # Init test driver if os.access(SIPP_SCEN_XML[:-4]+".py", os.R_OK): # Load test driver file (the corresponding .py file), if any cfg_file = util.load_module_from_file("cfg_file", SIPP_SCEN_XML[:-4]+".py") for ua_idx, ua_param in enumerate(cfg_file.PJSUA): ua_param = resolve_driver_macros(ua_param) PJSUA_INST_PARAM.append(InstanceParam("pjsua"+str(ua_idx), ua_param)) if DEFAULT_TELNET and hasattr(cfg_file, 'PJSUA_CLI_EXPECTS'): PJSUA_EXPECTS = cfg_file.PJSUA_CLI_EXPECTS else: PJSUA_EXPECTS = cfg_file.PJSUA_EXPECTS else: # Generate default test driver if os.path.basename(SIPP_SCEN_XML)[0:3] == "uas": # auto make call when SIPp is as UAS ua_param = PJSUA_DEF_PARAM + " sip:127.0.0.1:" + str(SIPP_PORT) else: # auto answer when SIPp is as UAC ua_param = PJSUA_DEF_PARAM + " --auto-answer=200" PJSUA_INST_PARAM.append(InstanceParam("pjsua", ua_param)) # Start SIPp process, returning PID def start_sipp(): global SIPP_BG_MODE sipp_proc = None sipp_param = SIPP_PARAM + " -sf " + SIPP_SCEN_XML if SIPP_BG_MODE: sipp_param = sipp_param + " -bg" if SIPP_TIMEOUT: sipp_param = sipp_param + " -timeout "+str(SIPP_TIMEOUT)+"s -timeout_error" + " -deadcall_wait "+str(SIPP_TIMEOUT)+"s" # add target param sipp_param = sipp_param + " 127.0.0.1:" + str(PJSUA_INST_PARAM[0].sip_port) # run SIPp fullcmd = os.path.normpath(SIPP_PATH) + " " + sipp_param print("Running SIPP: " + fullcmd) if SIPP_BG_MODE: sipp_proc = subprocess.Popen(fullcmd, bufsize=0, stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=G_INUNIX, universal_newlines=False) else: # redirect output to NULL global FDEVNULL #FDEVNULL = open(os.devnull, 'w') FDEVNULL = open("logs/sipp_output.tmp", 'w') sipp_proc = subprocess.Popen(fullcmd, shell=G_INUNIX, stdout=FDEVNULL, stderr=FDEVNULL) if not SIPP_BG_MODE: if sipp_proc == None or sipp_proc.poll(): return None return sipp_proc else: # get SIPp child process PID pid = 0 r = re.compile("PID=\[(\d+)\]", re.I) while True: line = sipp_proc.stdout.readline() pid_r = r.search(line) if pid_r: pid = int(pid_r.group(1)) break if not sipp_proc.poll(): break if pid != 0: # Win specific: get process handle from PID, as on win32, os.waitpid() takes process handle instead of pid if (sys.platform == "win32"): SYNCHRONIZE = 0x00100000 PROCESS_QUERY_INFORMATION = 0x0400 hnd = ctypes.windll.kernel32.OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION, False, pid) pid = hnd return pid # Wait SIPp process to exit, returning SIPp exit code def wait_sipp(sipp): if not SIPP_BG_MODE: global FDEVNULL sipp.wait() FDEVNULL.close() return sipp.returncode else: print("Waiting SIPp (PID=" + str(sipp) + ") to exit..") wait_cnt = 0 while True: try: wait_cnt = wait_cnt + 1 [pid_, ret_code] = os.waitpid(sipp, 0) if sipp == pid_: #print "SIPP returned ", ret_code ret_code = ret_code >> 8 # Win specific: Close process handle if (sys.platform == "win32"): ctypes.windll.kernel32.CloseHandle(sipp) return ret_code except os.error: if wait_cnt <= 5: print("Retry ("+str(wait_cnt)+") waiting SIPp..") else: return -99 # Execute PJSUA flow def exec_pjsua_expects(t, sipp): # Get all PJSUA instances ua = [] for ua_idx in range(len(PJSUA_INST_PARAM)): ua.append(t.process[ua_idx]) ua_err_st = "" while len(PJSUA_EXPECTS): expect = PJSUA_EXPECTS.pop(0) ua_idx = expect[0] expect_st = expect[1] send_cmd = resolve_driver_macros(expect[2]) timeout = expect[3] if len(expect)>=4 else 0 # Handle exception in pjsua flow, to avoid zombie SIPp process try: if expect_st != "": if timeout > 0: ua[ua_idx].expect(expect_st, raise_on_error = True, timeout = timeout) else: ua[ua_idx].expect(expect_st, raise_on_error = True) if send_cmd != "": ua[ua_idx].send(send_cmd) except TestError as e: ua_err_st = e.desc break; except: ua_err_st = "Unknown error" break; # Need to poll here for handling these cases: # - If there is no PJSUA EXPECT scenario, we must keep polling the stdout, # otherwise PJSUA process may stuck (due to stdout pipe buffer full?). # - last PJSUA_EXPECT contains a pjsua command that needs time to # finish, for example "v" (re-INVITE), the SIPp XML scenario may expect # that re-INVITE transaction to be completed and without stdout poll # PJSUA process may stuck. # Ideally the poll should be done contiunously until SIPp process is # terminated. # Update: now pjsua stdout is polled continuously by a dedicated thread, # so the poll is no longer needed #for ua_idx in range(len(ua)): # ua[ua_idx].expect(inc_const.STDOUT_REFRESH, raise_on_error = False) return ua_err_st def sipp_err_to_str(err_code): if err_code == 0: return "All calls were successful" elif err_code == 1: return "At least one call failed" elif err_code == 97: return "exit on internal command. Calls may have been processed" elif err_code == 99: return "Normal exit without calls processed" elif err_code == -1: return "Fatal error (timeout)" elif err_code == -2: return "Fatal error binding a socket" else: return "Unknown error" # Test body function def TEST_FUNC(t): sipp_ret_code = 0 ua_err_st = "" sipp = start_sipp() if not sipp: raise TestError("Failed starting SIPp") ua_err_st = exec_pjsua_expects(t, sipp) sipp_ret_code = wait_sipp(sipp) if ua_err_st != "": raise TestError(ua_err_st) if sipp_ret_code: rc = ctypes.c_byte(sipp_ret_code).value raise TestError("SIPp returned error " + str(rc) + ": " + sipp_err_to_str(rc)) # Here where it all comes together test = TestParam(SIPP_SCEN_XML[:-4], PJSUA_INST_PARAM, TEST_FUNC)