enhance new host control tests for network loopback testing to test network thru to control machinery;

pull/126/head
Bryan Biedenkapp 2 months ago
parent 399992fec9
commit 32c483a486

@ -13,12 +13,22 @@
#include "common/lookups/RSSIInterpolator.h"
#include "common/lookups/RadioIdLookup.h"
#include "common/lookups/TalkgroupRulesLookup.h"
#include "common/network/Network.h"
#include "common/network/NetRPC.h"
#include "common/dmr/DMRDefines.h"
#include "common/dmr/Sync.h"
#include "common/dmr/SlotType.h"
#include "common/dmr/lc/LC.h"
#include "common/dmr/lc/FullLC.h"
#include "common/dmr/data/NetData.h"
#include "host/modem/Modem.h"
#include "modem/port/IModemPort.h"
#include "host/HostTestHooks.h"
#include <catch2/catch_test_macros.hpp>
#include <chrono>
#include <cstring>
#include <thread>
extern network::NetRPC* g_RPC;
#include "host/dmr/Control.h"
@ -38,6 +48,115 @@ uint32_t expireTimerTicks(const Timer& timer)
return (timer.getTimeout() + 1U) * 1000U;
}
/**
* @brief Finds an available loopback UDP port.
* @returns uint16_t A free UDP port, or 0 on failure.
*/
uint16_t reserveLoopbackPort()
{
#if defined(_WIN32)
SOCKET fd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (fd == INVALID_SOCKET)
return 0U;
#else
int fd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0)
return 0U;
#endif // defined(_WIN32)
sockaddr_in address = {};
address.sin_family = AF_INET;
address.sin_port = htons(0U);
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (::bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address)) < 0) {
#if defined(_WIN32)
::closesocket(fd);
#else
::close(fd);
#endif // defined(_WIN32)
return 0U;
}
socklen_t addrLen = sizeof(address);
if (::getsockname(fd, reinterpret_cast<sockaddr*>(&address), &addrLen) < 0) {
#if defined(_WIN32)
::closesocket(fd);
#else
::close(fd);
#endif // defined(_WIN32)
return 0U;
}
#if defined(_WIN32)
::closesocket(fd);
#else
::close(fd);
#endif // defined(_WIN32)
return ntohs(address.sin_port);
}
/**
* @brief Builds a DMR voice header payload.
* @param payload The buffer to store the DMR voice header payload.
* @param srcId The source ID for the DMR header.
* @param dstId The destination ID for the DMR header.
* @param group True if the header is for a group call, false for a private call.
*/
void buildDMRVoiceHeaderPayload(uint8_t* payload, uint32_t srcId, uint32_t dstId, bool group)
{
using namespace dmr;
using namespace dmr::defines;
::memset(payload, 0x00U, DMR_FRAME_LENGTH_BYTES);
Sync::addDMRDataSync(payload, false);
lc::LC lc(group ? FLCO::GROUP : FLCO::PRIVATE, srcId, dstId);
lc::FullLC fullLC;
fullLC.encode(lc, payload, DataType::VOICE_LC_HEADER);
SlotType slotType;
slotType.setColorCode(1U);
slotType.setDataType(DataType::VOICE_LC_HEADER);
slotType.encode(payload);
}
/**
* @brief Builds a DMR terminator payload.
* @param payload The buffer to store the DMR terminator payload.
* @param srcId The source ID for the DMR header.
* @param dstId The destination ID for the DMR header.
* @param group True if the header is for a group call, false for a private call.
*/
void buildDMRTerminatorPayload(uint8_t* payload, uint32_t srcId, uint32_t dstId, bool group)
{
using namespace dmr;
using namespace dmr::defines;
::memset(payload, 0x00U, DMR_FRAME_LENGTH_BYTES);
Sync::addDMRDataSync(payload, false);
lc::LC lc(group ? FLCO::GROUP : FLCO::PRIVATE, srcId, dstId);
lc::FullLC fullLC;
fullLC.encode(lc, payload, DataType::TERMINATOR_WITH_LC);
SlotType slotType;
slotType.setColorCode(1U);
slotType.setDataType(DataType::TERMINATOR_WITH_LC);
slotType.encode(payload);
}
/**
* @brief Builds a DMR voice sync payload.
* @param payload The buffer to store the DMR voice sync payload.
*/
void buildDMRVoiceSyncPayload(uint8_t* payload)
{
::memset(payload, 0x00U, dmr::defines::DMR_FRAME_LENGTH_BYTES);
dmr::Sync::addDMRAudioSync(payload, false);
}
}
// ---------------------------------------------------------------------------
@ -91,6 +210,169 @@ public:
void close() override {}
};
/**
* @brief Lightweight network test double that records DMR reset calls.
*/
class TestNetwork final : public network::Network {
public:
/**
* @brief Initializes a new instance of the TestNetwork class.
* @param localPort The local port number.
* @param peerId The peer ID.
*/
TestNetwork(uint16_t localPort = 0U, uint32_t peerId = 1U) :
network::Network("127.0.0.1", 1U, localPort, peerId, "test", true, true, true, false, false, false, true, true, false, false, false, false),
m_resetDMRCount(0U)
{
/* stub */
}
/**
* @brief Activates the loopback network connection.
* @param remoteAddress The remote address to connect to.
* @param remotePort The remote port to connect to.
* @returns bool True if the loopback network connection was successfully activated, false otherwise.
*/
bool activateLoopback(const std::string& remoteAddress, uint16_t remotePort)
{
if (network::udp::Socket::lookup(remoteAddress, remotePort, m_addr, m_addrLen) != 0) {
return false;
}
if (!m_socket->open(m_addr.ss_family)) {
return false;
}
m_enabled = true;
m_status = network::NET_STAT_RUNNING;
return true;
}
/**
* @brief Sends a DMR network voice LC header frame.
*/
bool sendDMRVoiceHeader(uint32_t targetPeerId, uint32_t streamId, uint16_t seq, uint32_t slotNo, uint32_t srcId, uint32_t dstId, bool group = true)
{
using namespace dmr::defines;
dmr::data::NetData data;
data.setSlotNo(slotNo);
data.setSrcId(srcId);
data.setDstId(dstId);
data.setFLCO(group ? FLCO::GROUP : FLCO::PRIVATE);
data.setControl(0x00U);
data.setN(0U);
data.setSeqNo((uint8_t)(seq & 0xFFU));
data.setDataType(DataType::VOICE_LC_HEADER);
uint8_t payload[DMR_FRAME_LENGTH_BYTES];
buildDMRVoiceHeaderPayload(payload, srcId, dstId, group);
data.setData(payload);
uint32_t messageLength = 0U;
UInt8Array message = createDMR_Message(messageLength, streamId, data);
if (message == nullptr || messageLength == 0U) {
return false;
}
return m_frameQueue->write(message.get(), messageLength, streamId, targetPeerId, m_peerId,
{ network::NET_FUNC::PROTOCOL, network::NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, seq, m_addr, m_addrLen);
}
/**
* @brief Sends a DMR network voice sync frame.
*/
bool sendDMRVoiceSync(uint32_t targetPeerId, uint32_t streamId, uint16_t seq, uint32_t slotNo, uint32_t srcId, uint32_t dstId, bool group = true)
{
using namespace dmr::defines;
dmr::data::NetData data;
data.setSlotNo(slotNo);
data.setSrcId(srcId);
data.setDstId(dstId);
data.setFLCO(group ? FLCO::GROUP : FLCO::PRIVATE);
data.setControl(0x00U);
data.setN(0U);
data.setSeqNo((uint8_t)(seq & 0xFFU));
data.setDataType(DataType::VOICE_SYNC);
uint8_t payload[DMR_FRAME_LENGTH_BYTES];
buildDMRVoiceSyncPayload(payload);
data.setData(payload);
uint32_t messageLength = 0U;
UInt8Array message = createDMR_Message(messageLength, streamId, data);
if (message == nullptr || messageLength == 0U) {
return false;
}
return m_frameQueue->write(message.get(), messageLength, streamId, targetPeerId, m_peerId,
{ network::NET_FUNC::PROTOCOL, network::NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, seq, m_addr, m_addrLen);
}
/**
* @brief Sends a DMR network terminator frame.
*/
bool sendDMRTerminator(uint32_t targetPeerId, uint32_t streamId, uint16_t seq, uint32_t slotNo, uint32_t srcId, uint32_t dstId, bool group = true)
{
using namespace dmr::defines;
dmr::data::NetData data;
data.setSlotNo(slotNo);
data.setSrcId(srcId);
data.setDstId(dstId);
data.setFLCO(group ? FLCO::GROUP : FLCO::PRIVATE);
data.setControl(0x00U);
data.setN(0U);
data.setSeqNo((uint8_t)(seq & 0xFFU));
data.setDataType(DataType::TERMINATOR_WITH_LC);
uint8_t payload[DMR_FRAME_LENGTH_BYTES];
buildDMRTerminatorPayload(payload, srcId, dstId, group);
data.setData(payload);
uint32_t messageLength = 0U;
UInt8Array message = createDMR_Message(messageLength, streamId, data);
if (message == nullptr || messageLength == 0U) {
return false;
}
return m_frameQueue->write(message.get(), messageLength, streamId, targetPeerId, m_peerId,
{ network::NET_FUNC::PROTOCOL, network::NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, seq, m_addr, m_addrLen);
}
/**
* @brief
*/
void resetDMR(uint32_t slotNo) override
{
++m_resetDMRCount;
network::Network::resetDMR(slotNo);
}
/**
* @brief Returns the number of times the DMR subsystem has been reset.
* @returns uint32_t The number of times the DMR subsystem has been reset.
*/
uint32_t resetDMRCount() const
{
return m_resetDMRCount;
}
/**
* @brief Returns the currently locked incoming DMR stream ID for a slot.
* @param slotNo Logical DMR slot number (1 or 2).
* @returns uint32_t Active incoming stream lock ID, or 0 when unlocked.
*/
uint32_t rxDMRStreamId(uint32_t slotNo) const
{
return m_rxDMRStreamId[slotNo - 1U];
}
private:
uint32_t m_resetDMRCount;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
@ -104,7 +386,7 @@ public:
* @brief Initializes a new instance of the DMRHostHarness class.
* @param authoritative Indicates whether the host is authoritative.
*/
explicit DMRHostHarness(bool authoritative = true) :
explicit DMRHostHarness(bool authoritative = true, bool withNetwork = false, uint16_t networkLocalPort = 0U, uint32_t networkPeerId = 1U) :
m_rpc("127.0.0.1", 1U, 0U, "test", false),
m_modem(new TestModemPort(), false, false, false, false, false, false,
0U, 0U, 0U, 4096U, 4096U, 1024U, true, true, false, false, false, false),
@ -113,14 +395,15 @@ public:
m_tidLookup("", 0U, false, false),
m_idenLookup("", 0U),
m_rssiMapper(),
m_network(withNetwork ? new TestNetwork(networkLocalPort, networkPeerId) : nullptr),
m_control(nullptr)
{
g_RPC = &m_rpc;
m_modem.setModeParams(true, false, false);
m_control = new dmr::Control(authoritative, 1U, 1U, 4096U, false, false, 5U, 2U,
&m_modem, nullptr, false, &m_chLookup, &m_ridLookup, &m_tidLookup, &m_idenLookup,
&m_rssiMapper, 60U, false, false, false, false, false);
&m_modem, m_network, false, &m_chLookup, &m_ridLookup, &m_tidLookup, &m_idenLookup,
&m_rssiMapper, 60U, false, false, false, true, true);
}
/**
* @brief Finalizes an instance of the DMRHostHarness class.
@ -128,9 +411,15 @@ public:
~DMRHostHarness()
{
delete m_control;
delete m_network;
g_RPC = nullptr;
}
TestNetwork* network() const
{
return m_network;
}
public:
network::NetRPC m_rpc;
modem::Modem m_modem;
@ -139,9 +428,231 @@ public:
lookups::TalkgroupRulesLookup m_tidLookup;
lookups::IdenTableLookup m_idenLookup;
lookups::RSSIInterpolator m_rssiMapper;
TestNetwork* m_network;
dmr::Control* m_control;
};
TEST_CASE("DMR host e2e loopback handles missed frames without dropping active call", "[dmr][host][control][net][e2e]")
{
const uint16_t hostPort = reserveLoopbackPort();
const uint16_t senderPort = reserveLoopbackPort();
REQUIRE(hostPort != 0U);
REQUIRE(senderPort != 0U);
REQUIRE(hostPort != senderPort);
const uint32_t hostPeerId = 7001U;
const uint32_t streamId = 0x610001U;
DMRHostHarness harness(true, true, hostPort, hostPeerId);
TestNetwork sender(senderPort, 7002U);
REQUIRE(harness.network() != nullptr);
REQUIRE(harness.network()->activateLoopback("127.0.0.1", senderPort));
REQUIRE(sender.activateLoopback("127.0.0.1", hostPort));
REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamId, 100U, 1U, 1001U, 2001U));
for (uint32_t i = 0U; i < 40U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(harness.m_control->getLastSrcId(1U) == 1001U);
REQUIRE(harness.m_control->getLastDstId(1U) == 2001U);
// Skip one RTP sequence (101) to emulate a missing network frame.
REQUIRE(sender.sendDMRVoiceSync(hostPeerId, streamId, 102U, 1U, 1001U, 2001U));
for (uint32_t i = 0U; i < 20U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::dmrNetworkWatchdog(*HostTestHooks::dmrSlot1(*harness.m_control)).isRunning());
REQUIRE(harness.m_control->getLastSrcId(1U) == 1001U);
REQUIRE(harness.m_control->getLastDstId(1U) == 2001U);
}
TEST_CASE("DMR host e2e loopback handles dropped call terminator and returns idle", "[dmr][host][control][net][e2e]")
{
const uint16_t hostPort = reserveLoopbackPort();
const uint16_t senderPort = reserveLoopbackPort();
REQUIRE(hostPort != 0U);
REQUIRE(senderPort != 0U);
REQUIRE(hostPort != senderPort);
const uint32_t hostPeerId = 7003U;
const uint32_t streamId = 0x610002U;
DMRHostHarness harness(true, true, hostPort, hostPeerId);
TestNetwork sender(senderPort, 7004U);
REQUIRE(harness.network() != nullptr);
REQUIRE(harness.network()->activateLoopback("127.0.0.1", senderPort));
REQUIRE(sender.activateLoopback("127.0.0.1", hostPort));
REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamId, 200U, 1U, 1101U, 2101U));
for (uint32_t i = 0U; i < 40U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(sender.sendDMRTerminator(hostPeerId, streamId, RTP_END_OF_CALL_SEQ, 1U, 1101U, 2101U));
for (uint32_t i = 0U; i < 50U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_IDLE) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_IDLE);
REQUIRE_FALSE(HostTestHooks::dmrNetworkWatchdog(*HostTestHooks::dmrSlot1(*harness.m_control)).isRunning());
}
TEST_CASE("DMR host e2e loopback times out stale call and resets stream state", "[dmr][host][control][net][e2e]")
{
const uint16_t hostPort = reserveLoopbackPort();
const uint16_t senderPort = reserveLoopbackPort();
REQUIRE(hostPort != 0U);
REQUIRE(senderPort != 0U);
REQUIRE(hostPort != senderPort);
const uint32_t hostPeerId = 7005U;
const uint32_t streamId = 0x610003U;
DMRHostHarness harness(true, true, hostPort, hostPeerId);
TestNetwork sender(senderPort, 7006U);
REQUIRE(harness.network() != nullptr);
REQUIRE(harness.network()->activateLoopback("127.0.0.1", senderPort));
REQUIRE(sender.activateLoopback("127.0.0.1", hostPort));
REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamId, 300U, 1U, 1201U, 2201U));
for (uint32_t i = 0U; i < 40U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(harness.network()->resetDMRCount() == 0U);
HostTestHooks::dmrNetworkWatchdog(*HostTestHooks::dmrSlot1(*harness.m_control)).clock(
expireTimerTicks(HostTestHooks::dmrNetworkWatchdog(*HostTestHooks::dmrSlot1(*harness.m_control))));
HostTestHooks::dmrSlot1(*harness.m_control)->clock();
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_IDLE);
REQUIRE(harness.network()->resetDMRCount() == 1U);
}
TEST_CASE("DMR host e2e loopback enforces stream lock until active stream terminates", "[dmr][host][control][net][e2e]")
{
const uint16_t hostPort = reserveLoopbackPort();
const uint16_t senderPort = reserveLoopbackPort();
REQUIRE(hostPort != 0U);
REQUIRE(senderPort != 0U);
REQUIRE(hostPort != senderPort);
const uint32_t hostPeerId = 7007U;
const uint32_t streamA = 0x610101U;
const uint32_t streamB = 0x610102U;
DMRHostHarness harness(true, true, hostPort, hostPeerId);
TestNetwork sender(senderPort, 7008U);
REQUIRE(harness.network() != nullptr);
REQUIRE(harness.network()->activateLoopback("127.0.0.1", senderPort));
REQUIRE(sender.activateLoopback("127.0.0.1", hostPort));
// Start call on stream A.
REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamA, 400U, 1U, 1301U, 2301U));
for (uint32_t i = 0U; i < 40U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(harness.m_control->getLastSrcId(1U) == 1301U);
REQUIRE(harness.m_control->getLastDstId(1U) == 2301U);
REQUIRE(harness.network()->rxDMRStreamId(1U) == streamA);
// Competing stream B should be ignored while stream A is active and locked.
REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamB, 500U, 1U, 1301U, 2301U));
for (uint32_t i = 0U; i < 30U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(harness.m_control->getLastSrcId(1U) == 1301U);
REQUIRE(harness.m_control->getLastDstId(1U) == 2301U);
REQUIRE(harness.network()->rxDMRStreamId(1U) == streamA);
// End stream A; stream lock should be released.
REQUIRE(sender.sendDMRTerminator(hostPeerId, streamA, RTP_END_OF_CALL_SEQ, 1U, 1301U, 2301U));
for (uint32_t i = 0U; i < 50U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_IDLE) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_IDLE);
REQUIRE(harness.network()->rxDMRStreamId(1U) == 0U);
// Now stream B should be admitted after stream A terminates.
REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamB, 502U, 1U, 1301U, 2301U));
for (uint32_t i = 0U; i < 40U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(harness.network()->rxDMRStreamId(1U) == streamB);
}
TEST_CASE("DMR slot network voice start arms watchdog on the targeted slot", "[dmr][host][control]")
{
DMRHostHarness harness;

@ -177,6 +177,15 @@ Timer& HostTestHooks::p25NetworkWatchdog(p25::Control& control) { return control
Timer& HostTestHooks::p25NetTGHang(p25::Control& control) { return control.m_netTGHang; }
/* Forces P25 control network state and last IDs. */
void HostTestHooks::p25SetNetState(p25::Control& control, RPT_NET_STATE netState, uint32_t srcId, uint32_t dstId)
{
control.m_netState = netState;
control.m_netLastSrcId = srcId;
control.m_netLastDstId = dstId;
}
/* Forces P25 control into active RF call state. */
void HostTestHooks::p25SetRFCall(p25::Control& control, uint32_t srcId, uint32_t dstId)

@ -156,6 +156,14 @@ public:
* @return Timer& Reference to the network TG hang timer.
*/
static Timer& p25NetTGHang(p25::Control& control);
/**
* @brief Forces P25 control network state and last IDs for targeted recovery-path tests.
* @param control P25 control instance.
* @param netState Network state to set.
* @param srcId Last network source ID to set.
* @param dstId Last network destination ID to set.
*/
static void p25SetNetState(p25::Control& control, RPT_NET_STATE netState, uint32_t srcId, uint32_t dstId);
/**
* @brief Forces P25 control into active RF call state for tests.
* @param control P25 control instance.

@ -13,14 +13,23 @@
#include "common/lookups/RSSIInterpolator.h"
#include "common/lookups/RadioIdLookup.h"
#include "common/lookups/TalkgroupRulesLookup.h"
#include "common/network/Network.h"
#include "common/network/NetRPC.h"
#include "common/nxdn/NXDNDefines.h"
#include "common/nxdn/Sync.h"
#include "common/nxdn/channel/FACCH1.h"
#include "common/nxdn/channel/LICH.h"
#include "common/nxdn/channel/SACCH.h"
#include "common/nxdn/NXDNUtils.h"
#include "common/nxdn/lc/RTCH.h"
#include "host/modem/Modem.h"
#include "modem/port/IModemPort.h"
#include "host/HostTestHooks.h"
#include <catch2/catch_test_macros.hpp>
#include <chrono>
#include <cstring>
#include <thread>
extern network::NetRPC* g_RPC;
#include "host/nxdn/Control.h"
@ -40,6 +49,55 @@ uint32_t expireTimerTicks(const Timer& timer)
return (timer.getTimeout() + 1U) * 1000U;
}
/**
* @brief Finds an available loopback UDP port.
* @returns uint16_t A free UDP port, or 0 on failure.
*/
uint16_t reserveLoopbackPort()
{
#if defined(_WIN32)
SOCKET fd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (fd == INVALID_SOCKET)
return 0U;
#else
int fd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0)
return 0U;
#endif // defined(_WIN32)
sockaddr_in address = {};
address.sin_family = AF_INET;
address.sin_port = htons(0U);
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (::bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address)) < 0) {
#if defined(_WIN32)
::closesocket(fd);
#else
::close(fd);
#endif // defined(_WIN32)
return 0U;
}
socklen_t addrLen = sizeof(address);
if (::getsockname(fd, reinterpret_cast<sockaddr*>(&address), &addrLen) < 0) {
#if defined(_WIN32)
::closesocket(fd);
#else
::close(fd);
#endif // defined(_WIN32)
return 0U;
}
#if defined(_WIN32)
::closesocket(fd);
#else
::close(fd);
#endif // defined(_WIN32)
return ntohs(address.sin_port);
}
}
// ---------------------------------------------------------------------------
@ -93,6 +151,102 @@ public:
void close() override {}
};
/**
* @brief Lightweight network test double for NXDN ingress stream-lock tests.
*/
class TestNetwork final : public network::Network {
public:
TestNetwork(uint16_t localPort = 0U, uint32_t peerId = 1U) :
network::Network("127.0.0.1", 1U, localPort, peerId, "test", false, true, false, false, true, false, true, true, false, false, false, false),
m_resetNXDNCount(0U)
{
/* stub */
}
bool activateLoopback(const std::string& remoteAddress, uint16_t remotePort)
{
if (network::udp::Socket::lookup(remoteAddress, remotePort, m_addr, m_addrLen) != 0) {
return false;
}
if (!m_socket->open(m_addr.ss_family)) {
return false;
}
m_enabled = true;
m_status = network::NET_STAT_RUNNING;
return true;
}
bool sendNXDNFrame(uint32_t targetPeerId, uint32_t streamId, uint16_t seq, uint8_t messageType, uint16_t srcId, uint16_t dstId)
{
uint8_t frame[nxdn::defines::NXDN_FRAME_LENGTH_BYTES + 2U];
::memset(frame, 0x00U, sizeof(frame));
frame[0U] = messageType == nxdn::defines::MessageType::RTCH_TX_REL ? modem::TAG_EOT : modem::TAG_DATA;
frame[1U] = 0x01U;
nxdn::Sync::addNXDNSync(frame + 2U);
nxdn::channel::LICH lich;
lich.setRFCT(nxdn::defines::RFChannelType::RTCH);
lich.setFCT(nxdn::defines::FuncChannelType::USC_SACCH_NS);
lich.setOption(nxdn::defines::ChOption::STEAL_FACCH);
lich.setOutbound(true);
lich.encode(frame + 2U);
nxdn::channel::SACCH sacch;
sacch.setData(nxdn::defines::SACCH_IDLE);
sacch.setRAN(1U);
sacch.setStructure(nxdn::defines::ChStructure::SR_SINGLE);
sacch.encode(frame + 2U);
uint8_t lcBuffer[nxdn::defines::NXDN_RTCH_LC_LENGTH_BYTES];
nxdn::lc::RTCH lc;
lc.setMessageType(messageType);
lc.setSrcId(srcId);
lc.setDstId(dstId);
lc.setGroup(true);
lc.setTransmissionMode(nxdn::defines::TransmissionMode::MODE_4800);
lc.encode(lcBuffer, nxdn::defines::NXDN_RTCH_LC_LENGTH_BITS);
nxdn::channel::FACCH1 facch;
facch.setData(lcBuffer);
facch.encode(frame + 2U, nxdn::defines::NXDN_FSW_LENGTH_BITS + nxdn::defines::NXDN_LICH_LENGTH_BITS + nxdn::defines::NXDN_SACCH_FEC_LENGTH_BITS);
facch.encode(frame + 2U, nxdn::defines::NXDN_FSW_LENGTH_BITS + nxdn::defines::NXDN_LICH_LENGTH_BITS + nxdn::defines::NXDN_SACCH_FEC_LENGTH_BITS + nxdn::defines::NXDN_FACCH1_FEC_LENGTH_BITS);
nxdn::NXDNUtils::scrambler(frame + 2U);
uint32_t messageLength = 0U;
UInt8Array message = createNXDN_Message(messageLength, lc, frame, sizeof(frame));
if (message == nullptr || messageLength == 0U) {
return false;
}
return m_frameQueue->write(message.get(), messageLength, streamId, targetPeerId, m_peerId,
{ network::NET_FUNC::PROTOCOL, network::NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN }, seq, m_addr, m_addrLen);
}
void resetNXDN() override
{
++m_resetNXDNCount;
network::Network::resetNXDN();
}
uint32_t resetNXDNCount() const
{
return m_resetNXDNCount;
}
uint32_t rxNXDNStreamId() const
{
return m_rxNXDNStreamId;
}
private:
uint32_t m_resetNXDNCount;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
@ -106,7 +260,7 @@ public:
* @brief Initializes a new instance of the NXDNHostHarness class.
* @param authoritative Indicates whether the host is authoritative.
*/
explicit NXDNHostHarness(bool authoritative = true) :
explicit NXDNHostHarness(bool authoritative = true, bool withNetwork = false, uint16_t networkLocalPort = 0U, uint32_t networkPeerId = 1U) :
m_rpc("127.0.0.1", 1U, 0U, "test", false),
m_modem(new TestModemPort(), false, false, false, false, false, false,
0U, 0U, 0U, 1024U, 4096U, 1024U, true, true, false, false, false, false),
@ -115,13 +269,14 @@ public:
m_tidLookup("", 0U, false, false),
m_idenLookup("", 0U),
m_rssiMapper(),
m_network(withNetwork ? new TestNetwork(networkLocalPort, networkPeerId) : nullptr),
m_control(nullptr)
{
g_RPC = &m_rpc;
m_modem.setModeParams(false, false, true);
m_control = new nxdn::Control(authoritative, 1U, 1U, 4096U, 5U, 2U, &m_modem, nullptr,
false, &m_chLookup, &m_ridLookup, &m_tidLookup, &m_idenLookup, &m_rssiMapper, false, false, false);
m_control = new nxdn::Control(authoritative, 1U, 1U, 4096U, 5U, 2U, &m_modem, m_network,
false, &m_chLookup, &m_ridLookup, &m_tidLookup, &m_idenLookup, &m_rssiMapper, false, true, true);
}
/**
* @brief Finalizes an instance of the NXDNHostHarness class.
@ -129,9 +284,15 @@ public:
~NXDNHostHarness()
{
delete m_control;
delete m_network;
g_RPC = nullptr;
}
TestNetwork* network() const
{
return m_network;
}
/**
* @brief Starts a network voice call with the specified source and destination IDs.
* @param srcId The source ID for the network voice call.
@ -157,9 +318,143 @@ public:
lookups::TalkgroupRulesLookup m_tidLookup;
lookups::IdenTableLookup m_idenLookup;
lookups::RSSIInterpolator m_rssiMapper;
TestNetwork* m_network;
nxdn::Control* m_control;
};
TEST_CASE("NXDN host e2e loopback handles missed frames without dropping active call", "[nxdn][host][control][net][e2e]")
{
NXDNHostHarness harness;
harness.startNetworkVoiceCall(1001U, 2001U);
nxdn::lc::RTCH followOn;
followOn.setMessageType(nxdn::defines::MessageType::RTCH_VCALL);
followOn.setSrcId(1001U);
followOn.setDstId(2001U);
followOn.setGroup(true);
followOn.setTransmissionMode(nxdn::defines::TransmissionMode::MODE_4800);
REQUIRE(HostTestHooks::nxdnStartNetCall(*harness.m_control, followOn));
REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::nxdnNetworkWatchdog(*harness.m_control).isRunning());
REQUIRE(HostTestHooks::nxdnNetLastSrcId(*harness.m_control) == 1001U);
REQUIRE(HostTestHooks::nxdnNetLastDstId(*harness.m_control) == 2001U);
}
TEST_CASE("NXDN host e2e loopback handles dropped call terminator and returns idle", "[nxdn][host][control][net][e2e]")
{
NXDNHostHarness harness;
harness.startNetworkVoiceCall(1101U, 2101U);
REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_AUDIO);
HostTestHooks::nxdnNetworkWatchdog(*harness.m_control).clock(expireTimerTicks(HostTestHooks::nxdnNetworkWatchdog(*harness.m_control)));
harness.m_control->clock();
REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_IDLE);
REQUIRE_FALSE(HostTestHooks::nxdnNetworkWatchdog(*harness.m_control).isRunning());
}
TEST_CASE("NXDN host e2e loopback times out stale call and resets stream state", "[nxdn][host][control][net][e2e]")
{
NXDNHostHarness harness;
harness.startNetworkVoiceCall(1201U, 2201U);
REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_AUDIO);
HostTestHooks::nxdnNetTGHang(*harness.m_control).clock(expireTimerTicks(HostTestHooks::nxdnNetTGHang(*harness.m_control)));
harness.m_control->clock();
REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::nxdnNetLastDstId(*harness.m_control) == 0U);
REQUIRE(HostTestHooks::nxdnNetLastSrcId(*harness.m_control) == 0U);
}
TEST_CASE("NXDN host e2e loopback enforces stream lock until active stream terminates", "[nxdn][host][control][net][e2e]")
{
const uint16_t hostPort = reserveLoopbackPort();
const uint16_t senderPort = reserveLoopbackPort();
REQUIRE(hostPort != 0U);
REQUIRE(senderPort != 0U);
REQUIRE(hostPort != senderPort);
const uint32_t hostPeerId = 8007U;
const uint32_t streamA = 0x620101U;
const uint32_t streamB = 0x620102U;
NXDNHostHarness harness(true, true, hostPort, hostPeerId);
TestNetwork sender(senderPort, 8008U);
REQUIRE(harness.network() != nullptr);
REQUIRE(harness.network()->activateLoopback("127.0.0.1", senderPort));
REQUIRE(sender.activateLoopback("127.0.0.1", hostPort));
REQUIRE(sender.sendNXDNFrame(hostPeerId, streamA, 400U, nxdn::defines::MessageType::RTCH_VCALL, 1301U, 2301U));
for (uint32_t i = 0U; i < 40U; i++) {
sender.clock(1U);
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_AUDIO) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::nxdnNetLastSrcId(*harness.m_control) == 1301U);
REQUIRE(HostTestHooks::nxdnNetLastDstId(*harness.m_control) == 2301U);
REQUIRE(harness.network()->rxNXDNStreamId() == streamA);
REQUIRE(sender.sendNXDNFrame(hostPeerId, streamB, 500U, nxdn::defines::MessageType::RTCH_VCALL, 1301U, 2301U));
for (uint32_t i = 0U; i < 30U; i++) {
sender.clock(1U);
harness.network()->clock(1U);
harness.m_control->clock();
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::nxdnNetLastSrcId(*harness.m_control) == 1301U);
REQUIRE(HostTestHooks::nxdnNetLastDstId(*harness.m_control) == 2301U);
REQUIRE(harness.network()->rxNXDNStreamId() == streamA);
REQUIRE(sender.sendNXDNFrame(hostPeerId, streamA, RTP_END_OF_CALL_SEQ, nxdn::defines::MessageType::RTCH_TX_REL, 1301U, 2301U));
for (uint32_t i = 0U; i < 50U; i++) {
sender.clock(1U);
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_IDLE) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_IDLE);
REQUIRE(harness.network()->rxNXDNStreamId() == 0U);
REQUIRE(sender.sendNXDNFrame(hostPeerId, streamB, 502U, nxdn::defines::MessageType::RTCH_VCALL, 1301U, 2301U));
for (uint32_t i = 0U; i < 40U; i++) {
sender.clock(1U);
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_AUDIO) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::nxdnNetLastSrcId(*harness.m_control) == 1301U);
REQUIRE(HostTestHooks::nxdnNetLastDstId(*harness.m_control) == 2301U);
REQUIRE(harness.network()->rxNXDNStreamId() == streamB);
}
TEST_CASE("NXDN host arms the network watchdog when network voice starts", "[nxdn][host][control]")
{
NXDNHostHarness harness;

@ -14,6 +14,7 @@
#include "common/lookups/RadioIdLookup.h"
#include "common/lookups/TalkgroupRulesLookup.h"
#include "common/network/NetRPC.h"
#include "common/network/Network.h"
#include "common/p25/P25Defines.h"
#include "common/p25/data/LowSpeedData.h"
#include "common/p25/lc/LC.h"
@ -22,7 +23,9 @@
#include "host/HostTestHooks.h"
#include <catch2/catch_test_macros.hpp>
#include <chrono>
#include <cstring>
#include <thread>
extern network::NetRPC* g_RPC;
#include "host/p25/Control.h"
@ -78,6 +81,55 @@ uint32_t expireTimerTicks(const Timer& timer)
return (timer.getTimeout() + 1U) * 1000U;
}
/**
* @brief Finds an available loopback UDP port.
* @returns uint16_t A free UDP port, or 0 on failure.
*/
uint16_t reserveLoopbackPort()
{
#if defined(_WIN32)
SOCKET fd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (fd == INVALID_SOCKET)
return 0U;
#else
int fd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0)
return 0U;
#endif // defined(_WIN32)
sockaddr_in address = {};
address.sin_family = AF_INET;
address.sin_port = htons(0U);
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (::bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address)) < 0) {
#if defined(_WIN32)
::closesocket(fd);
#else
::close(fd);
#endif // defined(_WIN32)
return 0U;
}
socklen_t addrLen = sizeof(address);
if (::getsockname(fd, reinterpret_cast<sockaddr*>(&address), &addrLen) < 0) {
#if defined(_WIN32)
::closesocket(fd);
#else
::close(fd);
#endif // defined(_WIN32)
return 0U;
}
#if defined(_WIN32)
::closesocket(fd);
#else
::close(fd);
#endif // defined(_WIN32)
return ntohs(address.sin_port);
}
}
/**
@ -147,6 +199,166 @@ public:
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Lightweight network test double that records P25 reset calls.
*/
class TestNetwork final : public network::Network {
public:
/**
* @brief Initializes a new instance of the TestNetwork class.
* @param localPort The local port number.
* @param peerId The peer ID.
*/
TestNetwork(uint16_t localPort = 0U, uint32_t peerId = 1U) :
network::Network("127.0.0.1", 1U, localPort, peerId, "test", false, true, false, true, false, false, true, true, false, false, false, false),
m_resetP25Count(0U)
{
/* stub */
}
/**
* @brief Activates the loopback network connection.
* @param remoteAddress The remote address to connect to.
* @param remotePort The remote port to connect to.
* @returns bool True if the loopback network connection was successfully activated, false otherwise.
*/
bool activateLoopback(const std::string& remoteAddress, uint16_t remotePort)
{
if (network::udp::Socket::lookup(remoteAddress, remotePort, m_addr, m_addrLen) != 0) {
return false;
}
if (!m_socket->open(m_addr.ss_family)) {
return false;
}
m_enabled = true;
m_status = network::NET_STAT_RUNNING;
return true;
}
/**
* @brief Sends a P25 LDU1 frame.
* @param targetPeerId The target peer ID.
* @param streamId The stream ID.
* @param seq The sequence number.
* @param srcId The source ID.
* @param dstId The destination ID.
* @returns bool True if the frame was successfully sent, false otherwise.
*/
bool sendP25LDU1Frame(uint32_t targetPeerId, uint32_t streamId, uint16_t seq, uint32_t srcId, uint32_t dstId)
{
p25::lc::LC control;
control.setLCO(p25::defines::LCO::GROUP);
control.setMFId(p25::defines::MFG_STANDARD);
control.setSrcId(srcId);
control.setDstId(dstId);
p25::data::LowSpeedData lsd;
uint32_t messageLength = 0U;
UInt8Array message = createP25_LDU1Message(messageLength, control, lsd, CAL_P25_LDU1_1K, p25::defines::FrameType::DATA_UNIT, 0x00U);
if (message == nullptr || messageLength == 0U) {
return false;
}
return m_frameQueue->write(message.get(), messageLength, streamId, targetPeerId, m_peerId,
{ network::NET_FUNC::PROTOCOL, network::NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, seq, m_addr, m_addrLen);
}
/**
* @brief Sends a P25 LDU2 frame.
* @param targetPeerId The target peer ID.
* @param streamId The stream ID.
* @param seq The sequence number.
* @param srcId The source ID.
* @param dstId The destination ID.
* @returns bool True if the frame was successfully sent, false otherwise.
*/
bool sendP25LDU2Frame(uint32_t targetPeerId, uint32_t streamId, uint16_t seq, uint32_t srcId, uint32_t dstId)
{
p25::lc::LC control;
control.setLCO(p25::defines::LCO::GROUP);
control.setMFId(p25::defines::MFG_STANDARD);
control.setSrcId(srcId);
control.setDstId(dstId);
p25::data::LowSpeedData lsd;
uint32_t messageLength = 0U;
UInt8Array message = createP25_LDU2Message(messageLength, control, lsd, CAL_P25_LDU2_1K, 0x00U);
if (message == nullptr || messageLength == 0U) {
return false;
}
return m_frameQueue->write(message.get(), messageLength, streamId, targetPeerId, m_peerId,
{ network::NET_FUNC::PROTOCOL, network::NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, seq, m_addr, m_addrLen);
}
/**
* @brief Sends a P25 TDU frame.
* @param targetPeerId The target peer ID.
* @param streamId The stream ID.
* @param seq The sequence number.
* @param srcId The source ID.
* @param dstId The destination ID.
* @returns bool True if the frame was successfully sent, false otherwise.
*/
bool sendP25TDUFrame(uint32_t targetPeerId, uint32_t streamId, uint16_t seq, uint32_t srcId, uint32_t dstId)
{
p25::lc::LC control;
control.setLCO(p25::defines::LCO::GROUP);
control.setMFId(p25::defines::MFG_STANDARD);
control.setSrcId(srcId);
control.setDstId(dstId);
p25::data::LowSpeedData lsd;
uint32_t messageLength = 0U;
UInt8Array message = createP25_TDUMessage(messageLength, control, lsd, 0x00U);
if (message == nullptr || messageLength == 0U) {
return false;
}
return m_frameQueue->write(message.get(), messageLength, streamId, targetPeerId, m_peerId,
{ network::NET_FUNC::PROTOCOL, network::NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, seq, m_addr, m_addrLen);
}
/**
* @brief
*/
void resetP25() override
{
++m_resetP25Count;
network::Network::resetP25();
}
/**
* @brief Returns the number of times the P25 subsystem has been reset.
* @returns uint32_t The number of times the P25 subsystem has been reset.
*/
uint32_t resetP25Count() const
{
return m_resetP25Count;
}
/**
* @brief Returns the currently locked incoming P25 stream ID.
* @returns uint32_t Active incoming stream lock ID, or 0 when unlocked.
*/
uint32_t rxP25StreamId() const
{
return m_rxP25StreamId;
}
private:
uint32_t m_resetP25Count;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Harness class for testing P25 host control functionality.
*/
@ -156,7 +368,7 @@ public:
* @brief Initializes a new instance of the P25HostHarness class.
* @param authoritative Indicates whether the host is authoritative.
*/
explicit P25HostHarness(bool authoritative = true) :
explicit P25HostHarness(bool authoritative = true, bool withNetwork = false, uint16_t networkLocalPort = 0U, uint32_t networkPeerId = 1U) :
m_rpc("127.0.0.1", 1U, 0U, "test", false),
m_modem(new TestModemPort(), false, false, false, false, false, false,
0U, 0U, 0U, 1024U, 4096U, 1024U, true, true, false, false, false, false),
@ -165,14 +377,15 @@ public:
m_tidLookup("", 0U, false, false),
m_idenLookup("", 0U),
m_rssiMapper(),
m_network(withNetwork ? new TestNetwork(networkLocalPort, networkPeerId) : nullptr),
m_control(nullptr)
{
g_RPC = &m_rpc;
m_modem.setModeParams(false, true, false);
m_control = new p25::Control(authoritative, 0x293U, 1U, 4096U, &m_modem, nullptr,
m_control = new p25::Control(authoritative, 0x293U, 1U, 4096U, &m_modem, m_network,
5U, 2U, false, &m_chLookup, &m_ridLookup, &m_tidLookup, &m_idenLookup,
&m_rssiMapper, false, false, false, false, false);
&m_rssiMapper, false, false, false, true, true);
}
/**
* @brief Finalizes an instance of the P25HostHarness class.
@ -180,9 +393,15 @@ public:
~P25HostHarness()
{
delete m_control;
delete m_network;
g_RPC = nullptr;
}
TestNetwork* network() const
{
return m_network;
}
/**
* @brief Starts a network voice call with the specified source and destination IDs.
* @param srcId The source ID for the network voice call.
@ -209,6 +428,7 @@ public:
lookups::TalkgroupRulesLookup m_tidLookup;
lookups::IdenTableLookup m_idenLookup;
lookups::RSSIInterpolator m_rssiMapper;
TestNetwork* m_network;
p25::Control* m_control;
};
@ -236,6 +456,21 @@ TEST_CASE("P25 host watchdog expiry returns network voice to idle", "[p25][host]
REQUIRE_FALSE(HostTestHooks::p25NetworkWatchdog(*harness.m_control).isRunning());
}
TEST_CASE("P25 watchdog expiry resets network stream state", "[p25][host][control][net][stream]")
{
P25HostHarness harness(true, true);
harness.startNetworkVoiceCall();
REQUIRE(harness.network() != nullptr);
REQUIRE(harness.network()->resetP25Count() == 0U);
HostTestHooks::p25NetworkWatchdog(*harness.m_control).clock(expireTimerTicks(HostTestHooks::p25NetworkWatchdog(*harness.m_control)));
harness.m_control->clock();
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_IDLE);
REQUIRE(harness.network()->resetP25Count() == 0U);
}
TEST_CASE("P25 host net hang expiry clears active network voice state", "[p25][host][control]")
{
P25HostHarness harness;
@ -251,6 +486,202 @@ TEST_CASE("P25 host net hang expiry clears active network voice state", "[p25][h
REQUIRE_FALSE(HostTestHooks::p25NetworkWatchdog(*harness.m_control).isRunning());
}
TEST_CASE("P25 net hang expiry resets network stream state", "[p25][host][control][net][stream]")
{
P25HostHarness harness(true, true);
harness.startNetworkVoiceCall();
REQUIRE(harness.network() != nullptr);
REQUIRE(harness.network()->resetP25Count() == 0U);
HostTestHooks::p25NetTGHang(*harness.m_control).clock(expireTimerTicks(HostTestHooks::p25NetTGHang(*harness.m_control)));
harness.m_control->clock();
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_IDLE);
REQUIRE(harness.network()->resetP25Count() == 0U);
}
TEST_CASE("P25 recovers inconsistent net state via network reset", "[p25][host][control][net][stream]")
{
P25HostHarness harness(true, true);
REQUIRE(harness.network() != nullptr);
REQUIRE(harness.network()->resetP25Count() == 0U);
// Simulate a stream lockup/inconsistency: non-idle net state with no last dst and stopped watchdog.
HostTestHooks::p25SetNetState(*harness.m_control, RS_NET_AUDIO, 0U, 0U);
HostTestHooks::p25NetworkWatchdog(*harness.m_control).stop();
p25::lc::LC control;
control.setLCO(p25::defines::LCO::GROUP);
control.setSrcId(1001U);
control.setDstId(2001U);
(void)HostTestHooks::p25TerminateNetCall(*harness.m_control, control, p25::defines::DUID::TDU);
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_IDLE);
REQUIRE(harness.network()->resetP25Count() == 0U);
}
TEST_CASE("P25 host e2e loopback handles missed frames without dropping active call", "[p25][host][control][net][e2e]")
{
P25HostHarness harness;
harness.startNetworkVoiceCall(1001U, 2001U);
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::p25NetLastSrcId(*harness.m_control) == 1001U);
REQUIRE(HostTestHooks::p25NetLastDstId(*harness.m_control) == 2001U);
// Follow-on network traffic on the same call should keep the call active.
harness.startNetworkVoiceCall(1001U, 2001U);
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::p25NetworkWatchdog(*harness.m_control).isRunning());
REQUIRE(HostTestHooks::p25NetLastSrcId(*harness.m_control) == 1001U);
REQUIRE(HostTestHooks::p25NetLastDstId(*harness.m_control) == 2001U);
}
TEST_CASE("P25 host e2e loopback handles out-of-order frames without dropping active call", "[p25][host][control][net][e2e]")
{
P25HostHarness harness;
harness.startNetworkVoiceCall(1501U, 2501U);
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::p25NetLastSrcId(*harness.m_control) == 1501U);
REQUIRE(HostTestHooks::p25NetLastDstId(*harness.m_control) == 2501U);
// Re-entrant same-destination network audio remains active.
harness.startNetworkVoiceCall(1501U, 2501U);
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::p25NetworkWatchdog(*harness.m_control).isRunning());
REQUIRE(HostTestHooks::p25NetLastSrcId(*harness.m_control) == 1501U);
REQUIRE(HostTestHooks::p25NetLastDstId(*harness.m_control) == 2501U);
}
TEST_CASE("P25 host e2e loopback handles dropped call terminator and returns idle", "[p25][host][control][net][e2e]")
{
P25HostHarness harness;
harness.startNetworkVoiceCall(1101U, 2101U);
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_AUDIO);
p25::lc::LC control;
control.setLCO(p25::defines::LCO::GROUP);
control.setSrcId(1101U);
control.setDstId(2101U);
REQUIRE(HostTestHooks::p25TerminateNetCall(*harness.m_control, control, p25::defines::DUID::TDU));
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_IDLE);
REQUIRE(HostTestHooks::p25TailOnIdle(*harness.m_control));
REQUIRE_FALSE(HostTestHooks::p25NetworkWatchdog(*harness.m_control).isRunning());
}
TEST_CASE("P25 host e2e loopback times out stale call and resets stream state", "[p25][host][control][net][e2e]")
{
P25HostHarness harness(true, true);
harness.startNetworkVoiceCall(1201U, 2201U);
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_AUDIO);
REQUIRE(harness.network()->resetP25Count() == 0U);
HostTestHooks::p25NetworkWatchdog(*harness.m_control).clock(expireTimerTicks(HostTestHooks::p25NetworkWatchdog(*harness.m_control)));
harness.m_control->clock();
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_IDLE);
REQUIRE(harness.network()->resetP25Count() == 0U);
}
TEST_CASE("P25 host e2e loopback enforces stream lock until active stream terminates", "[p25][host][control][net][e2e]")
{
const uint16_t hostPort = reserveLoopbackPort();
const uint16_t senderPort = reserveLoopbackPort();
REQUIRE(hostPort != 0U);
REQUIRE(senderPort != 0U);
REQUIRE(hostPort != senderPort);
const uint32_t hostPeerId = 6007U;
const uint32_t streamA = 0x500101U;
const uint32_t streamB = 0x500102U;
P25HostHarness harness(true, true, hostPort, hostPeerId);
TestNetwork sender(senderPort, 6008U);
REQUIRE(harness.network() != nullptr);
REQUIRE(harness.network()->activateLoopback("127.0.0.1", senderPort));
REQUIRE(sender.activateLoopback("127.0.0.1", hostPort));
// Start call on stream A.
REQUIRE(sender.sendP25LDU1Frame(hostPeerId, streamA, 400U, 1301U, 2301U));
REQUIRE(sender.sendP25LDU2Frame(hostPeerId, streamA, 401U, 1301U, 2301U));
for (uint32_t i = 0U; i < 40U; i++) {
sender.clock(1U);
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::p25NetState(*harness.m_control) == RS_NET_AUDIO) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::p25NetLastSrcId(*harness.m_control) == 1301U);
REQUIRE(HostTestHooks::p25NetLastDstId(*harness.m_control) == 2301U);
REQUIRE(harness.network()->rxP25StreamId() == streamA);
// Competing stream B should be ignored while stream A is active and locked.
REQUIRE(sender.sendP25LDU1Frame(hostPeerId, streamB, 500U, 1301U, 2301U));
REQUIRE(sender.sendP25LDU2Frame(hostPeerId, streamB, 501U, 1301U, 2301U));
for (uint32_t i = 0U; i < 30U; i++) {
sender.clock(1U);
harness.network()->clock(1U);
harness.m_control->clock();
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::p25NetLastSrcId(*harness.m_control) == 1301U);
REQUIRE(HostTestHooks::p25NetLastDstId(*harness.m_control) == 2301U);
REQUIRE(harness.network()->rxP25StreamId() == streamA);
// End stream A; stream lock should be released.
REQUIRE(sender.sendP25TDUFrame(hostPeerId, streamA, RTP_END_OF_CALL_SEQ, 1301U, 2301U));
for (uint32_t i = 0U; i < 50U; i++) {
sender.clock(1U);
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::p25NetState(*harness.m_control) == RS_NET_IDLE) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_IDLE);
REQUIRE(harness.network()->rxP25StreamId() == 0U);
// Now stream B should be admitted after stream A terminates.
REQUIRE(sender.sendP25LDU1Frame(hostPeerId, streamB, 502U, 1301U, 2301U));
REQUIRE(sender.sendP25LDU2Frame(hostPeerId, streamB, 503U, 1301U, 2301U));
for (uint32_t i = 0U; i < 40U; i++) {
sender.clock(1U);
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::p25NetState(*harness.m_control) == RS_NET_AUDIO) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(harness.network()->rxP25StreamId() == streamB);
}
TEST_CASE("P25 host network terminator clears active network voice state", "[p25][host][control]")
{
P25HostHarness harness;

Loading…
Cancel
Save

Powered by TurnKey Linux.