Files
HaloKeymind/test/test_config_serializer/test_config_serializer.cpp
T

437 lines
13 KiB
C++

#include <gtest/gtest.h>
#include "helpers/ConfigSerializer.h"
#include "helpers/DynamicConfigSerializer.h"
class NativeFileSystem {
public:
void mkdir(const char*) { }
};
#define FILESYSTEM NativeFileSystem
#include "helpers/CommonCLI.h"
#undef FILESYSTEM
#define TEST_INT_S "56"
#define TEST_INT 56
#define TEST_FLOAT_S "-6.123"
#define TEST_FLOAT -6.1230f
#define TEST_DOUBLE_S "12.123456"
#define TEST_DOUBLE 12.123456
class MockInputStream : public Stream {
const char* _text;
int pos, len;
public:
MockInputStream(const char* text) : _text(text) { pos = 0; len = strlen(text); }
int available() override { return len - pos; }
int read() override { if (pos < len) { return _text[pos++]; } return -1; }
int peek() override { if (pos < len) { return _text[pos]; } return -1; }
};
class MockPrintStream : public Stream {
int len = 0;
uint8_t _buf[1024];
size_t printSigned(long long value) {
char text[24];
snprintf(text, sizeof(text), "%lld", value);
return Print::print(text);
}
size_t printUnsigned(unsigned long long value) {
char text[24];
snprintf(text, sizeof(text), "%llu", value);
return Print::print(text);
}
public:
size_t write(uint8_t b) override {
if (len < sizeof(_buf)) {
_buf[len++] = b;
return 1;
}
return 0;
}
size_t print(unsigned char v, int r) override { return printUnsigned(v); }
size_t print(int v, int r) override { return printSigned(v); }
size_t print(unsigned int v, int r) override { return printUnsigned(v); }
size_t print(long v, int r) override { return printSigned(v); }
size_t print(unsigned long v, int r) override { return printUnsigned(v); }
size_t print(long long v, int r) override { return printSigned(v); }
size_t print(unsigned long long v, int r) override { return printUnsigned(v); }
size_t print(double v, int p = 2) override {
char text[32];
snprintf(text, sizeof(text), "%.*f", p, v);
return Print::print(text);
}
int getLength() const { return len; }
const uint8_t* getBytes() const { return _buf; }
};
class TestStruct : public ConfigSerializer {
protected:
void structure() override {
def("age", age);
def("flags", flags);
def("name", name, sizeof(name));
}
public:
int32_t age;
char name[16];
uint8_t flags;
};
class DecimalStruct : public ConfigSerializer {
protected:
void structure() override {
def("latitude", latitude);
def("frequency", frequency);
}
public:
double latitude = 0.0;
float frequency = 0.0f;
};
// ── saveSerial: basic ───────────────────────────────────────────────────────
TEST(ConfigSerializer, SaveSerial_Basic) {
MockPrintStream s;
TestStruct data;
data.age = TEST_INT;
data.flags = TEST_INT;
strcpy(data.name, "Scott");
bool success = data.saveSerial(s);
EXPECT_TRUE(success);
auto l = s.getLength();
const char* expect = "{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\"}";
EXPECT_EQ(strlen(expect), l);
bool match = memcmp(s.getBytes(), expect, l) == 0;
EXPECT_TRUE(match);
}
TEST(ConfigSerializer, SaveSerial_EscChars) {
MockPrintStream s;
TestStruct data;
data.age = TEST_INT;
data.flags = TEST_INT;
strcpy(data.name, "\"Scott\"\n");
bool success = data.saveSerial(s);
EXPECT_TRUE(success);
auto l = s.getLength();
const char* expect = "{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"\\\"Scott\\\"\\n\"}";
EXPECT_EQ(strlen(expect), l);
bool match = memcmp(s.getBytes(), expect, l) == 0;
EXPECT_TRUE(match);
}
// ── loadSerial: basic ───────────────────────────────────────────────────────
TEST(ConfigSerializer, LoadSerial_Basic) {
MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\"}");
TestStruct data;
bool success = data.loadSerial(s);
EXPECT_TRUE(success);
EXPECT_EQ(TEST_INT, data.age);
EXPECT_EQ(TEST_INT, data.flags);
bool match = strcmp("Scott", data.name) == 0;
EXPECT_TRUE(match);
}
TEST(ConfigSerializer, LoadSerial_HandleWhitespace) {
MockInputStream s(" { age: " TEST_INT_S " , flags: " TEST_INT_S " , name: \"Scott\" } ");
TestStruct data;
bool success = data.loadSerial(s);
EXPECT_TRUE(success);
EXPECT_EQ(TEST_INT, data.age);
EXPECT_EQ(TEST_INT, data.flags);
bool match = strcmp("Scott", data.name) == 0;
EXPECT_TRUE(match);
}
TEST(ConfigSerializer, LoadSerial_EscChars) {
MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"\\\"Scott\\\"\\n\"}");
TestStruct data;
bool success = data.loadSerial(s);
EXPECT_TRUE(success);
bool match = strcmp("\"Scott\"\n", data.name) == 0;
EXPECT_TRUE(match);
}
TEST(ConfigSerializer, LoadSerial_UnmatchedBraces) {
MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\"");
TestStruct data;
bool success = data.loadSerial(s);
EXPECT_FALSE(success);
}
TEST(ConfigSerializer, LoadSerial_MissingCommas) {
MockInputStream s("{age:" TEST_INT_S " flags:" TEST_INT_S " name:\"Scott\"}");
TestStruct data;
bool success = data.loadSerial(s);
EXPECT_FALSE(success);
}
TEST(ConfigSerializer, LoadSerial_MissingCommaAfterObject) {
MockInputStream s("{radio:{} bridge:{}}");
TestStruct data;
EXPECT_FALSE(data.loadSerial(s));
}
TEST(ConfigSerializer, LoadSerial_RequiresOneCompleteRootObject) {
for (const char* text : {
"",
" \r\n\t ",
"{age:" TEST_INT_S ",}",
"{age:" TEST_INT_S "},",
"{age:" TEST_INT_S "}garbage",
"{age:" TEST_INT_S "}{flags:1}"}) {
MockInputStream s(text);
TestStruct data;
EXPECT_FALSE(data.loadSerial(s)) << text;
}
}
TEST(ConfigSerializer, LoadSerial_AcceptsEmptyObject) {
MockInputStream s("{} \r\n\t");
TestStruct data;
EXPECT_TRUE(data.loadSerial(s));
}
TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) {
MockInputStream s("{age:" TEST_INT_S ",xxx:" TEST_INT_S ",name:\"Scott\"}");
TestStruct data;
data.flags = 1;
// should ignore the 'xxx' property
bool success = data.loadSerial(s);
EXPECT_TRUE(success);
EXPECT_EQ(TEST_INT, data.age);
EXPECT_EQ(1, data.flags); // flags should be unmodified
bool match = strcmp("Scott", data.name) == 0;
EXPECT_TRUE(match);
}
TEST(ConfigSerializer, LoadSerial_FixedDecimals) {
MockInputStream s("{latitude:-47.123456,frequency:910.5250}");
DecimalStruct data;
ASSERT_TRUE(data.loadSerial(s));
EXPECT_NEAR(-47.123456, data.latitude, 0.0000001);
EXPECT_NEAR(910.525f, data.frequency, 0.0001f);
}
TEST(ConfigSerializer, SaveAndLoadSerial_FixedDecimals) {
DecimalStruct saved;
saved.latitude = -12.345678;
saved.frequency = 62.5f;
MockPrintStream output;
ASSERT_TRUE(saved.saveSerial(output));
std::string encoded(reinterpret_cast<const char*>(output.getBytes()),
output.getLength());
MockInputStream input(encoded.c_str());
DecimalStruct loaded;
ASSERT_TRUE(loaded.loadSerial(input));
EXPECT_NEAR(saved.latitude, loaded.latitude, 0.0000001);
EXPECT_NEAR(saved.frequency, loaded.frequency, 0.0001f);
}
TEST(NodePrefs, AdvertLocationDefaultsToStoredPrefs) {
NodePrefs prefs;
EXPECT_EQ(ADVERT_LOC_PREFS, prefs.advert_loc_policy);
EXPECT_EQ(ADVERT_LOC_PREFS, DEFAULT_ADVERT_LOC_POLICY);
// An explicit privacy choice remains persistent and is not replaced by
// the first-boot default when preferences are loaded.
prefs.advert_loc_policy = ADVERT_LOC_NONE;
MockPrintStream output;
ASSERT_TRUE(prefs.saveSerial(output));
std::string serialised(reinterpret_cast<const char*>(output.getBytes()),
output.getLength());
MockInputStream input(serialised.c_str());
NodePrefs loaded;
ASSERT_EQ(ADVERT_LOC_PREFS, loaded.advert_loc_policy);
ASSERT_TRUE(loaded.loadSerial(input));
EXPECT_EQ(ADVERT_LOC_NONE, loaded.advert_loc_policy);
}
TEST(NodePrefs, FemGainSettingsRoundTrip) {
NodePrefs saved;
saved.radio_fem_rxgain = 0;
saved.radio_fem_txgain = 1;
saved.usb_logging_enabled = 0;
saved.bridge_uart = 2;
saved.bridge_format = mesh::bridge::ESPNOW_FORMAT_RAW;
MockPrintStream output;
ASSERT_TRUE(saved.saveSerial(output));
std::string serialised(reinterpret_cast<const char*>(output.getBytes()), output.getLength());
EXPECT_NE(std::string::npos, serialised.find("fem_rxgain:0"));
EXPECT_NE(std::string::npos, serialised.find("fem_txgain:1"));
EXPECT_NE(std::string::npos, serialised.find("usb_log:0"));
EXPECT_NE(std::string::npos, serialised.find("uart:2"));
EXPECT_NE(std::string::npos, serialised.find("format:1"));
MockInputStream input(serialised.c_str());
NodePrefs loaded;
loaded.radio_fem_rxgain = 1;
loaded.radio_fem_txgain = 0;
loaded.usb_logging_enabled = 1;
loaded.bridge_uart = 1;
loaded.bridge_format = mesh::bridge::ESPNOW_FORMAT_WRAPPED;
ASSERT_TRUE(loaded.loadSerial(input));
EXPECT_EQ(0, loaded.radio_fem_rxgain);
EXPECT_EQ(1, loaded.radio_fem_txgain);
EXPECT_EQ(0, loaded.usb_logging_enabled);
EXPECT_EQ(2, loaded.bridge_uart);
EXPECT_EQ(mesh::bridge::ESPNOW_FORMAT_RAW, loaded.bridge_format);
}
TEST(NodePrefs, BridgeFormatDefaultsToWrappedAndIsWrittenExplicitly) {
NodePrefs prefs;
EXPECT_EQ(mesh::bridge::ESPNOW_FORMAT_WRAPPED, prefs.bridge_format);
MockPrintStream output;
ASSERT_TRUE(prefs.saveSerial(output));
std::string serialised(reinterpret_cast<const char*>(output.getBytes()),
output.getLength());
EXPECT_NE(std::string::npos, serialised.find("format:0"));
}
TEST(NodePrefs, LegacySerialWithoutBridgeFormatKeepsWrappedDefault) {
MockInputStream legacy("{bridge:{ch:13,secret:\"legacy\"}}");
NodePrefs loaded;
ASSERT_EQ(mesh::bridge::ESPNOW_FORMAT_WRAPPED, loaded.bridge_format);
ASSERT_TRUE(loaded.loadSerial(legacy));
EXPECT_EQ(13, loaded.bridge_channel);
EXPECT_STREQ("legacy", loaded.bridge_secret);
EXPECT_EQ(mesh::bridge::ESPNOW_FORMAT_WRAPPED, loaded.bridge_format);
}
TEST(NodePrefs, ExplicitWrappedBridgeFormatReplacesRawInMemoryValue) {
MockInputStream serialised("{bridge:{format:0}}");
NodePrefs loaded;
loaded.bridge_format = mesh::bridge::ESPNOW_FORMAT_RAW;
ASSERT_TRUE(loaded.loadSerial(serialised));
EXPECT_EQ(mesh::bridge::ESPNOW_FORMAT_WRAPPED, loaded.bridge_format);
}
TEST(DynamicConfigSerializer, GetSet_Basic) {
DynamicConfigSerializer data;
bool s1 = data.setByKey("age", "11");
bool s2 = data.setByKey("name", "Scott");
EXPECT_TRUE(s1 && s2);
char tmp[32];
bool g1 = data.getByKey("age", tmp, 31);
EXPECT_TRUE(g1);
EXPECT_STREQ("11", tmp);
bool g2 = data.getByKey("name", tmp, 31);
EXPECT_TRUE(g2);
EXPECT_STREQ("Scott", tmp);
}
TEST(DynamicConfigSerializer, Set_Replaces) {
DynamicConfigSerializer data;
bool s1 = data.setByKey("age", "11");
bool s2 = data.setByKey("name", "Scott");
EXPECT_TRUE(s1 && s2);
bool s3 = data.setByKey("age", "333");
EXPECT_TRUE(s3);
char tmp[32];
bool g1 = data.getByKey("age", tmp, 31);
EXPECT_TRUE(g1);
EXPECT_STREQ("333", tmp);
bool g2 = data.getByKey("name", tmp, 31);
EXPECT_TRUE(g2);
EXPECT_STREQ("Scott", tmp);
}
TEST(DynamicConfigSerializer, GetUnknown_Fail) {
DynamicConfigSerializer data;
bool s1 = data.setByKey("age", "11");
EXPECT_TRUE(s1);
char tmp[32];
bool g2 = data.getByKey("name", tmp, 31);
EXPECT_FALSE(g2);
}
TEST(DynamicConfigSerializer, SaveCustom_Basic) {
MockPrintStream s;
DynamicConfigSerializer data;
bool s1 = data.setByKey("age", "11");
bool s2 = data.setByKey("name", "Scott");
EXPECT_TRUE(s1 && s2);
bool success = data.saveSerial(s);
EXPECT_TRUE(success);
auto l = s.getLength();
char tmp[128];
memcpy(tmp, s.getBytes(), l);
tmp[l] = 0;
const char* expect = "{age:\"11\",name:\"Scott\"}";
EXPECT_STREQ(expect, tmp);
}
TEST(DynamicConfigSerializer, LoadCustom_Basic) {
MockInputStream s("{age:\"" TEST_INT_S "\",name:\"Scott\"}");
DynamicConfigSerializer data;
bool success = data.loadSerial(s);
EXPECT_TRUE(success);
char tmp[32];
bool g1 = data.getByKey("age", tmp, 31);
EXPECT_TRUE(g1);
EXPECT_STREQ(TEST_INT_S, tmp);
bool g2 = data.getByKey("name", tmp, 31);
EXPECT_TRUE(g2);
EXPECT_STREQ("Scott", tmp);
}
// ── main ───────────────────────────────────────────────────────
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}