diff --git a/.gitignore b/.gitignore
index a75ffaef..5184b5d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,6 +43,7 @@ compile_commands.json
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
+build/
# Ignore *.xml in root dir, but not in regression tests dir.
/*.xml
@@ -54,3 +55,7 @@ cmake_install.cmake
# Ignore autogenerated version.h.
/include/version.cmake
/version.h
+
+# Ignore debug files from regression tests
+regress/*/debug*_*
+regress/*/srtpctx*
diff --git a/include/call.hpp b/include/call.hpp
index f5fa0728..c9ff5943 100644
--- a/include/call.hpp
+++ b/include/call.hpp
@@ -277,6 +277,9 @@ class call : virtual public task, virtual public listener, public virtual socket
char* createSendingMessage(char* src, int P_index, bool skip_sanity=false);
char* createSendingMessage(SendingMessage*src, int P_index, char *msg_buffer, int buflen, int *msgLen=nullptr);
+ // Helper method to resolve timeout values with variables
+ unsigned int resolveTimeoutValue(const char* timeout_str);
+
// method for the management of unexpected messages
bool checkInternalCmd(char* cmd); // check of specific internal command
// received from the twin socket
diff --git a/include/scenario.hpp b/include/scenario.hpp
index 0ae347bd..37b20a72 100644
--- a/include/scenario.hpp
+++ b/include/scenario.hpp
@@ -85,6 +85,8 @@ class message
unsigned int retrans_delay;
/* The receive/send timeout. */
unsigned int timeout;
+ /* The timeout string for variable substitution */
+ char * timeout_str;
/* 3pcc extended mode: if this is a sendCmd */
char * peer_dest;
diff --git a/regress/github-#0801/run b/regress/github-#0801/run
new file mode 100755
index 00000000..c46ea663
--- /dev/null
+++ b/regress/github-#0801/run
@@ -0,0 +1,30 @@
+#!/bin/sh
+# Test to verify timeout variable functionality works correctly
+. "`dirname "$0"`/../functions"; init
+
+# Create test data with different timeout values
+echo "SEQUENTIAL" > test_data.csv
+echo "call1;user1;100;1000" >> test_data.csv # 1000ms timeout
+echo "call2;user2;200;500" >> test_data.csv # 500ms timeout
+
+# Test 1: Parse scenario with CSV field timeout - should not produce parsing errors
+timeout 3 "`get_sipp`" -sf uac_csv.xml -inf test_data.csv -p 5070 -m 0 127.0.0.1 2>&1 | grep -q "timeout.*field" && fail "parsing error with CSV field timeout"
+
+# Test 2: Parse scenario with variable timeout - should not produce parsing errors
+timeout 3 "`get_sipp`" -sf uac_variable.xml -inf test_data.csv -p 5071 -m 0 127.0.0.1 2>&1 | grep -q "timeout.*variable" && fail "parsing error with variable timeout"
+
+# Test 3: Verify timeout variable resolution doesn't cause parse errors
+# Run with very short timeout to minimize test time, looking for variable resolution errors
+echo "SEQUENTIAL" > timeout_test.csv
+echo "call1;user1;100;100" >> timeout_test.csv # 100ms timeout for quick test
+timeout 5 "`get_sipp`" -sf uac_csv.xml -inf timeout_test.csv -p 5072 -m 1 127.0.0.1 -nostdin -timeout 2 2>&1 | grep -iE "(invalid.*timeout|timeout.*not.*valid|variable.*error)" && fail "timeout variable resolution failed"
+
+# Test 4: Verify computed variable timeout doesn't cause parse errors
+echo "SEQUENTIAL" > var_test.csv
+echo "call1;user1;100;150" >> var_test.csv # 150ms timeout for quick test
+timeout 5 "`get_sipp`" -sf uac_variable.xml -inf var_test.csv -p 5073 -m 1 127.0.0.1 -nostdin -timeout 2 2>&1 | grep -iE "(invalid.*timeout|timeout.*not.*valid|variable.*error)" && fail "variable timeout resolution failed"
+
+# Clean up
+rm -f test_data.csv timeout_test.csv var_test.csv
+
+ok
diff --git a/regress/github-#0801/uac_csv.xml b/regress/github-#0801/uac_csv.xml
new file mode 100644
index 00000000..40f37959
--- /dev/null
+++ b/regress/github-#0801/uac_csv.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [field1]
+ Call-ID: [call_id]
+ CSeq: 1 INVITE
+ Contact: sip:sipp@[local_ip]:[local_port]
+ Max-Forwards: 70
+ Subject: Performance Test - [field0]
+ Content-Type: application/sdp
+ Content-Length: [len]
+
+ v=0
+ o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
+ s=-
+ c=IN IP[local_ip_type] [local_ip]
+ t=0 0
+ m=audio [media_port] RTP/AVP 0
+ a=rtpmap:0 PCMU/8000
+ ]]>
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [field1] [peer_tag_param]
+ Call-ID: [call_id]
+ CSeq: 1 ACK
+ Contact: sip:sipp@[local_ip]:[local_port]
+ Max-Forwards: 70
+ Content-Length: 0
+ ]]>
+
+
+
+
+
+
+
+ [peer_tag_param]
+ To: sipp ;tag=[pid]SIPpTag00[call_number]
+ Call-ID: [call_id]
+ CSeq: [cseq] BYE
+ Content-Length: 0
+ ]]>
+
+
+
diff --git a/regress/github-#0801/uac_variable.xml b/regress/github-#0801/uac_variable.xml
new file mode 100644
index 00000000..80a74ac1
--- /dev/null
+++ b/regress/github-#0801/uac_variable.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [field1]
+ Call-ID: [call_id]
+ CSeq: 1 INVITE
+ Contact: sip:sipp@[local_ip]:[local_port]
+ Max-Forwards: 70
+ Subject: Performance Test - [field0]
+ Content-Type: application/sdp
+ Content-Length: [len]
+
+ v=0
+ o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
+ s=-
+ c=IN IP[local_ip_type] [local_ip]
+ t=0 0
+ m=audio [media_port] RTP/AVP 0
+ a=rtpmap:0 PCMU/8000
+ ]]>
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [field1] [peer_tag_param]
+ Call-ID: [call_id]
+ CSeq: 1 ACK
+ Contact: sip:sipp@[local_ip]:[local_port]
+ Max-Forwards: 70
+ Content-Length: 0
+ ]]>
+
+
+
+
+
+
+
+ [peer_tag_param]
+ To: sipp ;tag=[pid]SIPpTag00[call_number]
+ Call-ID: [call_id]
+ CSeq: [cseq] BYE
+ Content-Length: 0
+ ]]>
+
+
+
diff --git a/regress/github-#0801/uas_csv.xml b/regress/github-#0801/uas_csv.xml
new file mode 100644
index 00000000..c2526c85
--- /dev/null
+++ b/regress/github-#0801/uas_csv.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+ Content-Length: 0
+ ]]>
+
+
+
+
+ Content-Type: application/sdp
+ Content-Length: [len]
+
+ v=0
+ o=uas 1 1 IN IP[local_ip_type] [local_ip]
+ s=-
+ c=IN IP[local_ip_type] [local_ip]
+ t=0 0
+ m=audio [media_port] RTP/AVP 0
+ a=rtpmap:0 PCMU/8000
+ ]]>
+
+
+
+
+
+
+
+
+
+
+ Content-Length: 0
+ ]]>
+
+
+
+
+
+
diff --git a/regress/github-#0801/uas_variable.xml b/regress/github-#0801/uas_variable.xml
new file mode 100644
index 00000000..fd9caa05
--- /dev/null
+++ b/regress/github-#0801/uas_variable.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+ Content-Length: 0
+ ]]>
+
+
+
+
+ Content-Type: application/sdp
+ Content-Length: [len]
+
+ v=0
+ o=uas 1 1 IN IP[local_ip_type] [local_ip]
+ s=-
+ c=IN IP[local_ip_type] [local_ip]
+ t=0 0
+ m=audio [media_port] RTP/AVP 0
+ a=rtpmap:0 PCMU/8000
+ ]]>
+
+
+
+
+
+
+
+
+
+
+ Content-Length: 0
+ ]]>
+
+
+
+
+
+
diff --git a/src/call.cpp b/src/call.cpp
index 6da83483..42ce8a9c 100644
--- a/src/call.cpp
+++ b/src/call.cpp
@@ -1967,9 +1967,16 @@ bool call::executeMessage(message *curmsg)
return false;
}
}
- } else if (curmsg->timeout) {
+ } else if (curmsg->timeout || curmsg->timeout_str) {
/* Initialize the send timeout to the per message timeout. */
- send_timeout = clock_tick + curmsg->timeout;
+ unsigned int resolved_timeout = 0;
+ if (curmsg->timeout_str) {
+ // Resolve variables in timeout string at runtime
+ resolved_timeout = resolveTimeoutValue(curmsg->timeout_str);
+ } else {
+ resolved_timeout = curmsg->timeout;
+ }
+ send_timeout = clock_tick + resolved_timeout;
} else if (defl_send_timeout) {
/* Initialize the send timeout to the global timeout. */
send_timeout = clock_tick + defl_send_timeout;
@@ -2081,10 +2088,18 @@ bool call::executeMessage(message *curmsg)
delete this;
return false;
}
- } else if (curmsg->timeout || defl_recv_timeout) {
- if (curmsg->timeout)
+ } else if (curmsg->timeout || curmsg->timeout_str || defl_recv_timeout) {
+ unsigned int resolved_timeout = 0;
+ if (curmsg->timeout_str) {
+ // Resolve variables in timeout string at runtime
+ resolved_timeout = resolveTimeoutValue(curmsg->timeout_str);
+ } else if (curmsg->timeout) {
+ resolved_timeout = curmsg->timeout;
+ }
+
+ if (resolved_timeout)
// If timeout is specified on message receive, use it
- recv_timeout = getmilliseconds() + curmsg->timeout;
+ recv_timeout = getmilliseconds() + resolved_timeout;
else
// Else use the default timeout if specified
recv_timeout = getmilliseconds() + defl_recv_timeout;
@@ -6838,6 +6853,71 @@ SessionState call::getSessionStateOld()
return _sessionStateOld;
}
+unsigned int call::resolveTimeoutValue(const char* timeout_str) {
+ if (!timeout_str) return 0;
+
+ char resolved_str[256];
+ strcpy(resolved_str, timeout_str);
+
+ // Handle CSV field variables like [field0], [field1], etc.
+ char* field_start = strstr(resolved_str, "[field");
+ if (field_start) {
+ char* field_end = strchr(field_start, ']');
+ if (field_end) {
+ char* field_num_str = field_start + 6; // Skip "[field"
+ *field_end = '\0';
+ int field_num = atoi(field_num_str);
+
+ // Get the field value from CSV data directly
+ char field_value[256] = "";
+ if (m_lineNumber && !inFiles.empty()) {
+ // Use the first available input file (default behavior)
+ auto file_it = inFiles.begin();
+ const char* fileName = file_it->first.c_str();
+ int line = (*m_lineNumber)[fileName];
+ if (line >= 0) {
+ file_it->second->getField(line, field_num, field_value, sizeof(field_value));
+ }
+ }
+
+ // Replace [fieldN] with the actual value
+ strcpy(field_start, field_value);
+ strcat(field_start, field_end + 1);
+ }
+ }
+
+ // Handle computed variables like [$varname]
+ char* var_start = strstr(resolved_str, "[$");
+ if (var_start) {
+ char* var_end = strchr(var_start, ']');
+ if (var_end) {
+ char* var_name = var_start + 2; // Skip "[$"
+ *var_end = '\0';
+
+ // Get the variable value
+ int varId = call_scenario->get_var(var_name, "timeout variable");
+ CCallVariable *var = M_callVariableTable->getVar(varId);
+ if (var && var->isSet()) {
+ if (var->isDouble()) {
+ sprintf(var_start, "%.0lf", var->getDouble());
+ strcat(var_start, var_end + 1);
+ } else if (var->isString()) {
+ strcpy(var_start, var->getString());
+ strcat(var_start, var_end + 1);
+ }
+ }
+ }
+ }
+
+ // Convert the resolved string to unsigned int
+ char* endptr;
+ unsigned int result = strtoul(resolved_str, &endptr, 0);
+ if (*endptr) {
+ ERROR("Invalid timeout value after variable substitution: '%s'", resolved_str);
+ }
+ return result;
+}
+
#ifdef PCAPPLAY
void *send_wrapper(void *arg)
{
diff --git a/src/scenario.cpp b/src/scenario.cpp
index a47324dc..80cc8be5 100644
--- a/src/scenario.cpp
+++ b/src/scenario.cpp
@@ -52,6 +52,7 @@ message::message(int index, const char *desc)
send_scheme = nullptr; // delete on exit
retrans_delay = 0;
timeout = 0;
+ timeout_str = nullptr; // free on exit
recv_response = nullptr; // free on exit
recv_request = nullptr; // free on exit
@@ -125,6 +126,7 @@ message::~message()
free(display_str);
free(nextLabel);
free(onTimeoutLabel);
+ free(timeout_str);
free(peer_dest);
free(peer_src);
@@ -465,6 +467,23 @@ static int xp_get_optional(const char *name, const char *what)
return OPTIONAL_FALSE;
}
+static void xp_set_timeout(message* curmsg, const char* context_name)
+{
+ const char* cptr = xp_get_value("timeout");
+ if (cptr) {
+ curmsg->timeout_str = strdup(cptr);
+ // If the timeout string contains no variables, parse it as integer now
+ if (strchr(cptr, '[') == nullptr && strchr(cptr, '$') == nullptr) {
+ curmsg->timeout = get_long(cptr, context_name);
+ } else {
+ curmsg->timeout = 0; // Will be resolved at runtime
+ }
+ } else {
+ curmsg->timeout = 0;
+ curmsg->timeout_str = nullptr;
+ }
+}
+
int scenario::xp_get_var(const char *name, const char *what, int defval)
{
@@ -887,7 +906,7 @@ scenario::scenario(char * filename, int deflt)
}
curmsg -> retrans_delay = xp_get_long("retrans", "retransmission timer", 0);
- curmsg -> timeout = xp_get_long("timeout", "message send timeout", 0);
+ xp_set_timeout(curmsg, "message send timeout");
} else if (!strcmp(elem, "recv")) {
curmsg->M_type = MSG_TYPE_RECV;
/* Received messages descriptions */
@@ -921,7 +940,7 @@ scenario::scenario(char * filename, int deflt)
}
}
- curmsg->timeout = xp_get_long("timeout", "message timeout", 0);
+ xp_set_timeout(curmsg, "message timeout");
/* record the route set */
/* TODO disallow optional and rrs to coexist? */