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? */