preserve transport outage validation evidence

This commit is contained in:
Jay
2026-08-17 14:30:56 -04:00
parent 291c6b54e6
commit 1cf0a9ac77
9 changed files with 176 additions and 26 deletions
+25
View File
@@ -0,0 +1,25 @@
# Hardware outage fixtures
These captures came from the assembled XIAO ESP32-C3 prototype. A temporary
validation build made the packet writer report failure while acquisition kept
running; that failure injection was removed before the production firmware was
built and flashed. The files contain only complete, CRC-valid `TRK1` frames.
- `forced_outage_3s.trk` — SHA-256
`01482816cdaa668e4681c33c8baa1df331d733b9bbcbc4f448ece25e88185ad6`.
It contains 2,144 contiguous samples, sequences 0 through 2,143, with no
reported drops, overruns, or timestamp saturation.
- `forced_outage_7s.trk` — SHA-256
`2ea8a5742944bdebc13bec2ccdbceba75f0bb71e48c856b0f86285878e190cd3`.
It contains 1,840 delivered samples. The sole sequence gap is 511 to 650:
138 samples were dropped after the 512-entry queue filled, the cumulative
drop count reached 138, and the corresponding timestamp delta is exactly
1,390,000 us.
`tests/test_trikke_protocol.py` verifies the hashes, parses the captures in
fragmented chunks, and asserts these signatures so the hardware evidence remains
executable regression data. To inspect either file manually:
```sh
python3 tools/decode_binary.py tests/fixtures/forced_outage_3s.trk /tmp/outage.csv
```
Binary file not shown.
Binary file not shown.
+12 -6
View File
@@ -3,6 +3,12 @@
#include "trikke_protocol.h"
static int fail(int code, const char *message)
{
fprintf(stderr, "protocol fixture failure %d: %s\n", code, message);
return code;
}
int main(void)
{
uint8_t packet[TRIKKE_WIRE_MAX_PACKET_SIZE] = {0};
@@ -20,7 +26,7 @@ int main(void)
size_t size = trikke_encode_metadata_packet(
packet, sizeof(packet), 41, 1234567, 2, 3, &metadata);
if (size == 0 || fwrite(packet, 1, size, stdout) != size) {
return 1;
return fail(1, "metadata encoding or output");
}
const trikke_wire_sample_t samples[] = {
@@ -52,7 +58,7 @@ int main(void)
size = trikke_encode_sample_packet(packet, sizeof(packet), 42, 2, 3,
samples, 2);
if (size == 0 || fwrite(packet, 1, size, stdout) != size) {
return 1;
return fail(2, "basic sample encoding or output");
}
trikke_wire_sample_t full_packet[TRIKKE_WIRE_MAX_RECORDS] = {0};
@@ -69,7 +75,7 @@ int main(void)
TRIKKE_WIRE_MAX_RECORDS);
if (size != TRIKKE_WIRE_MAX_PACKET_SIZE ||
fwrite(packet, 1, size, stdout) != size) {
return 2;
return fail(3, "maximum-size sample encoding or output");
}
const trikke_wire_sample_t saturated[] = {
@@ -79,13 +85,13 @@ int main(void)
size = trikke_encode_sample_packet(packet, sizeof(packet), 44, 2, 3,
saturated, 2);
if (size == 0 || fwrite(packet, 1, size, stdout) != size) {
return 3;
return fail(4, "saturated timestamp encoding or output");
}
if (!trikke_wire_timestamp_delta_fits(0, 655350) ||
trikke_wire_timestamp_delta_fits(0, 655351) ||
trikke_wire_timestamp_delta_fits(1, 0)) {
return 4;
return fail(5, "timestamp-delta boundary contract");
}
if (trikke_encode_metadata_packet(
packet, TRIKKE_WIRE_HEADER_SIZE + TRIKKE_WIRE_METADATA_SIZE - 1,
@@ -98,7 +104,7 @@ int main(void)
trikke_encode_sample_packet(packet, TRIKKE_WIRE_MAX_PACKET_SIZE - 1,
0, 0, 0, full_packet,
TRIKKE_WIRE_MAX_RECORDS) != 0) {
return 5;
return fail(6, "invalid argument rejection contract");
}
return 0;
}
+74 -3
View File
@@ -1,3 +1,4 @@
import hashlib
import shutil
import subprocess
import sys
@@ -41,9 +42,13 @@ class ProtocolContractTest(unittest.TestCase):
],
check=True,
)
cls.encoded = subprocess.run(
[str(executable)], check=True, capture_output=True
).stdout
fixture = subprocess.run([str(executable)], capture_output=True)
if fixture.returncode != 0:
stderr = fixture.stderr.decode(errors="replace").strip()
raise AssertionError(
f"protocol fixture exited {fixture.returncode}: {stderr}"
)
cls.encoded = fixture.stdout
@classmethod
def tearDownClass(cls) -> None:
@@ -131,6 +136,72 @@ class ProtocolContractTest(unittest.TestCase):
self.assertEqual(3, len(frames))
self.assertEqual(36 + 2 * 20 - 5, parser.buffered_bytes)
def test_hardware_outage_validation_artifacts(self) -> None:
expected = {
"forced_outage_3s.trk": {
"sha256": "01482816cdaa668e4681c33c8baa1df331d733b9bbcbc4f448ece25e88185ad6",
"sample_count": 2144,
"last_sequence": 2143,
"max_dropped": 0,
"gaps": [],
},
"forced_outage_7s.trk": {
"sha256": "2ea8a5742944bdebc13bec2ccdbceba75f0bb71e48c856b0f86285878e190cd3",
"sample_count": 1840,
"last_sequence": 1977,
"max_dropped": 138,
"gaps": [(511, 650, 1_390_000)],
},
}
for name, contract in expected.items():
with self.subTest(fixture=name):
data = (ROOT / "tests" / "fixtures" / name).read_bytes()
self.assertEqual(
contract["sha256"], hashlib.sha256(data).hexdigest()
)
parser = StreamParser()
frames = []
for offset in range(0, len(data), 257):
frames.extend(parser.feed(data[offset : offset + 257]))
self.assertEqual(0, parser.startup_crc_errors)
self.assertEqual(0, parser.crc_errors)
self.assertEqual(0, parser.header_errors)
self.assertEqual(0, parser.skipped_bytes)
self.assertEqual(0, parser.buffered_bytes)
self.assertTrue(frames)
samples = [sample for frame in frames for sample in frame.samples]
self.assertEqual(contract["sample_count"], len(samples))
self.assertEqual(0, samples[0].sequence)
self.assertEqual(contract["last_sequence"], samples[-1].sequence)
self.assertEqual(
contract["max_dropped"],
max(frame.dropped_sample_count for frame in frames),
)
self.assertEqual(
0, max(frame.loop_overrun_count for frame in frames)
)
self.assertFalse(
any(
frame.flags & PACKET_FLAG_TIMESTAMP_DELTA_SATURATED
for frame in frames
)
)
gaps = [
(
left.sequence,
right.sequence,
right.timestamp_us - left.timestamp_us,
)
for left, right in zip(samples, samples[1:])
if right.sequence != left.sequence + 1
]
self.assertEqual(contract["gaps"], gaps)
if __name__ == "__main__":
unittest.main()