preserve transport outage validation evidence
This commit is contained in:
Vendored
+25
@@ -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
|
||||
```
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user