Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/cpp-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 14 additions & 3 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
-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
33 changes: 32 additions & 1 deletion test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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/<platform>`)

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.
Expand Down
27 changes: 19 additions & 8 deletions test/suites/test_adapters_posix/test_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ int main()
#include <type_traits>
#include <vector>

#include "support/tinylink/TestDiagnostics.h"

#include <errno.h>
#include <fcntl.h>
#include <termios.h>
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -282,9 +288,14 @@ namespace {
TEST_ASSERT_EQUAL_INT(static_cast<int>(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<int>(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<int>(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());
}
Expand Down
17 changes: 11 additions & 6 deletions test/suites/test_engine_tinylink/ack_sequence_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t>(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<uint8_t>(value), sequence, context);
TEST_ASSERT_TRUE_MESSAGE(tinylink_test::queueAck(
adapter, sequence, tinylink::AckResult::Ok), context);
link.update();
}

Expand Down
9 changes: 7 additions & 2 deletions test/suites/test_engine_tinylink/application_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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));
Expand Down
64 changes: 41 additions & 23 deletions test/suites/test_internal_codec_cobs/test_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,27 @@
#include <unity.h>

#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 };
const size_t expected[] = { 1, 2, 254, 256, 257, 511, 512 };

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<int>(tinylink::codec::CobsStatus::Success),
static_cast<int>(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);
}
}

Expand Down Expand Up @@ -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<int>(tinylink::codec::CobsStatus::Success),
static_cast<int>(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<int>(tinylink::codec::CobsStatus::Success),
static_cast<int>(encodedResult.status));
TEST_ASSERT_EQUAL_UINT32(required, encodedResult.bytesWritten);
TEST_ASSERT_EQUAL_UINT8(
static_cast<int>(encodedResult.status),
context);
TEST_ASSERT_EQUAL_UINT32_MESSAGE(required, encodedResult.bytesWritten, context);
TEST_ASSERT_EQUAL_UINT8_MESSAGE(
length < 254u
? static_cast<uint8_t>(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<int>(tinylink::codec::CobsStatus::Success),
static_cast<int>(decodedResult.status));
TEST_ASSERT_EQUAL_UINT32(length, decodedResult.bytesWritten);
TEST_ASSERT_EQUAL_UINT8_ARRAY(source, decoded, length);
static_cast<int>(decodedResult.status),
context);
TEST_ASSERT_EQUAL_UINT32_MESSAGE(length, decodedResult.bytesWritten, context);
TEST_ASSERT_EQUAL_UINT8_ARRAY_MESSAGE(source, decoded, length, context);
}
}

Expand Down Expand Up @@ -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<int>(tinylink::codec::CobsStatus::Success),
static_cast<int>(enc.status));
static_cast<int>(enc.status),
context);
size_t measured = 0;
TEST_ASSERT_EQUAL(
TEST_ASSERT_EQUAL_MESSAGE(
static_cast<int>(tinylink::codec::CobsStatus::Success),
static_cast<int>(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<int>(tinylink::codec::CobsStatus::Success),
static_cast<int>(dec.status));
TEST_ASSERT_EQUAL_UINT32(length, dec.bytesWritten);
static_cast<int>(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);
}
}
}
Expand Down
Loading
Loading