test: add payload codec regression suite

This commit is contained in:
2026-02-20 21:22:10 +01:00
parent 6acb588069
commit cef1d184ed
3 changed files with 316 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
#pragma once
#include <Arduino.h>
struct BatchInput {
uint16_t sender_id;
uint16_t batch_id;
uint32_t t_last;
uint32_t present_mask;
uint8_t n;
uint16_t battery_mV;
uint8_t err_m;
uint8_t err_d;
uint8_t err_tx;
uint8_t err_last;
uint8_t err_rx_reject;
uint32_t energy_wh[30];
int16_t p1_w[30];
int16_t p2_w[30];
int16_t p3_w[30];
};
bool encode_batch(const BatchInput &in, uint8_t *out, size_t out_cap, size_t *out_len);
bool decode_batch(const uint8_t *buf, size_t len, BatchInput *out);
size_t uleb128_encode(uint32_t v, uint8_t *out, size_t cap);
bool uleb128_decode(const uint8_t *in, size_t len, size_t *pos, uint32_t *v);
uint32_t zigzag32(int32_t x);
int32_t unzigzag32(uint32_t u);
size_t svarint_encode(int32_t x, uint8_t *out, size_t cap);
bool svarint_decode(const uint8_t *in, size_t len, size_t *pos, int32_t *x);
#ifdef PAYLOAD_CODEC_TEST
bool payload_codec_self_test();
#endif

View File

@@ -0,0 +1,279 @@
#include <Arduino.h>
#include <unity.h>
#include "dd3_legacy_core.h"
#include "payload_codec.h"
static constexpr uint8_t kMaxSamples = 30;
static void fill_sparse_batch(BatchInput &in) {
memset(&in, 0, sizeof(in));
in.sender_id = 1;
in.batch_id = 42;
in.t_last = 1700000000;
in.present_mask = (1UL << 0) | (1UL << 2) | (1UL << 3) | (1UL << 10) | (1UL << 29);
in.n = 5;
in.battery_mV = 3750;
in.err_m = 2;
in.err_d = 1;
in.err_tx = 3;
in.err_last = 2;
in.err_rx_reject = 1;
in.energy_wh[0] = 100000;
in.energy_wh[1] = 100001;
in.energy_wh[2] = 100050;
in.energy_wh[3] = 100050;
in.energy_wh[4] = 100200;
in.p1_w[0] = -120;
in.p1_w[1] = -90;
in.p1_w[2] = 1910;
in.p1_w[3] = -90;
in.p1_w[4] = 500;
in.p2_w[0] = 50;
in.p2_w[1] = -1950;
in.p2_w[2] = 60;
in.p2_w[3] = 2060;
in.p2_w[4] = -10;
in.p3_w[0] = 0;
in.p3_w[1] = 10;
in.p3_w[2] = -1990;
in.p3_w[3] = 10;
in.p3_w[4] = 20;
}
static void fill_full_batch(BatchInput &in) {
memset(&in, 0, sizeof(in));
in.sender_id = 1;
in.batch_id = 0xBEEF;
in.t_last = 1769904999;
in.present_mask = 0x3FFFFFFFUL;
in.n = kMaxSamples;
in.battery_mV = 4095;
in.err_m = 10;
in.err_d = 20;
in.err_tx = 30;
in.err_last = 3;
in.err_rx_reject = 6;
for (uint8_t i = 0; i < kMaxSamples; ++i) {
in.energy_wh[i] = 500000UL + static_cast<uint32_t>(i) * static_cast<uint32_t>(i) * 3UL;
in.p1_w[i] = static_cast<int16_t>(-1000 + static_cast<int16_t>(i) * 25);
in.p2_w[i] = static_cast<int16_t>(500 - static_cast<int16_t>(i) * 30);
in.p3_w[i] = static_cast<int16_t>(((i % 2) == 0 ? 100 : -100) + static_cast<int16_t>(i) * 5);
}
}
static void assert_batch_equals(const BatchInput &expected, const BatchInput &actual) {
TEST_ASSERT_EQUAL_UINT16(expected.sender_id, actual.sender_id);
TEST_ASSERT_EQUAL_UINT16(expected.batch_id, actual.batch_id);
TEST_ASSERT_EQUAL_UINT32(expected.t_last, actual.t_last);
TEST_ASSERT_EQUAL_UINT32(expected.present_mask, actual.present_mask);
TEST_ASSERT_EQUAL_UINT8(expected.n, actual.n);
TEST_ASSERT_EQUAL_UINT16(expected.battery_mV, actual.battery_mV);
TEST_ASSERT_EQUAL_UINT8(expected.err_m, actual.err_m);
TEST_ASSERT_EQUAL_UINT8(expected.err_d, actual.err_d);
TEST_ASSERT_EQUAL_UINT8(expected.err_tx, actual.err_tx);
TEST_ASSERT_EQUAL_UINT8(expected.err_last, actual.err_last);
TEST_ASSERT_EQUAL_UINT8(expected.err_rx_reject, actual.err_rx_reject);
for (uint8_t i = 0; i < expected.n; ++i) {
TEST_ASSERT_EQUAL_UINT32(expected.energy_wh[i], actual.energy_wh[i]);
TEST_ASSERT_EQUAL_INT16(expected.p1_w[i], actual.p1_w[i]);
TEST_ASSERT_EQUAL_INT16(expected.p2_w[i], actual.p2_w[i]);
TEST_ASSERT_EQUAL_INT16(expected.p3_w[i], actual.p3_w[i]);
}
for (uint8_t i = expected.n; i < kMaxSamples; ++i) {
TEST_ASSERT_EQUAL_UINT32(0, actual.energy_wh[i]);
TEST_ASSERT_EQUAL_INT16(0, actual.p1_w[i]);
TEST_ASSERT_EQUAL_INT16(0, actual.p2_w[i]);
TEST_ASSERT_EQUAL_INT16(0, actual.p3_w[i]);
}
}
static void test_encode_decode_roundtrip_schema_v3() {
BatchInput in = {};
fill_sparse_batch(in);
uint8_t encoded[256] = {};
size_t encoded_len = 0;
TEST_ASSERT_TRUE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
TEST_ASSERT_TRUE(encoded_len > 24);
BatchInput out = {};
TEST_ASSERT_TRUE(decode_batch(encoded, encoded_len, &out));
assert_batch_equals(in, out);
}
static void test_decode_rejects_bad_magic_schema_flags() {
BatchInput in = {};
fill_sparse_batch(in);
uint8_t encoded[256] = {};
size_t encoded_len = 0;
TEST_ASSERT_TRUE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
BatchInput out = {};
uint8_t bad_magic[256] = {};
memcpy(bad_magic, encoded, encoded_len);
bad_magic[0] = 0x00;
TEST_ASSERT_FALSE(decode_batch(bad_magic, encoded_len, &out));
uint8_t bad_schema[256] = {};
memcpy(bad_schema, encoded, encoded_len);
bad_schema[2] = 0x02;
TEST_ASSERT_FALSE(decode_batch(bad_schema, encoded_len, &out));
uint8_t bad_flags[256] = {};
memcpy(bad_flags, encoded, encoded_len);
bad_flags[3] = 0x00;
TEST_ASSERT_FALSE(decode_batch(bad_flags, encoded_len, &out));
}
static void test_decode_rejects_truncated_and_length_mismatch() {
BatchInput in = {};
fill_sparse_batch(in);
uint8_t encoded[256] = {};
size_t encoded_len = 0;
TEST_ASSERT_TRUE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
BatchInput out = {};
TEST_ASSERT_FALSE(decode_batch(encoded, encoded_len - 1, &out));
TEST_ASSERT_FALSE(decode_batch(encoded, 12, &out));
uint8_t with_tail[257] = {};
memcpy(with_tail, encoded, encoded_len);
with_tail[encoded_len] = 0xAA;
TEST_ASSERT_FALSE(decode_batch(with_tail, encoded_len + 1, &out));
}
static void test_encode_and_decode_reject_invalid_present_mask() {
BatchInput in = {};
fill_sparse_batch(in);
uint8_t encoded[256] = {};
size_t encoded_len = 0;
in.present_mask = 0x40000000UL;
TEST_ASSERT_FALSE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
fill_sparse_batch(in);
TEST_ASSERT_TRUE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
BatchInput out = {};
uint8_t invalid_bits[256] = {};
memcpy(invalid_bits, encoded, encoded_len);
invalid_bits[15] |= 0x40;
TEST_ASSERT_FALSE(decode_batch(invalid_bits, encoded_len, &out));
uint8_t bitcount_mismatch[256] = {};
memcpy(bitcount_mismatch, encoded, encoded_len);
bitcount_mismatch[16] = 0x01; // n=1 while mask has 5 bits set
TEST_ASSERT_FALSE(decode_batch(bitcount_mismatch, encoded_len, &out));
}
static void test_encode_rejects_invalid_n_and_regression_cases() {
BatchInput in = {};
fill_sparse_batch(in);
uint8_t encoded[256] = {};
size_t encoded_len = 0;
in.n = 31;
TEST_ASSERT_FALSE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
fill_sparse_batch(in);
in.n = 0;
in.present_mask = 1;
TEST_ASSERT_FALSE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
fill_sparse_batch(in);
in.n = 2;
in.present_mask = 0x00000003UL;
in.energy_wh[1] = in.energy_wh[0] - 1;
TEST_ASSERT_FALSE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
fill_sparse_batch(in);
TEST_ASSERT_FALSE(encode_batch(in, encoded, 10, &encoded_len));
}
static const uint8_t VECTOR_SYNC_EMPTY[] = {
0xB3, 0xDD, 0x03, 0x01, 0x01, 0x00, 0x34, 0x12, 0xE4, 0x97, 0x7E, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA6, 0x0E,
0x00, 0x00, 0x00, 0x00, 0x00};
static const uint8_t VECTOR_SPARSE_5[] = {
0xB3, 0xDD, 0x03, 0x01, 0x01, 0x00, 0x2A, 0x00, 0x00, 0xF1, 0x53, 0x65, 0x0D, 0x04, 0x00, 0x20, 0x05, 0xA6, 0x0E,
0x02, 0x01, 0x03, 0x02, 0x01, 0xA0, 0x86, 0x01, 0x00, 0x01, 0x31, 0x00, 0x96, 0x01, 0x88, 0xFF, 0x3C, 0xA0, 0x1F,
0x9F, 0x1F, 0x9C, 0x09, 0x32, 0x00, 0x9F, 0x1F, 0xB4, 0x1F, 0xA0, 0x1F, 0xAB, 0x20, 0x00, 0x00, 0x14, 0x9F, 0x1F,
0xA0, 0x1F, 0x14};
static const uint8_t VECTOR_FULL_30[] = {
0xB3, 0xDD, 0x03, 0x01, 0x01, 0x00, 0xEF, 0xBE, 0x67, 0x9B, 0x7E, 0x69, 0xFF, 0xFF, 0xFF, 0x3F, 0x1E, 0xFF, 0x0F,
0x0A, 0x14, 0x1E, 0x03, 0x06, 0x20, 0xA1, 0x07, 0x00, 0x03, 0x09, 0x0F, 0x15, 0x1B, 0x21, 0x27, 0x2D, 0x33, 0x39,
0x3F, 0x45, 0x4B, 0x51, 0x57, 0x5D, 0x63, 0x69, 0x6F, 0x75, 0x7B, 0x81, 0x01, 0x87, 0x01, 0x8D, 0x01, 0x93, 0x01,
0x99, 0x01, 0x9F, 0x01, 0xA5, 0x01, 0xAB, 0x01, 0x18, 0xFC, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
0x32, 0xF4, 0x01, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B,
0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x64, 0x00, 0x85, 0x03, 0x9A, 0x03,
0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A,
0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03,
0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03};
static void test_payload_golden_vectors() {
BatchInput expected_sync = {};
expected_sync.sender_id = 1;
expected_sync.batch_id = 0x1234;
expected_sync.t_last = 1769904100;
expected_sync.present_mask = 0;
expected_sync.n = 0;
expected_sync.battery_mV = 3750;
expected_sync.err_m = 0;
expected_sync.err_d = 0;
expected_sync.err_tx = 0;
expected_sync.err_last = 0;
expected_sync.err_rx_reject = 0;
BatchInput expected_sparse = {};
fill_sparse_batch(expected_sparse);
BatchInput expected_full = {};
fill_full_batch(expected_full);
struct VectorCase {
const char *name;
const uint8_t *bytes;
size_t len;
const BatchInput *expected;
} cases[] = {
{"sync_empty", VECTOR_SYNC_EMPTY, sizeof(VECTOR_SYNC_EMPTY), &expected_sync},
{"sparse_5", VECTOR_SPARSE_5, sizeof(VECTOR_SPARSE_5), &expected_sparse},
{"full_30", VECTOR_FULL_30, sizeof(VECTOR_FULL_30), &expected_full},
};
for (size_t i = 0; i < (sizeof(cases) / sizeof(cases[0])); ++i) {
BatchInput decoded = {};
TEST_ASSERT_TRUE_MESSAGE(decode_batch(cases[i].bytes, cases[i].len, &decoded), cases[i].name);
assert_batch_equals(*cases[i].expected, decoded);
uint8_t reencoded[512] = {};
size_t reencoded_len = 0;
TEST_ASSERT_TRUE_MESSAGE(encode_batch(*cases[i].expected, reencoded, sizeof(reencoded), &reencoded_len), cases[i].name);
TEST_ASSERT_EQUAL_UINT_MESSAGE(cases[i].len, reencoded_len, cases[i].name);
TEST_ASSERT_EQUAL_UINT8_ARRAY_MESSAGE(cases[i].bytes, reencoded, cases[i].len, cases[i].name);
}
}
void setup() {
dd3_legacy_core_force_link();
UNITY_BEGIN();
RUN_TEST(test_encode_decode_roundtrip_schema_v3);
RUN_TEST(test_decode_rejects_bad_magic_schema_flags);
RUN_TEST(test_decode_rejects_truncated_and_length_mismatch);
RUN_TEST(test_encode_and_decode_reject_invalid_present_mask);
RUN_TEST(test_encode_rejects_invalid_n_and_regression_cases);
RUN_TEST(test_payload_golden_vectors);
UNITY_END();
}
void loop() {}