parent
8b96163d6e
commit
e810d0a547
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
Loading…
Reference in new issue