- name
- tdd
- description
- Test-driven development for firmware logic using PlatformIO's native env. TRIGGER when adding or changing code under lib/sampler/, lib/packetizer/, lib/ringbuf/, lib/config/, or any pure-logic C++ in this repo. DO NOT trigger for docs, platformio.ini, scripts, or hardware-only modules (lib/imu/, lib/net/, lib/uploader/) where behavior can't be exercised without a board.
# TDD — host-side firmware loop
Embedded TDD only works if you can run the code on the host. This project
is structured so that every non-hardware module builds and tests under
`pio test -e native` on the Mac in under a second. Use that ruthlessly.
## The cycle
### 1. Red — failing host test
Add a test file under `test/test_native/test_<module>/test_<module>.cpp`.
PlatformIO + Unity discovers one `void setup()` / `void loop()` per group.
```cpp
#include <unity.h>
#include "packetizer/Packetizer.h"
void test_batch_respects_size_cap(void) {
Packetizer p(/*cap=*/10);
for (int i = 0; i < 15; i++) p.add(sample_at(i));
TEST_ASSERT_EQUAL(10, p.size());
TEST_ASSERT_EQUAL(5, p.dropped());
}
void setup() {
UNITY_BEGIN();
RUN_TEST(test_batch_respects_size_cap);
UNITY_END();
}
void loop() {}
```
Run it and confirm it **fails for the reason you expect**, not a link error:
```bash
pio test -e native -f test_packetizer
```
### 2. Green — minimum implementation
Write the least code that makes it pass. No speculative generality. Sampler
rate logic doesn't need a state machine if an `if` works.
```bash
pio test -e native -f test_packetizer
```
### 3. Refactor — run the whole host suite
```bash
pio test -e native
```
All host tests must be green. If a refactor touches the interface between a
pure-logic module and a hardware module, also verify the device build still
links:
```bash
pio run -e esp32c3
```
### 4. Format
```bash
clang-format -i $(git ls-files 'src/*.cpp' 'src/*.h' 'lib/**/*.cpp' 'lib/**/*.h' 'test/**/*.cpp' 'test/**/*.h')
```
### 5. Commit
Test and implementation in the same commit. Conventional commit format,
include issue number if applicable: `feat(packetizer): cap batch size (#12)`.
## What belongs in host tests
- **Packetizer** — JSON shape, `seq` monotonicity, mount_quat application,
batch boundaries, gzip framing, pre_sntp handling
- **Sampler** — rate limiting, backpressure when ringbuf is full, clock skew
detection, dropped-sample accounting
- **Ringbuf** — wrap behavior, oldest-drop-on-overflow, segment recovery
after simulated crash, size accounting
- **Config** — NVS round-trip (use a fake NVS), schema version migration,
default values
- **Anything with math** — quaternion multiplication, frame transforms,
heave integration — write a test with known-answer inputs
## What does NOT belong in host tests
- `lib/imu/` device impl — needs `Wire` and `sh2`, use `ImuFake` in host tests
- `lib/net/` — uses `WiFi.h`, mock at the interface boundary
- `lib/uploader/` — uses `HTTPClient`, inject a fake HTTP sink
- `main.cpp` — no logic lives there by convention; nothing to test
If you catch yourself writing `#ifdef ARDUINO` inside a module that's supposed
to be pure logic, stop — the hardware dependency needs to move behind an
interface.
## Fake injection pattern
```cpp
// lib/imu/Imu.h
class Imu {
public:
virtual ~Imu() = default;
virtual bool begin() = 0;
virtual bool read(Sample& out) = 0;
};
// lib/imu/ImuFake.h — host-only
class ImuFake : public Imu {
public:
std::vector<Sample> canned;
size_t idx = 0;
bool begin() override { return true; }
bool read(Sample& out) override {
if (idx >= canned.size()) return false;
out = canned[idx++];
return true;
}
};
```
Sampler takes `Imu&`, not `Imu*` or a concrete type. Tests pass `ImuFake`.
`main.cpp` passes the real one.
## Known gotchas
- **Unity's `TEST_ASSERT_EQUAL_FLOAT` uses a fixed epsilon** — for quaternion
math, use `TEST_ASSERT_FLOAT_WITHIN(1e-6f, expected, actual)`
- **`pio test -e native` rebuilds slowly on cold cache** — first run takes
~15s, subsequent runs are sub-second. Don't conflate the two
- **Arduino types leak in** — if a host test fails to compile because of
`String` or `HardwareSerial`, the module under test has a hidden Arduino
dependency. Move it behind an interface
Ver no GitHub