diff --git a/tests/dmr/DataHeader_Tests.cpp b/tests/dmr/DataHeader_Tests.cpp index 8e652f20..cf921ae2 100644 --- a/tests/dmr/DataHeader_Tests.cpp +++ b/tests/dmr/DataHeader_Tests.cpp @@ -136,3 +136,64 @@ TEST_CASE("DataHeader handles maximum blocks to follow", "[dmr][dataheader]") { REQUIRE(decoded.decode(frame + 2U)); REQUIRE(decoded.getBlocksToFollow() == 127U); } + +TEST_CASE("DataHeader encodes and decodes defined raw data", "[dmr][dataheader]") { + uint8_t frame[DMR_FRAME_LENGTH_BYTES + 2U]; + ::memset(frame, 0x00U, sizeof(frame)); + + DataHeader hdr; + hdr.setDPF(DPF::DEFINED_RAW); + hdr.setSAP(0x03U); + hdr.setGI(true); + hdr.setSrcId(1234U); + hdr.setDstId(5678U); + hdr.setBlocksToFollow(2U); + hdr.setFullMesage(true); + hdr.setSynchronize(false); + hdr.setSrcPort(3U); + hdr.setDstPort(5U); + + hdr.encode(frame + 2U); + + DataHeader decoded; + REQUIRE(decoded.decode(frame + 2U)); + REQUIRE(decoded.getDPF() == DPF::DEFINED_RAW); + REQUIRE(decoded.getSrcPort() == 3U); + REQUIRE(decoded.getDstPort() == 5U); + REQUIRE(decoded.getFullMesage() == true); + REQUIRE(decoded.getSynchronize() == false); +} + +TEST_CASE("DataHeader copy and assignment preserve fields", "[dmr][dataheader]") { + DataHeader original; + original.setDPF(DPF::CONFIRMED_DATA); + original.setSAP(0x0AU); + original.setGI(false); + original.setSrcId(111U); + original.setDstId(222U); + original.setBlocksToFollow(4U); + original.setFullMesage(true); + original.setSynchronize(true); + original.setNs(5U); + original.setFSN(9U); + + DataHeader copied(original); + REQUIRE(copied.getDPF() == original.getDPF()); + REQUIRE(copied.getSAP() == original.getSAP()); + REQUIRE(copied.getGI() == original.getGI()); + REQUIRE(copied.getSrcId() == original.getSrcId()); + REQUIRE(copied.getDstId() == original.getDstId()); + REQUIRE(copied.getBlocksToFollow() == original.getBlocksToFollow()); + REQUIRE(copied.getFullMesage() == original.getFullMesage()); + REQUIRE(copied.getSynchronize() == original.getSynchronize()); + REQUIRE(copied.getNs() == original.getNs()); + REQUIRE(copied.getFSN() == original.getFSN()); + + DataHeader assigned; + assigned = original; + REQUIRE(assigned.getDPF() == original.getDPF()); + REQUIRE(assigned.getSAP() == original.getSAP()); + REQUIRE(assigned.getGI() == original.getGI()); + REQUIRE(assigned.getSrcId() == original.getSrcId()); + REQUIRE(assigned.getDstId() == original.getDstId()); +} diff --git a/tests/network/AdaptiveJitterBuffer_Tests.cpp b/tests/network/AdaptiveJitterBuffer_Tests.cpp new file mode 100644 index 00000000..65a638c6 --- /dev/null +++ b/tests/network/AdaptiveJitterBuffer_Tests.cpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Test Suite + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES FROM THIS FILE HEADER. + * + * Copyright (C) 2026 Bryan Biedenkapp, N2PLL + * + */ +#include "Defines.h" +#include "common/network/AdaptiveJitterBuffer.h" + +#include + +#include +#include +#include +#include + +using namespace network; + +// --------------------------------------------------------------------------- +// Global Functions +// --------------------------------------------------------------------------- + +/** + * @brief Deletes all BufferedFrame pointers in the given vector and clears the vector. + * @param frames A vector of BufferedFrame pointers to be deleted and cleared. + */ +static void cleanupFrames(std::vector& frames) +{ + for (BufferedFrame* frame : frames) + delete frame; + frames.clear(); +} + +/** + * @brief Converts the data of a BufferedFrame into a std::string. If the frame is nullptr, or its data is nullptr, + * or its length is 0, an empty string is returned. + * @param frame A pointer to the BufferedFrame whose data is to be converted to a std::string. + * @return std::string + */ +static std::string frameToString(const BufferedFrame* frame) +{ + if (frame == nullptr || frame->data == nullptr || frame->length == 0U) + return std::string(); + + return std::string(reinterpret_cast(frame->data), frame->length); +} + +TEST_CASE("AdaptiveJitterBuffer delivers in-order frames immediately", "[network][jitter]") +{ + AdaptiveJitterBuffer buffer(4U, 40000U); + std::vector readyFrames; + + const std::string one = "one"; + const std::string two = "two"; + const std::string three = "three"; + + REQUIRE(buffer.processFrame(1000U, reinterpret_cast(one.data()), (uint32_t)one.size(), readyFrames)); + REQUIRE(readyFrames.size() == 1U); + REQUIRE(readyFrames[0U]->seq == 1000U); + REQUIRE(frameToString(readyFrames[0U]) == one); + cleanupFrames(readyFrames); + + REQUIRE(buffer.processFrame(1001U, reinterpret_cast(two.data()), (uint32_t)two.size(), readyFrames)); + REQUIRE(readyFrames.size() == 1U); + REQUIRE(readyFrames[0U]->seq == 1001U); + REQUIRE(frameToString(readyFrames[0U]) == two); + cleanupFrames(readyFrames); + + REQUIRE(buffer.processFrame(1002U, reinterpret_cast(three.data()), (uint32_t)three.size(), readyFrames)); + REQUIRE(readyFrames.size() == 1U); + REQUIRE(readyFrames[0U]->seq == 1002U); + REQUIRE(frameToString(readyFrames[0U]) == three); + cleanupFrames(readyFrames); + + uint64_t totalFrames = 0ULL; + uint64_t reorderedFrames = 0ULL; + uint64_t droppedFrames = 0ULL; + uint64_t timedOutFrames = 0ULL; + buffer.getStatistics(totalFrames, reorderedFrames, droppedFrames, timedOutFrames); + REQUIRE(totalFrames == 3U); + REQUIRE(reorderedFrames == 0U); + REQUIRE(droppedFrames == 0U); + REQUIRE(timedOutFrames == 0U); + REQUIRE(buffer.getNextExpectedSeq() == 1003U); +} + +TEST_CASE("AdaptiveJitterBuffer reorders and drops duplicates", "[network][jitter]") +{ + AdaptiveJitterBuffer buffer(4U, 40000U); + std::vector readyFrames; + + const std::string first = "first"; + const std::string second = "second"; + const std::string third = "third"; + + REQUIRE(buffer.processFrame(2000U, reinterpret_cast(first.data()), (uint32_t)first.size(), readyFrames)); + cleanupFrames(readyFrames); + + REQUIRE(buffer.processFrame(2002U, reinterpret_cast(third.data()), (uint32_t)third.size(), readyFrames)); + REQUIRE(readyFrames.empty()); + + REQUIRE(buffer.processFrame(2001U, reinterpret_cast(second.data()), (uint32_t)second.size(), readyFrames)); + REQUIRE(readyFrames.size() == 2U); + REQUIRE(readyFrames[0U]->seq == 2001U); + REQUIRE(readyFrames[1U]->seq == 2002U); + REQUIRE(frameToString(readyFrames[0U]) == second); + REQUIRE(frameToString(readyFrames[1U]) == third); + cleanupFrames(readyFrames); + + REQUIRE_FALSE(buffer.processFrame(2002U, reinterpret_cast(third.data()), (uint32_t)third.size(), readyFrames)); + REQUIRE(readyFrames.empty()); + + uint64_t totalFrames = 0ULL; + uint64_t reorderedFrames = 0ULL; + uint64_t droppedFrames = 0ULL; + uint64_t timedOutFrames = 0ULL; + buffer.getStatistics(totalFrames, reorderedFrames, droppedFrames, timedOutFrames); + REQUIRE(totalFrames == 4U); + REQUIRE(reorderedFrames == 1U); + REQUIRE(droppedFrames == 1U); + REQUIRE(timedOutFrames == 0U); +} + +TEST_CASE("AdaptiveJitterBuffer handles wraparound and timeout delivery", "[network][jitter]") +{ + AdaptiveJitterBuffer buffer(4U, 1000U); + std::vector readyFrames; + std::vector timedOutFrames; + + const std::string tail = "tail"; + const std::string head = "head"; + + REQUIRE(buffer.processFrame(65535U, reinterpret_cast(tail.data()), (uint32_t)tail.size(), readyFrames)); + REQUIRE(readyFrames.size() == 1U); + REQUIRE(readyFrames[0U]->seq == 65535U); + cleanupFrames(readyFrames); + + REQUIRE(buffer.processFrame(0U, reinterpret_cast(head.data()), (uint32_t)head.size(), readyFrames)); + REQUIRE(readyFrames.size() == 1U); + REQUIRE(readyFrames[0U]->seq == 0U); + cleanupFrames(readyFrames); + + REQUIRE(buffer.processFrame(2U, reinterpret_cast(head.data()), (uint32_t)head.size(), readyFrames)); + REQUIRE(readyFrames.empty()); + buffer.checkTimeouts(timedOutFrames, UINT64_MAX); + REQUIRE(timedOutFrames.size() == 1U); + REQUIRE(timedOutFrames[0U]->seq == 2U); + cleanupFrames(timedOutFrames); + + REQUIRE(buffer.getNextExpectedSeq() == 3U); +} + +TEST_CASE("AdaptiveJitterBuffer reset clears buffered frames and stats", "[network][jitter]") +{ + AdaptiveJitterBuffer buffer(4U, 1000U); + std::vector readyFrames; + + const std::string payload = "payload"; + REQUIRE(buffer.processFrame(10U, reinterpret_cast(payload.data()), (uint32_t)payload.size(), readyFrames)); + cleanupFrames(readyFrames); + REQUIRE(buffer.processFrame(12U, reinterpret_cast(payload.data()), (uint32_t)payload.size(), readyFrames)); + REQUIRE(buffer.getBufferSize() == 1U); + + buffer.reset(true); + REQUIRE(buffer.getBufferSize() == 0U); + REQUIRE(buffer.getNextExpectedSeq() == 0U); + + uint64_t totalFrames = 0ULL; + uint64_t reorderedFrames = 0ULL; + uint64_t droppedFrames = 0ULL; + uint64_t timedOutFrames = 0ULL; + buffer.getStatistics(totalFrames, reorderedFrames, droppedFrames, timedOutFrames); + REQUIRE(totalFrames == 0U); + REQUIRE(reorderedFrames == 0U); + REQUIRE(droppedFrames == 0U); + REQUIRE(timedOutFrames == 0U); +} \ No newline at end of file diff --git a/tests/network/FrameQueue.cpp b/tests/network/FrameQueue_Tests.cpp similarity index 98% rename from tests/network/FrameQueue.cpp rename to tests/network/FrameQueue_Tests.cpp index da2e2191..71316716 100644 --- a/tests/network/FrameQueue.cpp +++ b/tests/network/FrameQueue_Tests.cpp @@ -25,6 +25,10 @@ using namespace network; using namespace network::frame; using namespace network::udp; +// --------------------------------------------------------------------------- +// Global Functions +// --------------------------------------------------------------------------- + /** * @brief Finds an available loopback UDP port. * @return uint16_t A free UDP port, or 0 on failure. diff --git a/tests/network/NetRPC.cpp b/tests/network/NetRPC_Tests.cpp similarity index 89% rename from tests/network/NetRPC.cpp rename to tests/network/NetRPC_Tests.cpp index 59999ebe..f46e82b4 100644 --- a/tests/network/NetRPC.cpp +++ b/tests/network/NetRPC_Tests.cpp @@ -202,6 +202,28 @@ TEST_CASE("NetRPC request receives JSON reply from registered remote handler", " REQUIRE(clientReplyCalled); REQUIRE(echoed == "ping"); + server.close(); + client.close(); +} + +TEST_CASE("NetRPC blocking request times out without a reply", "[network][rpc][netrpc]") +{ + const uint16_t serverPort = reserveLoopbackPort(); + const uint16_t clientPort = reserveLoopbackPort(); + REQUIRE(serverPort != 0U); + REQUIRE(clientPort != 0U); + + NetRPC server("127.0.0.1", serverPort, serverPort, "shared-password", false); + NetRPC client("127.0.0.1", clientPort, clientPort, "shared-password", false); + + REQUIRE(server.open()); + REQUIRE(client.open()); + + json::object request; + request["op"].set("timeout"); + + REQUIRE_FALSE(client.req(0x3333U, request, NetRPC::RPCType(), "127.0.0.1", serverPort, true)); + server.close(); client.close(); } \ No newline at end of file diff --git a/tests/network/PacketBuffer_Tests.cpp b/tests/network/PacketBuffer_Tests.cpp new file mode 100644 index 00000000..9d08888f --- /dev/null +++ b/tests/network/PacketBuffer_Tests.cpp @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Test Suite + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES FROM THIS FILE HEADER. + * + * Copyright (C) 2026 Bryan Biedenkapp, N2PLL + * + */ +#include "Defines.h" +#include "common/network/PacketBuffer.h" + +#include + +#include +#include +#include +#include +#include +#include + +using namespace network; + +// --------------------------------------------------------------------------- +// Global Functions +// --------------------------------------------------------------------------- + +/** + * @brief Retrieves the fragments of a PacketBuffer in order based on their index. The fragments + * are returned as a vector of pointers to PacketBuffer::Fragment objects. The order is determined + * by the fragment index, starting from 0 and stopping when a missing or null fragment is encountered. + * @param buffer The PacketBuffer instance whose fragments are to be retrieved in order. + * @return std::vector + */ +static std::vector fragmentsInOrder(PacketBuffer& buffer) +{ + std::vector fragments; + for (uint32_t index = 0U; index < 32U; index++) { + auto it = buffer.fragments.find((uint8_t)index); + if (it == buffer.fragments.end() || it->second == nullptr) + break; + + fragments.push_back(it->second); + } + + return fragments; +} + +/** + * @brief Sets the header information for a fragment. This includes the total size of the fragment, + * the compressed size, the block ID, and the total number of blocks. The header is written directly + * into the provided fragment byte array. + * @param fragment The byte array representing the fragment where the header will be written. + * @param size The total size of the fragment. + * @param compressedSize The size of the compressed data within the fragment. + * @param blockId The ID of the block within the fragment sequence. + * @param blockCount The total number of blocks in the fragment sequence. + */ +static void setFragmentHeader(uint8_t* fragment, uint32_t size, uint32_t compressedSize, uint8_t blockId, uint8_t blockCount) +{ + SET_UINT32(size, fragment, 0U); + SET_UINT32(compressedSize, fragment, 4U); + fragment[8U] = blockId; + fragment[9U] = blockCount; +} + +TEST_CASE("PacketBuffer reassembles uncompressed fragments in order", "[network][packetbuffer]") +{ + PacketBuffer encoder(false, "packetbuffer-plain"); + std::vector payload(800U); + for (size_t i = 0U; i < payload.size(); i++) + payload[i] = (uint8_t)(i & 0xFFU); + + encoder.encode(payload.data(), (uint32_t)payload.size()); + REQUIRE(encoder.fragments.size() == 2U); + + PacketBuffer decoder(false, "packetbuffer-plain"); + uint8_t* message = nullptr; + uint32_t messageLength = 0U; + + auto encodedFragments = fragmentsInOrder(encoder); + REQUIRE(encodedFragments.size() == 2U); + + REQUIRE_FALSE(decoder.decode(encodedFragments[1U]->data, &message, &messageLength)); + REQUIRE(message == nullptr); + REQUIRE(messageLength == 0U); + + REQUIRE(decoder.decode(encodedFragments[0U]->data, &message, &messageLength)); + REQUIRE(message != nullptr); + REQUIRE(messageLength == payload.size()); + REQUIRE(std::memcmp(message, payload.data(), payload.size()) == 0); + delete[] message; + + REQUIRE(decoder.fragments.size() == 0U); + encoder.clear(); + REQUIRE(encoder.fragments.size() == 0U); +} + +TEST_CASE("PacketBuffer reassembles compressed fragments", "[network][packetbuffer]") +{ + PacketBuffer encoder(true, "packetbuffer-compressed"); + std::vector payload; + for (uint32_t i = 0U; i < 1024U; i++) + payload.push_back('A'); + + encoder.encode(payload.data(), (uint32_t)payload.size()); + REQUIRE(encoder.fragments.size() >= 1U); + + PacketBuffer decoder(true, "packetbuffer-compressed"); + uint8_t* message = nullptr; + uint32_t messageLength = 0U; + + auto encodedFragments = fragmentsInOrder(encoder); + REQUIRE_FALSE(encodedFragments.empty()); + for (size_t i = 0U; i < encodedFragments.size(); i++) { + bool complete = decoder.decode(encodedFragments[i]->data, &message, &messageLength); + if (i + 1U < encodedFragments.size()) { + REQUIRE_FALSE(complete); + REQUIRE(message == nullptr); + } else { + REQUIRE(complete); + REQUIRE(message != nullptr); + REQUIRE(messageLength == payload.size()); + REQUIRE(std::memcmp(message, payload.data(), payload.size()) == 0); + delete[] message; + } + } +} + +TEST_CASE("PacketBuffer rejects oversized packet metadata", "[network][packetbuffer]") +{ + PacketBuffer buffer(false, "packetbuffer-invalid"); + + std::array fragment = {}; + setFragmentHeader(fragment.data(), 8192U * 1024U + 1U, 8192U * 1024U + 1U, 0U, 0U); + + uint8_t* message = nullptr; + uint32_t messageLength = 0U; + REQUIRE_FALSE(buffer.decode(fragment.data(), &message, &messageLength)); + REQUIRE(message == nullptr); + REQUIRE(messageLength == 0U); + REQUIRE(buffer.fragments.size() == 0U); +} diff --git a/tests/network/RTP_RPCHeaders.cpp b/tests/network/RTP_RPCHeader_Tests.cpp similarity index 86% rename from tests/network/RTP_RPCHeaders.cpp rename to tests/network/RTP_RPCHeader_Tests.cpp index d2d43521..57d4c3ba 100644 --- a/tests/network/RTP_RPCHeaders.cpp +++ b/tests/network/RTP_RPCHeader_Tests.cpp @@ -21,6 +21,14 @@ using namespace network; using namespace network::frame; +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief A test subclass of RTPExtensionHeader that exposes the type and length fields for + * testing purposes. + */ class TestRTPExtensionHeader : public RTPExtensionHeader { public: void setType(uint16_t type) { setPayloadType(type); } @@ -67,6 +75,21 @@ TEST_CASE("RTPHeader rejects invalid RTP version", "[network][rtp][header]") REQUIRE_FALSE(decoded.decode(buffer.data())); } +TEST_CASE("RTPHeader generates a timestamp when unset", "[network][rtp][header]") +{ + RTPHeader header = RTPHeader(); + header.setTimestamp(INVALID_TS); + header.setSSRC(0x01020304U); + + std::array buffer = {}; + header.encode(buffer.data()); + + RTPHeader decoded = RTPHeader(); + REQUIRE(decoded.decode(buffer.data())); + REQUIRE(decoded.getTimestamp() != INVALID_TS); + REQUIRE(decoded.getSSRC() == 0x01020304U); +} + TEST_CASE("RTPExtensionHeader encodes and decodes payload metadata", "[network][rtp][extension]") { TestRTPExtensionHeader encoded = TestRTPExtensionHeader(); diff --git a/tests/network/TCPSocket.cpp b/tests/network/TCPSocket_Tests.cpp similarity index 91% rename from tests/network/TCPSocket.cpp rename to tests/network/TCPSocket_Tests.cpp index edf6e6f2..3a3993d7 100644 --- a/tests/network/TCPSocket.cpp +++ b/tests/network/TCPSocket_Tests.cpp @@ -205,4 +205,22 @@ TEST_CASE("TCP operations fail on uninitialized socket descriptor", "[network][t REQUIRE(socket.listen("127.0.0.1", 6553U, 1) == -1); REQUIRE(socket.write(reinterpret_cast(payload.data()), payload.size()) == -1); REQUIRE(socket.read(buffer, sizeof(buffer)) == -1); +} + +TEST_CASE("TCP socket rejects invalid connect hostnames", "[network][tcp]") +{ + Socket client(AF_INET, SOCK_STREAM, 0); + REQUIRE_THROWS_AS(client.connect("not-an-ip-address", 12345U), std::runtime_error); +} + +TEST_CASE("TCP listen fails when the port is already in use", "[network][tcp]") +{ + const uint16_t port = reserveLoopbackPort(); + REQUIRE(port != 0U); + + Socket server1(AF_INET, SOCK_STREAM, 0); + REQUIRE(server1.listen("127.0.0.1", port, 1) == 0); + + Socket server2(AF_INET, SOCK_STREAM, 0); + REQUIRE(server2.listen("127.0.0.1", port, 1) == -1); } \ No newline at end of file diff --git a/tests/network/UDPSocket.cpp b/tests/network/UDPSocket_Tests.cpp similarity index 96% rename from tests/network/UDPSocket.cpp rename to tests/network/UDPSocket_Tests.cpp index f8e7c670..3ba5bb45 100644 --- a/tests/network/UDPSocket.cpp +++ b/tests/network/UDPSocket_Tests.cpp @@ -543,6 +543,29 @@ TEST_CASE("UDP socket does not report a partial sendmmsg send as success", "[net sender.close(); } +TEST_CASE("UDP socket resize helpers and address helpers work", "[network][udp]") +{ + Socket socket; + REQUIRE(socket.open(AF_INET)); + REQUIRE(socket.recvBufSize(65536U)); + REQUIRE(socket.sendBufSize(65536U)); + + sockaddr_storage loopback = {}; + uint32_t loopbackLen = 0U; + REQUIRE(Socket::lookup("127.0.0.1", 12345U, loopback, loopbackLen) == 0); + + sockaddr_storage same = loopback; + REQUIRE(Socket::match(loopback, same)); + REQUIRE(Socket::address(loopback) == "127.0.0.1"); + REQUIRE(Socket::port(loopback) == 12345U); + + sockaddr_storage invalid = {}; + uint32_t invalidLen = 0U; + REQUIRE(Socket::lookup("not-a-hostname.invalid", 12345U, invalid, invalidLen) != 0); + REQUIRE(Socket::isNone(invalid)); + socket.close(); +} + #if (defined(HAVE_SENDMSG) && !defined(HAVE_SENDMMSG)) || defined(_WIN32) TEST_CASE("UDP sendmmsg compatibility wrapper returns a datagram count", "[network][udp][compatibility]") { diff --git a/tests/nxdn/LICH_Tests.cpp b/tests/nxdn/LICH_Tests.cpp index c8dd6e37..88c0b15c 100644 --- a/tests/nxdn/LICH_Tests.cpp +++ b/tests/nxdn/LICH_Tests.cpp @@ -208,3 +208,20 @@ TEST_CASE("LICH golden test for voice call", "[nxdn][lich][golden]") { REQUIRE(decoded.getOption() == ChOption::STEAL_FACCH); REQUIRE(decoded.getOutbound() == false); } + +TEST_CASE("LICH rejects invalid parity", "[nxdn][lich]") { + uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U]; + ::memset(data, 0x00U, sizeof(data)); + + LICH lich; + lich.setRFCT(RFChannelType::RDCH); + lich.setFCT(FuncChannelType::USC_SACCH_NS); + lich.setOption(ChOption::DATA_NORMAL); + lich.setOutbound(true); + lich.encode(data); + + WRITE_BIT(data, NXDN_FSW_LENGTH_BITS, !READ_BIT(data, NXDN_FSW_LENGTH_BITS)); + + LICH decoded; + REQUIRE_FALSE(decoded.decode(data)); +} diff --git a/tests/p25/TSBK_Tests.cpp b/tests/p25/TSBK_Tests.cpp index 66754a6a..37aa2047 100644 --- a/tests/p25/TSBK_Tests.cpp +++ b/tests/p25/TSBK_Tests.cpp @@ -259,6 +259,22 @@ TEST_CASE("TSBK", "[p25][tsbk]") { REQUIRE(tsbk2.getMFId() == 0x00); } + SECTION("RawTSBK_Reports_Invalid_CRC_But_Decodes") { + OSP_TSBK_RAW tsbk; + + uint8_t rawTSBK[P25_TSBK_LENGTH_BYTES]; + ::memset(rawTSBK, 0x00U, P25_TSBK_LENGTH_BYTES); + rawTSBK[0] = 0x34; + rawTSBK[1] = 0x00; + edac::CRC::addCCITT162(rawTSBK, P25_TSBK_LENGTH_BYTES); + rawTSBK[P25_TSBK_LENGTH_BYTES - 1U] ^= 0xFFU; + + REQUIRE_FALSE(edac::CRC::checkCCITT162(rawTSBK, P25_TSBK_LENGTH_BYTES, true)); + REQUIRE(tsbk.decode(rawTSBK, true)); + REQUIRE(tsbk.getLCO() == 0x34U); + REQUIRE(tsbk.getMFId() == 0x00U); + } + SECTION("AllOnes_Pattern") { // Test all-ones pattern OSP_TSBK_RAW tsbk1;