From 5365557445570ba49392d5e677da61295c8559b7 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 00:30:36 -0800 Subject: [PATCH 01/27] Fix invalid C99 array initialization --- arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS.cpp b/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS.cpp index 0c9c97b5b9..bb47339f50 100644 --- a/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS.cpp +++ b/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS.cpp @@ -160,7 +160,8 @@ bool Adafruit_LittleFS::mkdir (char const *filepath) // make intermediate parent directory(ies) while ( NULL != (slash = strchr(slash, '/')) ) { - char parent[slash - filepath + 1] = { 0 }; + char parent[slash - filepath + 1]; + parent[0] = 0; memcpy(parent, filepath, slash - filepath); int rc = lfs_mkdir(&_lfs, parent); From f2a92b1f200b0fb6dfea8873a71437a370488b4e Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 00:31:37 -0800 Subject: [PATCH 02/27] Arduino compatibility fix on println() Libraries vary on whether they accept a no-argument println(), but they all accept a single string value. --- src/Identity.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Identity.cpp b/src/Identity.cpp index 8329892830..476eacf404 100644 --- a/src/Identity.cpp +++ b/src/Identity.cpp @@ -61,8 +61,8 @@ bool LocalIdentity::writeTo(Stream& s) const { } void LocalIdentity::printTo(Stream& s) const { - s.print("pub_key: "); Utils::printHex(s, pub_key, PUB_KEY_SIZE); s.println(); - s.print("prv_key: "); Utils::printHex(s, prv_key, PRV_KEY_SIZE); s.println(); + s.print("pub_key: "); Utils::printHex(s, pub_key, PUB_KEY_SIZE); s.println(""); + s.print("prv_key: "); Utils::printHex(s, prv_key, PRV_KEY_SIZE); s.println(""); } size_t LocalIdentity::writeTo(uint8_t* dest, size_t max_len) { @@ -96,4 +96,4 @@ void LocalIdentity::calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_k ed25519_key_exchange(secret, other_pub_key, prv_key); } -} \ No newline at end of file +} From a2d250ef32abf08ab192cb9dfc7751b2de0ad1c2 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 00:32:55 -0800 Subject: [PATCH 03/27] Mock SPI and Wire libraries. There aren't any standard mocks for these and our code invokes the adafruit LittleFS library and RTC library calls which assume they exist. --- test/mocks/SPI/SPI.h | 44 ++++++++++++++++++++++++++++++++++++++++++ test/mocks/Wire/Wire.h | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 test/mocks/SPI/SPI.h create mode 100644 test/mocks/Wire/Wire.h diff --git a/test/mocks/SPI/SPI.h b/test/mocks/SPI/SPI.h new file mode 100644 index 0000000000..404c747fe2 --- /dev/null +++ b/test/mocks/SPI/SPI.h @@ -0,0 +1,44 @@ +#ifndef SPI_H +#define SPI_H + +typedef int BitOrder; + + +typedef enum { + SPI_MODE0 = 0, + SPI_MODE1 = 1, + SPI_MODE2 = 2, + SPI_MODE3 = 3, +} SPIMode; + +class SPISettings { +public: + SPISettings(uint32_t clock, BitOrder bitOrder, SPIMode dataMode) {} + SPISettings(uint32_t clock, BitOrder bitOrder, uint8_t dataMode) {} +}; + +class SPIClass +{ +public: + uint8_t transfer(uint8_t data) { return 0; } + uint16_t transfer16(uint16_t data) { return 0; } + void transfer(void *buf, size_t count) {} + + void transfer(const void *txbuf, void *rxbuf, size_t count) {} + + void usingInterrupt(int interruptNumber) {} + void notUsingInterrupt(int interruptNumber) {} + void beginTransaction(SPISettings settings) {} + void endTransaction(void) {} + + void attachInterrupt() {} + void detachInterrupt() {} + + void begin() {} + void end() {} +}; + +SPIClass SPI; + + +#endif diff --git a/test/mocks/Wire/Wire.h b/test/mocks/Wire/Wire.h new file mode 100644 index 0000000000..f985da5ebc --- /dev/null +++ b/test/mocks/Wire/Wire.h @@ -0,0 +1,39 @@ +#ifndef Wire_h +#define Wire_h + +#include "Stream.h" + +class TwoWire : public Stream +{ +public: + TwoWire(uint8_t bus_num){}; + ~TwoWire(){}; + bool setPins(int sda, int scl){}; + bool begin(){return true;} + bool begin(uint8_t addr){return true;} + void beginTransmission(uint16_t address){} + void beginTransmission(uint8_t address){} + void beginTransmission(int address){} + uint8_t endTransmission(bool sendStop) { return 0; } + uint8_t endTransmission(void) { return 0; } + size_t requestFrom(uint16_t address, size_t size, bool sendStop) { return 0; } + uint8_t requestFrom(uint16_t address, uint8_t size, bool sendStop) { return 0; } + uint8_t requestFrom(uint16_t address, uint8_t size, uint8_t sendStop) { return 0; } + size_t requestFrom(uint8_t address, size_t len, bool stopBit) { return 0; } + uint8_t requestFrom(uint16_t address, uint8_t size) { return 0; } + uint8_t requestFrom(uint8_t address, uint8_t size, uint8_t sendStop) { return 0; } + uint8_t requestFrom(uint8_t address, uint8_t size) { return 0; } + uint8_t requestFrom(int address, int size, int sendStop) { return 0; } + size_t write(uint8_t) { return 1; } + size_t write(const uint8_t *b, size_t n) { return n; } + int available(){ return 0; } + int read(void) { return 0; } + int peek(void) { return 0; } + bool end(){}; +}; + +extern TwoWire Wire; +extern TwoWire Wire1; + +#endif + From 343a7661257482c3e72184316392c69a626955f1 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 00:34:39 -0800 Subject: [PATCH 04/27] Fix missing #include (references sprintf) --- src/helpers/AdvertDataHelpers.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers/AdvertDataHelpers.cpp b/src/helpers/AdvertDataHelpers.cpp index 0e05620ec2..6774e2576b 100644 --- a/src/helpers/AdvertDataHelpers.cpp +++ b/src/helpers/AdvertDataHelpers.cpp @@ -1,4 +1,5 @@ #include +#include uint8_t AdvertDataBuilder::encodeTo(uint8_t app_data[]) { app_data[0] = _type; @@ -84,4 +85,4 @@ void AdvertTimeHelper::formatRelativeTimeDiff(char dest[], int32_t seconds_from_ } } } -} \ No newline at end of file +} From a13eb42908999c8d66820da0552d841a1e8c43fa Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 00:35:23 -0800 Subject: [PATCH 05/27] Fix inconsistent typing (data is a uint8_t) --- src/helpers/BaseChatMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 9b1eb1ce78..d461afa343 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -159,7 +159,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) { uint32_t timestamp; memcpy(×tamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) - uint flags = data[4] >> 2; // message attempt number, and other flags + uint8_t flags = data[4] >> 2; // message attempt number, and other flags // len can be > original length, but 'text' will be padded with zeroes data[len] = 0; // need to make a C string again, with null terminator From 35c553cef02899b0cad951d5d5c5e892fc930841 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 00:36:29 -0800 Subject: [PATCH 06/27] Add missing #include (references ::random() from base arduino) --- src/helpers/radiolib/RadioLibWrappers.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 25cc53589a..0af3c656ca 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -2,6 +2,7 @@ #include #include +#include class RadioLibWrapper : public mesh::Radio { protected: From c06e197961c7e899feed168d44ac1f4e3d861fec Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 00:37:25 -0800 Subject: [PATCH 07/27] Use the adafruit LittleFS filesystem API when compiling for native. --- src/helpers/ClientACL.cpp | 2 +- src/helpers/CommonCLI.cpp | 2 +- src/helpers/IdentityStore.cpp | 4 ++-- src/helpers/IdentityStore.h | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/helpers/ClientACL.cpp b/src/helpers/ClientACL.cpp index 4ea19fd297..b49ad59d47 100644 --- a/src/helpers/ClientACL.cpp +++ b/src/helpers/ClientACL.cpp @@ -1,7 +1,7 @@ #include "ClientACL.h" static File openWrite(FILESYSTEM* _fs, const char* filename) { - #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NATIVE_PLATFORM) _fs->remove(filename); return _fs->open(filename, FILE_O_WRITE); #elif defined(RP2040_PLATFORM) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 88327aa89e..efc8a08529 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -98,7 +98,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { } void CommonCLI::savePrefs(FILESYSTEM* fs) { -#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NATIVE_PLATFORM) fs->remove("/com_prefs"); File file = fs->open("/com_prefs", FILE_O_WRITE); #elif defined(RP2040_PLATFORM) diff --git a/src/helpers/IdentityStore.cpp b/src/helpers/IdentityStore.cpp index dc85d69cdd..e598777240 100644 --- a/src/helpers/IdentityStore.cpp +++ b/src/helpers/IdentityStore.cpp @@ -46,7 +46,7 @@ bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id) { char filename[40]; sprintf(filename, "%s/%s.id", _dir, name); -#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NATIVE_PLATFORM) _fs->remove(filename); File file = _fs->open(filename, FILE_O_WRITE); #elif defined(RP2040_PLATFORM) @@ -68,7 +68,7 @@ bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id, const char filename[40]; sprintf(filename, "%s/%s.id", _dir, name); -#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NATIVE_PLATFORM) _fs->remove(filename); File file = _fs->open(filename, FILE_O_WRITE); #elif defined(RP2040_PLATFORM) diff --git a/src/helpers/IdentityStore.h b/src/helpers/IdentityStore.h index d0d7ee457e..021f38026a 100644 --- a/src/helpers/IdentityStore.h +++ b/src/helpers/IdentityStore.h @@ -3,7 +3,7 @@ #if defined(ESP32) || defined(RP2040_PLATFORM) #include #define FILESYSTEM fs::FS -#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NATIVE_PLATFORM) #include #define FILESYSTEM Adafruit_LittleFS From 77c9c3df20d0cfff8dfc3c9bee3894f0eb6731b3 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 00:53:26 -0800 Subject: [PATCH 08/27] add test invocation to build.sh Doesn't really add any value over `pio test -e native` but stays consistent with the current build procedure and any -D defines that may become mandatory later. --- build.sh | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/build.sh b/build.sh index f212794173..d7f8c6d587 100755 --- a/build.sh +++ b/build.sh @@ -13,6 +13,7 @@ Commands: build-companion-firmwares: Build all companion firmwares for all build targets. build-repeater-firmwares: Build all repeater firmwares for all build targets. build-room-server-firmwares: Build all chat room server firmwares for all build targets. + test: Run test on the given target (typically 'native') Examples: Build firmware for the "RAK_4631_repeater" device target @@ -68,9 +69,7 @@ get_pio_envs_ending_with_string() { done } -# build firmware for the provided pio env in $1 -build_firmware() { - +set_build_env() { # get git commit sha COMMIT_HASH=$(git rev-parse --short HEAD) @@ -87,13 +86,17 @@ build_firmware() { # e.g: v1.0.0-abcdef FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}-${COMMIT_HASH}" + # add firmware version info to end of existing platformio build flags in environment vars + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DFIRMWARE_BUILD_DATE='\"${FIRMWARE_BUILD_DATE}\"' -DFIRMWARE_VERSION='\"${FIRMWARE_VERSION_STRING}\"'" +} + +# build firmware for the provided pio env in $1 +build_firmware() { + # craft filename # e.g: RAK_4631_Repeater-v1.0.0-SHA FIRMWARE_FILENAME="$1-${FIRMWARE_VERSION_STRING}" - # add firmware version info to end of existing platformio build flags in environment vars - export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DFIRMWARE_BUILD_DATE='\"${FIRMWARE_BUILD_DATE}\"' -DFIRMWARE_VERSION='\"${FIRMWARE_VERSION_STRING}\"'" - # build firmware target pio run -e $1 @@ -189,6 +192,18 @@ build_firmwares() { build_room_server_firmwares } +run_tests() { + envs=($(get_pio_envs_containing_string "$1")) + for env in "${envs[@]}"; do + run_test $env + done +} + +run_test() { + set_build_env + pio test -e $1 +} + # clean build dir rm -rf out mkdir -p out @@ -219,4 +234,6 @@ elif [[ $1 == "build-repeater-firmwares" ]]; then build_repeater_firmwares elif [[ $1 == "build-room-server-firmwares" ]]; then build_room_server_firmwares +elif [[ $1 == "test" ]] ; then + run_tests "$2" fi From 6eb34a76e42fa36c3c0cd286702d7e8de6678c4f Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 00:58:36 -0800 Subject: [PATCH 09/27] Enable (native) unit testing. Uses the native platform, which compiles with the host system's compilers. This was tested on MacOS' GNU c++. Use the skaygin/ArduinoNative libdep for the Arduino platform library dependencies; it's the most complete native mock currently available in PlatformIO and covers almost everything we use. Reuses the preexisting stm32 clone of Adafruit's LittleFS for our filesystem dependency, which seemed to be the easiest path (though it's also the most demanding of the SPI interface.) Uses private mocks of SPI and Wire. The initial two sets of unit tests are fairly trivial, covering our SHA256 library and mesh::Utils::toHex(), just to show that everything's working. --- platformio.ini | 20 +++++++++++ test/test_common/test_utils.cpp | 59 +++++++++++++++++++++++++++++++++ test/test_native/trivial.cpp | 36 ++++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 test/test_common/test_utils.cpp create mode 100644 test/test_native/trivial.cpp diff --git a/platformio.ini b/platformio.ini index 3907cf64b2..f0a1beee00 100644 --- a/platformio.ini +++ b/platformio.ini @@ -146,3 +146,23 @@ lib_deps = stevemarple/MicroNMEA @ ^2.0.6 adafruit/Adafruit BME680 Library @ ^2.0.4 adafruit/Adafruit BMP085 Library @ ^1.2.4 + +; ----------------- Native (for tests) ----------------- +[env] +test_framework = googletest +test_speed = 115200 + +[env:native] +platform = native +lib_compat_mode = off +test_build_src = true +lib_deps = + ${arduino_base.lib_deps} + skaygin/ArduinoNative + file://arch/stm32/Adafruit_LittleFS_stm32 + file://test/mocks/Wire + file://test/mocks/SPI +build_flags = ${arduino_base.build_flags} + -D_USE_MATH_DEFINES -DNATIVE_PLATFORM -DINPUT_PULLDOWN=0x3 +build_src_filter = ${arduino_base.build_src_filter} + - diff --git a/test/test_common/test_utils.cpp b/test/test_common/test_utils.cpp new file mode 100644 index 0000000000..54f26dc913 --- /dev/null +++ b/test/test_common/test_utils.cpp @@ -0,0 +1,59 @@ +#include + +#include "Utils.h" + +using namespace mesh; + + +TEST(NopTest, UtilTests) +{ + EXPECT_EQ(1, 1); +} + +TEST(SHA256, UtilTests) +{ + uint8_t hash[257]; + memset(hash, 0, sizeof(hash)); + uint8_t msg[] = "foo"; + mesh::Utils::sha256(hash, (size_t)sizeof(hash), msg, 3); + EXPECT_STREQ((char*)hash, + (char*)"\x2c\x26\xb4\x6b\x68\xff\xc6\x8f\xf9\x9b\x45\x3c\x1d\x30\x41\x34\x13\x42\x2d\x70\x64\x83\xbf\xa0\xf9\x8a\x5e\x88\x62\x66\xe7\xae"); +} + +TEST(ToHex, UtilTests) +{ + char dst[20]; + uint8_t src[] = "\x01\x7f\x80\xff"; + mesh::Utils::toHex(&dst[0], src, 4); + EXPECT_STREQ(dst, (const char*)"017F80FF"); +} + +#if defined(ARDUINO) +#include + +void setup() +{ + Serial.begin(115200); + ::testing::InitGoogleTest(); +} + +void loop() +{ + if (RUN_ALL_TESTS()) + ; + delay(1000); +} + +#else + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + // or ::testing::InitGoogleMock(&argc, argv); + + if (RUN_ALL_TESTS()) + ; + return 0; +} + +#endif diff --git a/test/test_native/trivial.cpp b/test/test_native/trivial.cpp new file mode 100644 index 0000000000..250c7411a5 --- /dev/null +++ b/test/test_native/trivial.cpp @@ -0,0 +1,36 @@ +#include + +TEST(NopTest, ShouldPass) +{ + EXPECT_EQ(1, 1); +} + +#if defined(ARDUINO) +#include + +void setup() +{ + Serial.begin(115200); + ::testing::InitGoogleTest(); +} + +void loop() +{ + if (RUN_ALL_TESTS()) + ; + delay(1000); +} + +#else + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + // or ::testing::InitGoogleMock(&argc, argv); + + if (RUN_ALL_TESTS()) + ; + return 0; +} + +#endif From 28b4916446f6d8962371d4311300750430cf6207 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 11:53:36 -0800 Subject: [PATCH 10/27] Explain common vs native --- test/test_common/README.md | 8 ++++++++ test/test_native/README.md | 4 ++++ 2 files changed, 12 insertions(+) create mode 100644 test/test_common/README.md create mode 100644 test/test_native/README.md diff --git a/test/test_common/README.md b/test/test_common/README.md new file mode 100644 index 0000000000..76c3e17162 --- /dev/null +++ b/test/test_common/README.md @@ -0,0 +1,8 @@ +# Common tests + +This directory holds tests that are expected to pass on all platforms, +including native and on-device tests. + +Tests that exercise device-specific features should should not go here, +and should be capable of passing with hardware features mocked out +(e.g. SPI or Wire are present but return fake responses.) diff --git a/test/test_native/README.md b/test/test_native/README.md new file mode 100644 index 0000000000..68d886a4c2 --- /dev/null +++ b/test/test_native/README.md @@ -0,0 +1,4 @@ +# Native-only tests + +This directory holds tests that are only relevant when built for the native +platform (e.g. running tests that cannot work on any device). From 215f95653bb45c399957d6f59bfe6811be9a2a95 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 18:03:54 -0800 Subject: [PATCH 11/27] Fix suite/test arg ordering --- test/test_common/test_utils.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_common/test_utils.cpp b/test/test_common/test_utils.cpp index 54f26dc913..b5c20b164b 100644 --- a/test/test_common/test_utils.cpp +++ b/test/test_common/test_utils.cpp @@ -5,12 +5,12 @@ using namespace mesh; -TEST(NopTest, UtilTests) +TEST(UtilTests, NopTest) { EXPECT_EQ(1, 1); } -TEST(SHA256, UtilTests) +TEST(UtilTests, SHA256) { uint8_t hash[257]; memset(hash, 0, sizeof(hash)); @@ -20,7 +20,7 @@ TEST(SHA256, UtilTests) (char*)"\x2c\x26\xb4\x6b\x68\xff\xc6\x8f\xf9\x9b\x45\x3c\x1d\x30\x41\x34\x13\x42\x2d\x70\x64\x83\xbf\xa0\xf9\x8a\x5e\x88\x62\x66\xe7\xae"); } -TEST(ToHex, UtilTests) +TEST(UtilTests, toHex) { char dst[20]; uint8_t src[] = "\x01\x7f\x80\xff"; From 504bb65d4974c3d33d1734167509477fa238b973 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 18:20:36 -0800 Subject: [PATCH 12/27] more test coverage on mesh::Util Found the first bug, in fromHex() :) --- test/test_common/test_utils.cpp | 39 ++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/test/test_common/test_utils.cpp b/test/test_common/test_utils.cpp index b5c20b164b..7206ae4ddb 100644 --- a/test/test_common/test_utils.cpp +++ b/test/test_common/test_utils.cpp @@ -13,7 +13,7 @@ TEST(UtilTests, NopTest) TEST(UtilTests, SHA256) { uint8_t hash[257]; - memset(hash, 0, sizeof(hash)); + memset(hash, 0, sizeof(hash)); uint8_t msg[] = "foo"; mesh::Utils::sha256(hash, (size_t)sizeof(hash), msg, 3); EXPECT_STREQ((char*)hash, @@ -28,6 +28,43 @@ TEST(UtilTests, toHex) EXPECT_STREQ(dst, (const char*)"017F80FF"); } +TEST(UtilTests, fromHex) +{ + uint8_t dst[20]; + memset(dst, 0, sizeof(dst)); + uint8_t want[] = "\x01\x7f\x80\xff"; + EXPECT_TRUE(mesh::Utils::fromHex(&dst[0], 4, "017F80FF")); + EXPECT_STREQ((const char *)dst, (const char *)want); +} + +TEST(UtilTests, fromHexWrongSize) +{ + uint8_t dst[20]; + EXPECT_FALSE(mesh::Utils::fromHex(&dst[0], 5, "017F80FF")); +} + +// this should pass but does not, because fromHex() doesn't +// actually validate string contents and silently produces +// zeroes +// TEST(UtilTests, fromHexMalformed) +// { +// uint8_t dst[20]; +// memset(dst, 0, sizeof(dst)); +// EXPECT_FALSE(mesh::Utils::fromHex(&dst[0], 4, "01FG80FF")); +// } + +TEST(UtilTests, isHexChar) +{ + EXPECT_TRUE(mesh::Utils::isHexChar('0')); + EXPECT_TRUE(mesh::Utils::isHexChar('1')); + EXPECT_TRUE(mesh::Utils::isHexChar('9')); + EXPECT_TRUE(mesh::Utils::isHexChar('A')); + EXPECT_TRUE(mesh::Utils::isHexChar('F')); + EXPECT_FALSE(mesh::Utils::isHexChar('G')); + EXPECT_FALSE(mesh::Utils::isHexChar('\xff')); + EXPECT_FALSE(mesh::Utils::isHexChar('\x0')); +} + #if defined(ARDUINO) #include From de0dcfdeafe50d980be4b52230692ecef7f2dc4c Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 18:28:28 -0800 Subject: [PATCH 13/27] test parseTextParts --- test/test_common/test_utils.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/test_common/test_utils.cpp b/test/test_common/test_utils.cpp index 7206ae4ddb..d2f13327c0 100644 --- a/test/test_common/test_utils.cpp +++ b/test/test_common/test_utils.cpp @@ -65,6 +65,29 @@ TEST(UtilTests, isHexChar) EXPECT_FALSE(mesh::Utils::isHexChar('\x0')); } +TEST(UtilTests, parseTextParts) +{ + char text[10]; + memset(text, 0, sizeof(text)); + const char *parts[10]; + ASSERT_EQ(mesh::Utils::parseTextParts("", &parts[0], 10, ','), 0); + + strcpy(text, "a"); + ASSERT_EQ(mesh::Utils::parseTextParts(text, &parts[0], 10, ','), 1); + ASSERT_STREQ(parts[0], "a"); + + strcpy(text, "b,c"); + ASSERT_EQ(mesh::Utils::parseTextParts(text, &parts[0], 10, ','), 2); + ASSERT_STREQ(parts[0], "b"); + ASSERT_STREQ(parts[1], "c"); + + // This isn't normal string splitter behavior, but it's intentional + strcpy(text, "c,d,"); + ASSERT_EQ(mesh::Utils::parseTextParts(text, &parts[0], 10, ','), 2); + ASSERT_STREQ(parts[0], "c"); + ASSERT_STREQ(parts[1], "d"); +} + #if defined(ARDUINO) #include From 1cf4b7ee62e1b57d1e3d9daf8d8311630233a4de Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 21:02:13 -0800 Subject: [PATCH 14/27] Cover more of mesh::Utils - printHex (demonstrates a mock/spy stream) - 2-part sha256 --- test/test_common/test_utils.cpp | 59 +++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/test/test_common/test_utils.cpp b/test/test_common/test_utils.cpp index d2f13327c0..e49e93d19d 100644 --- a/test/test_common/test_utils.cpp +++ b/test/test_common/test_utils.cpp @@ -1,8 +1,12 @@ #include +#include +#include +#include #include "Utils.h" using namespace mesh; +using ::testing::InSequence; TEST(UtilTests, NopTest) @@ -18,6 +22,11 @@ TEST(UtilTests, SHA256) mesh::Utils::sha256(hash, (size_t)sizeof(hash), msg, 3); EXPECT_STREQ((char*)hash, (char*)"\x2c\x26\xb4\x6b\x68\xff\xc6\x8f\xf9\x9b\x45\x3c\x1d\x30\x41\x34\x13\x42\x2d\x70\x64\x83\xbf\xa0\xf9\x8a\x5e\x88\x62\x66\xe7\xae"); + + memset(hash, 0, sizeof(hash)); + mesh::Utils::sha256(hash, (size_t)sizeof(hash), msg, 1, msg+1, 2); + EXPECT_STREQ((char*)hash, + (char*)"\x2c\x26\xb4\x6b\x68\xff\xc6\x8f\xf9\x9b\x45\x3c\x1d\x30\x41\x34\x13\x42\x2d\x70\x64\x83\xbf\xa0\xf9\x8a\x5e\x88\x62\x66\xe7\xae"); } TEST(UtilTests, toHex) @@ -81,11 +90,55 @@ TEST(UtilTests, parseTextParts) ASSERT_STREQ(parts[0], "b"); ASSERT_STREQ(parts[1], "c"); + strcpy(text, "d,,e"); + ASSERT_EQ(mesh::Utils::parseTextParts(text, &parts[0], 10, ','), 3); + ASSERT_STREQ(parts[0], "d"); + ASSERT_STREQ(parts[1], ""); + ASSERT_STREQ(parts[2], "e"); + // This isn't normal string splitter behavior, but it's intentional - strcpy(text, "c,d,"); + strcpy(text, "f,g,"); ASSERT_EQ(mesh::Utils::parseTextParts(text, &parts[0], 10, ','), 2); - ASSERT_STREQ(parts[0], "c"); - ASSERT_STREQ(parts[1], "d"); + ASSERT_STREQ(parts[0], "f"); + ASSERT_STREQ(parts[1], "g"); +} + +class MockStream : public Stream { +public: + uint8_t *buffer; + size_t pos; + MockStream(uint8_t *b) + :buffer(b),pos(0) + { + buffer[0] = 0; + } + + void clear() { + pos = 0; + buffer[0] = 0; + } + + size_t write(uint8_t c) { + buffer[pos++] = c; + buffer[pos] = 0; + return 1; + } + + MOCK_METHOD(int, available, (), (override)); + MOCK_METHOD(size_t, write, (const uint8_t *buffer, size_t size), (override)); + MOCK_METHOD(int, availableForWrite, (), (override)); + MOCK_METHOD(int, read, (), (override)); + MOCK_METHOD(int, peek, (), (override)); +}; + +TEST(UtilTests, printHex) +{ + uint8_t out[10]; + MockStream s(&out[0]); + + const uint8_t src[] = "\x00\x7f\xab\xff"; + mesh::Utils::printHex(s, src, 4); + EXPECT_STREQ((const char *)out, "007FABFF"); } #if defined(ARDUINO) From 0912f1f4f42a618461a349dc2e6c5a491afacc47 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 21:58:38 -0800 Subject: [PATCH 15/27] Add a native-asan env with address sanitization checks. It's fast enough (and the current set of tests is small enough) to turn on for the main native env, but not all local compilers will support it so I made it a separate target. Usage: pio test -e native-asan Current tests all pass asan checks. --- platformio.ini | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/platformio.ini b/platformio.ini index f0a1beee00..0e940a2fec 100644 --- a/platformio.ini +++ b/platformio.ini @@ -166,3 +166,10 @@ build_flags = ${arduino_base.build_flags} -D_USE_MATH_DEFINES -DNATIVE_PLATFORM -DINPUT_PULLDOWN=0x3 build_src_filter = ${arduino_base.build_src_filter} - + +[env:native-asan] +extends = env:native +platform = native +build_flags = ${env:native.build_flags} + -fsanitize=address + -fsanitize=bounds From f7e618ae1c32375dae4982b0968336c875673603 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 22:25:30 -0800 Subject: [PATCH 16/27] Move main() to its own file so it doesn't get copied to other test suites --- test/test_common/test_main.cpp | 31 +++++++++++++++++++++++++++++++ test/test_common/test_utils.cpp | 30 ------------------------------ 2 files changed, 31 insertions(+), 30 deletions(-) create mode 100644 test/test_common/test_main.cpp diff --git a/test/test_common/test_main.cpp b/test/test_common/test_main.cpp new file mode 100644 index 0000000000..7312e60449 --- /dev/null +++ b/test/test_common/test_main.cpp @@ -0,0 +1,31 @@ +#include + +#if defined(ARDUINO) +#include + +void setup() +{ + Serial.begin(115200); + ::testing::InitGoogleTest(); +} + +void loop() +{ + if (RUN_ALL_TESTS()) + ; + delay(1000); +} + +#else + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + // or ::testing::InitGoogleMock(&argc, argv); + + if (RUN_ALL_TESTS()) + ; + return 0; +} + +#endif diff --git a/test/test_common/test_utils.cpp b/test/test_common/test_utils.cpp index e49e93d19d..e4a300a120 100644 --- a/test/test_common/test_utils.cpp +++ b/test/test_common/test_utils.cpp @@ -140,33 +140,3 @@ TEST(UtilTests, printHex) mesh::Utils::printHex(s, src, 4); EXPECT_STREQ((const char *)out, "007FABFF"); } - -#if defined(ARDUINO) -#include - -void setup() -{ - Serial.begin(115200); - ::testing::InitGoogleTest(); -} - -void loop() -{ - if (RUN_ALL_TESTS()) - ; - delay(1000); -} - -#else - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - // or ::testing::InitGoogleMock(&argc, argv); - - if (RUN_ALL_TESTS()) - ; - return 0; -} - -#endif From cab8938169e39a34f0a7236ea88e008b2f79d337 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 22:26:09 -0800 Subject: [PATCH 17/27] Initial mesh::Identity tests (constructors) --- test/test_common/test_identity.cpp | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 test/test_common/test_identity.cpp diff --git a/test/test_common/test_identity.cpp b/test/test_common/test_identity.cpp new file mode 100644 index 0000000000..411ab8c383 --- /dev/null +++ b/test/test_common/test_identity.cpp @@ -0,0 +1,46 @@ +#include +#include +#include +#include + +#include "Identity.h" + +using namespace mesh; + + +class ConstantValueStream : public Stream { +public: + const uint8_t *buffer_; + size_t pos_, len_; + + ConstantValueStream(const uint8_t *b, size_t len) + :buffer_(b),pos_(0),len_(len) + {} + + int available() { + return (int)(len_ - pos_); + } + MOCK_METHOD(size_t, write, (uint8_t c), (override)); + MOCK_METHOD(size_t, write, (const uint8_t *buffer, size_t size), (override)); + MOCK_METHOD(int, availableForWrite, (), (override)); + int read() { + if (pos_ >= len_) { + return 0; + } + return (int)buffer_[pos_++]; + } + MOCK_METHOD(int, peek, (), (override)); +}; + +TEST(IdentityTests, Identity) +{ + mesh::Identity id; + const uint8_t pubhex[] = + "87A47F423042DBEE25D1EA5CCC387FBAFE90FD435FA4A1237460E20C49D1EE74"; + + mesh::Identity fromPubkey(&pubhex[0]); + + ConstantValueStream cs(&pubhex[0], 64); + + ASSERT_TRUE(id.readFrom(cs)); +} From 996fb1d8dc65b1c3d35a780acfd946bcabec38dd Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 22:29:47 -0800 Subject: [PATCH 18/27] Group the mock streams together --- test/test_common/mock_streams.h | 57 ++++++++++++++++++++++++++++++ test/test_common/test_identity.cpp | 26 +------------- test/test_common/test_utils.cpp | 30 +--------------- 3 files changed, 59 insertions(+), 54 deletions(-) create mode 100644 test/test_common/mock_streams.h diff --git a/test/test_common/mock_streams.h b/test/test_common/mock_streams.h new file mode 100644 index 0000000000..875218bde7 --- /dev/null +++ b/test/test_common/mock_streams.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +class MockStream : public Stream { +public: + uint8_t *buffer; + size_t pos; + MockStream(uint8_t *b) + :buffer(b),pos(0) + { + buffer[0] = 0; + } + + void clear() { + pos = 0; + buffer[0] = 0; + } + + size_t write(uint8_t c) { + buffer[pos++] = c; + buffer[pos] = 0; + return 1; + } + + MOCK_METHOD(int, available, (), (override)); + MOCK_METHOD(size_t, write, (const uint8_t *buffer, size_t size), (override)); + MOCK_METHOD(int, availableForWrite, (), (override)); + MOCK_METHOD(int, read, (), (override)); + MOCK_METHOD(int, peek, (), (override)); +}; + +class ConstantValueStream : public Stream { +public: + const uint8_t *buffer_; + size_t pos_, len_; + + ConstantValueStream(const uint8_t *b, size_t len) + :buffer_(b),pos_(0),len_(len) + {} + + int available() { + return (int)(len_ - pos_); + } + MOCK_METHOD(size_t, write, (uint8_t c), (override)); + MOCK_METHOD(size_t, write, (const uint8_t *buffer, size_t size), (override)); + MOCK_METHOD(int, availableForWrite, (), (override)); + int read() { + if (pos_ >= len_) { + return 0; + } + return (int)buffer_[pos_++]; + } + MOCK_METHOD(int, peek, (), (override)); +}; + diff --git a/test/test_common/test_identity.cpp b/test/test_common/test_identity.cpp index 411ab8c383..a6f08879b5 100644 --- a/test/test_common/test_identity.cpp +++ b/test/test_common/test_identity.cpp @@ -3,35 +3,11 @@ #include #include +#include "mock_streams.h" #include "Identity.h" using namespace mesh; - -class ConstantValueStream : public Stream { -public: - const uint8_t *buffer_; - size_t pos_, len_; - - ConstantValueStream(const uint8_t *b, size_t len) - :buffer_(b),pos_(0),len_(len) - {} - - int available() { - return (int)(len_ - pos_); - } - MOCK_METHOD(size_t, write, (uint8_t c), (override)); - MOCK_METHOD(size_t, write, (const uint8_t *buffer, size_t size), (override)); - MOCK_METHOD(int, availableForWrite, (), (override)); - int read() { - if (pos_ >= len_) { - return 0; - } - return (int)buffer_[pos_++]; - } - MOCK_METHOD(int, peek, (), (override)); -}; - TEST(IdentityTests, Identity) { mesh::Identity id; diff --git a/test/test_common/test_utils.cpp b/test/test_common/test_utils.cpp index e4a300a120..6e1815d3bc 100644 --- a/test/test_common/test_utils.cpp +++ b/test/test_common/test_utils.cpp @@ -3,10 +3,10 @@ #include #include +#include "mock_streams.h" #include "Utils.h" using namespace mesh; -using ::testing::InSequence; TEST(UtilTests, NopTest) @@ -103,34 +103,6 @@ TEST(UtilTests, parseTextParts) ASSERT_STREQ(parts[1], "g"); } -class MockStream : public Stream { -public: - uint8_t *buffer; - size_t pos; - MockStream(uint8_t *b) - :buffer(b),pos(0) - { - buffer[0] = 0; - } - - void clear() { - pos = 0; - buffer[0] = 0; - } - - size_t write(uint8_t c) { - buffer[pos++] = c; - buffer[pos] = 0; - return 1; - } - - MOCK_METHOD(int, available, (), (override)); - MOCK_METHOD(size_t, write, (const uint8_t *buffer, size_t size), (override)); - MOCK_METHOD(int, availableForWrite, (), (override)); - MOCK_METHOD(int, read, (), (override)); - MOCK_METHOD(int, peek, (), (override)); -}; - TEST(UtilTests, printHex) { uint8_t out[10]; From ca0de951cc5ed2be3145bac28a30b7b0198e7154 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 22:36:41 -0800 Subject: [PATCH 19/27] Test Identity::writeTo() --- test/test_common/mock_streams.h | 7 ++++++- test/test_common/test_identity.cpp | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/test/test_common/mock_streams.h b/test/test_common/mock_streams.h index 875218bde7..b3d24145e7 100644 --- a/test/test_common/mock_streams.h +++ b/test/test_common/mock_streams.h @@ -24,8 +24,13 @@ class MockStream : public Stream { return 1; } + size_t write(const uint8_t *src, size_t size) override { + memcpy(buffer, src, size); + pos += size; + return size; + } + MOCK_METHOD(int, available, (), (override)); - MOCK_METHOD(size_t, write, (const uint8_t *buffer, size_t size), (override)); MOCK_METHOD(int, availableForWrite, (), (override)); MOCK_METHOD(int, read, (), (override)); MOCK_METHOD(int, peek, (), (override)); diff --git a/test/test_common/test_identity.cpp b/test/test_common/test_identity.cpp index a6f08879b5..2c2de8bd75 100644 --- a/test/test_common/test_identity.cpp +++ b/test/test_common/test_identity.cpp @@ -12,11 +12,16 @@ TEST(IdentityTests, Identity) { mesh::Identity id; const uint8_t pubhex[] = - "87A47F423042DBEE25D1EA5CCC387FBAFE90FD435FA4A1237460E20C49D1EE74"; + "87A47F423042DBEE25D1EA5CCC387FBA"; mesh::Identity fromPubkey(&pubhex[0]); ConstantValueStream cs(&pubhex[0], 64); ASSERT_TRUE(id.readFrom(cs)); + + uint8_t buffer[80]; + MockStream bs(&buffer[0]); + ASSERT_TRUE(id.writeTo(bs)); + ASSERT_STREQ((const char *)bs.buffer, (const char *)pubhex); } From 9785cc6dd10c8669658d2c72c6379e7507562c09 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 22:49:51 -0800 Subject: [PATCH 20/27] mesh::LocalIdentity initializer tests --- test/test_common/test_identity.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/test_common/test_identity.cpp b/test/test_common/test_identity.cpp index 2c2de8bd75..ccccd605ab 100644 --- a/test/test_common/test_identity.cpp +++ b/test/test_common/test_identity.cpp @@ -3,6 +3,7 @@ #include #include +#include #include "mock_streams.h" #include "Identity.h" @@ -25,3 +26,21 @@ TEST(IdentityTests, Identity) ASSERT_TRUE(id.writeTo(bs)); ASSERT_STREQ((const char *)bs.buffer, (const char *)pubhex); } + +TEST(IdentityTests, LocalIdentity) +{ + uint8_t pub_key[PUB_KEY_SIZE], prv_key[PRV_KEY_SIZE], seed[SEED_SIZE]; + memset(seed, 0, SEED_SIZE); + ed25519_create_keypair(pub_key, prv_key, seed); + + uint8_t stored_key[PUB_KEY_SIZE+PRV_KEY_SIZE+SEED_SIZE]; + memcpy(stored_key, pub_key, PUB_KEY_SIZE); + memcpy(stored_key+PUB_KEY_SIZE, prv_key, PRV_KEY_SIZE); + // we're not saving seeds yet + memset(stored_key+PUB_KEY_SIZE+PRV_KEY_SIZE, 0, SEED_SIZE); + + ConstantValueStream skf(stored_key, sizeof(stored_key)); + mesh::LocalIdentity id; + ASSERT_TRUE(id.readFrom(skf)); + +} From 11030789036f6c30789207d2e3766872e0c696e2 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 23:13:24 -0800 Subject: [PATCH 21/27] Exerise mesh::LocalIdentity loading/saving --- test/test_common/mock_streams.h | 14 +++++++------- test/test_common/test_identity.cpp | 26 +++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/test/test_common/mock_streams.h b/test/test_common/mock_streams.h index b3d24145e7..6368d12e05 100644 --- a/test/test_common/mock_streams.h +++ b/test/test_common/mock_streams.h @@ -38,24 +38,24 @@ class MockStream : public Stream { class ConstantValueStream : public Stream { public: - const uint8_t *buffer_; - size_t pos_, len_; + const uint8_t *buffer; + size_t pos, len; - ConstantValueStream(const uint8_t *b, size_t len) - :buffer_(b),pos_(0),len_(len) + ConstantValueStream(const uint8_t *b, size_t l) + :buffer(b),pos(0),len(l) {} int available() { - return (int)(len_ - pos_); + return (int)(len - pos); } MOCK_METHOD(size_t, write, (uint8_t c), (override)); MOCK_METHOD(size_t, write, (const uint8_t *buffer, size_t size), (override)); MOCK_METHOD(int, availableForWrite, (), (override)); int read() { - if (pos_ >= len_) { + if (pos >= len) { return 0; } - return (int)buffer_[pos_++]; + return (int)buffer[pos++]; } MOCK_METHOD(int, peek, (), (override)); }; diff --git a/test/test_common/test_identity.cpp b/test/test_common/test_identity.cpp index ccccd605ab..a25e138d98 100644 --- a/test/test_common/test_identity.cpp +++ b/test/test_common/test_identity.cpp @@ -6,6 +6,7 @@ #include #include "mock_streams.h" #include "Identity.h" +#include "Utils.h" using namespace mesh; @@ -27,20 +28,43 @@ TEST(IdentityTests, Identity) ASSERT_STREQ((const char *)bs.buffer, (const char *)pubhex); } +#define ZERO_PUB_KEY \ + "\x3B\x6A\x27\xBC\xCE\xB6\xA4\x2D\x62\xA3\xA8\xD0\x2A\x6F\x0D" \ + "\x73\x65\x32\x15\x77\x1D\xE2\x43\xA6\x3A\xC0\x48\xA1\x8B\x59" +#define ZERO_PRV_KEY \ + "\x50\x46\xAD\xC1\xDB\xA8\x38\x86\x7B\x2B\xBB\xFD\xD0\xC3\x42" \ + "\x3E\x58\xB5\x79\x70\xB5\x26\x7A\x90\xF5\x79\x60\x92\x4A\x87" \ + "\xF1\x56\x0A\x6A\x85\xEA\xA6\x42\xDA\xC8\x35\x42\x4B\x5D\x7C" \ + "\x8D\x63\x7C\x00\x40\x8C\x7A\x73\xDA\x67\x2B\x7F\x49\x85\x21" \ + "\x42\x0B\x6D\xD3" + TEST(IdentityTests, LocalIdentity) { + // create a zero identity uint8_t pub_key[PUB_KEY_SIZE], prv_key[PRV_KEY_SIZE], seed[SEED_SIZE]; memset(seed, 0, SEED_SIZE); ed25519_create_keypair(pub_key, prv_key, seed); + // create a Stream containing that identity uint8_t stored_key[PUB_KEY_SIZE+PRV_KEY_SIZE+SEED_SIZE]; memcpy(stored_key, pub_key, PUB_KEY_SIZE); memcpy(stored_key+PUB_KEY_SIZE, prv_key, PRV_KEY_SIZE); // we're not saving seeds yet memset(stored_key+PUB_KEY_SIZE+PRV_KEY_SIZE, 0, SEED_SIZE); - ConstantValueStream skf(stored_key, sizeof(stored_key)); + mesh::LocalIdentity id; ASSERT_TRUE(id.readFrom(skf)); + ASSERT_EQ(skf.pos, PUB_KEY_SIZE + PRV_KEY_SIZE); + + uint8_t buffer[1024]; + MockStream dump(&buffer[0]); + + ASSERT_TRUE(id.writeTo(dump)); + // Correct serialization is pubkey || prvkey (for now) + ASSERT_TRUE(memcmp(buffer, ZERO_PUB_KEY, PUB_KEY_SIZE)); + ASSERT_TRUE(memcmp(buffer+PUB_KEY_SIZE, ZERO_PRV_KEY, PRV_KEY_SIZE)); + // ... and for the moment, nothing else + ASSERT_EQ(dump.pos, PUB_KEY_SIZE + PRV_KEY_SIZE); } From 5c284914ab2fa6234e69c1448112c0b62df5c839 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 23:16:17 -0800 Subject: [PATCH 22/27] Fix an asan catch (in the test code) --- test/test_common/test_identity.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_common/test_identity.cpp b/test/test_common/test_identity.cpp index a25e138d98..7b77dc1eaf 100644 --- a/test/test_common/test_identity.cpp +++ b/test/test_common/test_identity.cpp @@ -23,6 +23,7 @@ TEST(IdentityTests, Identity) ASSERT_TRUE(id.readFrom(cs)); uint8_t buffer[80]; + memset(buffer, 0, sizeof(buffer)); MockStream bs(&buffer[0]); ASSERT_TRUE(id.writeTo(bs)); ASSERT_STREQ((const char *)bs.buffer, (const char *)pubhex); From 9784c98ccf1e4d669e657356ad09b612843cd97c Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Wed, 5 Nov 2025 21:52:28 -0800 Subject: [PATCH 23/27] Slightly smarter MockStream that can act like a std::string --- test/test_common/mock_streams.h | 78 +++++++++++++++++++++++++++--- test/test_common/test_identity.cpp | 26 +++++----- test/test_common/test_mocks.cpp | 44 +++++++++++++++++ test/test_common/test_utils.cpp | 6 +-- 4 files changed, 129 insertions(+), 25 deletions(-) create mode 100644 test/test_common/test_mocks.cpp diff --git a/test/test_common/mock_streams.h b/test/test_common/mock_streams.h index 6368d12e05..bd164ff2e4 100644 --- a/test/test_common/mock_streams.h +++ b/test/test_common/mock_streams.h @@ -2,38 +2,100 @@ #include #include +#include +#include + +MATCHER_P2(MemcmpAs, cb, len, "") { + bool result = true; + for (size_t i = 0; i < len; i++) { + if (cb[i] != arg[i]) { + *result_listener << "element #" << std::dec << i + << " differ: " << " 0x" << std::hex << static_cast(arg[i]) + << " vs " << " 0x" << std::hex << static_cast(cb[i]) + << "\n"; + result = false; + } else { + *result_listener << "element #" << std::dec << i + << " matches: " << " 0x" << std::hex << static_cast(arg[i]) << "\n"; + } + } + return result; +} class MockStream : public Stream { public: uint8_t *buffer; - size_t pos; + size_t pos, cap; + bool own_buffer; + + // internal buffer; can expand + MockStream() + :pos(0),cap(0),own_buffer(true) + { + buffer = {0}; + } + + // external buffer; assumed infinite, can't expand MockStream(uint8_t *b) - :buffer(b),pos(0) + :buffer(b),pos(0),cap(SIZE_MAX),own_buffer(false) { buffer[0] = 0; } + // external buffer, known size, can't expand + MockStream(uint8_t *b, size_t sz) + :buffer(b),pos(0),cap(sz),own_buffer(false) + { + if (cap>0) buffer[0] = 0; + } + + virtual ~MockStream() { + if (own_buffer && buffer != nullptr) { + free(buffer); + } + } + void clear() { pos = 0; - buffer[0] = 0; + if (cap>0) { + buffer[0] = 0; + } } size_t write(uint8_t c) { + if (!expand(pos+1)) return 0; buffer[pos++] = c; - buffer[pos] = 0; + if (cap>pos) buffer[pos] = 0; return 1; } - size_t write(const uint8_t *src, size_t size) override { - memcpy(buffer, src, size); - pos += size; - return size; + size_t write(const uint8_t *src, size_t len) { + if (!expand(pos+len)) return 0; + memcpy(buffer+pos, src, len); + pos += len; + if (cap>pos) buffer[pos] = 0; + return len; } MOCK_METHOD(int, available, (), (override)); MOCK_METHOD(int, availableForWrite, (), (override)); MOCK_METHOD(int, read, (), (override)); MOCK_METHOD(int, peek, (), (override)); + +private: + bool expand(size_t newsize) { + if (newsize > cap) { + if (!own_buffer) return false; + newsize = (newsize+0x1f) & (~0x1f); // round up to next 32 + uint8_t *exp = (uint8_t *)realloc(buffer, newsize); + if (exp == nullptr) { + return false; + } + buffer = exp; + cap = newsize; + } + return true; + } }; class ConstantValueStream : public Stream { diff --git a/test/test_common/test_identity.cpp b/test/test_common/test_identity.cpp index 7b77dc1eaf..94228a5ee6 100644 --- a/test/test_common/test_identity.cpp +++ b/test/test_common/test_identity.cpp @@ -24,20 +24,19 @@ TEST(IdentityTests, Identity) uint8_t buffer[80]; memset(buffer, 0, sizeof(buffer)); - MockStream bs(&buffer[0]); + MockStream bs; ASSERT_TRUE(id.writeTo(bs)); ASSERT_STREQ((const char *)bs.buffer, (const char *)pubhex); } #define ZERO_PUB_KEY \ - "\x3B\x6A\x27\xBC\xCE\xB6\xA4\x2D\x62\xA3\xA8\xD0\x2A\x6F\x0D" \ - "\x73\x65\x32\x15\x77\x1D\xE2\x43\xA6\x3A\xC0\x48\xA1\x8B\x59" + "\x3B\x6A\x27\xBC\xCE\xB6\xA4\x2D\x62\xA3\xA8\xD0\x2A\x6F\x0D\x73" \ + "\x65\x32\x15\x77\x1D\xE2\x43\xA6\x3A\xC0\x48\xA1\x8B\x59\xDA\x29" #define ZERO_PRV_KEY \ - "\x50\x46\xAD\xC1\xDB\xA8\x38\x86\x7B\x2B\xBB\xFD\xD0\xC3\x42" \ - "\x3E\x58\xB5\x79\x70\xB5\x26\x7A\x90\xF5\x79\x60\x92\x4A\x87" \ - "\xF1\x56\x0A\x6A\x85\xEA\xA6\x42\xDA\xC8\x35\x42\x4B\x5D\x7C" \ - "\x8D\x63\x7C\x00\x40\x8C\x7A\x73\xDA\x67\x2B\x7F\x49\x85\x21" \ - "\x42\x0B\x6D\xD3" + "\x50\x46\xAD\xC1\xDB\xA8\x38\x86\x7B\x2B\xBB\xFD\xD0\xC3\x42\x3E" \ + "\x58\xB5\x79\x70\xB5\x26\x7A\x90\xF5\x79\x60\x92\x4A\x87\xF1\x56" \ + "\x0A\x6A\x85\xEA\xA6\x42\xDA\xC8\x35\x42\x4B\x5D\x7C\x8D\x63\x7C" \ + "\x00\x40\x8C\x7A\x73\xDA\x67\x2B\x7F\x49\x85\x21\x42\x0B\x6D\xD3" TEST(IdentityTests, LocalIdentity) { @@ -58,13 +57,14 @@ TEST(IdentityTests, LocalIdentity) ASSERT_TRUE(id.readFrom(skf)); ASSERT_EQ(skf.pos, PUB_KEY_SIZE + PRV_KEY_SIZE); - uint8_t buffer[1024]; - MockStream dump(&buffer[0]); - + MockStream dump; ASSERT_TRUE(id.writeTo(dump)); // Correct serialization is pubkey || prvkey (for now) - ASSERT_TRUE(memcmp(buffer, ZERO_PUB_KEY, PUB_KEY_SIZE)); - ASSERT_TRUE(memcmp(buffer+PUB_KEY_SIZE, ZERO_PRV_KEY, PRV_KEY_SIZE)); + ASSERT_EQ(dump.pos, PUB_KEY_SIZE + PRV_KEY_SIZE); + EXPECT_THAT(dump.buffer, MemcmpAs((uint8_t *)ZERO_PUB_KEY, PUB_KEY_SIZE)); + // ASSERT_EQ(memcmp(buffer, ZERO_PUB_KEY, PUB_KEY_SIZE), 0); + EXPECT_THAT(dump.buffer+PUB_KEY_SIZE, + MemcmpAs((uint8_t*)ZERO_PRV_KEY, PRV_KEY_SIZE)); // ... and for the moment, nothing else ASSERT_EQ(dump.pos, PUB_KEY_SIZE + PRV_KEY_SIZE); diff --git a/test/test_common/test_mocks.cpp b/test/test_common/test_mocks.cpp new file mode 100644 index 0000000000..70c6d0a82e --- /dev/null +++ b/test/test_common/test_mocks.cpp @@ -0,0 +1,44 @@ +#include + +#include "mock_streams.h" + +//using namespace testing; + +TEST(MockStreamTests, ExternalBuffer) +{ + uint8_t buf[21]; + MockStream s(buf); + s.write((uint8_t*)"0123456789", 10); + ASSERT_EQ(s.pos, 10); + ASSERT_STREQ((const char *)s.buffer, (const char*)"0123456789"); + s.write((uint8_t*)"ABCDEFGHIJ", 10); + ASSERT_EQ(s.pos, 20); + EXPECT_THAT(buf, MemcmpAs("0123456789ABCDEFGHIJ", 20)); + + MockStream s2(buf, 4); + ASSERT_EQ(s2.write((uint8_t *)"12345", 5), 0); + ASSERT_EQ(s2.pos, 0); + ASSERT_EQ(s2.cap, 4); + ASSERT_EQ(s2.write((uint8_t *)"1234", 4), 4); + ASSERT_EQ(s2.pos, 4); + EXPECT_THAT(buf, MemcmpAs("1234", 4)); +} + +TEST(MockStreamTests, InternalBuffer) +{ + MockStream s1; + uint8_t z[65]; + memset(z, 0, sizeof(z)); + s1.write(z, sizeof(z)); + ASSERT_EQ(s1.pos, sizeof(z)); + ASSERT_GE(s1.cap, sizeof(z)); + + MockStream s2; + for (int i = 0; i < 1024; i++) { + s2.write('A'); + } + ASSERT_EQ(s2.pos, 1024); + ASSERT_GE(s2.cap, 1024); + ASSERT_EQ(s2.buffer[1023], 'A'); +} + diff --git a/test/test_common/test_utils.cpp b/test/test_common/test_utils.cpp index 6e1815d3bc..763c839d07 100644 --- a/test/test_common/test_utils.cpp +++ b/test/test_common/test_utils.cpp @@ -105,10 +105,8 @@ TEST(UtilTests, parseTextParts) TEST(UtilTests, printHex) { - uint8_t out[10]; - MockStream s(&out[0]); - + MockStream s; const uint8_t src[] = "\x00\x7f\xab\xff"; mesh::Utils::printHex(s, src, 4); - EXPECT_STREQ((const char *)out, "007FABFF"); + EXPECT_STREQ((const char *)s.buffer, "007FABFF"); } From 2ddaaed3dc217d7daf9aa3ede497208f7f0cdd07 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Tue, 28 Apr 2026 21:44:00 +1000 Subject: [PATCH 24/27] Fix undefined size_t --- src/MeshCore.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/MeshCore.h b/src/MeshCore.h index 2db1d4c3ec..2a786f2228 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #define MAX_HASH_SIZE 8 From 759d8b8e43887aa6ac86e2755fadc104962f3d14 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Tue, 28 Apr 2026 21:45:13 +1000 Subject: [PATCH 25/27] Fix test breakers - NATIVE_PLATFORM needs the same filesystem open() args as STM32 and NRF52 - Not all Stream implementations have a printf() (fix is simpler anyway) --- src/helpers/RegionMap.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 7b8399e260..5fbc3a1dc5 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -59,7 +59,7 @@ static const char* skip_hash(const char* name) { } static File openWrite(FILESYSTEM* _fs, const char* filename) { - #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(NATIVE_PLATFORM) _fs->remove(filename); return _fs->open(filename, FILE_O_WRITE); #elif defined(RP2040_PLATFORM) @@ -287,11 +287,9 @@ void RegionMap::printChildRegions(int indent, const RegionEntry* parent, Stream& out.print(' '); } - if (parent->flags & REGION_DENY_FLOOD) { - out.printf("%s%s\n", skip_hash(parent->name), parent->id == home_id ? "^" : ""); - } else { - out.printf("%s%s F\n", skip_hash(parent->name), parent->id == home_id ? "^" : ""); - } + out.print(skip_hash(parent->name)); + out.print(parent->id == home_id ? "^" : ""); + out.println(parent->flags & REGION_DENY_FLOOD ? "" : " F"); for (int i = 0; i < num_regions; i++) { auto r = ®ions[i]; From f9d8bcfccd9a90a69219378d7724fce4727b577d Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Tue, 28 Apr 2026 21:50:28 +1000 Subject: [PATCH 26/27] Don't use STREQ on strings that aren't guaranteed to be null-terminates --- test/test_common/test_identity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_common/test_identity.cpp b/test/test_common/test_identity.cpp index 94228a5ee6..f61b301af6 100644 --- a/test/test_common/test_identity.cpp +++ b/test/test_common/test_identity.cpp @@ -26,7 +26,7 @@ TEST(IdentityTests, Identity) memset(buffer, 0, sizeof(buffer)); MockStream bs; ASSERT_TRUE(id.writeTo(bs)); - ASSERT_STREQ((const char *)bs.buffer, (const char *)pubhex); + ASSERT_EQ(memcmp(bs.buffer, pubhex, 32), 0); } #define ZERO_PUB_KEY \ From bf6dbc01518e3b8ebf46936807b5ba215d7a008c Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Tue, 28 Apr 2026 21:51:04 +1000 Subject: [PATCH 27/27] Don't include CayenneLPP when building native for tests. SensorManager as a virtual interface is fine for testing, but CayenneLPP makes assumptions about Arduino which aren't valid for ArduinoNative and sets up clashes between ArduinoNative and the native C++ libraries (in particular, ArduinoNative wants to #define round, which appears as a symbol in some STL implementations which CayenneLPP will try to fall back on.) Since we're not exercising SensorManager in tests yet, the short-term workaround is to hide the header and forward-declare the class. If we do get to the point of exercising it for testing, the right fix would probably be to send a PR against ArduinoNative to remove that #define as the mainline Arduino libraries did a few years ago. --- src/helpers/SensorManager.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/helpers/SensorManager.h b/src/helpers/SensorManager.h index 89a174c228..acdbe08ea6 100644 --- a/src/helpers/SensorManager.h +++ b/src/helpers/SensorManager.h @@ -1,6 +1,10 @@ #pragma once +#ifdef NATIVE_PLATFORM +struct CayenneLPP; // Work around a clash between CayenneLPP & ArduinoNative +#else #include +#endif #include "sensors/LocationProvider.h" #define TELEM_PERM_BASE 0x01 // 'base' permission includes battery