Queue up to eight directives for loop(); drop the repeat of a directive while it arrives

8ea7778 kept 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>
This commit is contained in:
David 2026-09-28 15:57:03 +00:00
parent e48f8580f9
commit 6fe8311a6f
8 changed files with 574 additions and 275 deletions

View file

@ -1,8 +1,10 @@
// Host tests of the bridge logic (src/AlexaBridgeLogic.cpp): what the MQTT callback decides about the fragments a
// broker delivers, which directives are repeats, what may be published, topics and time stamps.
// 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>
@ -26,6 +28,19 @@ static std::string directive(const char *name, const char *messageId,
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)
@ -54,7 +69,7 @@ void tearDown() {}
void test_directive_in_one_piece_is_complete()
{
AlexaDirectiveBuffer buffer(2047);
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()));
@ -73,12 +88,12 @@ void test_directive_in_fragments_is_reassembled_and_parses()
// 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(2047);
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, fragmentSize));
assertWaiting(buffer, turnOn);
}
AlexaDirectiveBuffer buffer(2047);
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
deliver(buffer, turnOn, 100);
JsonDocument parsed;
TEST_ASSERT_TRUE(deserializeJson(parsed, buffer.data(), buffer.length()) == DeserializationError::Ok);
@ -88,7 +103,7 @@ void test_directive_in_fragments_is_reassembled_and_parses()
void test_nothing_waits_before_the_last_fragment()
{
AlexaDirectiveBuffer buffer(2047);
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
std::string turnOn = directive("TurnOn", ID_1);
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOn.data(), 50, 0, turnOn.size()));
@ -100,7 +115,7 @@ void test_nothing_waits_before_the_last_fragment()
void test_directive_at_the_limit_is_accepted()
{
AlexaDirectiveBuffer buffer(2047);
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
std::string atLimit(2047, 'x');
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, atLimit, 536));
@ -109,7 +124,7 @@ void test_directive_at_the_limit_is_accepted()
void test_directive_over_the_limit_is_refused_once()
{
AlexaDirectiveBuffer buffer(2047);
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
std::string tooLarge(2048, 'x');
ASSERT_RESULT(Result::TOO_LARGE, buffer.append(tooLarge.data(), 536, 0, tooLarge.size()));
@ -121,7 +136,7 @@ void test_directive_over_the_limit_is_refused_once()
void test_refused_directive_does_not_block_the_next_one()
{
AlexaDirectiveBuffer buffer(2047);
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
std::string tooLarge(5000, 'x');
std::string turnOn = directive("TurnOn", ID_1);
@ -134,110 +149,271 @@ void test_refused_directive_does_not_block_the_next_one()
void test_empty_message_is_reported()
{
AlexaDirectiveBuffer buffer(2047);
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());
}
// --- one directive at a time ---
// --- directives wait for loop() ---
void test_second_directive_before_loop_is_dropped_and_the_first_kept()
void test_directives_wait_in_the_order_they_arrived()
{
AlexaDirectiveBuffer buffer(2047);
std::string turnOn = directive("TurnOn", ID_1);
std::string turnOff = directive("TurnOff", ID_2);
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
size_t bytes = 0;
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100));
ASSERT_RESULT(Result::BUSY, buffer.append(turnOff.data(), 100, 0, turnOff.size()));
ASSERT_RESULT(Result::IGNORED, buffer.append(turnOff.data() + 100, turnOff.size() - 100, 100, turnOff.size()));
assertWaiting(buffer, turnOn);
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());
// Once loop() has read the first, the buffer takes directives again
buffer.release();
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOff, 100));
assertWaiting(buffer, turnOff);
// 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_second_directive_of_the_same_size_is_dropped()
void test_directive_beyond_the_places_is_dropped_and_the_others_kept()
{
AlexaDirectiveBuffer buffer(2047);
std::string first = directive("TurnOn", ID_1);
std::string second = directive("TurnOn", ID_2); // same length, another messageId
TEST_ASSERT_EQUAL_UINT(first.size(), second.size());
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
for (unsigned n = 0; n < PLACES; n++)
{
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(n), 100));
}
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, first, 100));
ASSERT_RESULT(Result::BUSY, buffer.append(second.data(), second.size(), 0, second.size()));
assertWaiting(buffer, first);
// 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_the_waiting_directive_is_a_duplicate()
void test_repeat_of_a_waiting_directive_is_discarded()
{
AlexaDirectiveBuffer buffer(2047);
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));
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_that_differs_in_a_later_fragment_is_dropped()
void test_repeat_of_a_directive_that_was_read_does_not_hold_up_the_next()
{
AlexaDirectiveBuffer buffer(2047);
std::string first = directive("TurnOn", ID_1);
std::string second = first;
second[second.size() - 10] = '#'; // the same up to its last fragment
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, first, 100));
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(second.data(), 100, 0, second.size()));
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(second.data() + 100, 100, 100, second.size()));
ASSERT_RESULT(Result::BUSY, buffer.append(second.data() + 200, second.size() - 200, 200, second.size()));
assertWaiting(buffer, first);
}
void test_repeat_still_arriving_when_the_first_is_read()
{
AlexaDirectiveBuffer buffer(2047);
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));
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOn.data(), 100, 0, turnOn.size()));
buffer.release(); // loop() ran between two fragments of the repeat
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::INCOMPLETE, buffer.append(turnOn.data() + 100, 100, 100, turnOn.size()));
ASSERT_RESULT(Result::DUPLICATE, buffer.append(turnOn.data() + 200, turnOn.size() - 200, 200, turnOn.size()));
TEST_ASSERT_FALSE(buffer.ready());
// and the buffer is free for the next directive
std::string turnOff = directive("TurnOff", ID_2);
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOff, 100));
assertWaiting(buffer, turnOff);
}
void test_directive_that_starts_like_the_one_just_read_is_not_lost()
void test_group_of_five_with_repeats_in_one_burst()
{
AlexaDirectiveBuffer buffer(2047);
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] = '#';
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::INCOMPLETE, buffer.append(second.data(), 100, 0, second.size()));
buffer.release(); // the first has been read: nothing waits, so the second has a place to go
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(second.data() + 100, 100, 100, second.size()));
ASSERT_RESULT(Result::COMPLETE, buffer.append(second.data() + 200, second.size() - 200, 200, second.size()));
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(2047);
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
std::string turnOn = directive("TurnOn", ID_1);
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOn.data(), 100, 0, turnOn.size()));
@ -251,7 +427,7 @@ void test_fragment_with_a_gap_drops_the_message()
void test_fragment_beyond_the_total_is_refused()
{
AlexaDirectiveBuffer buffer(2047);
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
@ -266,7 +442,7 @@ void test_fragment_beyond_the_total_is_refused()
void test_fragment_with_another_total_is_refused()
{
AlexaDirectiveBuffer buffer(2047);
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
std::string message(300, 'x');
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(message.data(), 100, 0, 200));
@ -276,7 +452,7 @@ void test_fragment_with_another_total_is_refused()
void test_fragment_without_a_start_is_refused_once()
{
AlexaDirectiveBuffer buffer(2047);
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()));
@ -286,18 +462,19 @@ void test_fragment_without_a_start_is_refused_once()
void test_new_message_replaces_one_that_never_completed()
{
AlexaDirectiveBuffer buffer(2047);
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(2047);
AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES);
std::string turnOn = directive("TurnOn", ID_1);
std::string turnOff = directive("TurnOff", ID_2);
@ -307,11 +484,15 @@ void test_lost_connection_drops_the_arriving_message_only()
ASSERT_RESULT(Result::OUT_OF_ORDER, buffer.append(turnOn.data() + 100, 100, 100, turnOn.size()));
TEST_ASSERT_FALSE(buffer.ready());
// A directive waits, its repeat is arriving
// A directive waits, another one is arriving
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOff, 100));
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOff.data(), 100, 0, turnOff.size()));
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 ---
@ -327,36 +508,55 @@ void test_repeated_message_id_is_recognised()
TEST_ASSERT_TRUE(recent.seenBefore(ID_2));
}
void test_only_the_last_four_ids_are_remembered()
void test_only_the_last_ids_are_remembered()
{
AlexaRecentIds recent;
const char *ids[] = {"id-1", "id-2", "id-3", "id-4", "id-5"};
const unsigned remembered = AlexaRecentIds::CAPACITY;
char id[16];
for (const char *id : ids)
for (unsigned n = 0; n <= remembered; n++)
{
snprintf(id, sizeof(id), "id-%u", n);
TEST_ASSERT_FALSE(recent.seenBefore(id));
}
// id-5 took the place of id-1
TEST_ASSERT_TRUE(recent.seenBefore("id-2"));
TEST_ASSERT_TRUE(recent.seenBefore("id-3"));
TEST_ASSERT_TRUE(recent.seenBefore("id-4"));
TEST_ASSERT_TRUE(recent.seenBefore("id-5"));
TEST_ASSERT_FALSE(recent.seenBefore("id-1"));
// 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-1"));
for (int i = 0; i < 10; i++)
TEST_ASSERT_FALSE(recent.seenBefore("id-0"));
for (int i = 0; i < 100; i++)
{
TEST_ASSERT_TRUE(recent.seenBefore("id-1"));
TEST_ASSERT_TRUE(recent.seenBefore("id-0"));
}
TEST_ASSERT_FALSE(recent.seenBefore("id-2"));
TEST_ASSERT_FALSE(recent.seenBefore("id-3"));
TEST_ASSERT_FALSE(recent.seenBefore("id-4"));
TEST_ASSERT_TRUE(recent.seenBefore("id-1"));
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()
@ -489,7 +689,7 @@ void test_largest_directive_can_be_answered()
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);
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);
@ -609,13 +809,21 @@ int main(int, char **)
RUN_TEST(test_refused_directive_does_not_block_the_next_one);
RUN_TEST(test_empty_message_is_reported);
RUN_TEST(test_second_directive_before_loop_is_dropped_and_the_first_kept);
RUN_TEST(test_second_directive_of_the_same_size_is_dropped);
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_the_waiting_directive_is_a_duplicate);
RUN_TEST(test_repeat_that_differs_in_a_later_fragment_is_dropped);
RUN_TEST(test_repeat_still_arriving_when_the_first_is_read);
RUN_TEST(test_directive_that_starts_like_the_one_just_read_is_not_lost);
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);
@ -625,8 +833,9 @@ int main(int, char **)
RUN_TEST(test_lost_connection_drops_the_arriving_message_only);
RUN_TEST(test_repeated_message_id_is_recognised);
RUN_TEST(test_only_the_last_four_ids_are_remembered);
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);