The wait between attempts started at 1 s again when the broker accepted the session, so a session that was closed right after it opened (two boards with one client id) came back every 2 s without end. It now starts again only after a session that lasted 60 s; across shorter ones it keeps doubling. An attempt given up after 30 s prints "the broker did not answer within 30 s", and loss of Wi-Fi prints "Wi-Fi is down, waiting for it" once per loss. setServer(host, port) before begin() names another broker; the host is not copied. basicLight: static RAM 30,616 B (+16), flash 333,701 B (+264); 91 host tests (bridge logic 50). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
995 lines
38 KiB
C++
995 lines
38 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));
|
|
}
|
|
|
|
void test_log_prints_a_topic_without_its_root()
|
|
{
|
|
TEST_ASSERT_EQUAL_STRING("ESP-01/alexaResponce", AlexaBridgeLogic::topicForLog("root/ESP-01/alexaResponce", "root"));
|
|
TEST_ASSERT_EQUAL_STRING("discover", AlexaBridgeLogic::topicForLog("root/discover", "root"));
|
|
TEST_ASSERT_EQUAL_STRING("+/alexaDirective", AlexaBridgeLogic::topicForLog("a/b/+/alexaDirective", "a/b"));
|
|
// An empty root topic (begin() with "") still has the separator
|
|
TEST_ASSERT_EQUAL_STRING("ESP-01/alexaResponce", AlexaBridgeLogic::topicForLog("/ESP-01/alexaResponce", ""));
|
|
}
|
|
|
|
void test_log_never_prints_the_root_topic()
|
|
{
|
|
const char *topics[] = {"root", "root/", "rootless/ESP-01/alexaResponce", "other/root/discover", "", nullptr};
|
|
|
|
for (const char *topic : topics)
|
|
{
|
|
const char *printed = AlexaBridgeLogic::topicForLog(topic, "root");
|
|
TEST_ASSERT_NOT_NULL(printed);
|
|
TEST_ASSERT_NULL(strstr(printed, "root/"));
|
|
TEST_ASSERT_NULL(strstr(printed, "rootless"));
|
|
}
|
|
TEST_ASSERT_EQUAL_STRING("", AlexaBridgeLogic::topicForLog("root/", "root"));
|
|
TEST_ASSERT_NOT_NULL(AlexaBridgeLogic::topicForLog("root/discover", nullptr));
|
|
}
|
|
|
|
// --- reconnect ---
|
|
|
|
void test_wait_doubles_from_a_second_to_a_minute()
|
|
{
|
|
AlexaReconnectBackoff backoff;
|
|
const uint32_t expected[] = {1000, 2000, 4000, 8000, 16000, 32000, 60000, 60000, 60000};
|
|
|
|
for (uint32_t wait : expected)
|
|
{
|
|
TEST_ASSERT_EQUAL_UINT32(wait, backoff.wait());
|
|
backoff.attempt();
|
|
}
|
|
}
|
|
|
|
void test_session_that_lasted_a_minute_starts_the_wait_over()
|
|
{
|
|
AlexaReconnectBackoff backoff;
|
|
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
backoff.attempt();
|
|
}
|
|
TEST_ASSERT_EQUAL_UINT32(60000, backoff.wait());
|
|
|
|
backoff.sessionOpened(100000);
|
|
TEST_ASSERT_EQUAL_UINT32(60000, backoff.wait());
|
|
backoff.sessionEnded(160000);
|
|
TEST_ASSERT_EQUAL_UINT32(1000, backoff.wait());
|
|
backoff.attempt();
|
|
TEST_ASSERT_EQUAL_UINT32(2000, backoff.wait());
|
|
}
|
|
|
|
// A broker that accepts the session and closes it at once, as it does to one of two boards with the same client id
|
|
void test_wait_keeps_doubling_across_sessions_that_end_early()
|
|
{
|
|
AlexaReconnectBackoff backoff;
|
|
const uint32_t expected[] = {1000, 2000, 4000, 8000, 16000, 32000, 60000, 60000};
|
|
uint32_t now = 5000;
|
|
|
|
for (uint32_t wait : expected)
|
|
{
|
|
backoff.sessionEnded(now);
|
|
TEST_ASSERT_EQUAL_UINT32(wait, backoff.wait());
|
|
now += wait;
|
|
backoff.attempt();
|
|
now += 200; // until the broker has accepted the session
|
|
backoff.sessionOpened(now);
|
|
now += 59999;
|
|
}
|
|
}
|
|
|
|
void test_attempt_that_fails_after_a_long_session_does_not_start_the_wait_over()
|
|
{
|
|
AlexaReconnectBackoff backoff;
|
|
|
|
backoff.sessionOpened(1000);
|
|
backoff.sessionEnded(3600000);
|
|
TEST_ASSERT_EQUAL_UINT32(1000, backoff.wait());
|
|
|
|
// The broker is gone: every attempt ends without a session, however long it took
|
|
backoff.attempt();
|
|
backoff.sessionEnded(3700000);
|
|
TEST_ASSERT_EQUAL_UINT32(2000, backoff.wait());
|
|
backoff.attempt();
|
|
backoff.sessionEnded(3800000);
|
|
TEST_ASSERT_EQUAL_UINT32(4000, backoff.wait());
|
|
}
|
|
|
|
void test_length_of_a_session_is_counted_across_the_overflow_of_millis()
|
|
{
|
|
AlexaReconnectBackoff backoff;
|
|
|
|
backoff.attempt();
|
|
backoff.sessionOpened(0xFFFFFF00u); // 256 ms before millis() starts again at 0
|
|
backoff.sessionEnded(59743);
|
|
TEST_ASSERT_EQUAL_UINT32(2000, backoff.wait());
|
|
|
|
backoff.sessionOpened(0xFFFFFF00u);
|
|
backoff.sessionEnded(59744);
|
|
TEST_ASSERT_EQUAL_UINT32(1000, backoff.wait());
|
|
}
|
|
|
|
void test_attempt_is_due_when_the_wait_is_over()
|
|
{
|
|
AlexaReconnectBackoff backoff;
|
|
|
|
TEST_ASSERT_FALSE(backoff.due(5000, 5000));
|
|
TEST_ASSERT_FALSE(backoff.due(5999, 5000));
|
|
TEST_ASSERT_TRUE(backoff.due(6000, 5000));
|
|
|
|
backoff.attempt();
|
|
TEST_ASSERT_FALSE(backoff.due(6999, 5000));
|
|
TEST_ASSERT_TRUE(backoff.due(7000, 5000));
|
|
}
|
|
|
|
void test_wait_is_counted_across_the_overflow_of_millis()
|
|
{
|
|
AlexaReconnectBackoff backoff;
|
|
const uint32_t since = 0xFFFFFF00u; // 256 ms before millis() starts again at 0
|
|
|
|
TEST_ASSERT_FALSE(backoff.due(0xFFFFFFFFu, since));
|
|
TEST_ASSERT_FALSE(backoff.due(743, since));
|
|
TEST_ASSERT_TRUE(backoff.due(744, since));
|
|
}
|
|
|
|
// --- 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_log_prints_a_topic_without_its_root);
|
|
RUN_TEST(test_log_never_prints_the_root_topic);
|
|
|
|
RUN_TEST(test_wait_doubles_from_a_second_to_a_minute);
|
|
RUN_TEST(test_session_that_lasted_a_minute_starts_the_wait_over);
|
|
RUN_TEST(test_wait_keeps_doubling_across_sessions_that_end_early);
|
|
RUN_TEST(test_attempt_that_fails_after_a_long_session_does_not_start_the_wait_over);
|
|
RUN_TEST(test_length_of_a_session_is_counted_across_the_overflow_of_millis);
|
|
RUN_TEST(test_attempt_is_due_when_the_wait_is_over);
|
|
RUN_TEST(test_wait_is_counted_across_the_overflow_of_millis);
|
|
|
|
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();
|
|
}
|