#include #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(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(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(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(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(); }