diff --git a/.github/workflows/cpp-ci.yml b/.github/workflows/cpp-ci.yml index ac74652..8dc5bcf 100644 --- a/.github/workflows/cpp-ci.yml +++ b/.github/workflows/cpp-ci.yml @@ -25,6 +25,23 @@ jobs: - name: Check Payload Compile Contract run: python tools/compile_contract/check.py + test_sanitized: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install PlatformIO + run: pip install -U platformio + + - name: Run Native Tests (ASan + UBSan) + run: pio test -e native_sanitized + compile_check: runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 91613bf..c16a3d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed +- Native tests now build with `-std=c++11 -Wall -Wextra -Werror -pedantic` (no warnings needed fixing), + the unnecessary `test_build_src` setting was removed, and a `native_sanitized` (ASan + UBSan) + environment runs in Linux CI. Sanitizers could not be linked on the Windows/Clang host (missing + runtimes), so that job is verified by CI only; AVR/ESP8266 builds were not run locally. +- Documented the native test infrastructure (`test/README.md`, fake adapter, helpers, platform + stubs, compile-contract probes, capacity assumptions) and made loop-based assertions report the + failing index, length, or value via `TEST_ASSERT_*_MESSAGE` and the new `diag()` helper + (`test/support/tinylink/TestDiagnostics.h`). No production or coverage changes. - Reorganized native test suites under `test/suites` by adapter, engine, protocol, and codec responsibility (`test_dir` in `platformio.ini`). The TinyLink suite is split into lifecycle, application, ACK/sequence, receive/mailbox, timeout, overflow/recovery, logging, and telemetry diff --git a/README.md b/README.md index b526e00..4cc40e0 100644 --- a/README.md +++ b/README.md @@ -304,7 +304,7 @@ is recoverable; monitor `connectionState()` or `connected()` to observe it. `examples/`: Ready-to-run duplex, callback, and health-monitoring demos. -`test/`: Native C++ unit tests using Unity. +`test/`: Native C++ unit tests using Unity. Run with `pio test -e native` (built with `-Wall -Wextra -Werror -pedantic`); `pio test -e native_sanitized` adds ASan/UBSan (needs sanitizer runtimes; run in CI). ## 🔄 Migration Guide diff --git a/platformio.ini b/platformio.ini index ce2a640..589434f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -4,14 +4,25 @@ test_dir = test/suites [env:native] platform = native -test_build_src = yes test_framework = unity build_flags = - -std=gnu++11 + -std=c++11 + -Wall + -Wextra + -Werror + -pedantic -I src -I test -I test/support/stubs/arduino -I test/support/stubs/espidf -I test/support/stubs/stm32 -I test/support/stubs/windows - -D TINYLINK_NATIVE_TEST \ No newline at end of file + -D TINYLINK_NATIVE_TEST + +[env:native_sanitized] +extends = env:native +build_flags = + ${env:native.build_flags} + -fsanitize=address,undefined + -fno-omit-frame-pointer + -fno-sanitize-recover=undefined diff --git a/test/README.md b/test/README.md index 2323072..cd3ed2d 100644 --- a/test/README.md +++ b/test/README.md @@ -17,7 +17,7 @@ The `TinyLink.h` suite covers state transitions, ACK correlation, timeouts, malf The tests reuse production codecs, avoid access to private implementation details, and do not modify production code. -Some loop-based tests could improve failure diagnostics by reporting the input value or length that caused an assertion to fail. +Loop-based assertions now report the failing input, index, or length (see "Failure diagnostics" below). `TinyPosixAdapter.h` had no test coverage, despite being explicitly included in the prompt. It is now covered by `test/suites/test_adapters_posix`, which uses Linux pseudo-terminals and runs in the Linux native CI job; on Windows hosts it compiles to a single ignored test. @@ -50,6 +50,37 @@ Test-only support code lives in `test/support`: `tinylink/` (fake adapter, frame On Windows hosts `test_adapters_posix` registers a single ignored test; the 17 PTY tests run only on Linux. +## Test infrastructure reference + +### Shared helpers (`test/support/tinylink`) + +- `FakeAdapter.h`: deterministic in-memory adapter (inbound queue, captured output, manual clock, scripted partial/failed writes, `setOpen`). Input and output are each capped at 8192 bytes; `scriptWrites` holds 8 entries. +- `TinyLinkTestHelpers.h`: `queuePacket`/`queueApplication`/`queueHandshake`/`queueAck` build inbound frames with the production COBS and packet codecs (never hand-encoded); `writtenFrameCount`/`decodeWrittenFrame` decode what the engine wrote. +- `TinyLinkTestFixture.h` (+ `test_engine_tinylink/TinyLinkTestFixture.cpp`): `Payload` (4 bytes), `MaximumPayload` (64 bytes), callback recorders (reset with `resetCallbacks()` in `setUp`), `connectPeer`, and a `TinyLinkStatsAccess` friend used to read stats. +- `TestDiagnostics.h`: `diag(fmt, ...)` formats failure context for `TEST_ASSERT_*_MESSAGE`. + +### Failure diagnostics + +Assertions inside loops must say which input failed. Use the `_MESSAGE` variants with `tinylink_test::diag`, naming the index, length, or value under test, e.g. `TEST_ASSERT_EQUAL_UINT16_MESSAGE(expected, actual, diag("input length=%u", (unsigned)length))`. `diag` returns a single static buffer overwritten on the next call, so use it inline or store the pointer only until the next `diag` call. When a loop makes several assertions about one case, build the context once at the top of the iteration. + +### Platform stubs (`test/support/stubs/`) + +Minimal stand-ins for the Arduino, ESP-IDF UART, STM32 HAL, and Windows serial APIs, added to the include path in `platformio.ini`. They are scripted through file-scope variables and are not SDK replacements: they model only what the adapters call, never real timing, buffering, or errors beyond what a test sets. State is not reset automatically (the Windows stub provides `tinylink_reset_windows_stub()`), so reset it per test. + +### Compile-contract probes (`tools/compile_contract`) + +Unity cannot assert that code fails to compile, so `python tools/compile_contract/check.py [compiler]` (default `g++`, `-std=gnu++11 -fsyntax-only`) compiles each probe: `payload_valid.cpp` must compile; `payload_not_trivially_copyable.cpp` must fail with "must be trivially copyable"; `payload_too_large.cpp` must fail with "exceeds 64 bytes". The script exits non-zero if any case deviates. + +### Strict and sanitized builds + +The `native` environment compiles with `-std=c++11 -Wall -Wextra -Werror -pedantic`. `native_sanitized` extends it with `-fsanitize=address,undefined` and runs in the Linux CI job; it needs sanitizer runtimes unavailable on some hosts (e.g. Windows/Clang). + +### Capacity assumptions + +- Maximum application payload: 64 bytes (`PACKET_MAX_PAYLOAD_SIZE`); packet = 3-byte header + payload + 2-byte checksum = at most 69 bytes (`PACKET_MAX_SIZE`). +- COBS adds one byte per 254 bytes plus one, and a frame ends with a `0x00` delimiter, so encode buffers in helpers are `PACKET_MAX_SIZE + PACKET_MAX_SIZE / 254 + 1`. +- `FakeAdapter` buffers: 8192 bytes each way; tests that queue many frames should `clearInput()`/`clearOutput()`. + ## Prompts and pull requests All three prompts were run sequentially in one GitHub Copilot session. Each prompt continued from the repository state produced by the previous pull request. diff --git a/test/suites/test_adapters_posix/test_main.cpp b/test/suites/test_adapters_posix/test_main.cpp index fdb5f43..05ae3a7 100644 --- a/test/suites/test_adapters_posix/test_main.cpp +++ b/test/suites/test_adapters_posix/test_main.cpp @@ -36,6 +36,8 @@ int main() #include #include +#include "support/tinylink/TestDiagnostics.h" + #include #include #include @@ -153,11 +155,15 @@ namespace { }; for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); ++i) { TinyPosixAdapter adapter(paths[i]); - TEST_ASSERT_FALSE(adapter.isOpen()); - TEST_ASSERT_EQUAL_INT(0, adapter.available()); - TEST_ASSERT_EQUAL_INT(-1, adapter.read()); - TEST_ASSERT_EQUAL_INT(WriteStatus::Failed, statusOf(adapter.write(0x41))); - TEST_ASSERT_EQUAL_INT(before, lowestFreeDescriptor()); + const char* context = tinylink_test::diag( + "unopenable path index=%u path=%s", + (unsigned)i, paths[i] ? paths[i] : "(null)"); + TEST_ASSERT_FALSE_MESSAGE(adapter.isOpen(), context); + TEST_ASSERT_EQUAL_INT_MESSAGE(0, adapter.available(), context); + TEST_ASSERT_EQUAL_INT_MESSAGE(-1, adapter.read(), context); + TEST_ASSERT_EQUAL_INT_MESSAGE( + WriteStatus::Failed, statusOf(adapter.write(0x41)), context); + TEST_ASSERT_EQUAL_INT_MESSAGE(before, lowestFreeDescriptor(), context); } } @@ -282,9 +288,14 @@ namespace { TEST_ASSERT_EQUAL_INT(static_cast(sizeof(input)), adapter.available()); for (size_t i = 0; i < sizeof(input); ++i) { - TEST_ASSERT_EQUAL_INT(input[i], adapter.read()); - TEST_ASSERT_EQUAL_INT( - static_cast(sizeof(input) - i - 1), adapter.available()); + TEST_ASSERT_EQUAL_INT_MESSAGE( + input[i], adapter.read(), + tinylink_test::diag("byte index=%u of %u", + (unsigned)i, (unsigned)sizeof(input))); + TEST_ASSERT_EQUAL_INT_MESSAGE( + static_cast(sizeof(input) - i - 1), adapter.available(), + tinylink_test::diag("available() after reading byte index=%u of %u", + (unsigned)i, (unsigned)sizeof(input))); } TEST_ASSERT_EQUAL_INT(-1, adapter.read()); } diff --git a/test/suites/test_engine_tinylink/ack_sequence_tests.cpp b/test/suites/test_engine_tinylink/ack_sequence_tests.cpp index 40625d6..ce74790 100644 --- a/test/suites/test_engine_tinylink/ack_sequence_tests.cpp +++ b/test/suites/test_engine_tinylink/ack_sequence_tests.cpp @@ -55,17 +55,22 @@ void testApplicationSequenceNumbersWrapAfterAllByteValues() const Payload payload = payloadWith(0x10); for (uint16_t value = 0; value <= 0xFFu; ++value) { - TEST_ASSERT_TRUE(link.sendData(0x22, payload)); + const char* context = tinylink_test::diag( + "sequence wrap iteration=%u (expected sequence 0x%02X)", + (unsigned)value, (unsigned)(value & 0xFFu)); + TEST_ASSERT_TRUE_MESSAGE(link.sendData(0x22, payload), context); const size_t frame = tinylink_test::writtenFrameCount(adapter) - 1; uint8_t type = 0; uint8_t sequence = 0; uint8_t bytes[sizeof(Payload)] = {}; size_t length = 0; - TEST_ASSERT_TRUE(tinylink_test::decodeWrittenFrame( - adapter, frame, type, sequence, bytes, sizeof(bytes), length)); - TEST_ASSERT_EQUAL_UINT8(static_cast(value), sequence); - TEST_ASSERT_TRUE(tinylink_test::queueAck( - adapter, sequence, tinylink::AckResult::Ok)); + TEST_ASSERT_TRUE_MESSAGE(tinylink_test::decodeWrittenFrame( + adapter, frame, type, sequence, bytes, sizeof(bytes), length), + context); + TEST_ASSERT_EQUAL_UINT8_MESSAGE( + static_cast(value), sequence, context); + TEST_ASSERT_TRUE_MESSAGE(tinylink_test::queueAck( + adapter, sequence, tinylink::AckResult::Ok), context); link.update(); } diff --git a/test/suites/test_engine_tinylink/application_tests.cpp b/test/suites/test_engine_tinylink/application_tests.cpp index a7b99a2..0bcd533 100644 --- a/test/suites/test_engine_tinylink/application_tests.cpp +++ b/test/suites/test_engine_tinylink/application_tests.cpp @@ -33,7 +33,10 @@ void testReservedApplicationTypesAreRejectedWithoutConsumingSequence() }; for (size_t i = 0; i < sizeof(reserved); ++i) { - TEST_ASSERT_FALSE(link.sendData(reserved[i], payload)); + TEST_ASSERT_FALSE_MESSAGE( + link.sendData(reserved[i], payload), + tinylink_test::diag("reserved type 0x%02X ('%c') accepted by sendData", + (unsigned)reserved[i], (char)reserved[i])); } TEST_ASSERT_EQUAL_UINT32(0, tinylink_test::writtenFrameCount(adapter)); @@ -69,7 +72,9 @@ void testMaximumSupportedApplicationPayloadCanBeSentAndReceived() TEST_ASSERT_TRUE(tinylink_test::decodeWrittenFrame( adapter, 0, type, sequence, decoded, sizeof(decoded), length)); TEST_ASSERT_EQUAL_UINT32(sizeof(payload), length); - TEST_ASSERT_EQUAL_UINT8_ARRAY(payload.bytes, decoded, sizeof(payload.bytes)); + TEST_ASSERT_EQUAL_UINT8_ARRAY_MESSAGE( + payload.bytes, decoded, sizeof(payload.bytes), + "maximum payload bytes differ after send"); TEST_ASSERT_TRUE(tinylink_test::queueAck( adapter, sequence, tinylink::AckResult::Ok)); diff --git a/test/suites/test_internal_codec_cobs/test_main.cpp b/test/suites/test_internal_codec_cobs/test_main.cpp index 828e4a7..813841e 100644 --- a/test/suites/test_internal_codec_cobs/test_main.cpp +++ b/test/suites/test_internal_codec_cobs/test_main.cpp @@ -4,9 +4,12 @@ #include #include "internal/codec/CobsCodec.h" +#include "support/tinylink/TestDiagnostics.h" namespace { + using tinylink_test::diag; + void testEncodedSizeIncludesExactBlockOverhead() { const size_t lengths[] = { 0, 1, 253, 254, 255, 508, 509 }; @@ -14,11 +17,14 @@ namespace { for (size_t i = 0; i < sizeof(lengths) / sizeof(lengths[0]); ++i) { size_t required = 999; - TEST_ASSERT_EQUAL( + const char* context = diag( + "plain length=%u index=%u", (unsigned)lengths[i], (unsigned)i); + TEST_ASSERT_EQUAL_MESSAGE( static_cast(tinylink::codec::CobsStatus::Success), static_cast(tinylink::codec::cobs_encoded_size( - lengths[i], required))); - TEST_ASSERT_EQUAL_UINT32(expected[i], required); + lengths[i], required)), + context); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(expected[i], required, context); } } @@ -90,32 +96,38 @@ namespace { for (size_t i = 0; i < sizeof(lengths) / sizeof(lengths[0]); ++i) { const size_t length = lengths[i]; + const char* context = diag( + "nonzero run length=%u index=%u", (unsigned)length, (unsigned)i); size_t required = 0; - TEST_ASSERT_EQUAL( + TEST_ASSERT_EQUAL_MESSAGE( static_cast(tinylink::codec::CobsStatus::Success), static_cast(tinylink::codec::cobs_encoded_size( - length, required))); + length, required)), + context); const tinylink::codec::CobsResult encodedResult = tinylink::codec::cobs_encode( source, length, encoded, required); - TEST_ASSERT_EQUAL( + TEST_ASSERT_EQUAL_MESSAGE( static_cast(tinylink::codec::CobsStatus::Success), - static_cast(encodedResult.status)); - TEST_ASSERT_EQUAL_UINT32(required, encodedResult.bytesWritten); - TEST_ASSERT_EQUAL_UINT8( + static_cast(encodedResult.status), + context); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(required, encodedResult.bytesWritten, context); + TEST_ASSERT_EQUAL_UINT8_MESSAGE( length < 254u ? static_cast(length + 1u) : 0xFFu, - encoded[0]); + encoded[0], + context); const tinylink::codec::CobsResult decodedResult = tinylink::codec::cobs_decode( encoded, encodedResult.bytesWritten, decoded, length); - TEST_ASSERT_EQUAL( + TEST_ASSERT_EQUAL_MESSAGE( static_cast(tinylink::codec::CobsStatus::Success), - static_cast(decodedResult.status)); - TEST_ASSERT_EQUAL_UINT32(length, decodedResult.bytesWritten); - TEST_ASSERT_EQUAL_UINT8_ARRAY(source, decoded, length); + static_cast(decodedResult.status), + context); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(length, decodedResult.bytesWritten, context); + TEST_ASSERT_EQUAL_UINT8_ARRAY_MESSAGE(source, decoded, length, context); } } @@ -207,31 +219,37 @@ namespace { } for (size_t length = 0; length <= sizeof(source); ++length) { + const char* context = diag( + "deterministic pattern plain length=%u of %u", + (unsigned)length, (unsigned)sizeof(source)); const tinylink::codec::CobsResult enc = tinylink::codec::cobs_encode( length == 0 ? nullptr : source, length, encoded, sizeof(encoded)); - TEST_ASSERT_EQUAL( + TEST_ASSERT_EQUAL_MESSAGE( static_cast(tinylink::codec::CobsStatus::Success), - static_cast(enc.status)); + static_cast(enc.status), + context); size_t measured = 0; - TEST_ASSERT_EQUAL( + TEST_ASSERT_EQUAL_MESSAGE( static_cast(tinylink::codec::CobsStatus::Success), static_cast(tinylink::codec::cobs_decoded_size( - encoded, enc.bytesWritten, measured))); - TEST_ASSERT_EQUAL_UINT32(length, measured); + encoded, enc.bytesWritten, measured)), + context); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(length, measured, context); const tinylink::codec::CobsResult dec = tinylink::codec::cobs_decode( encoded, enc.bytesWritten, length == 0 ? nullptr : decoded, sizeof(decoded)); - TEST_ASSERT_EQUAL( + TEST_ASSERT_EQUAL_MESSAGE( static_cast(tinylink::codec::CobsStatus::Success), - static_cast(dec.status)); - TEST_ASSERT_EQUAL_UINT32(length, dec.bytesWritten); + static_cast(dec.status), + context); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(length, dec.bytesWritten, context); if (length > 0) { - TEST_ASSERT_EQUAL_UINT8_ARRAY(source, decoded, length); + TEST_ASSERT_EQUAL_UINT8_ARRAY_MESSAGE(source, decoded, length, context); } } } diff --git a/test/suites/test_internal_codec_fletcher16/test_main.cpp b/test/suites/test_internal_codec_fletcher16/test_main.cpp index e21dced..5747369 100644 --- a/test/suites/test_internal_codec_fletcher16/test_main.cpp +++ b/test/suites/test_internal_codec_fletcher16/test_main.cpp @@ -3,6 +3,7 @@ #include #include "internal/codec/Fletcher16.h" +#include "support/tinylink/TestDiagnostics.h" extern "C" void setUp(void) { @@ -78,9 +79,10 @@ namespace { const uint16_t expected = static_cast( (reduced << 8) | reduced); - TEST_ASSERT_EQUAL_UINT16( + TEST_ASSERT_EQUAL_UINT16_MESSAGE( expected, - tinylink::codec::fletcher16(&byte, 1)); + tinylink::codec::fletcher16(&byte, 1), + tinylink_test::diag("single byte value=0x%02X", (unsigned)value)); } } @@ -95,9 +97,12 @@ namespace { for (size_t i = 0; i < sizeof(lengths) / sizeof(lengths[0]); ++i) { const size_t length = lengths[i]; - TEST_ASSERT_EQUAL_UINT16( + TEST_ASSERT_EQUAL_UINT16_MESSAGE( referenceFletcher16(data, length), - tinylink::codec::fletcher16(data, length)); + tinylink::codec::fletcher16(data, length), + tinylink_test::diag( + "reduction boundary input length=%u index=%u", + (unsigned)length, (unsigned)i)); } } @@ -114,12 +119,14 @@ namespace { for (size_t i = 0; i < sizeof(lengths) / sizeof(lengths[0]); ++i) { const size_t length = lengths[i]; - TEST_ASSERT_EQUAL_UINT16( + TEST_ASSERT_EQUAL_UINT16_MESSAGE( referenceFletcher16(zeroes, length), - tinylink::codec::fletcher16(zeroes, length)); - TEST_ASSERT_EQUAL_UINT16( + tinylink::codec::fletcher16(zeroes, length), + tinylink_test::diag("all-zero input length=%u", (unsigned)length)); + TEST_ASSERT_EQUAL_UINT16_MESSAGE( referenceFletcher16(maximumBytes, length), - tinylink::codec::fletcher16(maximumBytes, length)); + tinylink::codec::fletcher16(maximumBytes, length), + tinylink_test::diag("all-0xFF input length=%u", (unsigned)length)); } } @@ -134,14 +141,16 @@ namespace { } for (size_t length = 0; length <= sizeof(data); ++length) { - TEST_ASSERT_EQUAL_UINT16( + TEST_ASSERT_EQUAL_UINT16_MESSAGE( referenceFletcher16(data, length), - tinylink::codec::fletcher16(data, length)); - } - } - -} // namespace - + tinylink::codec::fletcher16(data, length), + tinylink_test::diag( + "LCG pattern (seed 0xC001D00D) input length=%u of %u", + (unsigned)length, (unsigned)sizeof(data))); + } + } + + } // namespace int main() { UNITY_BEGIN(); diff --git a/test/suites/test_internal_codec_packet/test_main.cpp b/test/suites/test_internal_codec_packet/test_main.cpp index f97d2da..a11ffa6 100644 --- a/test/suites/test_internal_codec_packet/test_main.cpp +++ b/test/suites/test_internal_codec_packet/test_main.cpp @@ -4,9 +4,12 @@ #include #include "internal/codec/Packet.h" +#include "support/tinylink/TestDiagnostics.h" namespace { + using tinylink_test::diag; + void testPacketSizeCoversEmptyMaximumAndTooLarge() { size_t size = 999; @@ -57,16 +60,21 @@ namespace { for (size_t length = 0; length <= sizeof(payload); ++length) { const uint8_t* input = length == 0 ? nullptr : payload; + const char* context = diag( + "packet payload length=%u of max %u", + (unsigned)length, (unsigned)sizeof(payload)); const tinylink::packet::PacketResult packed = tinylink::packet::pack( 0xF2u, 0xD6u, input, length, packet, sizeof(packet)); - TEST_ASSERT_EQUAL( + TEST_ASSERT_EQUAL_MESSAGE( static_cast(tinylink::packet::PacketStatus::Success), - static_cast(packed.status)); - TEST_ASSERT_EQUAL_UINT32( + static_cast(packed.status), + context); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( tinylink::packet::PACKET_HEADER_SIZE + length + tinylink::packet::PACKET_CHECKSUM_SIZE, - packed.length); + packed.length, + context); uint8_t type = 0; uint8_t sequence = 0; @@ -75,14 +83,15 @@ namespace { tinylink::packet::unpack( packet, packed.length, &type, &sequence, decoded, sizeof(decoded), &decodedLength); - TEST_ASSERT_EQUAL( + TEST_ASSERT_EQUAL_MESSAGE( static_cast(tinylink::packet::PacketStatus::Success), - static_cast(unpacked.status)); - TEST_ASSERT_EQUAL_UINT8(0xF2u, type); - TEST_ASSERT_EQUAL_UINT8(0xD6u, sequence); - TEST_ASSERT_EQUAL_UINT32(length, decodedLength); + static_cast(unpacked.status), + context); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(0xF2u, type, context); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(0xD6u, sequence, context); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(length, decodedLength, context); if (length > 0) { - TEST_ASSERT_EQUAL_UINT8_ARRAY(payload, decoded, length); + TEST_ASSERT_EQUAL_UINT8_ARRAY_MESSAGE(payload, decoded, length, context); } } } @@ -272,11 +281,14 @@ namespace { uint8_t corrupted[tinylink::packet::PACKET_MAX_SIZE] = {}; memcpy(corrupted, packet, packed.length); corrupted[protectedByteOffsets[i]] ^= 0x01u; - TEST_ASSERT_EQUAL( + TEST_ASSERT_EQUAL_MESSAGE( static_cast(tinylink::packet::PacketStatus::ChecksumMismatch), static_cast(tinylink::packet::unpack( corrupted, packed.length, nullptr, nullptr, - nullptr, 0, nullptr).status)); + nullptr, 0, nullptr).status), + tinylink_test::diag( + "flipped bit 0 of protected byte offset=%u in %u-byte packet", + (unsigned)protectedByteOffsets[i], (unsigned)packed.length)); } uint8_t corruptedChecksum[tinylink::packet::PACKET_MAX_SIZE] = {}; diff --git a/test/suites/test_protocol_messages/test_main.cpp b/test/suites/test_protocol_messages/test_main.cpp index 8efacc2..afde61b 100644 --- a/test/suites/test_protocol_messages/test_main.cpp +++ b/test/suites/test_protocol_messages/test_main.cpp @@ -11,6 +11,7 @@ #include "protocol/LogLevel.h" #include "protocol/MessageType.h" #include "protocol/State.h" +#include "support/tinylink/TestDiagnostics.h" namespace { @@ -26,10 +27,17 @@ namespace { for (size_t i = 0; i < 3; ++i) { tinylink::AckResult decoded = tinylink::AckResult::Busy; - TEST_ASSERT_EQUAL_UINT8(wire[i], tinylink::ack_result_to_wire(results[i])); - TEST_ASSERT_TRUE(tinylink::ack_result_from_wire(wire[i], decoded)); - TEST_ASSERT_EQUAL(static_cast(results[i]), static_cast(decoded)); - TEST_ASSERT_EQUAL_STRING(names[i], tinylink::ack_result_to_string(results[i])); + const char* context = tinylink_test::diag( + "ack result=%s wire=0x%02X index=%u", + names[i], (unsigned)wire[i], (unsigned)i); + TEST_ASSERT_EQUAL_UINT8_MESSAGE( + wire[i], tinylink::ack_result_to_wire(results[i]), context); + TEST_ASSERT_TRUE_MESSAGE( + tinylink::ack_result_from_wire(wire[i], decoded), context); + TEST_ASSERT_EQUAL_MESSAGE( + static_cast(results[i]), static_cast(decoded), context); + TEST_ASSERT_EQUAL_STRING_MESSAGE( + names[i], tinylink::ack_result_to_string(results[i]), context); } tinylink::AckResult unchanged = tinylink::AckResult::Busy; @@ -53,18 +61,23 @@ namespace { for (size_t i = 0; i < 3; ++i) { uint8_t output[tinylink::ACK_MESSAGE_WIRE_SIZE] = { 0xA5u }; - TEST_ASSERT_EQUAL( + const char* context = tinylink_test::diag( + "ack round trip wire=0x%02X index=%u", (unsigned)wire[i], (unsigned)i); + TEST_ASSERT_EQUAL_MESSAGE( static_cast(tinylink::AckMessageCodecStatus::Success), static_cast(tinylink::encode_ack_message( - results[i], output, sizeof(output)))); - TEST_ASSERT_EQUAL_UINT8(wire[i], output[0]); + results[i], output, sizeof(output))), + context); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(wire[i], output[0], context); tinylink::AckResult decoded = tinylink::AckResult::Unknown; - TEST_ASSERT_EQUAL( + TEST_ASSERT_EQUAL_MESSAGE( static_cast(tinylink::AckMessageCodecStatus::Success), static_cast(tinylink::decode_ack_message( - output, sizeof(output), decoded))); - TEST_ASSERT_EQUAL(static_cast(results[i]), static_cast(decoded)); + output, sizeof(output), decoded)), + context); + TEST_ASSERT_EQUAL_MESSAGE( + static_cast(results[i]), static_cast(decoded), context); } } @@ -141,22 +154,30 @@ namespace { const tinylink::HandshakeMessage source = { kinds[i], TINYLINK_PROTOCOL_VERSION }; + const char* context = tinylink_test::diag( + "handshake kind=%u index=%u", + (unsigned)static_cast(kinds[i]), (unsigned)i); uint8_t output[tinylink::HANDSHAKE_MESSAGE_WIRE_SIZE] = { 0xA5u, 0xA6u }; - TEST_ASSERT_EQUAL( + TEST_ASSERT_EQUAL_MESSAGE( static_cast(tinylink::HandshakeMessageCodecStatus::Success), static_cast(tinylink::encode_handshake_message( - source, output, sizeof(output)))); - TEST_ASSERT_EQUAL_UINT8(static_cast(kinds[i]), output[0]); - TEST_ASSERT_EQUAL_UINT8(TINYLINK_PROTOCOL_VERSION, output[1]); + source, output, sizeof(output))), + context); + TEST_ASSERT_EQUAL_UINT8_MESSAGE( + static_cast(kinds[i]), output[0], context); + TEST_ASSERT_EQUAL_UINT8_MESSAGE( + TINYLINK_PROTOCOL_VERSION, output[1], context); tinylink::HandshakeMessage decoded = {}; - TEST_ASSERT_EQUAL( + TEST_ASSERT_EQUAL_MESSAGE( static_cast(tinylink::HandshakeMessageCodecStatus::Success), static_cast(tinylink::decode_handshake_message( - output, sizeof(output), decoded))); - TEST_ASSERT_EQUAL( - static_cast(source.kind), static_cast(decoded.kind)); - TEST_ASSERT_EQUAL_UINT8(source.protocolVersion, decoded.protocolVersion); + output, sizeof(output), decoded)), + context); + TEST_ASSERT_EQUAL_MESSAGE( + static_cast(source.kind), static_cast(decoded.kind), context); + TEST_ASSERT_EQUAL_UINT8_MESSAGE( + source.protocolVersion, decoded.protocolVersion, context); } } @@ -222,7 +243,10 @@ namespace { tinylink::set_log_message_text(message, nullptr); TEST_ASSERT_EQUAL_STRING("", message.text); for (size_t i = 0; i < sizeof(message.text); ++i) { - TEST_ASSERT_EQUAL_UINT8(0, message.text[i]); + TEST_ASSERT_EQUAL_UINT8_MESSAGE( + 0, message.text[i], + tinylink_test::diag("text byte not cleared at offset=%u of %u", + (unsigned)i, (unsigned)sizeof(message.text))); } tinylink::set_log_message_text(message, "short"); @@ -260,7 +284,10 @@ namespace { }; TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedPrefix, output, sizeof(expectedPrefix)); for (size_t i = 12; i < sizeof(output); ++i) { - TEST_ASSERT_EQUAL_UINT8(0, output[i]); + TEST_ASSERT_EQUAL_UINT8_MESSAGE( + 0, output[i], + tinylink_test::diag("log padding byte nonzero at wire offset=%u of %u", + (unsigned)i, (unsigned)sizeof(output))); } tinylink::LogMessage decoded = {}; @@ -304,7 +331,10 @@ namespace { static_cast(tinylink::LogMessageCodecStatus::UnterminatedText), static_cast(tinylink::encode_log_message(message, output, sizeof(output)))); for (size_t i = 0; i < sizeof(output); ++i) { - TEST_ASSERT_EQUAL_UINT8(0xA5u, output[i]); + TEST_ASSERT_EQUAL_UINT8_MESSAGE( + 0xA5u, output[i], + tinylink_test::diag("output mutated after failed encode at offset=%u of %u", + (unsigned)i, (unsigned)sizeof(output))); } uint8_t invalid[tinylink::LOG_MESSAGE_WIRE_SIZE] = {}; @@ -351,16 +381,23 @@ namespace { for (uint8_t i = 0; i < 5; ++i) { tinylink::LogLevel decoded = tinylink::LogLevel::ERROR; - TEST_ASSERT_TRUE(tinylink::is_valid_log_level(i)); - TEST_ASSERT_TRUE(tinylink::is_valid_log_level(levels[i])); - TEST_ASSERT_EQUAL_UINT8(i, tinylink::log_level_to_wire(levels[i])); - TEST_ASSERT_TRUE(tinylink::log_level_from_wire(i, decoded)); - TEST_ASSERT_EQUAL(static_cast(levels[i]), static_cast(decoded)); - TEST_ASSERT_EQUAL_STRING(names[i], tinylink::log_level_to_string(levels[i])); + const char* context = tinylink_test::diag( + "log level=%s wire=%u", names[i], (unsigned)i); + TEST_ASSERT_TRUE_MESSAGE(tinylink::is_valid_log_level(i), context); + TEST_ASSERT_TRUE_MESSAGE(tinylink::is_valid_log_level(levels[i]), context); + TEST_ASSERT_EQUAL_UINT8_MESSAGE( + i, tinylink::log_level_to_wire(levels[i]), context); + TEST_ASSERT_TRUE_MESSAGE(tinylink::log_level_from_wire(i, decoded), context); + TEST_ASSERT_EQUAL_MESSAGE( + static_cast(levels[i]), static_cast(decoded), context); + TEST_ASSERT_EQUAL_STRING_MESSAGE( + names[i], tinylink::log_level_to_string(levels[i]), context); for (uint8_t minimum = 0; minimum < 5; ++minimum) { - TEST_ASSERT_EQUAL( + TEST_ASSERT_EQUAL_MESSAGE( i >= minimum, - tinylink::log_level_meets_threshold(levels[i], levels[minimum])); + tinylink::log_level_meets_threshold(levels[i], levels[minimum]), + tinylink_test::diag("threshold level=%s minimum=%s", + names[i], names[minimum])); } } @@ -391,12 +428,18 @@ namespace { for (size_t i = 0; i < 3; ++i) { tinylink::MessageType decoded = tinylink::MessageType::Log; - TEST_ASSERT_EQUAL_UINT8(wire[i], tinylink::message_type_to_wire(types[i])); - TEST_ASSERT_TRUE(tinylink::is_reserved_message_type(wire[i])); - TEST_ASSERT_TRUE(tinylink::is_valid_message_type(types[i])); - TEST_ASSERT_TRUE(tinylink::message_type_from_wire(wire[i], decoded)); - TEST_ASSERT_EQUAL(static_cast(types[i]), static_cast(decoded)); - TEST_ASSERT_EQUAL_STRING(names[i], tinylink::message_type_to_string(types[i])); + const char* context = tinylink_test::diag( + "message type=%s wire='%c' index=%u", names[i], (char)wire[i], (unsigned)i); + TEST_ASSERT_EQUAL_UINT8_MESSAGE( + wire[i], tinylink::message_type_to_wire(types[i]), context); + TEST_ASSERT_TRUE_MESSAGE(tinylink::is_reserved_message_type(wire[i]), context); + TEST_ASSERT_TRUE_MESSAGE(tinylink::is_valid_message_type(types[i]), context); + TEST_ASSERT_TRUE_MESSAGE( + tinylink::message_type_from_wire(wire[i], decoded), context); + TEST_ASSERT_EQUAL_MESSAGE( + static_cast(types[i]), static_cast(decoded), context); + TEST_ASSERT_EQUAL_STRING_MESSAGE( + names[i], tinylink::message_type_to_string(types[i]), context); } tinylink::MessageType unchanged = tinylink::MessageType::Ack; @@ -426,13 +469,16 @@ namespace { }; for (size_t i = 0; i < 5; ++i) { - TEST_ASSERT_EQUAL_STRING( - names[i], tinylink::connection_state_to_string(states[i])); - TEST_ASSERT_EQUAL(i == 2, tinylink::is_connected(states[i])); - TEST_ASSERT_FALSE(tinylink::can_send( - states[i], tinylink::TransmitState::AwaitingAck)); - TEST_ASSERT_EQUAL(i == 2, tinylink::can_send( - states[i], tinylink::TransmitState::Ready)); + const char* context = tinylink_test::diag( + "connection state=%s index=%u", names[i], (unsigned)i); + TEST_ASSERT_EQUAL_STRING_MESSAGE( + names[i], tinylink::connection_state_to_string(states[i]), context); + TEST_ASSERT_EQUAL_MESSAGE(i == 2, tinylink::is_connected(states[i]), context); + TEST_ASSERT_FALSE_MESSAGE( + tinylink::can_send(states[i], tinylink::TransmitState::AwaitingAck), + context); + TEST_ASSERT_EQUAL_MESSAGE(i == 2, tinylink::can_send( + states[i], tinylink::TransmitState::Ready), context); } TEST_ASSERT_EQUAL_STRING( "Invalid", diff --git a/test/suites/test_protocol_stats/test_main.cpp b/test/suites/test_protocol_stats/test_main.cpp index 7a14073..4e47425 100644 --- a/test/suites/test_protocol_stats/test_main.cpp +++ b/test/suites/test_protocol_stats/test_main.cpp @@ -5,9 +5,12 @@ #include "protocol/Stats.h" #include "protocol/TelemetryEvent.h" +#include "support/tinylink/TestDiagnostics.h" namespace { + using tinylink_test::diag; + struct EventCounter { tinylink::TelemetryEvent event; const char* name; @@ -49,16 +52,27 @@ namespace { { tinylink::Stats stats; for (size_t i = 0; i < sizeof(eventCounters) / sizeof(eventCounters[0]); ++i) { - TEST_ASSERT_EQUAL_UINT8(i, static_cast(eventCounters[i].event)); - TEST_ASSERT_EQUAL_STRING( + TEST_ASSERT_EQUAL_UINT8_MESSAGE( + i, static_cast(eventCounters[i].event), + diag("event=%s index=%u", eventCounters[i].name, (unsigned)i)); + TEST_ASSERT_EQUAL_STRING_MESSAGE( eventCounters[i].name, - tinylink::telemetry_event_to_string(eventCounters[i].event)); - TEST_ASSERT_TRUE(stats.increment(eventCounters[i].event)); + tinylink::telemetry_event_to_string(eventCounters[i].event), + diag("event index=%u", (unsigned)i)); + TEST_ASSERT_TRUE_MESSAGE( + stats.increment(eventCounters[i].event), + diag("increment rejected event=%s index=%u", + eventCounters[i].name, (unsigned)i)); } for (size_t i = 0; i < sizeof(eventCounters) / sizeof(eventCounters[0]); ++i) { - TEST_ASSERT_EQUAL_UINT32(1, stats.countFor(eventCounters[i].event)); - TEST_ASSERT_EQUAL_UINT32(1, stats.*(eventCounters[i].counter)); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 1, stats.countFor(eventCounters[i].event), + diag("countFor event=%s index=%u", eventCounters[i].name, (unsigned)i)); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 1, stats.*(eventCounters[i].counter), + diag("member counter event=%s index=%u", + eventCounters[i].name, (unsigned)i)); } } @@ -78,7 +92,10 @@ namespace { { tinylink::Stats live; for (size_t i = 0; i < sizeof(eventCounters) / sizeof(eventCounters[0]); ++i) { - TEST_ASSERT_EQUAL_UINT32(0, live.countFor(eventCounters[i].event)); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 0, live.countFor(eventCounters[i].event), + diag("initial count event=%s index=%u", + eventCounters[i].name, (unsigned)i)); } live.increment(tinylink::TelemetryEvent::PacketReceived); @@ -104,12 +121,18 @@ namespace { left.merge(right); for (size_t i = 0; i < sizeof(eventCounters) / sizeof(eventCounters[0]); ++i) { - TEST_ASSERT_EQUAL_UINT32(8, left.countFor(eventCounters[i].event)); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 8, left.countFor(eventCounters[i].event), + diag("merge 3+5 event=%s index=%u", + eventCounters[i].name, (unsigned)i)); } left.merge(left); for (size_t i = 0; i < sizeof(eventCounters) / sizeof(eventCounters[0]); ++i) { - TEST_ASSERT_EQUAL_UINT32(16, left.countFor(eventCounters[i].event)); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 16, left.countFor(eventCounters[i].event), + diag("self-merge 8+8 event=%s index=%u", + eventCounters[i].name, (unsigned)i)); } } @@ -119,9 +142,14 @@ namespace { tinylink::Stats addition; for (size_t i = 0; i < sizeof(eventCounters) / sizeof(eventCounters[0]); ++i) { stats.*(eventCounters[i].counter) = UINT32_MAX; - TEST_ASSERT_TRUE(stats.increment(eventCounters[i].event)); - TEST_ASSERT_EQUAL_UINT32( - UINT32_MAX, stats.*(eventCounters[i].counter)); + TEST_ASSERT_TRUE_MESSAGE( + stats.increment(eventCounters[i].event), + diag("increment at max rejected event=%s index=%u", + eventCounters[i].name, (unsigned)i)); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + UINT32_MAX, stats.*(eventCounters[i].counter), + diag("increment must saturate event=%s index=%u", + eventCounters[i].name, (unsigned)i)); stats.*(eventCounters[i].counter) = UINT32_MAX - 1u; addition.*(eventCounters[i].counter) = 2; @@ -129,8 +157,10 @@ namespace { stats.merge(addition); for (size_t i = 0; i < sizeof(eventCounters) / sizeof(eventCounters[0]); ++i) { - TEST_ASSERT_EQUAL_UINT32( - UINT32_MAX, stats.*(eventCounters[i].counter)); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + UINT32_MAX, stats.*(eventCounters[i].counter), + diag("merge (UINT32_MAX-1)+2 must saturate event=%s index=%u", + eventCounters[i].name, (unsigned)i)); } } diff --git a/test/support/stubs/espidf/driver/uart.h b/test/support/stubs/espidf/driver/uart.h index 6e312f9..66e94c3 100644 --- a/test/support/stubs/espidf/driver/uart.h +++ b/test/support/stubs/espidf/driver/uart.h @@ -4,6 +4,14 @@ #include #include +/* + * Test-only ESP-IDF UART stand-in. Not the real driver: state lives in + * file-scope `tinylinkEsp*` variables that tests set directly to script driver + * results (buffered size, read result, write result) and to inspect the last + * write (capped at 64 bytes, the TinyLink maximum payload). Tests must reset + * these between cases; they are not reset automatically. + */ + typedef int uart_port_t; typedef int esp_err_t; diff --git a/test/support/stubs/stm32/stm32_hal_stub.h b/test/support/stubs/stm32/stm32_hal_stub.h index 7478ba4..242015d 100644 --- a/test/support/stubs/stm32/stm32_hal_stub.h +++ b/test/support/stubs/stm32/stm32_hal_stub.h @@ -4,6 +4,13 @@ #include #include +/* + * Test-only STM32 HAL stand-in covering just the UART calls TinySTM32HALAdapter + * uses. Behavior is scripted through file-scope `tinylinkHal*` variables + * (receive readiness/status/value, transmit status) with call counters and + * recorded timeouts for assertions. Variables are not reset automatically. + */ + typedef int HAL_StatusTypeDef; typedef uint32_t HAL_TickTypeDef; diff --git a/test/support/stubs/windows/windows.h b/test/support/stubs/windows/windows.h index 643dc74..a081a48 100644 --- a/test/support/stubs/windows/windows.h +++ b/test/support/stubs/windows/windows.h @@ -5,6 +5,15 @@ #include #include +/* + * Test-only Win32 serial API stand-in (CreateFile/DCB/COMMTIMEOUTS/ReadFile/ + * WriteFile subset used by TinyWindowsAdapter). It is an in-memory fake, not a + * Windows SDK replacement: no real handles or devices. Scripted state and call + * records are file-scope; call tinylink_reset_windows_stub() between tests. It + * is placed on the include path as , so it shadows the real SDK + * header in this environment only. + */ + typedef void* HANDLE; typedef uint32_t DWORD; typedef uint16_t WORD; diff --git a/test/support/tinylink/FakeAdapter.h b/test/support/tinylink/FakeAdapter.h index c3bc8d7..b51c958 100644 --- a/test/support/tinylink/FakeAdapter.h +++ b/test/support/tinylink/FakeAdapter.h @@ -8,7 +8,24 @@ namespace tinylink_test { - /** @brief Deterministic in-memory adapter for TinyLink engine tests. */ + /** + * @brief Deterministic in-memory adapter for TinyLink engine tests. + * + * Satisfies the adapter contract (isOpen/available/read/write/millis) with + * no I/O or real time. Tests drive it by queueing inbound bytes + * (queueByte/queueBytes), reading back everything the engine wrote + * (output/outputSize), and setting or advancing the clock explicitly + * (setTime/advanceTime; the clock never moves on its own). + * + * Limits: input and output are each capped at BUFFER_CAPACITY (8192) bytes. + * Queueing past that returns false; a write that would exceed the output + * capacity fails as a whole. TinyLink frames are at most + * PACKET_MAX_SIZE (69 bytes: 64 max payload + 3 header + 2 checksum) before + * COBS, so the + * cap only matters for tests that queue many frames without clearing. + * scriptWrites() holds at most 8 entries; extra entries are dropped. + * Unlike real transports it never blocks and never reorders bytes. + */ class FakeAdapter { public: /** @brief Maximum number of bytes retained in either test buffer. */ diff --git a/test/support/tinylink/TestDiagnostics.h b/test/support/tinylink/TestDiagnostics.h new file mode 100644 index 0000000..270dbbf --- /dev/null +++ b/test/support/tinylink/TestDiagnostics.h @@ -0,0 +1,31 @@ +#ifndef TINYLINK_TEST_TESTDIAGNOSTICS_H +#define TINYLINK_TEST_TESTDIAGNOSTICS_H + +#include +#include + +namespace tinylink_test { + + /** + * Formats failure context for TEST_ASSERT_*_MESSAGE inside loops, e.g. + * `TEST_ASSERT_EQUAL_UINT8_MESSAGE(e, a, diag("index=%u length=%u", i, n))`. + * + * The text lives in one static buffer that is overwritten by the next + * call, so use it directly as the message argument and never store the + * pointer. Not thread-safe; the native suites are single-threaded. + * Arguments follow printf rules: cast size_t to unsigned (or use %lu with + * unsigned long) to stay portable. Output is truncated to fit. + */ + inline const char* diag(const char* format, ...) + { + static char buffer[192]; + va_list args; + va_start(args, format); + vsnprintf(buffer, sizeof(buffer), format, args); + va_end(args); + return buffer; + } + +} // namespace tinylink_test + +#endif // TINYLINK_TEST_TESTDIAGNOSTICS_H diff --git a/test/support/tinylink/TinyLinkTestFixture.h b/test/support/tinylink/TinyLinkTestFixture.h index 1dbe528..c117f88 100644 --- a/test/support/tinylink/TinyLinkTestFixture.h +++ b/test/support/tinylink/TinyLinkTestFixture.h @@ -17,16 +17,25 @@ namespace tinylink { namespace tinylink_test { + /** @brief Payload type used by most engine tests (4 bytes). */ struct Payload { uint8_t bytes[4]; }; + /** @brief Payload at the maximum supported size (PACKET_MAX_PAYLOAD_SIZE, 64). */ struct MaximumPayload { uint8_t bytes[tinylink::packet::PACKET_MAX_PAYLOAD_SIZE]; }; typedef tinylink::TinyLink Link; + /* + * Callback recorders: onData/onLog/onHandshake/onAck bump a counter and + * store the last argument in the globals below. They are shared by the + * engine suite's translation units; setUp() calls resetCallbacks(). + * `ALL` (1000) is a scriptWrites() limit meaning "accept every byte" (it + * exceeds any frame size). + */ extern unsigned int dataCallbackCount; extern unsigned int logCallbackCount; extern unsigned int handshakeCallbackCount; diff --git a/test/support/tinylink/TinyLinkTestHelpers.h b/test/support/tinylink/TinyLinkTestHelpers.h index da1844e..c237f99 100644 --- a/test/support/tinylink/TinyLinkTestHelpers.h +++ b/test/support/tinylink/TinyLinkTestHelpers.h @@ -5,10 +5,23 @@ #include #include "FakeAdapter.h" +#include "TestDiagnostics.h" #include "TinyLink.h" namespace tinylink_test { + /* + * Frame helpers built on the production COBS and packet codecs (not + * reimplementations), so test frames stay valid if the wire format changes. + * A "frame" is COBS(packet) followed by a 0x00 delimiter. Encode buffers + * are sized PACKET_MAX_SIZE (69) plus COBS overhead. + * + * queue*: append a frame to the adapter's inbound bytes; return false if + * encoding fails or the 8192-byte input buffer is full. + * writtenFrameCount/decodeWrittenFrame: split the adapter's output on 0x00 + * delimiters and decode the Nth frame back to type/sequence/payload. + */ + inline bool queueFrame( FakeAdapter& adapter, const uint8_t* plain,