VOLTS
VoIP Open Linear Tester Suite
Functional tests for VoIP systems based on voip_patrol, sipp, sox, chromaprint, opensips, and docker.
Overview
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:
- Database Integration: VOLTS can integrate with your MySQL and/or PostgreSQL databases to write some data there before the test and remove it after
- Media Recording & Analysis: Record and play media during calls and perform media checks of these files via SoX and Chromaprint
- Custom Scripts: Bash/python checks before/after a scenario (built by default; runs only when declared) â see Custom Scripts
- TDD Support: Use it in Test-Driven Development approach when adding functionalities to your existing PBX system. Test-Fail-Fix.
System Architecture
The suite consists of 10 parts, running sequentially:
- Preparation - Transform templates to real scenarios using
Jinja2template engine withjinja2_timeextension - WebSocket-TLS Proxy - Start proxy to provide WSS transport for
voip_patrolscenarios - Database (Pre) - Run database scripts to put test data
- Script (Pre) - Run optional custom bash/python scripts before the call
- VoIP Testing - Run
voip_patrolorsippscenario - Database (Post) - Remove test data from databases
- Media Check - Analyze obtained media files if necessary
- Script (Post) - Run optional custom scripts last (all JSONL result files are readable)
- Proxy Teardown - Stop WebSocket-TLS proxy
- Report Generation - Analyze results and print them in desired format
Steps 3-8 run sequentially against scenario files prepared in step 1, one at a time. Again, itâs for Linear.
Getting Started
Building
Suite is designed to run locally from your Linux PC or Mac. Docker should be installed.
Notes on using podman: VOLTS can run 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.
Build 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 |
In case if voip_patrol or sipp is updated, you need to rebuild these containers:
./build.sh -r
Running
After building, run all scenarios:
./run.sh
Simple, isnât it? This will run all scenarios found in scenarios folder one by one.
To run a single scenario:
./run.sh <scenario_name>
# or
./run.sh scenarios/<scenario_name>
To run several scenarios, list them space- and/or comma-separated:
./run.sh 001-register 002-call-echo
# or
./run.sh 001-register,002-call-echo
To get a set of tests running using tag keyword:
./run.sh tag=set1,set2
Command Options
| Option | Description |
|---|---|
-h, --help |
Show help message |
-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 single 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 |
Special Commands
| Command | Description |
|---|---|
stop |
Stop tests and delete all containers |
sngrep |
Launch SIP packet capture tool |
dbclean |
Clean up test data from databases |
report-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 without touching your PBX again:
./run.sh -r json_full report-only
After running the suite you can always find voip_patrol results in tmp/output folder.
Environment Variables
You can also configure behavior via environment variables (command-line options take precedence):
| Variable | Default | Description |
|---|---|---|
REPORT_TYPE |
table_full |
Report type: table, json, table_full, json_full. Overridden by -r/--report |
LOG_LEVEL |
0 |
Log level (0-3). Note the default is 0 (silent) - raise it with -v/-d if you expect console output from the test containers. Overridden by -l/--log-level |
MAX_SINGLE_TEST_TIME |
600 |
Maximum time for a single test in seconds. Overridden by -t/--timeout |
OPENSIPS_TLS_PORT |
6051 |
OpenSIPS TLS port. Overridden by --tls-port |
OPENSIPS_WSS_PORT |
9443 |
OpenSIPS WSS port. Overridden by --wss-port |
OPENSIPS_HEPS_PORT |
8887 |
HEP source port. Overridden by --heps-port |
OPENSIPS_HEPD_PORT |
8888 |
HEP destination port. Overridden by --hepd-port |
Configuration
Scenarios
VOLTS scenarios are combined voip_patrol/sipp, database, and media_check scenarios, templatized with Jinja2 style. This is done to avoid repeating passwords, usernames, domains, etc.
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.
Global Config
Values for templates are taken from scenarios/config.yaml. Variables from global section transform to c. (for config) and from accounts to a. in templates for shorter notation.
There is a special name scenario_name that transforms to a scenario file name stripped .xml extension.
All settings from global section are inherited to the accounts section automatically unless defined there explicitly.
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 -->
Example 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'
VoIP Patrol Configuration
Basic Registration Example
<config>
<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 }}"
expected_cause_code="200"
/>
<action type="wait" complete="true" ms="2000"/>
</actions>
</section>
</config>
WebSocket Transport Notice
As voip_patrol itself doesnât support WebSocket transport, OpenSIPS is used as a TLS-WSS SIP proxy. Using it is simple: specify transport="wss" in your voip_patrol section.
You can debug proxy traffic using sngrep:
./run.sh sngrep
# or
docker exec -it volts_opensips sngrep -L udp:127.0.0.1:8888
# or using a local sngrep installation
sudo sngrep -L udp:127.0.0.1:8888 port 8888
Where 8888 is the OPENSIPS_HEPD_PORT (configurable with --hepd-port option).
Note: you need to use the turn section of your voip_patrol configuration to ensure media passes through. See the WSS example below.
SIPP Configuration
You can add existing SIPP scenarios mainly unchanged. They run with:
sipp <target> -sf <scenario.xml> -m 1 -mp <random_port> -i <container_ip>
Example SIPP OPTIONS test:
<config tag="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>
SIPP Attributes
| Attribute | Description |
|---|---|
transport |
Transport SIPP will use: udp (default), tcp, or tls |
socket_mode |
single (default) or multi sockets |
target |
Target address (port 5061 appended for TLS if not specified) |
call_rate |
Call rate in calls per second (default: 10) |
max_calls |
Maximum number of calls (default: 1) |
max_concurrent_calls |
Maximum simultaneous calls (default: 10) |
total_timeout |
Test timeout in seconds (default: 600) |
Custom Scripts
Built by default with ./build.sh. Scripts run only when a scenario declares a <section type="script">. Script sources live in the repo-root scripts/ folder, 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 ships curl, jq, ping, dig, python requests and the volts_results / volts-result result-query helpers (post scripts can read e.g. SIP Call-IDs: volts-result vp --get callid; a label matching several call legs returns all of them, --first/--last pick one); optional extra pip deps go in local scripts/requirements.txt and are installed at runtime into tmp/scripter-deps (no image rebuild). 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) |
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 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 (s_error from stderr/stdout). Multiple <section type="script"> blocks are merged, 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 the suite still continues into voip/sipp/media/post.
Example:
<config>
<section type="script">
<actions>
<action script="ping_host.sh" label="SBC reachable" stage="pre">
<param name="host" value="{{ c.domain }}"/>
</action>
</actions>
</section>
<section type="voip_patrol">
<!-- VoIP Patrol configuration here -->
</section>
<section type="script">
<actions>
<action script="http_check.py" label="CDR check" stage="post"
continue_on_error="true" timeout="30">
<param name="url" value="https://api.example.com/cdr/{{ scenario_name }}"/>
<param name="api_key" value="{{ c.api_key }}"/>
</action>
</actions>
</section>
</config>
Database
Database configuration is done in XML, section database. There are 2 stages:
| Stage | Description |
|---|---|
pre |
Launched before running voip_patrol. Usually to put accounts data, routing, etc. |
post |
Running after voip_patrol. For cleanup data inserted in pre stage. |
Table Operations
| Attribute | Description |
|---|---|
name |
Table name |
type |
Operation: insert, replace, delete or check. Forming actual INSERT, REPLACE, DELETE and SELECT COUNT(*) SQL statements |
continue_on_error |
Ignore errors and continue (optional). By default database actions stop after the first error |
cleanup_after_test |
Optional, false by default. For insert: automatically forms a delete on post stage. For check: deletes matching rows immediately after the check passes |
row_nums |
Required for check. Expected number of matching rows - exact ("1") or range ("1-3") |
Example with Database:
<config>
<section type="database">
<actions>
<action database="sippproxydb" stage="pre">
<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">
<!-- VoIP Patrol configuration here -->
</section>
</config>
Checking Database State After a Call
The check type verifies that the 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 features make time-scoped checks practical:
- 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 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">
<!-- register, accept and call actions here -->
</section>
</config>
Media Check
Analyze call recordings with various media tools.
Media Check Attributes
| Attribute | Description |
|---|---|
type |
Media check type: sox, sox_st, or fpcalc |
file |
Path to the file to check. Must be aligned with record in one of the voip_patrol actions. The /output/ prefix is optional - only the basename is used, and /output/ is prepended automatically |
delete_after |
Delete file after check: yes/no/keep_failed (default). keep_failed keeps the file if the media test did not pass |
print_debug |
Print debug info: yes/no (default) |
length |
Expected length of the sound file in seconds. Supports <min>-<max> format. Works for both sox/sox_st (where it becomes a length s filter) and fpcalc |
sox_filter |
Used if type is sox/sox_st. Semicolon-separated expressions for SoX validation |
fingerprint |
Used if type is fpcalc. Raw fingerprint from fpcalc tool |
likeness |
Used if type is fpcalc. Minimum similarity score (default: 0.9) |
max_offset |
Used if type is fpcalc. Integer offset allowed when comparing fingerprints; each N is roughly 0.5s. Use it when you expect audio âdriftâ during the test. 0 by default |
SoX Media Check
SoX collects parameters using sox --i <file>, sox <file> -n stat, and sox <file> -n stats.
Filter example:
sox_filter="length s -ge 10; length s -le 11; crest factor -lt 10"
This checks that:
- Length is between 10-11 seconds
- Crest factor is less than 10
Uses bash-style comparison operators (-eq, -lt, -gt, -le, -ge, -ne) instead of <, > symbols.
You can also use sox_st as a type to trim silence at the start and end of the recording before analysis â this allows slightly better file fingerprinting.
Chromaprint Media Check
The fpcalc type calculates the âlikenessâ (similarity) of a recorded audio file against a provided reference fingerprint using the fpcalc utility. More info about fingerprinting can be found here.
Note: make sure files have at least 3-4 seconds of audio. 10-15 seconds is recommended.
Likeness score reference:
| 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 |
Example with Media Check:
<config>
<section type="voip_patrol">
<actions>
<!-- Call configuration with record="/output/{{ scenario_name }}.wav" -->
</actions>
</section>
<section type="media_check">
<actions>
<action type="sox"
sox_filter="length s -ge 10; length s -le 11"
file="/output/{{ scenario_name }}.wav"
/>
</actions>
</section>
</config>
Examples
Basic Tests
Simple Registration Test
<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>
Basic Call Scenario
<config>
<section type="voip_patrol">
<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="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>
</section>
</config>
Advanced Scenarios
Call with Database Integration
<!-- Register with 90012 and receive a call from 90011 -->
<config>
<section type="database">
<actions>
<!-- add subscribers to sip proxy -->
<!-- "sippproxydb" here is referring to an entity in "databases" from config.yaml. -->
<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 -->
<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>
</actions>
</section>
<section type="voip_patrol">
<!-- VoIP testing actions -->
</section>
</config>
WSS Call with Media Check
<!-- 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"
<!-- We are expecting answer here -->
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 }}"
<!-- We need to record file on answer. To analyze it below now it MUST be with "/output/" path prefix -->
record="/output/{{ 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 9 and 11 seconds and checking amplitude -->
sox_filter="length s -ge 9; length s -le 11; maximum amplitude -ge 0.9"
<!-- File name is the same as in the "record" attribute in the "call" action above. Now it MUST be with "/output/" path prefix -->
file="/output/{{ scenario_name }}.wav"
delete_after="yes"
/>
</actions>
</section>
</config>
Chromaprint Media Check Example
<!-- Call echo service and verify audio fingerprint similarity -->
<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 }}"
expected_cause_code="200"
caller="{{ a.88881.label }}@{{ c.domain }}"
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 }}"
record="/output/{{ 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"
file="/output/{{ scenario_name }}.wav"
likeness="0.85"
/>
</actions>
</section>
</config>
SIPP Tests
Load Testing with SIPP
<config>
<section type="sipp">
<actions>
<action transport="{{ c.transport }}"
target="{{ c.domain }}"
max_calls="1000"
call_rate="10"
max_concurrent_calls="10"
socket_mode="single">
<!-- sipp {{ c.domain }}:5061 -sf scen.xml -m 1000 -r 10 -l 10 -t ln -->
<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];transport=[transport]>
Max-Forwards: 70
User-Agent: VOLTS/1.0
Expires: 60
Content-Length: 0
]]>
</send>
<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: 2 REGISTER
Contact: "VOLTS UAC TESTER" <sip:{{ a.88881.label }}@[local_ip]:[local_port];transport=[transport]>
Max-Forwards: 70
User-Agent: VOLTS/1.0
Expires: 60
[authentication username={{ a.88881.username }} password={{ a.88881.password }}]
Content-Length: 0
]]>
</send>
<recv response="200" crlf="true"></recv>
<!-- Unregister -->
<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: 3 REGISTER
Contact: "VOLTS UAC TESTER" <sip:{{ a.88881.label }}@[local_ip]:[local_port];transport=[transport]>
Max-Forwards: 70
User-Agent: VOLTS/1.0
Expires: 0
Content-Length: 0
]]>
</send>
<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: 4 REGISTER
Contact: "VOLTS UAC TESTER" <sip:{{ a.88881.label }}@[local_ip]:[local_port];transport=[transport]>
Max-Forwards: 70
User-Agent: VOLTS/1.0
Expires: 0
[authentication username={{ a.88881.username }} password={{ a.88881.password }}]
Content-Length: 0
]]>
</send>
<recv response="200"></recv>
</scenario>
</action>
</actions>
</section>
</config>
Tagged Test Execution
You can specify a tag on each test scenario:
<config tag='set1'>
<section type="voip_patrol">
<!-- Test configuration -->
</section>
</config>
Multiple tags per scenario are supported:
<config tag='set1,set3'>
<section type="voip_patrol">
<!-- Test configuration -->
</section>
</config>
Run tagged tests:
./run.sh tag=set1
./run.sh tag=set1,set2,test_set3
Results
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.
After running tests, youâll get a table like this:
+---------------------------------------+-----------------------------------------------------------+------+----------+-------+--------+------------------+
| 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 |
+---------------------------------------+-----------------------------------------------------------+------+----------+-------+--------+------------------+
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 |
+----------------------+-------------+------+----------+-------+--------+--------+------------------+
If any scenarios fail, youâll see:
Scenarios ['49-teams-follow-forward', '50-team-no-answer-forward'] are failed!
This means your system needs tuning or there are issues with the tests. Check the console output and logs in tmp/output folder for detailed information.
For more detailed examples and advanced configurations, refer to the scenarios folder in the repository.