8ea7778kept one directive for loop() and recognised the repeat that the broker mirror delivers in two ways: bytes compared with the directive that still waited, and the messageId once loop() had parsed it. A repeat that arrived after its directive had been read therefore took the one place until the next loop(), and a different directive behind it was dropped. Alexa sends a group command as one directive per endpoint, so a board with several endpoints lost directives whenever they arrived faster than loop() ran; 1.1.0 queued five tokens. AlexaDirectiveBuffer is now a queue. The arriving message is collected in a heap block of its own and hashed (FNV-1a, 64 bit) as its fragments come in. At the last fragment it is one of three things: a repeat, when the hash is among the last 16 that were queued, and then it is freed and never takes a place; a directive, which is queued; or lost, when eight directives (ALEX2ESP_MAX_QUEUED_DIRECTIVES) or 8188 bytes (ALEX2ESP_MAX_QUEUED_BYTES, four times the largest directive) already wait. Whether there is a place is decided at the last fragment, because loop() may have read a directive by then. A message for which there is no memory is still hashed, so its loss is reported only when it is not a repeat. loop() parses one directive per call, in the order of arrival, and frees its block before the handler runs. The messageId check stays for a directive that comes again with other bytes; it remembers 16 ids instead of 4 and shares the ring with the buffer (AlexaRecentHashes). The two limits are in src/AlexaLimits.h, with #error for values that cannot work. The constructor of the buffer takes the allocator, malloc by default, so the tests can let it fail. For a sketch: nothing to change. The error line of a directive that finds no place reads "directive of N bytes on <topic> dropped: 8 directives, M bytes, already wait for loop()". The heap holds up to 8188 bytes of waiting directives and one arriving directive of up to 2048, where it held one directive. Measured with the bridge built for the host against a fake MQTT client (not in the repository), directives of 793 bytes, every message delivered twice,8ea7778-> this commit: repeat of D1 and a new D2 after D1 was handled D2 dropped -> D2 handled group of 5 in one burst 1 of 5 handled, 8 error lines -> 5 of 5, none group of 8 in one burst 8 of 8 handled, no error line group of 10 in one burst, not mirrored 8 of 10 handled, 2 error lines group of 10, a loop() after every fourth message 10 of 10 handled, no error line Tests: 41 host tests (33 before). New: the order of the queue and the reuse of its places, the ninth directive, a place that becomes free while a directive arrives, the limit in bytes, the largest directive in an empty queue, the repeat of a waiting directive and of one that was read, a group of five with repeats, a repeat when no place is free, a directive that differs in one byte, how long a repeat is remembered, a directive and a repeat without memory, the FNV-1a test vectors. They also pass under -fsanitize=address,undefined. Seven faults planted in a copy of AlexaBridgeLogic.cpp (no repeat check, no limit in bytes, no limit in places, no bounds check, a lost directive remembered, release() that keeps the bytes, last in first out) were each noticed: six by failing tests, the missing bounds check by AddressSanitizer as a heap-buffer-overflow. Built for d1_mini with empty credentials (PlatformIO 6.2.0, espressif8266 4.2.1), static RAM / flash in bytes,e48f858-> this commit, no warnings: basicLight 34,116 / 336,757 -> 34,444 / 337,145 lightWithBrightness 34,232 / 340,517 -> 34,560 / 340,921 lightWithColorTemp 34,380 / 341,177 -> 34,708 / 341,581 tempSensor 34,024 / 335,457 -> 34,352 / 335,845 blindControl 34,256 / 338,925 -> 34,584 / 339,313 The 328 bytes of RAM are the two rings of 16 hashes (256) and the eight places of the queue. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
856 lines
33 KiB
C++
856 lines
33 KiB
C++
// Host tests of the bridge logic (src/AlexaBridgeLogic.cpp): what the MQTT callback decides about the fragments a
|
|
// broker delivers, how directives wait for loop(), which of them are repeats, what may be published, topics and
|
|
// time stamps.
|
|
// pio test -e native
|
|
#include <unity.h>
|
|
#include <ArduinoJson.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <string>
|
|
#include "AlexaBridgeLogic.h"
|
|
#include "AlexaLimits.h"
|
|
|
|
typedef AlexaDirectiveBuffer::Result Result;
|
|
|
|
#define ASSERT_RESULT(expected, actual) TEST_ASSERT_EQUAL_INT(static_cast<int>(expected), static_cast<int>(actual))
|
|
|
|
// A directive as Alex2MQTT publishes it on <root>/<endpointId>/alexaDirective
|
|
static std::string directive(const char *name, const char *messageId,
|
|
const std::string &correlationToken = "AAAAAAAAAQBnlqNbYnB0dHNmYW5zbGF0ZQ==")
|
|
{
|
|
return std::string("{\"header\":{\"namespace\":\"Alexa.PowerController\",\"name\":\"") + name +
|
|
"\",\"payloadVersion\":\"3\",\"messageId\":\"" + messageId +
|
|
"\",\"correlationToken\":\"" + correlationToken + "\"},"
|
|
"\"endpoint\":{\"endpointId\":\"ESP-01\",\"cookie\":{}},\"payload\":{}}";
|
|
}
|
|
|
|
static const char ID_1[] = "1bd5d003-31b9-476f-ad03-71d471922820";
|
|
static const char ID_2[] = "7c0e1f6a-52d4-4b8e-9a3c-0d9f4e2b6a11";
|
|
|
|
// The limits the bridge passes to its buffer by default
|
|
static const size_t LARGEST = 2047;
|
|
static const size_t MOST_BYTES = 4 * 2047;
|
|
static const size_t PLACES = AlexaDirectiveBuffer::CAPACITY;
|
|
|
|
// The n-th directive of a series: each has a messageId of its own, as each directive of Alexa has
|
|
static std::string numbered(unsigned n)
|
|
{
|
|
char messageId[37];
|
|
snprintf(messageId, sizeof(messageId), "00000000-0000-4000-8000-%012u", n);
|
|
return directive(n % 2 == 0 ? "TurnOn" : "TurnOff", messageId);
|
|
}
|
|
|
|
// Hands the message over as the MQTT client does: pieces of fragmentSize bytes with their offset and the total.
|
|
// Returns the result of the last piece; every piece before it has to be INCOMPLETE.
|
|
static Result deliver(AlexaDirectiveBuffer &buffer, const std::string &message, size_t fragmentSize)
|
|
{
|
|
Result result = Result::INCOMPLETE;
|
|
for (size_t index = 0; index < message.size(); index += fragmentSize)
|
|
{
|
|
ASSERT_RESULT(Result::INCOMPLETE, result);
|
|
size_t length = message.size() - index < fragmentSize ? message.size() - index : fragmentSize;
|
|
result = buffer.append(message.data() + index, length, index, message.size());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static void assertWaiting(const AlexaDirectiveBuffer &buffer, const std::string &message)
|
|
{
|
|
TEST_ASSERT_TRUE(buffer.ready());
|
|
TEST_ASSERT_EQUAL_UINT(message.size(), buffer.length());
|
|
TEST_ASSERT_EQUAL_STRING(message.c_str(), buffer.data()); // also proves the terminating NUL
|
|
}
|
|
|
|
void setUp() {}
|
|
void tearDown() {}
|
|
|
|
// --- reassembly ---
|
|
|
|
void test_directive_in_one_piece_is_complete()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
std::string turnOn = directive("TurnOn", ID_1);
|
|
|
|
ASSERT_RESULT(Result::COMPLETE, buffer.append(turnOn.data(), turnOn.size(), 0, turnOn.size()));
|
|
assertWaiting(buffer, turnOn);
|
|
|
|
buffer.release();
|
|
TEST_ASSERT_FALSE(buffer.ready());
|
|
TEST_ASSERT_NULL(buffer.data());
|
|
TEST_ASSERT_EQUAL_UINT(0, buffer.length());
|
|
}
|
|
|
|
void test_directive_in_fragments_is_reassembled_and_parses()
|
|
{
|
|
std::string turnOn = directive("TurnOn", ID_1);
|
|
|
|
// Every fragment size from one byte per fragment to the message in two halves
|
|
for (size_t fragmentSize = 1; fragmentSize < turnOn.size(); fragmentSize++)
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, fragmentSize));
|
|
assertWaiting(buffer, turnOn);
|
|
}
|
|
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
deliver(buffer, turnOn, 100);
|
|
JsonDocument parsed;
|
|
TEST_ASSERT_TRUE(deserializeJson(parsed, buffer.data(), buffer.length()) == DeserializationError::Ok);
|
|
TEST_ASSERT_EQUAL_STRING("TurnOn", parsed["header"]["name"]);
|
|
TEST_ASSERT_EQUAL_STRING("ESP-01", parsed["endpoint"]["endpointId"]);
|
|
}
|
|
|
|
void test_nothing_waits_before_the_last_fragment()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
std::string turnOn = directive("TurnOn", ID_1);
|
|
|
|
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOn.data(), 50, 0, turnOn.size()));
|
|
TEST_ASSERT_FALSE(buffer.ready());
|
|
TEST_ASSERT_NULL(buffer.data());
|
|
}
|
|
|
|
// --- limits ---
|
|
|
|
void test_directive_at_the_limit_is_accepted()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
std::string atLimit(2047, 'x');
|
|
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, atLimit, 536));
|
|
assertWaiting(buffer, atLimit);
|
|
}
|
|
|
|
void test_directive_over_the_limit_is_refused_once()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
std::string tooLarge(2048, 'x');
|
|
|
|
ASSERT_RESULT(Result::TOO_LARGE, buffer.append(tooLarge.data(), 536, 0, tooLarge.size()));
|
|
// Its other fragments still arrive: dropped without another report
|
|
ASSERT_RESULT(Result::IGNORED, buffer.append(tooLarge.data() + 536, 536, 536, tooLarge.size()));
|
|
ASSERT_RESULT(Result::IGNORED, buffer.append(tooLarge.data() + 1072, 976, 1072, tooLarge.size()));
|
|
TEST_ASSERT_FALSE(buffer.ready());
|
|
}
|
|
|
|
void test_refused_directive_does_not_block_the_next_one()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
std::string tooLarge(5000, 'x');
|
|
std::string turnOn = directive("TurnOn", ID_1);
|
|
|
|
ASSERT_RESULT(Result::TOO_LARGE, buffer.append(tooLarge.data(), 1460, 0, tooLarge.size()));
|
|
ASSERT_RESULT(Result::IGNORED, buffer.append(tooLarge.data() + 1460, 1460, 1460, tooLarge.size()));
|
|
// The connection drops before the rest of it arrives; the next message is a normal directive
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100));
|
|
assertWaiting(buffer, turnOn);
|
|
}
|
|
|
|
void test_empty_message_is_reported()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
|
|
// AsyncMqttClient hands a message without a payload over as (nullptr, 0, 0, 0)
|
|
ASSERT_RESULT(Result::EMPTY, buffer.append(nullptr, 0, 0, 0));
|
|
TEST_ASSERT_FALSE(buffer.ready());
|
|
}
|
|
|
|
// --- directives wait for loop() ---
|
|
|
|
void test_directives_wait_in_the_order_they_arrived()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
size_t bytes = 0;
|
|
|
|
for (unsigned n = 0; n < PLACES; n++)
|
|
{
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(n), 100));
|
|
bytes += numbered(n).size();
|
|
TEST_ASSERT_EQUAL_UINT(n + 1, buffer.waitingDirectives());
|
|
TEST_ASSERT_EQUAL_UINT(bytes, buffer.waitingBytes());
|
|
}
|
|
for (unsigned n = 0; n < PLACES; n++)
|
|
{
|
|
assertWaiting(buffer, numbered(n));
|
|
buffer.release();
|
|
}
|
|
TEST_ASSERT_FALSE(buffer.ready());
|
|
TEST_ASSERT_EQUAL_UINT(0, buffer.waitingDirectives());
|
|
TEST_ASSERT_EQUAL_UINT(0, buffer.waitingBytes());
|
|
|
|
// The places are a ring: they are used again, in the same order
|
|
for (unsigned round = 1; round <= 3; round++)
|
|
{
|
|
for (unsigned n = 0; n < PLACES - 1; n++)
|
|
{
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(100 * round + n), 100));
|
|
}
|
|
for (unsigned n = 0; n < PLACES - 1; n++)
|
|
{
|
|
assertWaiting(buffer, numbered(100 * round + n));
|
|
buffer.release();
|
|
}
|
|
}
|
|
TEST_ASSERT_FALSE(buffer.ready());
|
|
}
|
|
|
|
void test_directive_beyond_the_places_is_dropped_and_the_others_kept()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
for (unsigned n = 0; n < PLACES; n++)
|
|
{
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(n), 100));
|
|
}
|
|
|
|
// The loss is reported once, by the last fragment
|
|
ASSERT_RESULT(Result::QUEUE_FULL, deliver(buffer, numbered(PLACES), 100));
|
|
TEST_ASSERT_EQUAL_UINT(PLACES, buffer.waitingDirectives());
|
|
assertWaiting(buffer, numbered(0));
|
|
|
|
// A directive that was dropped is not taken for a repeat when it comes again and a place is free
|
|
buffer.release();
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(PLACES), 100));
|
|
for (unsigned n = 1; n <= PLACES; n++)
|
|
{
|
|
assertWaiting(buffer, numbered(n));
|
|
buffer.release();
|
|
}
|
|
TEST_ASSERT_FALSE(buffer.ready());
|
|
}
|
|
|
|
void test_place_that_becomes_free_while_a_directive_arrives_is_used()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
for (unsigned n = 0; n < PLACES; n++)
|
|
{
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(n), 100));
|
|
}
|
|
std::string late = numbered(PLACES);
|
|
|
|
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(late.data(), 100, 0, late.size()));
|
|
buffer.release(); // loop() ran between two fragments
|
|
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(late.data() + 100, 100, 100, late.size()));
|
|
ASSERT_RESULT(Result::COMPLETE, buffer.append(late.data() + 200, late.size() - 200, 200, late.size()));
|
|
TEST_ASSERT_EQUAL_UINT(PLACES, buffer.waitingDirectives());
|
|
}
|
|
|
|
void test_waiting_directives_are_limited_in_bytes()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, 3000);
|
|
std::string first(1500, 'a');
|
|
std::string second(1500, 'b');
|
|
std::string third(1, 'c');
|
|
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, first, 536));
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, second, 536)); // 3000 bytes wait: at the limit
|
|
ASSERT_RESULT(Result::QUEUE_FULL, deliver(buffer, third, 536));
|
|
TEST_ASSERT_EQUAL_UINT(2, buffer.waitingDirectives());
|
|
TEST_ASSERT_EQUAL_UINT(3000, buffer.waitingBytes());
|
|
|
|
buffer.release();
|
|
TEST_ASSERT_EQUAL_UINT(1500, buffer.waitingBytes());
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, third, 536));
|
|
assertWaiting(buffer, second);
|
|
}
|
|
|
|
void test_empty_queue_takes_the_largest_directive()
|
|
{
|
|
// A limit for the waiting bytes under the limit for one directive is raised to it
|
|
AlexaDirectiveBuffer buffer(LARGEST, 100);
|
|
std::string largest(LARGEST, 'x');
|
|
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, largest, 536));
|
|
assertWaiting(buffer, largest);
|
|
ASSERT_RESULT(Result::QUEUE_FULL, deliver(buffer, std::string("y"), 536));
|
|
}
|
|
|
|
// --- the repeat a mirroring broker delivers ---
|
|
|
|
void test_repeat_of_a_waiting_directive_is_discarded()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
std::string turnOn = directive("TurnOn", ID_1);
|
|
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100));
|
|
ASSERT_RESULT(Result::DUPLICATE, buffer.append(turnOn.data(), turnOn.size(), 0, turnOn.size()));
|
|
ASSERT_RESULT(Result::DUPLICATE, deliver(buffer, turnOn, 64)); // in other fragments than the first time
|
|
TEST_ASSERT_EQUAL_UINT(1, buffer.waitingDirectives());
|
|
assertWaiting(buffer, turnOn);
|
|
}
|
|
|
|
void test_repeat_of_a_directive_that_was_read_does_not_hold_up_the_next()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
std::string turnOn = directive("TurnOn", ID_1);
|
|
std::string turnOff = directive("TurnOff", ID_2);
|
|
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100));
|
|
buffer.release(); // loop() has handled it
|
|
// Its repeat and the next directive arrive together
|
|
ASSERT_RESULT(Result::DUPLICATE, deliver(buffer, turnOn, 100));
|
|
TEST_ASSERT_FALSE(buffer.ready());
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOff, 100));
|
|
assertWaiting(buffer, turnOff);
|
|
}
|
|
|
|
void test_group_of_five_with_repeats_in_one_burst()
|
|
{
|
|
TEST_ASSERT_TRUE(PLACES >= 5);
|
|
|
|
// Every directive followed by its repeat
|
|
AlexaDirectiveBuffer interleaved(LARGEST, MOST_BYTES);
|
|
for (unsigned n = 0; n < 5; n++)
|
|
{
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(interleaved, numbered(n), 536));
|
|
ASSERT_RESULT(Result::DUPLICATE, deliver(interleaved, numbered(n), 536));
|
|
}
|
|
TEST_ASSERT_EQUAL_UINT(5, interleaved.waitingDirectives());
|
|
for (unsigned n = 0; n < 5; n++)
|
|
{
|
|
assertWaiting(interleaved, numbered(n));
|
|
interleaved.release();
|
|
}
|
|
|
|
// The five directives, then the five repeats
|
|
AlexaDirectiveBuffer trailing(LARGEST, MOST_BYTES);
|
|
for (unsigned n = 0; n < 5; n++)
|
|
{
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(trailing, numbered(n), 536));
|
|
}
|
|
for (unsigned n = 0; n < 5; n++)
|
|
{
|
|
ASSERT_RESULT(Result::DUPLICATE, deliver(trailing, numbered(n), 536));
|
|
}
|
|
TEST_ASSERT_EQUAL_UINT(5, trailing.waitingDirectives());
|
|
}
|
|
|
|
void test_repeat_is_recognised_when_no_place_is_free()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
for (unsigned n = 0; n < PLACES; n++)
|
|
{
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(n), 100));
|
|
}
|
|
|
|
// Not a loss, so not QUEUE_FULL
|
|
ASSERT_RESULT(Result::DUPLICATE, deliver(buffer, numbered(0), 100));
|
|
ASSERT_RESULT(Result::DUPLICATE, deliver(buffer, numbered(PLACES - 1), 100));
|
|
TEST_ASSERT_EQUAL_UINT(PLACES, buffer.waitingDirectives());
|
|
}
|
|
|
|
void test_directive_that_differs_in_one_byte_is_not_a_repeat()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
std::string first = directive("TurnOn", ID_1);
|
|
std::string second = first;
|
|
second[second.size() - 10] = '#'; // the same length, the same up to its last fragment
|
|
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, first, 100));
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, second, 100));
|
|
assertWaiting(buffer, first);
|
|
buffer.release();
|
|
assertWaiting(buffer, second);
|
|
}
|
|
|
|
void test_repeat_is_forgotten_after_as_many_directives_as_are_remembered()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
const unsigned remembered = AlexaRecentHashes::CAPACITY;
|
|
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(0), 100));
|
|
buffer.release();
|
|
for (unsigned n = 1; n < remembered; n++)
|
|
{
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(n), 100));
|
|
buffer.release();
|
|
}
|
|
ASSERT_RESULT(Result::DUPLICATE, deliver(buffer, numbered(0), 100)); // the oldest that is remembered
|
|
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(remembered), 100)); // takes its place
|
|
buffer.release();
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(0), 100));
|
|
}
|
|
|
|
// --- no memory ---
|
|
|
|
static bool memoryLeft = true;
|
|
|
|
static void *scarceMemory(size_t size)
|
|
{
|
|
return memoryLeft ? malloc(size) : nullptr;
|
|
}
|
|
|
|
void test_directive_without_memory_is_reported_by_its_last_fragment()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES, scarceMemory);
|
|
std::string turnOn = directive("TurnOn", ID_1);
|
|
|
|
memoryLeft = false;
|
|
ASSERT_RESULT(Result::NO_MEMORY, deliver(buffer, turnOn, 100));
|
|
TEST_ASSERT_FALSE(buffer.ready());
|
|
|
|
// It was not remembered: when it comes again and there is memory, it is handled
|
|
memoryLeft = true;
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100));
|
|
assertWaiting(buffer, turnOn);
|
|
}
|
|
|
|
void test_repeat_without_memory_is_still_a_repeat()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES, scarceMemory);
|
|
std::string turnOn = directive("TurnOn", ID_1);
|
|
|
|
memoryLeft = true;
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100));
|
|
memoryLeft = false;
|
|
ASSERT_RESULT(Result::DUPLICATE, deliver(buffer, turnOn, 100));
|
|
memoryLeft = true;
|
|
assertWaiting(buffer, turnOn);
|
|
}
|
|
|
|
// --- fragments that do not fit ---
|
|
|
|
void test_fragment_with_a_gap_drops_the_message()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
std::string turnOn = directive("TurnOn", ID_1);
|
|
|
|
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOn.data(), 100, 0, turnOn.size()));
|
|
ASSERT_RESULT(Result::OUT_OF_ORDER, buffer.append(turnOn.data() + 150, 50, 150, turnOn.size()));
|
|
ASSERT_RESULT(Result::IGNORED, buffer.append(turnOn.data() + 200, turnOn.size() - 200, 200, turnOn.size()));
|
|
TEST_ASSERT_FALSE(buffer.ready());
|
|
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100));
|
|
assertWaiting(buffer, turnOn);
|
|
}
|
|
|
|
void test_fragment_beyond_the_total_is_refused()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
std::string message(300, 'x');
|
|
|
|
// The block holds 200 + 1 bytes; a fragment that would end at 250 must not be copied
|
|
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(message.data(), 100, 0, 200));
|
|
ASSERT_RESULT(Result::OUT_OF_ORDER, buffer.append(message.data() + 100, 150, 100, 200));
|
|
TEST_ASSERT_FALSE(buffer.ready());
|
|
|
|
// A first fragment longer than its own total
|
|
ASSERT_RESULT(Result::OUT_OF_ORDER, buffer.append(message.data(), 300, 0, 200));
|
|
TEST_ASSERT_FALSE(buffer.ready());
|
|
}
|
|
|
|
void test_fragment_with_another_total_is_refused()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
std::string message(300, 'x');
|
|
|
|
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(message.data(), 100, 0, 200));
|
|
ASSERT_RESULT(Result::OUT_OF_ORDER, buffer.append(message.data() + 100, 100, 100, 300));
|
|
TEST_ASSERT_FALSE(buffer.ready());
|
|
}
|
|
|
|
void test_fragment_without_a_start_is_refused_once()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
std::string turnOn = directive("TurnOn", ID_1);
|
|
|
|
ASSERT_RESULT(Result::OUT_OF_ORDER, buffer.append(turnOn.data() + 100, 100, 100, turnOn.size()));
|
|
ASSERT_RESULT(Result::IGNORED, buffer.append(turnOn.data() + 200, turnOn.size() - 200, 200, turnOn.size()));
|
|
TEST_ASSERT_FALSE(buffer.ready());
|
|
}
|
|
|
|
void test_new_message_replaces_one_that_never_completed()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
std::string turnOn = directive("TurnOn", ID_1);
|
|
std::string turnOff = directive("TurnOff", ID_2);
|
|
|
|
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOn.data(), 100, 0, turnOn.size()));
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOff, 100));
|
|
TEST_ASSERT_EQUAL_UINT(1, buffer.waitingDirectives());
|
|
assertWaiting(buffer, turnOff);
|
|
}
|
|
|
|
void test_lost_connection_drops_the_arriving_message_only()
|
|
{
|
|
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
|
|
std::string turnOn = directive("TurnOn", ID_1);
|
|
std::string turnOff = directive("TurnOff", ID_2);
|
|
|
|
// Nothing waits, a message is arriving
|
|
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOn.data(), 100, 0, turnOn.size()));
|
|
buffer.cancelArrival();
|
|
ASSERT_RESULT(Result::OUT_OF_ORDER, buffer.append(turnOn.data() + 100, 100, 100, turnOn.size()));
|
|
TEST_ASSERT_FALSE(buffer.ready());
|
|
|
|
// A directive waits, another one is arriving
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOff, 100));
|
|
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOn.data(), 100, 0, turnOn.size()));
|
|
buffer.cancelArrival();
|
|
TEST_ASSERT_EQUAL_UINT(1, buffer.waitingDirectives());
|
|
assertWaiting(buffer, turnOff);
|
|
|
|
// What arrived of the message that was cut off does not make the whole message a repeat
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100));
|
|
}
|
|
|
|
// --- repeated messageIds ---
|
|
|
|
void test_repeated_message_id_is_recognised()
|
|
{
|
|
AlexaRecentIds recent;
|
|
|
|
TEST_ASSERT_FALSE(recent.seenBefore(ID_1));
|
|
TEST_ASSERT_TRUE(recent.seenBefore(ID_1));
|
|
TEST_ASSERT_FALSE(recent.seenBefore(ID_2));
|
|
TEST_ASSERT_TRUE(recent.seenBefore(ID_1));
|
|
TEST_ASSERT_TRUE(recent.seenBefore(ID_2));
|
|
}
|
|
|
|
void test_only_the_last_ids_are_remembered()
|
|
{
|
|
AlexaRecentIds recent;
|
|
const unsigned remembered = AlexaRecentIds::CAPACITY;
|
|
char id[16];
|
|
|
|
for (unsigned n = 0; n <= remembered; n++)
|
|
{
|
|
snprintf(id, sizeof(id), "id-%u", n);
|
|
TEST_ASSERT_FALSE(recent.seenBefore(id));
|
|
}
|
|
// The last one took the place of id-0
|
|
for (unsigned n = 1; n <= remembered; n++)
|
|
{
|
|
snprintf(id, sizeof(id), "id-%u", n);
|
|
TEST_ASSERT_TRUE(recent.seenBefore(id));
|
|
}
|
|
TEST_ASSERT_FALSE(recent.seenBefore("id-0"));
|
|
}
|
|
|
|
void test_recognising_a_repeat_does_not_use_a_place()
|
|
{
|
|
AlexaRecentIds recent;
|
|
const unsigned remembered = AlexaRecentIds::CAPACITY;
|
|
char id[16];
|
|
|
|
TEST_ASSERT_FALSE(recent.seenBefore("id-0"));
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
TEST_ASSERT_TRUE(recent.seenBefore("id-0"));
|
|
}
|
|
for (unsigned n = 1; n < remembered; n++)
|
|
{
|
|
snprintf(id, sizeof(id), "id-%u", n);
|
|
TEST_ASSERT_FALSE(recent.seenBefore(id));
|
|
}
|
|
TEST_ASSERT_TRUE(recent.seenBefore("id-0"));
|
|
}
|
|
|
|
void test_hash_is_fnv_1a_64()
|
|
{
|
|
// Test vectors of the FNV reference code
|
|
TEST_ASSERT_EQUAL_HEX64(0xcbf29ce484222325ULL, AlexaBridgeLogic::hashBytes("", 0));
|
|
TEST_ASSERT_EQUAL_HEX64(0xaf63dc4c8601ec8cULL, AlexaBridgeLogic::hashBytes("a", 1));
|
|
TEST_ASSERT_EQUAL_HEX64(0x85944171f73967e8ULL, AlexaBridgeLogic::hashBytes("foobar", 6));
|
|
|
|
// In pieces, as the fragments of a message are hashed
|
|
uint64_t hash = AlexaBridgeLogic::hashBytes("foo", 3);
|
|
TEST_ASSERT_EQUAL_HEX64(0x85944171f73967e8ULL, AlexaBridgeLogic::hashBytes("bar", 3, hash));
|
|
}
|
|
|
|
void test_directive_without_a_message_id_is_never_a_repeat()
|
|
{
|
|
AlexaRecentIds recent;
|
|
|
|
TEST_ASSERT_FALSE(recent.seenBefore(""));
|
|
TEST_ASSERT_FALSE(recent.seenBefore(""));
|
|
TEST_ASSERT_FALSE(recent.seenBefore(nullptr));
|
|
TEST_ASSERT_FALSE(recent.seenBefore(nullptr));
|
|
}
|
|
|
|
// --- what may be published ---
|
|
|
|
void test_message_within_the_limit_is_measured()
|
|
{
|
|
JsonDocument doc;
|
|
doc["event"]["header"]["name"] = "Response";
|
|
size_t length = 0;
|
|
|
|
ASSERT_RESULT(AlexaSendResult::OK, AlexaBridgeLogic::checkMessage(doc, 2047, &length));
|
|
TEST_ASSERT_EQUAL_UINT(strlen("{\"event\":{\"header\":{\"name\":\"Response\"}}}"), length);
|
|
}
|
|
|
|
void test_message_at_the_limit_is_accepted_and_one_byte_more_is_refused()
|
|
{
|
|
// {"v":"xxx...x"} is 8 bytes and the text
|
|
JsonDocument doc;
|
|
size_t length = 0;
|
|
|
|
doc["v"] = std::string(2047 - 8, 'x');
|
|
ASSERT_RESULT(AlexaSendResult::OK, AlexaBridgeLogic::checkMessage(doc, 2047, &length));
|
|
TEST_ASSERT_EQUAL_UINT(2047, length);
|
|
|
|
doc["v"] = std::string(2048 - 8, 'x');
|
|
ASSERT_RESULT(AlexaSendResult::TOO_LARGE, AlexaBridgeLogic::checkMessage(doc, 2047, &length));
|
|
TEST_ASSERT_EQUAL_UINT(2048, length);
|
|
}
|
|
|
|
void test_empty_document_is_not_published()
|
|
{
|
|
JsonDocument doc;
|
|
size_t length = 99;
|
|
|
|
ASSERT_RESULT(AlexaSendResult::EMPTY, AlexaBridgeLogic::checkMessage(doc, 2047, &length));
|
|
TEST_ASSERT_EQUAL_UINT(0, length);
|
|
}
|
|
|
|
// An allocator that runs dry, as the heap of the board does
|
|
struct ScarceAllocator : ArduinoJson::Allocator
|
|
{
|
|
size_t left;
|
|
explicit ScarceAllocator(size_t bytes) : left(bytes) {}
|
|
void *allocate(size_t size) override
|
|
{
|
|
if (size > left)
|
|
{
|
|
return nullptr;
|
|
}
|
|
left -= size;
|
|
return malloc(size);
|
|
}
|
|
void deallocate(void *pointer) override { free(pointer); }
|
|
void *reallocate(void *pointer, size_t size) override
|
|
{
|
|
if (size > left)
|
|
{
|
|
return nullptr;
|
|
}
|
|
left -= size;
|
|
return realloc(pointer, size);
|
|
}
|
|
};
|
|
|
|
void test_document_that_ran_out_of_memory_is_not_published()
|
|
{
|
|
ScarceAllocator allocator(8192);
|
|
JsonDocument doc(&allocator);
|
|
for (int i = 0; i < 200 && !doc.overflowed(); i++)
|
|
{
|
|
doc["context"]["properties"][i]["value"] = std::string(100, 'x');
|
|
}
|
|
TEST_ASSERT_TRUE(doc.overflowed());
|
|
|
|
size_t length = 99;
|
|
ASSERT_RESULT(AlexaSendResult::TOO_LARGE, AlexaBridgeLogic::checkMessage(doc, 1000000, &length));
|
|
TEST_ASSERT_EQUAL_UINT(0, length); // 0 tells the two reasons for TOO_LARGE apart
|
|
}
|
|
|
|
// --- the limit of a directive and the limit of its answer ---
|
|
|
|
// One property of a report as AlexaStatusMessage::AddProperty writes it
|
|
static void addProperty(JsonArray properties, const char *interfaceName, const char *name, const char *value)
|
|
{
|
|
JsonObject property = properties.add<JsonObject>();
|
|
property["namespace"] = interfaceName;
|
|
property["name"] = name;
|
|
property["value"] = value;
|
|
property["timeOfSample"] = "2026-09-28T13:05:09Z";
|
|
property["uncertaintyInMilliseconds"] = 0;
|
|
}
|
|
|
|
// The Response of a lamp as AlexaStatusMessage builds it (src/AlexaStatusMessage.cpp): the correlationToken of the
|
|
// directive, the health of the endpoint and the power state
|
|
static void buildResponse(JsonDocument &answer, const JsonDocument &received)
|
|
{
|
|
JsonObject header = answer["event"]["header"].to<JsonObject>();
|
|
header["namespace"] = "Alexa";
|
|
header["name"] = "Response";
|
|
header["payloadVersion"] = "3";
|
|
header["messageId"] = "OQpZQ2l8Pr2f9kkS8g6ffwpx7bJgJARngGUEQ"; // 37 characters, as generateMessageId() returns
|
|
header["correlationToken"] = received["header"]["correlationToken"];
|
|
answer["event"]["endpoint"]["endpointId"] = received["endpoint"]["endpointId"];
|
|
answer["event"]["payload"].to<JsonObject>();
|
|
|
|
JsonArray properties = answer["context"]["properties"].to<JsonArray>();
|
|
JsonObject health = properties.add<JsonObject>();
|
|
health["namespace"] = "Alexa.EndpointHealth";
|
|
health["name"] = "connectivity";
|
|
health["value"]["value"] = "OK";
|
|
health["timeOfSample"] = "2026-09-28T13:05:09Z";
|
|
health["uncertaintyInMilliseconds"] = 0;
|
|
addProperty(properties, "Alexa.PowerController", "powerState", "ON");
|
|
}
|
|
|
|
void test_largest_directive_can_be_answered()
|
|
{
|
|
// The correlationToken is what makes a directive large, and the answer repeats it
|
|
std::string withoutToken = directive("TurnOn", ID_1, "");
|
|
std::string largest = directive("TurnOn", ID_1, std::string(ALEX2ESP_MAX_DIRECTIVE - withoutToken.size(), 'T'));
|
|
TEST_ASSERT_EQUAL_UINT(ALEX2ESP_MAX_DIRECTIVE, largest.size());
|
|
|
|
AlexaDirectiveBuffer buffer(ALEX2ESP_MAX_DIRECTIVE, ALEX2ESP_MAX_QUEUED_BYTES);
|
|
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, largest, 536));
|
|
JsonDocument received;
|
|
TEST_ASSERT_TRUE(deserializeJson(received, buffer.data(), buffer.length()) == DeserializationError::Ok);
|
|
buffer.release();
|
|
|
|
JsonDocument answer;
|
|
buildResponse(answer, received);
|
|
size_t length = 0;
|
|
ASSERT_RESULT(AlexaSendResult::OK, AlexaBridgeLogic::checkMessage(answer, ALEX2ESP_MAX_MESSAGE, &length));
|
|
// The answer is the larger of the two: a limit for messages as low as the limit for directives refuses it,
|
|
// after the sketch has switched the lamp
|
|
TEST_ASSERT_GREATER_THAN_UINT(ALEX2ESP_MAX_DIRECTIVE, length);
|
|
ASSERT_RESULT(AlexaSendResult::TOO_LARGE, AlexaBridgeLogic::checkMessage(answer, ALEX2ESP_MAX_DIRECTIVE, &length));
|
|
|
|
// The default leaves room for six properties
|
|
JsonArray properties = answer["context"]["properties"];
|
|
addProperty(properties, "Alexa.BrightnessController", "brightness", "100");
|
|
addProperty(properties, "Alexa.ColorTemperatureController", "colorTemperatureInKelvin", "2700");
|
|
addProperty(properties, "Alexa.ToggleController", "toggleState", "ON");
|
|
addProperty(properties, "Alexa.ToggleController", "toggleState", "OFF");
|
|
TEST_ASSERT_EQUAL_UINT(6, properties.size());
|
|
ASSERT_RESULT(AlexaSendResult::OK, AlexaBridgeLogic::checkMessage(answer, ALEX2ESP_MAX_MESSAGE, &length));
|
|
}
|
|
|
|
// --- topics ---
|
|
|
|
void test_endpoint_is_taken_from_the_directive_topic()
|
|
{
|
|
size_t length = 0;
|
|
const char *topic = "AEXAMPLEROOT/ESP-01/alexaDirective";
|
|
|
|
const char *endpointId = AlexaBridgeLogic::directiveEndpoint(topic, "AEXAMPLEROOT", &length);
|
|
TEST_ASSERT_NOT_NULL(endpointId);
|
|
TEST_ASSERT_EQUAL_UINT(6, length);
|
|
TEST_ASSERT_EQUAL_STRING_LEN("ESP-01", endpointId, length);
|
|
TEST_ASSERT_EQUAL_PTR(topic + strlen("AEXAMPLEROOT/"), endpointId);
|
|
|
|
// An empty root topic (begin() with "") still has the separator
|
|
TEST_ASSERT_NOT_NULL(AlexaBridgeLogic::directiveEndpoint("/ESP-01/alexaDirective", "", &length));
|
|
TEST_ASSERT_EQUAL_UINT(6, length);
|
|
}
|
|
|
|
void test_other_topics_are_not_directive_topics()
|
|
{
|
|
size_t length = 0;
|
|
|
|
// the token topic of the 1.x HTTP fallback, published next to every directive
|
|
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root/ESP-01/alexaDirective_e", "root", &length));
|
|
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root/discover", "root", &length));
|
|
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root/ESP-01/alexaResponce", "root", &length));
|
|
// another root, also one that only starts like ours
|
|
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("other/ESP-01/alexaDirective", "root", &length));
|
|
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("rootless/ESP-01/alexaDirective", "root", &length));
|
|
// no endpoint id, more than one level
|
|
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root//alexaDirective", "root", &length));
|
|
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root/alexaDirective", "root", &length));
|
|
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root/a/b/alexaDirective", "root", &length));
|
|
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("", "root", &length));
|
|
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint(nullptr, "root", &length));
|
|
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root/ESP-01/alexaDirective", nullptr, &length));
|
|
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root/ESP-01/alexaDirective", "root", nullptr));
|
|
}
|
|
|
|
// --- time ---
|
|
|
|
void test_timestamp_is_iso_8601_in_utc()
|
|
{
|
|
char stamp[ALEXA_TIMESTAMP_SIZE];
|
|
|
|
TEST_ASSERT_TRUE(AlexaBridgeLogic::formatTimestamp(0, stamp, sizeof(stamp)));
|
|
TEST_ASSERT_EQUAL_STRING("1970-01-01T00:00:00Z", stamp);
|
|
|
|
TEST_ASSERT_TRUE(AlexaBridgeLogic::formatTimestamp(1709210096, stamp, sizeof(stamp)));
|
|
TEST_ASSERT_EQUAL_STRING("2024-02-29T12:34:56Z", stamp); // a leap day
|
|
|
|
TEST_ASSERT_TRUE(AlexaBridgeLogic::formatTimestamp(1790600709, stamp, sizeof(stamp)));
|
|
TEST_ASSERT_EQUAL_STRING("2026-09-28T13:05:09Z", stamp);
|
|
|
|
TEST_ASSERT_TRUE(AlexaBridgeLogic::formatTimestamp(1798761599, stamp, sizeof(stamp)));
|
|
TEST_ASSERT_EQUAL_STRING("2026-12-31T23:59:59Z", stamp);
|
|
}
|
|
|
|
void test_timestamp_needs_a_buffer_of_its_size()
|
|
{
|
|
char small[ALEXA_TIMESTAMP_SIZE - 1];
|
|
memset(small, 'x', sizeof(small));
|
|
|
|
TEST_ASSERT_FALSE(AlexaBridgeLogic::formatTimestamp(1790600709, small, sizeof(small)));
|
|
TEST_ASSERT_EQUAL_STRING("", small);
|
|
TEST_ASSERT_FALSE(AlexaBridgeLogic::formatTimestamp(1790600709, nullptr, ALEXA_TIMESTAMP_SIZE));
|
|
|
|
// The year 10000 does not fit the format
|
|
char stamp[ALEXA_TIMESTAMP_SIZE + 4];
|
|
TEST_ASSERT_FALSE(AlexaBridgeLogic::formatTimestamp(253402300800LL, stamp, sizeof(stamp)));
|
|
TEST_ASSERT_EQUAL_STRING("", stamp);
|
|
}
|
|
|
|
void test_clock_counts_as_set_from_2024()
|
|
{
|
|
TEST_ASSERT_FALSE(AlexaBridgeLogic::clockIsSet(0));
|
|
TEST_ASSERT_FALSE(AlexaBridgeLogic::clockIsSet(5)); // seconds since boot, before SNTP has answered
|
|
TEST_ASSERT_FALSE(AlexaBridgeLogic::clockIsSet(1704067199)); // 2023-12-31T23:59:59Z
|
|
TEST_ASSERT_TRUE(AlexaBridgeLogic::clockIsSet(1704067200)); // 2024-01-01T00:00:00Z
|
|
TEST_ASSERT_TRUE(AlexaBridgeLogic::clockIsSet(1790600709));
|
|
}
|
|
|
|
int main(int, char **)
|
|
{
|
|
UNITY_BEGIN();
|
|
|
|
RUN_TEST(test_directive_in_one_piece_is_complete);
|
|
RUN_TEST(test_directive_in_fragments_is_reassembled_and_parses);
|
|
RUN_TEST(test_nothing_waits_before_the_last_fragment);
|
|
|
|
RUN_TEST(test_directive_at_the_limit_is_accepted);
|
|
RUN_TEST(test_directive_over_the_limit_is_refused_once);
|
|
RUN_TEST(test_refused_directive_does_not_block_the_next_one);
|
|
RUN_TEST(test_empty_message_is_reported);
|
|
|
|
RUN_TEST(test_directives_wait_in_the_order_they_arrived);
|
|
RUN_TEST(test_directive_beyond_the_places_is_dropped_and_the_others_kept);
|
|
RUN_TEST(test_place_that_becomes_free_while_a_directive_arrives_is_used);
|
|
RUN_TEST(test_waiting_directives_are_limited_in_bytes);
|
|
RUN_TEST(test_empty_queue_takes_the_largest_directive);
|
|
|
|
RUN_TEST(test_repeat_of_a_waiting_directive_is_discarded);
|
|
RUN_TEST(test_repeat_of_a_directive_that_was_read_does_not_hold_up_the_next);
|
|
RUN_TEST(test_group_of_five_with_repeats_in_one_burst);
|
|
RUN_TEST(test_repeat_is_recognised_when_no_place_is_free);
|
|
RUN_TEST(test_directive_that_differs_in_one_byte_is_not_a_repeat);
|
|
RUN_TEST(test_repeat_is_forgotten_after_as_many_directives_as_are_remembered);
|
|
|
|
RUN_TEST(test_directive_without_memory_is_reported_by_its_last_fragment);
|
|
RUN_TEST(test_repeat_without_memory_is_still_a_repeat);
|
|
|
|
RUN_TEST(test_fragment_with_a_gap_drops_the_message);
|
|
RUN_TEST(test_fragment_beyond_the_total_is_refused);
|
|
RUN_TEST(test_fragment_with_another_total_is_refused);
|
|
RUN_TEST(test_fragment_without_a_start_is_refused_once);
|
|
RUN_TEST(test_new_message_replaces_one_that_never_completed);
|
|
RUN_TEST(test_lost_connection_drops_the_arriving_message_only);
|
|
|
|
RUN_TEST(test_repeated_message_id_is_recognised);
|
|
RUN_TEST(test_only_the_last_ids_are_remembered);
|
|
RUN_TEST(test_recognising_a_repeat_does_not_use_a_place);
|
|
RUN_TEST(test_hash_is_fnv_1a_64);
|
|
RUN_TEST(test_directive_without_a_message_id_is_never_a_repeat);
|
|
|
|
RUN_TEST(test_message_within_the_limit_is_measured);
|
|
RUN_TEST(test_message_at_the_limit_is_accepted_and_one_byte_more_is_refused);
|
|
RUN_TEST(test_empty_document_is_not_published);
|
|
RUN_TEST(test_document_that_ran_out_of_memory_is_not_published);
|
|
|
|
RUN_TEST(test_largest_directive_can_be_answered);
|
|
|
|
RUN_TEST(test_endpoint_is_taken_from_the_directive_topic);
|
|
RUN_TEST(test_other_topics_are_not_directive_topics);
|
|
|
|
RUN_TEST(test_timestamp_is_iso_8601_in_utc);
|
|
RUN_TEST(test_timestamp_needs_a_buffer_of_its_size);
|
|
RUN_TEST(test_clock_counts_as_set_from_2024);
|
|
|
|
return UNITY_END();
|
|
}
|