Voip Open Linear Tester Suite
Functional tests for VoIP systems based on voip_patrol, sipp, sox, chromaprint, opensips, and docker
The system is designed to run simple call scenarios, that you usually do with your desk phones.
Scenarios are run one by one from scenarios folder in alphabetical order, which could be considered a limitation, but also allows you to reuse the same accounts in a different set of tests. This stands for Linear in the name ;)
So, call some destination(s) with one (or more) device(s) and control call arrival on another phone(s).
But wait, there is more. VOLTS also can integrate with your MySQL and/or PostgreSQL databases to write some data there before the test and remove it after.
Also, it can record (and play, obviously) media during the call and do media checks of these files
It will make and receive calls and configure the database.
And to add, you can definitely use it in TDD approach when adding functionalities to your existing PBX system. Test-Fail-Fix.
The suite consists of 10 parts, that are running sequentially
- Preparation - at this part we're transforming templates to real scenarios of
voip_patrol,sipp,database,media_checkandscriptusingJinja2template engine withjinja2_timeextension to put some dynamic data based on time. - Start of a
Websocket-TLSproxy to provide possibility of use WSS transport for thevoip_patrolscenarios.
- Running database scripts. Usually - put some data inside some routing or subscriber data.
- Running custom scripts (
stage=pre), if the scenario declares them. - Running
voip_patrolorsippscenario. - Again running database scripts. Usually - remove data that had been put at stage 3.
- Run
media_checkif necessary to analyse obtained media files. - Running custom scripts (
stage=post) last, so all JSONL result files are readable.
- Tearing down a
Websocket-TLSproxy. - Report - at this part we're analyzing the results of the previous steps reading and interpreting file obtained running steps 3-8. Printing results in the desired way.
table_fullby default. Steps 3-8 are running sequentially against scenario files prepared in step 1. One at a time. Again, it's forLinear
You can build images locally or pull existing from a dockerhub.
Suite is designed to run locally from your Linux PC or Mac. And of course, docker should be installed. It's up to you.
Notes on using podman: I was able to run VOLTS using podman-docker package. One obstacle by default - the volumes permissions inside a container. To address this issue please refer to this article.
To build, just run ./build.sh. Script will build 8 docker images.
In a case if voip_patrol or sipp is updated, you need to rebuild these containers again, you can do it with ./build.sh -r <component>, refer to ./build.sh --help.
./build.sh [OPTIONS]| Option | Description |
|---|---|
-c, --clean |
Stop and remove all VOLTS containers and images |
-r, --refresh |
Force rebuild all components (--no-cache) |
-r, --refresh comp1[,comp2,...] |
Force rebuild specific component(s) (--no-cache) |
-p, --push |
Tag and push images to the registry. Change the REGISTRY variable in a script accordingly |
# Build all components
./build.sh
# Force rebuild all components from scratch
./build.sh --refresh
# Rebuild only the voip_patrol component
./build.sh --refresh vp
# Rebuild specific components
./build.sh --refresh vp,report
# Clean up all containers and images
./build.sh --clean
# Build and push to registry
./build.sh --pushAfter building, just run
./run.shSimple, isn't it? This will run all scenarios found in scenarios folder one by one.
The run.sh script supports various command-line options for flexible test execution:
./run.sh [OPTIONS] [SCENARIO]-h, --help- Show help message with all available options-l, --log-level N- Set log level (0=silent, 1=normal, 2=verbose, 3=debug)-r, --report TYPE- Set report type (table|json|table_full|json_full)-t, --timeout N- Set maximum test time in seconds-v, --verbose- Enable verbose output (equivalent to -l 2)-d, --debug- Enable debug output (equivalent to -l 3)--tls-port N- Set OpenSIPS TLS port--wss-port N- Set OpenSIPS WSS port--heps-port N- Set HEP source port--hepd-port N- Set HEP destination port
<scenario_name>...- Run one or more scenarios, space- and/or comma-separated (e.g.,001-register 002-call-echoor001-register,002-call-echo)scenarios/<scenario_name>- Run specific scenario with full pathtag=<tags>- Run scenarios with specific tags (e.g., tag=set1,set2)stop- Stop and delete all containerssngrep- Launch SIP packet capture tooldbclean- Clean up test data from databasesreport-only- Skip test execution and only generate the report from existing data
report-only re-reads the JSONL results left in tmp/output by the previous run instead of running any tests, so pair it with -r/--report to re-render that same run in a different format (for example ./run.sh -r json_full report-only after a table run) without touching your PBX again.
# Run all scenarios
./run.sh
# Run specific scenario
./run.sh 001-register
./run.sh scenarios/001-register.xml
# Run several scenarios (space- and/or comma-separated)
./run.sh 001-register 002-call-echo
./run.sh 001-register,002-call-echo
# Run with debug logging
./run.sh -l 3 001-register
./run.sh --debug 001-register
# Run with JSON output and verbose logging
./run.sh -r json -v
# Run scenarios with specific tags
./run.sh tag=smoke,regression
# Run with custom timeout
./run.sh --timeout 300 001-register
# Utility commands
./run.sh stop # Stop all containers
./run.sh sngrep # Launch SIP packet capture
./run.sh dbclean # Clean up databases
./run.sh report-only # Re-generate the report from the previous run
# Re-render the previous run as JSON without re-testing
./run.sh -r json_full report-onlyAfter running the suite you can always find voip_patrol presented results in tmp/output folder.
But simply running something blindly is boring, so before this, best to do some
We suppose to configure 2 parts here. First, and most complex are
VOLTS scenarios are combined voip_patrol/sipp, database, and media_check scenarios, that are just being templatized with Jinja2 style. Mostly done not to repeat some passwords, usernames, domains, etc.
Also, due to using jinja2-time extension, it's possible to use dynamic time/date values in your scenarios, for example testing some time-based rules on your PBX. For full documentation on how to use this type of data, please refer to jinja2-time documentation.
Standard Jinja2 {% include %} also works, resolved relative to the scenarios folder, so repeated blocks can be factored into reusable snippets - see scenarios/helpers/ and their use in scenarios/33-register-wait-for-call-wss.xml and scenarios/34-sipp-load-register.xml.
As you will see below, the core for all types of tests is actually voip_patrol or sipp, others are just helpers around.
Values for templates are taken from scenarios/config.yaml
One thing to mention here, is that vars from global section transform to c. (for config or g. for global, c. and g. are equal) and from accounts to a. in templates for shorter notation.
There is a special name scenario_name that is transforming to a scenario file name stripped .xml extension.
There is also an env variable that exposes system environment variables to templates. Use it when you need to pass runtime values without modifying config.yaml:
{{ env.MY_VAR }} <!-- empty string if not set -->
{{ env.MY_VAR | default('fallback') }} <!-- explicit fallback value -->
{{ env.DOMAIN | default(c.domain) }} <!-- fall back to config.yaml value -->Also, all settings from global section are inherited to the accounts section automatically unless they are defined there explicitly.
config.yaml
global:
domain: '<SOME_DOMAIN>'
transport: 'tls'
srtp: 'dtls,sdes,force'
play_file: '/voice_ref_files/8000_12s.wav'
databases:
'sipproxydb':
type: 'mysql'
user: 'sipproxydbrw'
password: 'sipproxydbrwpass'
base: 'sipproxydb'
host: 'mysipproxydb.local'
accounts:
'88881':
username: '88881'
auth_username: '88881-1'
password: 'SuperSecretPass1'
'88882':
username: '88882'
auth_username: '88882-67345'
password: 'SuperSecretPass2'
'90001':
username: '90001'
auth_username: '90001'
password: 'SuperSecretPass3'To get most of it, please refer to voip_patrol config, but here follows some basic example to show the idea of the templating.
Make a register
<config>
<section type="voip_patrol">
<actions>
<action type="register" label="Register {{ a.88881.label }}"
transport="{{ a.88881.transport }}"
<!-- Account parameter is more used in receiving calls on this account later -->
account="{{ a.88881.label }}"
<!-- username would be a part of AOR - <sip:username@realm> -->
username="{{ a.88881.username }}"
<!-- auth_username would be used in WWW-Authorize procedure -->
auth_username="{{ a.88881.auth_username }}"
password="{{ a.88881.password }}"
registrar="{{ c.domain }}"
realm="{{ a.88881.domain }}"
<!-- We are expecting to get 200 code here, so REGISTER is successful -->
expected_cause_code="200"
/>
<!-- Just wait 2 sec for all timeouts -->
<action type="wait" complete="true" ms="2000"/>
</actions>
</section>
</config>As voip_patrol itself is not supporting websocket transport, OpenSIPS is used as a TLS-WSS SIP proxy. Using of it is simple, you just specify transport="wss" in your voip_patrol section, but under the hood it's using combination of tls transport and proxy option, so you can't use proxy option along with WebSocket transport.
There is also a possibility to debug what is passing the proxy as it posts HEP encapsulated SIP traffic on a localhost. You can capture it with sngrep using
$ docker exec -it volts_opensips sngrep -L udp:127.0.0.1:8888or
./run.sh sngrepin a separate terminal window during running tests, or using local installation of sngrep with
$ sudo sngrep -L udp:127.0.0.1:8888 port 8888where 8888 is the OPENSIPS_HEPD_PORT (can be configured with --hepd-port option).
Point, you need to use turn section of your voip_patrol configuration to make sure media is passing. See the example below.
You can just add your existing SIPP scenarios mainly unchanged and they would be run with the following command:
sipp <target> -sf <scenario.xml> -m 1 -mp <random_port> -i <container_ip>Send an OPTIONS
<config tag="sipp">
<!-- Tag this test as sipp -->
<section type="sipp">
<actions>
<action transport="{{ c.transport }}" target="{{ c.domain }}">
<scenario name="Options">
<send>
<![CDATA[
OPTIONS sip:check_server_health@{{ c.domain }} SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
Max-Forwards: 70
To: <sip:check_server_health@{{ c.domain }}>
From: sipp <sip:check_server_health@[local_ip]:[local_port]>;tag=[call_number]
Call-ID: [call_id]
CSeq: 1 OPTIONS
Contact: <sip:check_server_health@[local_ip]:[local_port]>
Accept: application/sdp
Content-Length: 0
]]>
</send>
<recv response="200" timeout="200"/>
</scenario>
</action>
</actions>
</section>
</config>| Attribute | Description |
|---|---|
transport |
Actual transport SIPP will use. Values are udp(default), tcp or tls. |
socket_mode |
single (default) - with one socket for all calls or multi - with one socket for each call. |
target |
What you usually specify as target when running standalone SIPP. If transport is TLS and no port is specified, 5061 is appended by default. |
call_rate |
-r option in SIPP. Set the call rate (in calls per seconds). 10 by default. |
max_calls |
-m option in SIPP. Stop the test and exit when 'max_calls' calls are processed. 1 by default. |
max_concurrent_calls |
-l option in SIPP. Set the maximum number of simultaneous calls. 10 by default. |
total_timeout |
How long to wait for a test to perform in seconds. 600 (10 minutes) by default |
Built by default with ./build.sh. Scripts run only when a scenario declares a <section type="script">. Your .sh / .py files live in the repo-root scripts/ folder, which is mounted read-only into the container on every run — adding or editing a script needs no rebuild. Shipped samples are inert *.sample files; activate with cp scripts/ping_host.sh.sample scripts/ping_host.sh. User scripts are gitignored, so git pull / docker pull never touch them. The image is batteries-included (curl, jq, ping, dig, python requests, plus the volts_results / volts-result result-query helpers); optional extra pip deps go in local scripts/requirements.txt and are installed at runtime into tmp/scripter-deps (no image rebuild — see the guide). Full developer guide: scripts/README.md. End-to-end wiring demos: scenarios/37-call-codec-priority-assert.xml (codec offer + assert_codec) and scenarios/38-call-media-cdr-http.xml (media + CDR http_check by Call-ID).
| Attribute | Default | Description |
|---|---|---|
script |
(required) | Basename of a .sh / .py file in the mounted scripts/ folder |
stage |
pre |
pre (before voip_patrol) or post (after media, so all JSONLs exist) |
continue_on_error |
false |
Keep running later actions in this stage on failure |
timeout |
60 |
Seconds before the script process tree is killed |
label |
script name | Human-readable name in logs and report error text |
Params are <param name="" value=""/> children (or text content for multi-line values), exported as UPPERCASE env vars inside the container. They travel via the mounted script.xml (not docker --env CLI) so special characters round-trip; they still appear in the container process environment — never print params or VOLTS_PARAMS_JSON. Exit 0 = PASS, non-zero = FAIL (stderr/stdout tail becomes s_error). Multiple <section type="script"> blocks in one scenario are merged into a single action list, preserving document order. pre and post are independent — there is no database-style cleanup inversion. A failed pre script fails the scenario in the report, but run.sh still continues into voip/sipp/media/post.
post scripts can query earlier results (e.g. SIP Call-IDs) via the baked-in helpers: volts-result vp --get callid in bash or from volts_results import vp_tests in python. A label matching several call legs (call_count > 1) returns all of them — use --first/--last to pick one.
<config>
<section type="script">
<actions>
<action script="ping_host.sh" label="SBC reachable before test" stage="pre">
<param name="host" value="{{ c.domain }}"/>
</action>
</actions>
</section>
<section type="voip_patrol">
<actions>
<!-- usual call/register actions -->
</actions>
</section>
<section type="script">
<actions>
<action script="http_check.py" label="CDR landed in billing API" stage="post"
continue_on_error="true" timeout="30">
<param name="url" value="https://api.example.com/cdr/{{ scenario_name }}"/>
<param name="expected_status" value="200"/>
<param name="api_key" value="{{ c.api_key }}"/>
</action>
</actions>
</section>
</config>Database config is also done in XML, section database. We have 2 stages of database scripts.
| Stage | Description |
|---|---|
pre |
Launched before running voip_patrol. Usually stage to put some accounts data, routing, etc. |
post |
Obviously, running after voip_patrol. For cleanup data inserted in pre stage. |
So, inside the database action you specify the tables you're working with. Each table section has these attributes.
| Attribute | Description |
|---|---|
name |
Actually name of the table we're working with. |
type |
Could be insert, replace, delete and check. Forming actual INSERT, REPLACE, DELETE and SELECT COUNT(*) SQL statements for the database. |
continue_on_error |
Optional. Em.. ignore errors on performed actions and continue no matter what. By default database actions will be stopped after encountering the first error. |
cleanup_after_test |
Optional. For insert: automatically forms a delete on post stage. For check: deletes matching rows immediately after the check passes. false by default. |
row_nums |
Required for check. Expected number of matching rows — exact ("1") or range ("1-3"). |
Make a register with the database
<!-- Test simple register -->
<config>
<section type="database">
<actions>
<!-- "kamdb" here is referring to an entity in "databases" from config.yaml. -->
<action database="kamdb" stage="pre">
<!-- what data are we gonna insert into the "subscriber" table? -->
<table name="subscriber" type="insert" cleanup_after_test="true">
<field name="username" value="{{ a.88881.username }}"/>
<field name="domain" value="{{ c.domain }}"/>
<field name="password" value="{{ a.88881.password }}"/>
</table>
</action>
</actions>
</section>
<section type="voip_patrol">
<actions>
<action type="register" label="Register {{ a.88881.label }}"
transport="{{ a.88881.transport }}"
<!-- Account parameter is more used in receiving calls on this account later -->
account="{{ a.88881.label }}"
<!-- username would be a part of AOR - <sip:username@realm> -->
username="{{ a.88881.username }}"
<!-- auth_username would be used in WWW-Authorize procedure -->
auth_username="{{ a.88881.auth_username }}"
password="{{ a.88881.password }}"
registrar="{{ c.domain }}"
realm="{{ a.88881.domain }}"
<!-- We are expecting to get 200 code here, so REGISTER is successful -->
expected_cause_code="200"
/>
<!-- Just wait 2 sec for all timeouts -->
<action type="wait" complete="true" ms="2000"/>
</actions>
</section>
</config>The check type verifies that database got the expected rows during a test. CDR as is. It runs SELECT COUNT(*) with the specified field conditions and fails the scenario if the count doesn't match row_nums.
Two useful features for time-scoped checks:
- Prefix a
valuewith a bash-style operator flag — the same convention used insox_filter. Supported flags:-eq=,-ne!=,-lt<,-le<=,-gt>,-ge>=,-likeLIKE. Without a flag,=is assumed (orLIKEwhen the value contains%). - Use
{test_start}and{test_end}tokens anywhere in avalueto reference when the scenario started and finished. These are not Jinja2 — they pass through the prepare step unchanged and are substituted at runtime by the database container. An optional strftime format can be appended after:, for example{test_start:%Y-%m-%d %H:%M}.
Verify a call was recorded in Asterisk CDR
After a completed call, Asterisk writes a row to its cdr table. The example below makes a call and then asserts in the post stage that the row exists. The calldate range pins the check to rows written during this specific test run, so a stale CDR from a previous run of the same scenario won't cause a false pass.
<config>
<section type="database">
<actions>
<!-- Check that Asterisk wrote a CDR row for this call -->
<action database="astdb" stage="post">
<table name="cdr" type="check" row_nums="1">
<field name="src" value="{{ a.90001.label }}"/>
<field name="dst" value="{{ a.88881.label }}"/>
<field name="disposition" value="ANSWERED"/>
<!-- Scope to rows written during this test run only -->
<field name="calldate" value="-ge {test_start}"/>
<field name="calldate" value="-le {test_end}"/>
</table>
</action>
</actions>
</section>
<section type="voip_patrol">
<actions>
<action type="codec" disable="all"/>
<action type="codec" enable="pcma" priority="250"/>
<action type="codec" enable="pcmu" priority="249"/>
<action type="register" label="Register {{ a.88881.label }}"
transport="{{ a.88881.transport }}"
account="{{ a.88881.label }}"
username="{{ a.88881.username }}"
auth_username="{{ a.88881.auth_username }}"
password="{{ a.88881.password }}"
registrar="{{ c.domain }}"
realm="{{ c.domain }}"
expected_cause_code="200"
srtp="{{ a.88881.srtp }}"
/>
<action type="wait" complete="true" ms="2000"/>
<action type="accept" label="Receive call on {{ a.88881.label }}"
call_count="1"
match_account="{{ a.88881.label }}"
hangup="10"
code="200" reason="OK"
transport="{{ a.88881.transport }}"
srtp="{{ a.88881.srtp }}"
play="{{ c.play_file }}"
/>
<action type="call" label="Call {{ a.90001.label }} -> {{ a.88881.label }}"
transport="{{ a.90001.transport }}"
expected_cause_code="200"
caller="{{ a.90001.label }}@{{ c.domain }}"
callee="{{ a.88881.label }}@{{ c.domain }}"
from="sip:{{ a.90001.label }}@{{ c.domain }}"
to_uri="{{ a.88881.label }}@{{ c.domain }}"
max_duration="20" hangup="10"
auth_username="{{ a.90001.username }}"
password="{{ a.90001.password }}"
realm="{{ c.domain }}"
rtp_stats="true"
max_ring_duration="15"
srtp="{{ a.90001.srtp }}"
play="{{ c.play_file }}"
/>
<action type="wait" complete="true" ms="30000"/>
</actions>
</section>
</config>You can analyze call recordings with various media tools.
Media check is also described in XML
| Attribute | Description |
|---|---|
type |
Mandatory. Media check test to be performed. Currently sox/sox_st/fpcalc. |
file |
Mandatory. Path to file to check. Has to be aligned with record in one of voip_patrol actions. Best to have it with distinct names, see the example below for a better picture |
delete_after |
Do we delete file after media check? yes/no/keep_failed. keep_failed by default. This means we keep the file if the media test did not pass |
print_debug |
Print debug info on file on the console while testing. Useful for adjusting filter parameters or getting actual fingerprint via fpcalc. yes/no. no by default |
length |
Expected length of a sound file in seconds. Supports <min>-<max> format. (In a case of sox uses length s field, see below) |
sox_filter |
Used if type is sox/sox_st. Semicolon-separated expressions to test values obtained by SoX utility with the given file. Usually to check some float values like length or amplitude. See below more detailed description |
fingerprint |
Used if type is fpcalc. Fingerprint in a -raw form obtained from fpcalc tool |
likeness |
Used if type is fpcalc. Minimal likeness or similarity. Default value - 0.9. See the explanation below |
max_offset |
Used if type is fpcalc. Possible integer offset when comparing fingerprints. Each N corresponds roughly to 0.5s. Allows to check if you expect sort of audio "drifts" during the test. 0 by default |
Within this check parameters from the file are collected by sox utility, more precisely - sox --i <file>, sox <file> -n stat, sox <file> -n stats.
In a sox_filter attribute you can write a string to check some given values against collected parameters. All filter expressions should be true for the test to pass. Best to be explained on the example
Note: you can trim silence at the start and the end of the record with using sox_st as a type. This allows a bit better file fingerprinting
sox_filter="length s -ge 10; length s -le 11"
Here we have 1 parameter - length s that should be greater than or equal to 10 and less than or equal to 11. length s is one of the result parameters that are obtained by sox <file> -n stats.
Point here is used not traditional <= style notation, but bash (-eq is ==, -lt is <, -gt is >, -le is <=, -ge is >=, -ne is !=) style comparison operators.
Note: in a case of length you can use length parameter, like length="10-11" for shorter notation.
This is done because traditional comparison symbols (<,>) are part of XML notation
Getting parameters names is simple - they are converted from sox outputs, for example:
# sox 8000_12s.wav -n stats
DC offset -0.000078 -> 'dc offset' float
Min level -0.321594 -> 'min level' float
Max level 0.430359 -> 'max level' float
Pk lev dB -7.32 -> 'pk lev db' float
RMS lev dB -29.63 -> 'rms lev db' float
RMS Pk dB -16.78 -> 'rms pk db' float
RMS Tr dB -74.77 ...
Crest factor 13.04 ...
Flat factor 0.00 ...
Pk count 2 ...
Bit-depth 15/16 -> 'bit-depth' string
Num samples 531k -> 'num samples' string
Length s 12.034 -> 'length s' float
Scale max 1.000000 ...
Window s 0.050 ...
Here all parameter names are lowercase. One more example with the data above:
sox_filter="length s -ge 11; crest factor -lt 10; bit-depth -eq 15/16"
All number-like values are automatically treated as numbers and you can apply -lt, -ge type of comparisons.
Make a call to echo number and analyze the outcome
<!-- Call echo service and make sure receive an answer -->
<config>
<section type="voip_patrol">
<actions>
<action type="codec" disable="all"/>
<action type="codec" enable="pcma" priority="250"/>
<action type="codec" enable="pcmu" priority="249"/>
<action type="call" label="Call to 11111 (echo)"
transport="{{ a.88881.transport }}"
<!-- We are expecting answer here -->
expected_cause_code="200"
caller="{{ a.88881.label }}@{{ c.domain }}"
<!-- Setting R-URI -->
callee="11111@{{ a.88881.domain }}"
from="sip:{{ a.88881.label }}@{{ c.domain }}"
to_uri="11111@{{ c.domain }}"
max_duration="20" hangup="10"
auth_username="{{ a.88881.username }}"
password="{{ a.88881.password }}"
realm="{{ c.domain }}"
play="{{ c.play_file }}"
rtp_stats="true"
srtp="{{ a.88881.srtp }}"
<!-- We need to record file on an answer -->
record="{{ scenario_name }}.wav"
/>
<action type="wait" complete="true" ms="30000"/>
</actions>
</section>
<section type="media_check">
<actions>
<action type="sox_st"
<!-- The same can be achieved with length="10-11" option -->
sox_filter="length s -ge 10; length s -le 11"
<!-- File name is the same as in the "record" attribute in the "call" action above -->
file="{{ scenario_name }}.wav"
/>
</actions>
</section>
</config>
Actually, calculate "likeness" or similarity of an audio to provided fingerprint using fpcalc utility. More info about fingerprinting can be found here.
Note: make sure that files have at least 3-4 seconds of audio for this tool. 10-15 seconds is recommended
Make a call to echo number and analyze the outcome
<!-- Call echo service and make sure receive an answer -->
<config>
<section type="voip_patrol">
<actions>
<action type="codec" disable="all"/>
<action type="codec" enable="pcma" priority="250"/>
<action type="codec" enable="pcmu" priority="249"/>
<action type="call" label="Call to 11111 (echo)"
transport="{{ a.88881.transport }}"
<!-- We are expecting answer here -->
expected_cause_code="200"
caller="{{ a.88881.label }}@{{ c.domain }}"
<!-- Setting R-URI -->
callee="11111@{{ a.88881.domain }}"
from="sip:{{ a.88881.label }}@{{ c.domain }}"
to_uri="11111@{{ c.domain }}"
max_duration="20" hangup="10"
auth_username="{{ a.88881.username }}"
password="{{ a.88881.password }}"
realm="{{ c.domain }}"
play="{{ c.play_file }}"
rtp_stats="true"
srtp="{{ a.88881.srtp }}"
<!-- We need to record file on an answer -->
record="{{ scenario_name }}.wav"
/>
<action type="wait" complete="true" ms="30000"/>
</actions>
</section>
<section type="media_check">
<actions>
<action type="fpcalc"
length="9.5-10.5"
fingerprint="3089777192, 3089826344, 3089901096, 3089902136, 3111922232, 3120310648, 2577140987, 2577204442, 2577346698, 2560585866, 2564477114, 2558182570, 2557117614, 3643442334, 3744380046, 3745423566, 3711739086, 4246514814, 4229277806, 4233271406, 4243753262, 2096285998, 2138131766, 1987051526, 1982865414, 1445076997, 371593508, 375784228, 1986789920, 1986544240, 1978334916, 1416555204, 1416465028, 1344834180, 1349098132, 1357627060, 1353380532, 4037665462, 4037600166, 4093407654, 4068241582, 4147963054, 4148095226, 4152256522, 4152240141, 4113377292, 3508807692, 4045892460, 4117123676, 4121297432, 3584492060, 3618050572, 3601162780, 3601096740, 3596887079, 3592685607, 3592685615, 3592820798, 3602275086, 3583958542"
<!-- File name is the same as in the "record" attribute in the "call" action above -->
file="{{ scenario_name }}.wav"
likeness="0.85"
/>
</actions>
</section>
</config>
Note on likeness:
| Likeness score | Meaning |
|---|---|
| 0.95 – 1.00 | Near identical (codec change, minor distortion) |
| 0.85 – 0.95 | Same content, more significant degradation |
| 0.70 – 0.85 | Possibly related, uncertain |
| < 0.70 | Likely different content |
The run.sh script can be configured through command-line options (preferred) or environment variables:
Use the command-line options described in the Running section for most configuration needs.
You can also set these environment variables to override default behavior:
| Variable name | Default | Description |
|---|---|---|
REPORT_TYPE |
table_full |
Report type provided at the end: table, json, table_full, json_full. Can be overridden with -r/--report option |
LOG_LEVEL |
0 |
voip_patrol/sipp log level on console (0-3). Note the default is 0 (silent) - raise it with -v/-d if you expect console output from the test containers. Can be overridden with -l/--log-level option |
MAX_SINGLE_TEST_TIME |
600 |
Maximum time single test is allowed to run in seconds. Can be overridden with -t/--timeout option |
Additional variables for advanced users:
| Variable name | Default | Description |
|---|---|---|
OPENSIPS_TLS_PORT |
6051 |
OpenSIPS TLS port. Can be overridden with --tls-port option |
OPENSIPS_WSS_PORT |
9443 |
OpenSIPS WSS port. Can be overridden with --wss-port option |
OPENSIPS_HEPS_PORT |
8887 |
HEP source port. Can be overridden with --heps-port option |
OPENSIPS_HEPD_PORT |
8888 |
HEP destination port. Can be overridden with --hepd-port option |
As a result, you will have a table like this.
A component column (VoIP Patrol, SIPP, Database, Media, Script) is printed only when at least one scenario in the run actually exercised that component. Scenarios in the same run that did not use it show N/A. So the exact set of columns depends on what you ran - a run of nothing but sipp scenarios prints no Database, Media or Script column at all.
+---------------------------------------+-----------------------------------------------------------+------+----------+-------+--------+------------------+
| Scenario | VoIP Patrol | SIPP | Database | Media | Status | Text |
+---------------------------------------+-----------------------------------------------------------+------+----------+-------+--------+------------------+
| 01-register | PASS | N/A | N/A | N/A | PASS | Scenario passed |
| | Register 88881 | | | | PASS | Main test passed |
| 02-call-echo | PASS | N/A | N/A | N/A | PASS | Scenario passed |
| | Call to 11111 (echo) | | | | PASS | Main test passed |
....
| 51-call-echo-media-control | PASS | N/A | N/A | PASS | PASS | Scenario passed |
| | Call to 11111 (echo) | | | | PASS | Main test passed |
| 52-delayed-call-forward-unconditional | PASS | N/A | PASS | N/A | PASS | Scenario passed |
| | Register 90012 | | | | PASS | Main test passed |
| | Register 90013 | | | | PASS | Main test passed |
| | Receive call on 90012 and not answer | | | | PASS | Call canceled |
| | Call from 90011 to 90012 (delay forward 25 sec) ->90013 | | | | PASS | Main test passed |
| | Receive call on 90013 finally | | | | PASS | Main test passed |
| 53-server-check-health | N/A | PASS | N/A | N/A | PASS | SIPP test passed |
+---------------------------------------+-----------------------------------------------------------+------+----------+-------+--------+------------------+
Scenarios ['49-teams-follow-forward', '50-team-no-answer-forward'] are failed!
The same run with custom script actions added to one scenario gains a Script column, by exactly the rule above:
+----------------------+-------------+------+----------+-------+--------+--------+------------------+
| Scenario | VoIP Patrol | SIPP | Database | Media | Script | Status | Text |
+----------------------+-------------+------+----------+-------+--------+--------+------------------+
| 60-call-with-scripts | PASS | N/A | N/A | N/A | PASS | PASS | Scenario passed |
| 02-call-echo | PASS | N/A | N/A | N/A | N/A | PASS | Scenario passed |
+----------------------+-------------+------+----------+-------+--------+--------+------------------+
That means your system is not OK, or something needs to be tuned with the tests.
Not really much to describe here, just read info on the console
Examples shown in this section can duplicate examples from the section above. For more examples, refer to the scenarios folder.
- Here we assume that account data is known to our PBX.
<config>
<section type="voip_patrol">
<actions>
<action type="register" label="Register {{ a.88881.label }}"
transport="{{ a.88881.transport }}"
<!-- Account parameter is more used in receiving a call on this account later -->
account="{{ a.88881.label }}"
<!-- username would be a part of AOR - <sip:username@realm> -->
username="{{ a.88881.username }}"
<!-- auth_username would be used in WWW-Authorize procedure -->
auth_username="{{ a.88881.auth_username }}"
password="{{ a.88881.password }}"
registrar="{{ c.domain }}"
realm="{{ a.88881.domain }}"
<!-- We are expecting to get 200 code here, so REGISTER is successful -->
expected_cause_code="200"
/>
<!-- Just wait 2 sec for all timeouts -->
<action type="wait" complete="true" ms="2000"/>
</actions>
</section>
</config>- .. but what if we need to add account data to PBX dynamically? Assume, that we have a SIP Proxy as a target system here.
<!-- Test simple register -->
<config>
<section type="database">
<actions>
<!-- "sippproxydb" here is referring to an entity in "databases" from config.yaml. "stage" is explained a bit below -->
<action database="sippproxydb" stage="pre">
<!-- what data are we gonna insert into the "subscriber" table? -->
<table name="subscriber" type="insert" cleanup_after_test="true">
<field name="username" value="{{ a.88881.username }}"/>
<field name="domain" value="{{ c.domain }}"/>
<field name="password" value="{{ a.88881.password }}"/>
</table>
</action>
</actions>
</section>
<section type="voip_patrol">
<actions>
<action type="register" label="Register {{ a.88881.label }}"
transport="{{ a.88881.transport }}"
<!-- Account parameter is more used in receiving a call on this account later -->
account="{{ a.88881.label }}"
<!-- username would be a part of AOR - <sip:username@realm> -->
username="{{ a.88881.username }}"
<!-- auth_username would be used in WWW-Authorize procedure -->
auth_username="{{ a.88881.auth_username }}"
password="{{ a.88881.password }}"
registrar="{{ c.domain }}"
realm="{{ a.88881.domain }}"
<!-- We are expecting to get 200 code here, so REGISTER is successful -->
expected_cause_code="200"
/>
<!-- Just wait 2 sec for all timeouts -->
<action type="wait" complete="true" ms="2000"/>
</actions>
</section>
</config>We're deleting the data from the database and restoring it afterward.
<config>
<section type="database">
<actions>
<action database="sippproxydb" stage="pre">
<table name="subscriber" type="delete" cleanup_after_test="true">
<field name="username" value="{{ a.88881.username }}"/>
<field name="domain" value="{{ c.domain }}"/>
<field name="password" value="{{ a.88881.password }}"/>
</table>
</action>
</actions>
</section>
<section type="voip_patrol">
<actions>
<action type="register" label="Register {{ a.88881.label }}"
transport="{{ a.88881.transport }}"
account="{{ a.88881.label }}"
username="{{ a.88881.username }}"
auth_username="{{ a.88881.auth_username }}"
password="{{ a.88881.password }}"
registrar="{{ c.domain }}"
realm="{{ a.88881.domain }}"
<!-- We are expecting to get 407 code here, maybe your registrar sending 401 or 403 code. So - adjust it here. -->
expected_cause_code="407"
/>
<action type="wait" complete="true" ms="2000"/>
</actions>
</section>
</config>Register with 1 account and make a call from 90001 to 88881. Max wait time to answer - 15 sec, duration of connected call - 10 sec.
Point we don't register account 90001 here, as we're not receiving calls on it, just need to provide credentials on INVITE.
Also trick, match_account in accept perfectly links with account in register.
<config>
<actions>
<!-- As we're using call functionality here - define the list of codecs -->
<action type="codec" disable="all"/>
<action type="codec" enable="pcma" priority="250"/>
<action type="codec" enable="pcmu" priority="249"/>
<action type="codec" enable="opus" priority="248"/>
<action type="register" label="Register {{ a.88881.label }}"
transport="{{ a.88881.transport }}"
account="{{ a.88881.label }}"
username="{{ a.88881.username }}"
auth_username="{{ a.88881.auth_username }}"
password="{{ a.88881.password }}"
registrar="{{ c.domain }}"
realm="{{ c.domain }}"
expected_cause_code="200"
<!-- Make sure we are using SRTP on a call received. This is done here as accounts are created before accept(answer) action -->
srtp="{{ a.88881.srtp }}"
/>
<action type="wait" complete="true" ms="2000"/>
<action type="accept" label="Receive call on {{ a.88881.label }}"
<!-- This is not a load test - so only 1 call is expected -->
call_count="1"
<!-- Make sure we have received a call on a previously registered account -->
match_account="{{ a.88881.label }}"
<!-- Hangup in 10 seconds after answer -->
hangup="10"
<!-- Send back "200 OK" -->
code="200" reason="OK"
transport="{{ a.88881.transport }}"
<!-- Make sure we are using SRTP -->
srtp="{{ a.88881.srtp }}"
<!-- Play a file back to gather RTCP stats in the report -->
play="{{ c.play_file }}"
/>
<action type="call" label="Call {{ a.90001.label }} -> {{ a.88881.label }}"
transport="tls"
<!-- We are waiting for an answer -->
expected_cause_code="200"
caller="{{ a.90001.label }}@{{ c.domain }}"
callee="{{ a.88881.label }}@{{ c.domain }}"
from="sip:{{ a.90001.label }}@{{ c.domain }}"
to_uri="{{ a.88881.label }}@{{ c.domain }}"
max_duration="20" hangup="10"
<!-- We are specifying all auth data here for INVITE -->
auth_username="{{ a.90001.username }}"
password="{{ a.90001.password }}"
realm="{{ c.domain }}"
rtp_stats="true"
max_ring_duration="15"
srtp="{{ a.90001.srtp }}"
play="{{ c.play_file }}"
/>
<action type="wait" complete="true" ms="30000"/>
</actions>
</config>Register with 2 accounts and call from the third one, not answer on 1st and make sure we receive a call on the second. So, your PBX should be configured to make a Forward-No-Answer from 88881 to 88882.
Also make sure, that on 88882 we got the call from 90001 (based on CallerID).
<config>
<actions>
<action type="codec" disable="all"/>
<action type="codec" enable="pcma" priority="250"/>
<action type="codec" enable="pcmu" priority="249"/>
<action type="codec" enable="opus" priority="248"/>
<action type="register" label="Register {{ a.88881.label }}"
transport="{{ a.88881.transport }}"
account="{{ a.88881.label }}"
username="{{ a.88881.username }}"
auth_username="{{ a.88881.auth_username }}"
password="{{ a.88881.password }}"
registrar="{{ c.domain }}"
realm="{{ c.domain }}"
expected_cause_code="200"
srtp="{{ a.88881.srtp }}"
/>
<action type="register" label="Register {{ a.88882.label }}"
transport="{{ a.88882.transport }}"
account="{{ a.88882.label }}"
username="{{ a.88882.username }}"
auth_username="{{ a.88882.auth_username }}"
password="{{ a.88882.password }}"
registrar="{{ c.domain }}"
realm="{{ c.domain }}"
expected_cause_code="200"
srtp="{{ a.88882.srtp }}"
/>
<action type="wait" complete="true" ms="2000"/>
<action type="call" label="Call from 90001 to 88881->88882"
transport="{{ a.90001.transport }}"
expected_cause_code="200"
caller="{{ a.90001.label }}@{{ c.domain }}"
callee="88881@{{ c.domain }}"
from="sip:{{ a.90001.label }}@{{ c.domain }}"
to_uri="88881@{{ c.domain }}"
max_duration="20" hangup="10"
auth_username="{{ a.90001.username }}"
password="{{ a.90001.password }}"
realm="{{ c.domain }}"
rtp_stats="true"
<!-- Set some high ring timeout, so delayed forward will happen -->
max_ring_duration="60"
srtp="{{ a.90001.srtp }}"
play="{{ c.play_file }}"
/>
<action type="accept" label="Receive call on {{ a.88881.label }}"
match_account="{{ a.88881.label }}"
call_count="1"
hangup="10"
ring_duration="30"
<!-- We're expecting a CANCEL here. And it's not optional -->
cancel="force"
transport="{{ a.88881.transport }}"
srtp="{{ a.88881.srtp }}"
/>
<action type="accept" label="Receive call on {{ a.88882.label }}"
match_account="{{ a.88882.label }}"
call_count="1"
hangup="10"
code="200" reason="OK"
transport="{{ a.88882.transport }}"
srtp="{{ a.88882.srtp }}"
play="{{ c.play_file }}">
<!-- Check that From header matching what we need. This way we can control CallerID. Adjust domain (and whole regex) accordingly -->
<check-header name="From" regex="^.*sip:{{ a.90001.label }}@example\.com>.*$"/>
</action>
<action type="wait" complete="true" ms="20000"/>
</actions>
</config>Schema - SIP Proxy subscriber and then we have Asterisk behind as PBX.
config.yaml
global:
domain: '<SIP_DOMAIN>'
domain_wss: '<SIP_WSS_DOMAIN>'
transport: 'tls'
srtp: 'sdes,force'
dtls: 'dtls,force'
stun_address: 'stun.ekiga.net:3478'
play_file: '/voice_ref_files/8000_2m30.wav'
asterisk_context: 'default'
databases:
'sippproxydb':
type: 'mysql'
user: 'sipproxyrw'
password: 'sipproxyrwpass'
base: 'sipproxy'
host: 'mysipproxydb.local'
'astdb':
type: 'pgsql'
user: 'asteriskrw'
password: 'asteriskrwpass'
base: 'asterisk'
host: 'myasteriskdb.local'
accounts:
'90011':
username: '90011'
password: 'SuperSecretPass1'
ha1: 'SuperSecretHA1'
'90012':
username: '90012'
password: 'SuperSecretPass2'
ha1: 'SuperSecretHA2'And now we need to populate all databases and make a call!
<!-- Register with 90012 and receive a call from 90011 -->
<config>
<section type="database">
<actions>
<!-- add subscribers to sip proxy -->
<action database="sippproxydb" stage="pre">
<table name="subscriber" type="insert" cleanup_after_test="true">
<field name="username" value="{{ a.90011.username }}"/>
<field name="domain" value="{{ c.domain }}"/>
<field name="ha1" value="{{ a.90011.ha1 }}"/>
<!-- here password due to ha1 is useless, so we can put some data based on jinja2_time.TimeExtension (https://github.com/hackebrot/jinja2-time) -->
<field name="password" value="{% now 'local' %}"/>
</table>
<table name="subscriber" type="insert" cleanup_after_test="true">
<field name="username" value="{{ a.90012.username }}"/>
<field name="domain" value="{{ c.domain }}"/>
<field name="ha1" value="{{ a.90012.ha1 }}"/>
<field name="password" value="{% now 'local' + 'days=1', '%D' %}"/>
</table>
</action>
<!-- add endpoints and aors to Asterisk -->
<action database="astdb" stage="pre">
<table name="ps_endpoints" type="insert" cleanup_after_test="true">
<field name="id" value="{{ a.90011.label }}"/>
<field name="transport" value="transport-udp"/>
<field name="aors" value="{{ a.90011.label }}"/>
<field name="context" value="{{ c.asterisk_context }}"/>
<field name="disallow" value="all"/>
<field name="allow" value="!all,opus,alaw"/>
<field name="direct_media" value="no"/>
<field name="ice_support" value="no"/>
<field name="rtp_timeout" value="3600"/>
</table>
<table name="ps_aors" type="insert" cleanup_after_test="true">
<field name="id" value="{{ a.90011.label }}"/>
<field name="contact" value="sip:{{ a.90011.label }}@{{ c.domain }}:5060"/>
</table>
<table name="ps_endpoints" type="insert" cleanup_after_test="true">
<field name="id" value="{{ a.90012.label }}"/>
<field name="transport" value="transport-udp"/>
<field name="aors" value="{{ a.90012.label }}"/>
<field name="context" value="{{ c.asterisk_context }}"/>
<field name="disallow" value="all"/>
<field name="allow" value="!all,opus,alaw"/>
<field name="direct_media" value="no"/>
<field name="ice_support" value="no"/>
<field name="rtp_timeout" value="3600"/>
</table>
<table name="ps_aors" type="insert" cleanup_after_test="true">
<field name="id" value="{{ a.90012.label }}"/>
<field name="contact" value="sip:{{ a.90012.label }}@{{ c.domain }}:5060"/>
</table>
</action>
</actions>
</section>
<section type="voip_patrol">
<!-- Make a call from one endpoint to other -->
<actions>
<action type="codec" disable="all"/>
<action type="codec" enable="pcma" priority="250"/>
<action type="codec" enable="pcmu" priority="249"/>
<action type="codec" enable="opus" priority="248"/>
<action type="register" label="Register {{ a.90012.label }}"
transport="{{ a.90012.transport }}"
account="{{ a.90012.username }}"
username="{{ a.90012.label }}"
auth_username="{{ a.90012.username }}"
password="{{ a.90012.password }}"
registrar="{{ c.domain }}"
realm="{{ c.domain }}"
expected_cause_code="200"
srtp="{{ a.90012.srtp }}"
/>
<action type="wait" complete="true" ms="2000"/>
<action type="accept" label="Receive call on {{ a.90012.label }} from {{ a.90011.label }}"
call_count="1"
match_account="{{ a.90012.username }}"
hangup="10"
code="200" reason="OK"
transport="{{ a.90012.transport }}"
srtp="{{ a.90012.srtp }}"
play="{{ c.play_file }}">
<check-header name="From" regex="^.*sip:{{ a.90011.label }}@.*$"/>
</action>
<action type="call" label="Call {{ a.90011.label }} -> {{ a.90012.label }}"
transport="tls"
expected_cause_code="200"
caller="{{ a.90011.label }}@{{ c.domain }}"
callee="{{ a.90012.label }}@{{ c.domain }}"
from="sip:{{ a.90011.label }}@{{ c.domain }}"
to_uri="{{ a.90012.label }}@{{ c.domain }}"
max_duration="20" hangup="10"
auth_username="{{ a.90011.username }}"
password="{{ a.90011.password }}"
realm="{{ c.domain }}"
rtp_stats="true"
max_ring_duration="15"
srtp="{{ a.90011.srtp }}"
play="{{ c.play_file }}"
/>
<action type="wait" complete="true" ms="30000"/>
</actions>
</section>
</config><!-- Call echo service and make sure receive an answer -->
<config>
<section type="voip_patrol">
<actions>
<action type="codec" disable="all"/>
<action type="codec" enable="pcma" priority="250"/>
<action type="codec" enable="pcmu" priority="249"/>
<action type="call" label="Call to 11111 (echo)"
transport="{{ a.88881.transport }}"
<!-- We are expecting answer here -->
expected_cause_code="200"
caller="{{ a.88881.label }}@{{ c.domain }}"
<!-- Setting R-URI -->
callee="11111@{{ a.88881.domain }}"
from="sip:{{ a.88881.label }}@{{ c.domain }}"
to_uri="11111@{{ c.domain }}"
max_duration="20" hangup="10"
auth_username="{{ a.88881.username }}"
password="{{ a.88881.password }}"
realm="{{ c.domain }}"
play="{{ c.play_file }}"
rtp_stats="true"
srtp="{{ a.88881.srtp }}"
<!-- We need to record file on answer -->
record="{{ scenario_name }}.wav"
/>
<action type="wait" complete="true" ms="30000"/>
</actions>
</section>
<section type="media_check">
<actions>
<action type="sox"
<!-- We are testing that the outcome of the recorded file is between 10 and 11 seconds and checking amplitude -->
sox_filter="length s -ge 10; length s -le 11; maximum amplitude -ge 0.9; minimum amplitude -le -0.5"
<!-- File name is the same as in the "record" attribute in the "call" action above -->
file="{{ scenario_name }}.wav"
/>
</actions>
</section>
</config><!-- Call echo service and make sure receive an answer with media -->
<config>
<section type="voip_patrol">
<actions>
<action type="codec" disable="all"/>
<action type="codec" enable="pcma" priority="250"/>
<action type="codec" enable="pcmu" priority="249"/>
<action type="turn" enabled="true" server="{{ c.stun_address }}" stun_only="true"/>
<action type="wait" ms="5000"/>
<action type="call" label="Call to 11111 (echo) WSS"
transport="wss"
expected_cause_code="200"
caller="{{ a.88881.label }}@{{ c.domain }}"
callee="11111@{{ a.88881.domain_wss }}"
from="sip:{{ a.88881.label }}@{{ c.domain }}"
to_uri="11111@{{ c.domain }}"
max_duration="20" hangup="10"
auth_username="{{ a.88881.username }}"
password="{{ a.88881.password }}"
realm="{{ c.domain }}"
play="{{ c.play_file }}"
rtp_stats="true"
srtp="{{ a.88881.dtls }}"
record="{{ scenario_name }}.wav"
/>
<action type="wait" complete="true" ms="30000"/>
</actions>
</section>
<section type="media_check">
<actions>
<action type="sox"
sox_filter="length s -ge 9; length s -le 11; maximum amplitude -ge 0.9; minimum amplitude -le -0.5"
file="{{ scenario_name }}.wav"
print_debug="no"
delete_after="yes"
/>
</actions>
</section>
</config><config>
<section type="sipp">
<actions>
<action transport="{{ c.transport }}" target="{{ c.domain }}">
<!-- sipp {{ c.domain }}:5061 -sf scen.xml -m 1 -r 1 -t ln -tls_cert /etc/ssl/certs/ssl-cert-snakeoil.pem -tls_key /etc/ssl/private/ssl-cert-snakeoil.key -->
<scenario name="Options">
<send>
<![CDATA[
OPTIONS sip:check_server_health@{{ c.domain }} SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
Max-Forwards: 70
To: <sip:check_server_health@{{ c.domain }}>
From: sipp <sip:check_server_health@[local_ip]:[local_port]>;tag=[call_number]
Call-ID: [call_id]
CSeq: 1 OPTIONS
Contact: <sip:check_server_health@[local_ip]:[local_port]>
Accept: application/sdp
Content-Length: 0
]]>
</send>
<recv response="200" timeout="200"/>
</scenario>
</action>
</actions>
</section>
</config>Adding some auth to SIPP
<config>
<section type="sipp">
<actions>
<action transport="{{ c.transport }}" target="{{ c.domain }}">
...
<recv response="407" auth="true">
</recv>
<send>
<![CDATA[
ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port]
From: {{ a.88881.label }} <sip:{{ a.88881.label }}@[local_ip]:[local_port]>;tag=[call_number]
To: sut <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:{{ a.88881.label }}@[local_ip]:[local_port]
Max-Forwards: 70
Content-Length: 0
]]>
</send>
<send retrans="500">
<![CDATA[
INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port]
From: {{ a.88881.label }} <sip:{{ a.88881.label }}@[local_ip]:[local_port]>;tag=[call_number]
To: sut <sip:[service]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 2 INVITE
Contact: sip:{{ a.88881.label }}@[local_ip]:[local_port]
[authentication username={{ a.88881.username }} password={{ a.88881.password }}]
Max-Forwards: 70
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
t=0 0
c=IN IP[media_ip_type] [media_ip]
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
...
</action>
</actions>
</section>
</config>and some load as well. Why not?
Note: this example in particular was to test push notification server, that's why it has iOS references
<config>
<section type="database">
<actions>
<!-- "sippproxydb" here is referring to an entity in "databases" from config.yaml. -->
<action database="sippproxydb" stage="pre">
<!-- what data are we gonna insert into the "subscriber" table? -->
<table name="subscriber" type="insert" cleanup_after_test="true">
<field name="username" value="{{ a.88881.username }}"/>
<field name="domain" value="{{ c.domain }}"/>
<field name="password" value="{{ a.88881.password }}"/>
</table>
</action>
</actions>
</section>
<section type="sipp">
<actions>
<action transport="{{ c.transport }}"
target="{{ c.domain }}"
max_calls="1000"
call_rate="10"
max_concurrent_calls="10"
socket_mode="single"
>
<scenario name="UAC REGISTER - UnREGISTER with Auth">
<send retrans="500">
<![CDATA[
REGISTER sip:{{ c.domain }}:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: "VOLTS UAC TESTER" <sip:{{ a.88881.label }}@{{ c.domain }}>;tag=[pid]VOLTsTag00[call_number]
To: "VOLTS UAC TESTER" <sip:{{ a.88881.label }}@{{ c.domain }}:[remote_port]>
Call-ID: {% now 'utc', '%H%M%S' %}///[call_id]
CSeq: 1 REGISTER
Contact: "VOLTS UAC TESTER" <sip:{{ a.88881.label }}@[local_ip]:[local_port];pn-prid=EFFDB4D8C32F845036CA3555003D44E98C82EA3F8437BD3499D14A17B039D5B6:voip&FC86D3D61AAFCC5D18AAC9D4CEDC6C70505080693FD21D0E62B5061FD4516ADE:remote;pn-provider=apns;pn-param=ABCD1234.generic.linphone.voip&remote;pn-silent=1;pn-timeout=0;pn-msg-str=IM_MSG;pn-call-str=IC_MSG;pn-groupchat-str=GC_MSG;pn-call-snd=notes_of_the_optimistic.caf;pn-msg-snd=msg.caf;transport=[transport]>;+sip.instance="<urn:uuid:65f9b73f-d655-4dd3-8633-10d8122c5299>"
Max-Forwards: 70
Supported: replaces, outbound, gruu, path
User-Agent: LinphoneiOS/4.6.5 (iPhone) LinphoneSDK/5.2.45
Accept: application/sdp
Accept: text/plain
Accept: application/vnd.gsma.rcs-ft-http+xml
Expires: 60
Content-Length: 0
]]>
</send>
<recv response="100" optional="true" rrs="true"></recv>
<recv response="401" auth="true"></recv>
<send retrans="500">
<![CDATA[
REGISTER sip:{{ c.domain }}:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: "VOLTS UAC TESTER" <sip:{{ a.88881.label }}@{{ c.domain }}>;tag=[pid]VOLTsTag00[call_number]
To: "VOLTS UAC TESTER" <sip:{{ a.88881.label }}@{{ c.domain }}:[remote_port]>
Call-ID: {% now 'utc', '%H%M%S' %}///[call_id]
CSeq: 1 REGISTER
Contact: "VOLTS UAC TESTER" <sip:{{ a.88881.label }}@[local_ip]:[local_port];pn-prid=EFFDB4D8C32F845036CA3555003D44E98C82EA3F8437BD3499D14A17B039D5B6:voip&FC86D3D61AAFCC5D18AAC9D4CEDC6C70505080693FD21D0E62B5061FD4516ADE:remote;pn-provider=apns;pn-param=ABCD1234.generic.linphone.voip&remote;pn-silent=1;pn-timeout=0;pn-msg-str=IM_MSG;pn-call-str=IC_MSG;pn-groupchat-str=GC_MSG;pn-call-snd=notes_of_the_optimistic.caf;pn-msg-snd=msg.caf;transport=[transport]>;+sip.instance="<urn:uuid:65f9b73f-d655-4dd3-8633-10d8122c5299>"
Max-Forwards: 70
Supported: replaces, outbound, gruu, path
User-Agent: LinphoneiOS/4.6.5 (iPhone) LinphoneSDK/5.2.45
Accept: application/sdp
Accept: text/plain
Accept: application/vnd.gsma.rcs-ft-http+xml
Expires: 60
[authentication username={{ a.88881.username }} password={{ a.88881.password }}]
Content-Length: 0
]]>
</send>
<recv response="100" optional="true"></recv>
<recv response="200" crlf="true"></recv>
<pause milliseconds="5000"/>
<send retrans="500">
<![CDATA[
REGISTER sip:{{ c.domain }}:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: "VOLTS UAC TESTER" <sip:{{ a.88881.label }}@{{ c.domain }}>;tag=[pid]VOLTsTag00[call_number]
To: "VOLTS UAC TESTER" <sip:{{ a.88881.label }}@{{ c.domain }}:[remote_port]>
Call-ID: {% now 'utc', '%H%M%S' %}///[call_id]
CSeq: 1 REGISTER
Contact: "VOLTS UAC TESTER" <sip:{{ a.88881.label }}@[local_ip]:[local_port];pn-prid=EFFDB4D8C32F845036CA3555003D44E98C82EA3F8437BD3499D14A17B039D5B6:voip&FC86D3D61AAFCC5D18AAC9D4CEDC6C70505080693FD21D0E62B5061FD4516ADE:remote;pn-provider=apns;pn-param=ABCD1234.ch.cern.linphone.voip&remote;pn-silent=1;pn-timeout=0;pn-msg-str=IM_MSG;pn-call-str=IC_MSG;pn-groupchat-str=GC_MSG;pn-call-snd=notes_of_the_optimistic.caf;pn-msg-snd=msg.caf;transport=[transport]>;+sip.instance="<urn:uuid:65f9b73f-d655-4dd3-8633-10d8122c5299>"
Max-Forwards: 70
Supported: replaces, outbound, gruu, path
User-Agent: LinphoneiOS/4.6.5 (iPhone) LinphoneSDK/5.2.45
Accept: application/sdp
Accept: text/plain
Accept: application/vnd.gsma.rcs-ft-http+xml
Expires: 0
Content-Length: 0
]]>
</send>
<recv response="100" optional="true" rrs="true"></recv>
<recv response="401" auth="true"></recv>
<send retrans="500">
<![CDATA[
REGISTER sip:{{ c.domain }}:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: "VOLTS UAC TESTER" <sip:{{ a.88881.label }}@{{ c.domain }}>;tag=[pid]VOLTsTag00[call_number]
To: "VOLTS UAC TESTER" <sip:{{ a.88881.label }}@{{ c.domain }}:[remote_port]>
Call-ID: {% now 'utc', '%H%M%S' %}///[call_id]
CSeq: 1 REGISTER
Contact: "VOLTS UAC TESTER" <sip:{{ a.88881.label }}@[local_ip]:[local_port];pn-prid=EFFDB4D8C32F845036CA3555003D44E98C82EA3F8437BD3499D14A17B039D5B6:voip&FC86D3D61AAFCC5D18AAC9D4CEDC6C70505080693FD21D0E62B5061FD4516ADE:remote;pn-provider=apns;pn-param=ABCD1234.ch.cern.linphone.voip&remote;pn-silent=1;pn-timeout=0;pn-msg-str=IM_MSG;pn-call-str=IC_MSG;pn-groupchat-str=GC_MSG;pn-call-snd=notes_of_the_optimistic.caf;pn-msg-snd=msg.caf;transport=[transport]>;+sip.instance="<urn:uuid:65f9b73f-d655-4dd3-8633-10d8122c5299>"
Max-Forwards: 70
Supported: replaces, outbound, gruu, path
User-Agent: LinphoneiOS/4.6.5 (iPhone) LinphoneSDK/5.2.45
Accept: application/sdp
Accept: text/plain
Accept: application/vnd.gsma.rcs-ft-http+xml
Expires: 0
[authentication username={{ a.88881.username }} password={{ a.88881.password }}]
Content-Length: 0
]]>
</send>
<recv response="100" optional="true" rrs="true"></recv>
<recv response="200"></recv>
<!-- definition of the response time repartition table (unit is ms) -->
<ResponseTimeRepartition value="10, 20, 30, 40, 50, 100, 150, 200"/>
<!-- definition of the call length repartition table (unit is ms) -->
<CallLengthRepartition value="10, 50, 100, 500, 1000, 5000, 10000"/>
</scenario>
</action>
</actions>
</section>
</config>You can name the scenarios to run directly on the CLI (see Scenario Options), or you can group them by tag - one or several comma-separated - by adding a tag attribute to a config element.
<config tag='set1'>
...
</config><config tag='set1,set3'>
...
</config>Specify a tag by running
./run.sh tag=set1In this case only tests that are holding this specific tag will be executed. And yes, in a run.sh you can specify as many tags as you want, using comma-separated list.
./run.sh tag=set1,set2,test_set3