diff --git a/tests/dmr/HostControl_Tests.cpp b/tests/dmr/HostControl_Tests.cpp index 58f02e79..6da8b5ff 100644 --- a/tests/dmr/HostControl_Tests.cpp +++ b/tests/dmr/HostControl_Tests.cpp @@ -573,6 +573,61 @@ TEST_CASE("DMR host e2e loopback times out stale call and resets stream state", REQUIRE(harness.network()->resetDMRCount() == 1U); } +TEST_CASE("DMR host e2e loopback preserves a continuing network stream across an RF collision", "[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 = 7011U; + const uint32_t streamId = 0x610201U; + + DMRHostHarness harness(true, true, hostPort, hostPeerId); + DMRTestNetwork sender(senderPort, 7012U); + + REQUIRE(harness.network() != nullptr); + REQUIRE(harness.network()->activateLoopback("127.0.0.1", senderPort)); + REQUIRE(sender.activateLoopback("127.0.0.1", hostPort)); + + HostTestHooks::dmrSetRFCall(*HostTestHooks::dmrSlot1(*harness.m_control), 1401U, 2401U); + REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamId, 600U, 1U, 1501U, 2501U)); + + for (uint32_t i = 0U; i < 40U; i++) { + sender.clock(1U); + harness.network()->clock(1U); + harness.m_control->clock(); + if (harness.network()->rxDMRStreamId(1U) == streamId) { + 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) == streamId); + REQUIRE(harness.network()->resetDMRCount() == 0U); + + HostTestHooks::dmrClearRFCall(*HostTestHooks::dmrSlot1(*harness.m_control)); + REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamId, 601U, 1U, 1501U, 2501U)); + + for (uint32_t i = 0U; i < 40U; i++) { + sender.clock(1U); + 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) == streamId); + REQUIRE(harness.network()->resetDMRCount() == 0U); +} + TEST_CASE("DMR host e2e loopback enforces stream lock until active stream terminates", "[dmr][host][control][net][e2e]") { const uint16_t hostPort = reserveLoopbackPort(); diff --git a/tests/host/HostTestHooks.cpp b/tests/host/HostTestHooks.cpp index 176efdbc..ed3865ae 100644 --- a/tests/host/HostTestHooks.cpp +++ b/tests/host/HostTestHooks.cpp @@ -83,6 +83,16 @@ void HostTestHooks::dmrSetRFCall(dmr::Slot& slot, uint32_t srcId, uint32_t dstId slot.m_rfTGHang.start(); } +/* Returns DMR slot to RF listening state. */ + +void HostTestHooks::dmrClearRFCall(dmr::Slot& slot) +{ + slot.m_rfState = RS_RF_LISTENING; + slot.m_rfLastSrcId = 0U; + slot.m_rfLastDstId = 0U; + slot.m_rfTGHang.stop(); +} + /* Forces DMR slot into RF rejected state. */ void HostTestHooks::dmrSetRFRejected(dmr::Slot& slot) @@ -281,6 +291,16 @@ void HostTestHooks::nxdnSetRFCall(nxdn::Control& control, uint32_t srcId, uint32 control.m_rfTGHang.start(); } +/* Returns NXDN control to RF listening state. */ + +void HostTestHooks::nxdnClearRFCall(nxdn::Control& control) +{ + control.m_rfState = RS_RF_LISTENING; + control.m_rfLastSrcId = 0U; + control.m_rfLastDstId = 0U; + control.m_rfTGHang.stop(); +} + /* Forces NXDN control into RF rejected state. */ void HostTestHooks::nxdnSetRFRejected(nxdn::Control& control) diff --git a/tests/host/HostTestHooks.h b/tests/host/HostTestHooks.h index bd703f8f..a1d21e04 100644 --- a/tests/host/HostTestHooks.h +++ b/tests/host/HostTestHooks.h @@ -80,6 +80,11 @@ public: * @param dstId Destination ID to apply to slot state. */ static void dmrSetRFCall(dmr::Slot& slot, uint32_t srcId, uint32_t dstId); + /** + * @brief Returns a DMR slot to RF listening state after a synthetic RF call. + * @param slot DMR slot instance. + */ + static void dmrClearRFCall(dmr::Slot& slot); /** * @brief Forces a DMR slot into RF rejected state for tests. * @param slot DMR slot instance. @@ -258,6 +263,11 @@ public: * @param dstId Destination ID to apply to control state. */ static void nxdnSetRFCall(nxdn::Control& control, uint32_t srcId, uint32_t dstId); + /** + * @brief Returns NXDN control to RF listening state after a synthetic RF call. + * @param control NXDN control instance. + */ + static void nxdnClearRFCall(nxdn::Control& control); /** * @brief Forces NXDN control into RF rejected state for tests. * @param control NXDN control instance. diff --git a/tests/nxdn/HostControl_Tests.cpp b/tests/nxdn/HostControl_Tests.cpp index 9db8ac3a..600bc747 100644 --- a/tests/nxdn/HostControl_Tests.cpp +++ b/tests/nxdn/HostControl_Tests.cpp @@ -373,6 +373,61 @@ TEST_CASE("NXDN host e2e loopback times out stale call and resets stream state", REQUIRE(HostTestHooks::nxdnNetLastSrcId(*harness.m_control) == 0U); } +TEST_CASE("NXDN host e2e loopback preserves a continuing network stream across an RF collision", "[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 = 8011U; + const uint32_t streamId = 0x620201U; + + NXDNHostHarness harness(true, true, hostPort, hostPeerId); + NXDNTestNetwork sender(senderPort, 8012U); + + REQUIRE(harness.network() != nullptr); + REQUIRE(harness.network()->activateLoopback("127.0.0.1", senderPort)); + REQUIRE(sender.activateLoopback("127.0.0.1", hostPort)); + + HostTestHooks::nxdnSetRFCall(*harness.m_control, 1401U, 2401U); + REQUIRE(sender.sendNXDNFrame(hostPeerId, streamId, 600U, nxdn::defines::MessageType::RTCH_VCALL, 1501U, 2501U)); + + for (uint32_t i = 0U; i < 40U; i++) { + sender.clock(1U); + harness.network()->clock(1U); + harness.m_control->clock(); + if (harness.network()->rxNXDNStreamId() == streamId) { + break; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + + REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_IDLE); + REQUIRE(harness.network()->rxNXDNStreamId() == streamId); + REQUIRE(harness.network()->resetNXDNCount() == 0U); + + HostTestHooks::nxdnClearRFCall(*harness.m_control); + REQUIRE(sender.sendNXDNFrame(hostPeerId, streamId, 601U, nxdn::defines::MessageType::RTCH_VCALL, 1501U, 2501U)); + + 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(harness.network()->rxNXDNStreamId() == streamId); + REQUIRE(harness.network()->resetNXDNCount() == 0U); +} + TEST_CASE("NXDN host e2e loopback enforces stream lock until active stream terminates", "[nxdn][host][control][net][e2e]") { const uint16_t hostPort = reserveLoopbackPort();