continue implementing catch2 tests;

pull/126/head
Bryan Biedenkapp 4 months ago
parent 8b96163d6e
commit e810d0a547

@ -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());
}

@ -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 <catch2/catch_test_macros.hpp>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
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<BufferedFrame*>& 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<const char*>(frame->data), frame->length);
}
TEST_CASE("AdaptiveJitterBuffer delivers in-order frames immediately", "[network][jitter]")
{
AdaptiveJitterBuffer buffer(4U, 40000U);
std::vector<BufferedFrame*> readyFrames;
const std::string one = "one";
const std::string two = "two";
const std::string three = "three";
REQUIRE(buffer.processFrame(1000U, reinterpret_cast<const uint8_t*>(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<const uint8_t*>(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<const uint8_t*>(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<BufferedFrame*> readyFrames;
const std::string first = "first";
const std::string second = "second";
const std::string third = "third";
REQUIRE(buffer.processFrame(2000U, reinterpret_cast<const uint8_t*>(first.data()), (uint32_t)first.size(), readyFrames));
cleanupFrames(readyFrames);
REQUIRE(buffer.processFrame(2002U, reinterpret_cast<const uint8_t*>(third.data()), (uint32_t)third.size(), readyFrames));
REQUIRE(readyFrames.empty());
REQUIRE(buffer.processFrame(2001U, reinterpret_cast<const uint8_t*>(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<const uint8_t*>(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<BufferedFrame*> readyFrames;
std::vector<BufferedFrame*> timedOutFrames;
const std::string tail = "tail";
const std::string head = "head";
REQUIRE(buffer.processFrame(65535U, reinterpret_cast<const uint8_t*>(tail.data()), (uint32_t)tail.size(), readyFrames));
REQUIRE(readyFrames.size() == 1U);
REQUIRE(readyFrames[0U]->seq == 65535U);
cleanupFrames(readyFrames);
REQUIRE(buffer.processFrame(0U, reinterpret_cast<const uint8_t*>(head.data()), (uint32_t)head.size(), readyFrames));
REQUIRE(readyFrames.size() == 1U);
REQUIRE(readyFrames[0U]->seq == 0U);
cleanupFrames(readyFrames);
REQUIRE(buffer.processFrame(2U, reinterpret_cast<const uint8_t*>(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<BufferedFrame*> readyFrames;
const std::string payload = "payload";
REQUIRE(buffer.processFrame(10U, reinterpret_cast<const uint8_t*>(payload.data()), (uint32_t)payload.size(), readyFrames));
cleanupFrames(readyFrames);
REQUIRE(buffer.processFrame(12U, reinterpret_cast<const uint8_t*>(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);
}

@ -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.

@ -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<std::string>("timeout");
REQUIRE_FALSE(client.req(0x3333U, request, NetRPC::RPCType(), "127.0.0.1", serverPort, true));
server.close();
client.close();
}

@ -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 <catch2/catch_test_macros.hpp>
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
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<const PacketBuffer::Fragment*>
*/
static std::vector<const PacketBuffer::Fragment*> fragmentsInOrder(PacketBuffer& buffer)
{
std::vector<const PacketBuffer::Fragment*> 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<uint8_t> 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<uint8_t> 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<uint8_t, FRAG_SIZE> 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);
}

@ -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<uint8_t, RTP_HEADER_LENGTH_BYTES> 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();

@ -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<const uint8_t*>(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);
}

@ -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]")
{

@ -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));
}

@ -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;

Loading…
Cancel
Save

Powered by TurnKey Linux.