summaryrefslogtreecommitdiff
path: root/tests/pjsua/mod_sipp.py
blob: 82e6b75a8415073a60d8991d0a9d96fbb64d44e5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# $Id$
import ctypes
import time
import imp
import sys
import os
import re
import subprocess
from inc_cfg import *
import inc_const

# 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:\\Program Files (x86)\\Sipp_3.2\\sipp.exe"'
SIPP_PATH = 'sipp'
SIPP_PARAM = "-i 127.0.0.1 -p 6000 -m 1 127.0.0.1"
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 configuration file (a .py file whose the same name as SIPp XML file)
PJSUA_INST_PARAM = []
PJSUA_EXPECTS = []

# Default PJSUA param if configuration file (the corresponding .py file) 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)


# Init PJSUA test instance
if os.access(SIPP_SCEN_XML[:-4]+".py", os.R_OK):
    # Load from configuration file (the corresponding .py file), if any
    cfg_file = imp.load_source("cfg_file", SIPP_SCEN_XML[:-4]+".py")
    for ua_idx, ua_param in enumerate(cfg_file.PJSUA):
	PJSUA_INST_PARAM.append(InstanceParam("pjsua"+str(ua_idx+1), ua_param, sip_port=5060+ua_idx*2))
    PJSUA_EXPECTS = cfg_file.PJSUA_EXPECTS
else:
    # Just use the SIPp XML scenario
    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:6000"
    else:
	# auto answer when SIPp is as UAC
	ua_param = PJSUA_DEF_PARAM + " --auto-answer=200" 
    PJSUA_INST_PARAM.append(InstanceParam("pjsua", ua_param, sip_port=5060))
    


# Start SIPp process, returning PID
def start_sipp():
    global SIPP_BG_MODE
    sipp_proc = None

    # run SIPp
    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"
    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')
	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 = expect[2]
	# Handle exception in pjsua flow, to avoid zombie SIPp process
	try:
	    if expect_st != "":
		ua[ua_idx].expect(expect_st, raise_on_error = True)
	    if send_cmd != "":
		ua[ua_idx].send(send_cmd)
	except TestError, 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.
    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)