From 52c67b12f85cfc3d6bf498cc75cca42fe742bbb7 Mon Sep 17 00:00:00 2001 From: cjkas Date: Sun, 11 Oct 2026 12:54:39 +0200 Subject: [PATCH] test: add native Unity env with Utils tests Adds an [env:native] for host-side unit tests (pio test -e native) with a minimal Arduino.h stub, and covers the Utils helpers: _trim, toBoolean, SETCHARPROP, sortArray and the Timestamp conversions. The firmware envs now extend a shared [esp32] section instead of [env] so the native env doesn't inherit the Arduino framework, the ESP32 libraries or the build scripts. The half-hour calcTZOffset cases fail: +05:30 comes back as 2300. Co-Authored-By: Claude Opus 5.5 --- platformio.ini | 29 ++++- test/stubs/Arduino.h | 20 ++++ test/test_utils/test_utils.cpp | 197 +++++++++++++++++++++++++++++++++ 3 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 test/stubs/Arduino.h create mode 100644 test/test_utils/test_utils.cpp diff --git a/platformio.ini b/platformio.ini index 3333977..a794091 100644 --- a/platformio.ini +++ b/platformio.ini @@ -12,9 +12,13 @@ default_envs = esp32devdbg src_dir = src -[env] +; Shared by every firmware env (they extend it). Kept out of [env] so the +; native test env doesn't inherit the Arduino framework and ESP32 libraries. +[esp32] platform = espressif32 @ 6.13.0 framework = arduino +; Unit tests only run on the PC: pio test -e native +test_ignore = * lib_deps = bblanchon/ArduinoJson@^7.2.2 lsatan/SmartRC-CC1101-Driver-Lib@^2.5.7 @@ -43,35 +47,40 @@ monitor_filters = ; task on core 1 (with loop()) never competes with network handling. [dualcore] build_flags = - ${env.build_flags} + ${esp32.build_flags} -DCONFIG_ASYNC_TCP_RUNNING_CORE=0 [env:esp32dev] +extends = esp32 board = esp32dev board_build.partitions = esp32_3MB.csv build_flags = ${dualcore.build_flags} [env:esp32devdbg] +extends = esp32 board = esp32dev build_type = debug board_build.partitions = esp32_3MB.csv build_flags = ${dualcore.build_flags} [env:esp32c3] +extends = esp32 board = esp32-c3-devkitm-1 board_build.partitions = esp32_3MB.csv [env:esp32s3] +extends = esp32 board = esp32-s3-devkitc-1 build_flags = ${dualcore.build_flags} [env:esp32c6] +extends = esp32 platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip board = seeed_xiao_esp32c6 build_type = debug board_build.partitions = esp32_huge_app.csv -build_flags = - ${env.build_flags} +build_flags = + ${esp32.build_flags} -DCONFIG_FREERTOS_HZ=1000 -include "driver/gpio.h" lib_deps = @@ -86,3 +95,15 @@ lib_deps = ; High-level wrappers/servers ESP32Async/ESPAsyncWebServer +; Host-side unit tests: pio test -e native (needs gcc/g++ on PATH). +; Only the sources the tests cover are built; test/stubs stands in for Arduino.h. +[env:native] +platform = native +test_framework = unity +test_build_src = yes +build_src_filter = -<*> + +build_flags = + -std=gnu++17 + -Isrc + -Itest/stubs + -D_POSIX_THREAD_SAFE_FUNCTIONS diff --git a/test/stubs/Arduino.h b/test/stubs/Arduino.h new file mode 100644 index 0000000..3755257 --- /dev/null +++ b/test/stubs/Arduino.h @@ -0,0 +1,20 @@ +// Minimal stand-in for the Arduino core so firmware sources can be built for +// the native test env (pio test -e native). Only add what tested code uses. +#ifndef arduino_stub_h +#define arduino_stub_h +#include +#include +#include +#include +#include +#include +#include + +// Fixed so anything formatted from it is deterministic. +inline unsigned long millis() { return 0; } +inline bool getLocalTime(struct tm *info, uint32_t ms = 5000) { + (void)ms; + time_t now = time(nullptr); + return localtime_r(&now, info) != nullptr; +} +#endif diff --git a/test/test_utils/test_utils.cpp b/test/test_utils/test_utils.cpp new file mode 100644 index 0000000..0a14039 --- /dev/null +++ b/test/test_utils/test_utils.cpp @@ -0,0 +1,197 @@ +#include +#include "Utils.h" + +// The C runtime only picks up TZ after it is re-read. +static void setTZ(const char *tz) { +#ifdef _WIN32 + _putenv_s("TZ", tz); + _tzset(); +#else + setenv("TZ", tz, 1); + tzset(); +#endif +} + +// 2024-03-05T07:08:09Z +static const time_t SAMPLE_UTC = 1709622489; + +void setUp() { setTZ("UTC0"); } +void tearDown() {} + +/********************************************************************* + * _trim + ********************************************************************/ +static void test_trim_strips_whitespace_and_quotes() { + char str[] = " \t\"Living Room\"\r\n"; + _trim(str); + TEST_ASSERT_EQUAL_STRING("Living Room", str); +} +static void test_trim_keeps_clean_string() { + char str[] = "Kitchen"; + _trim(str); + TEST_ASSERT_EQUAL_STRING("Kitchen", str); +} +static void test_trim_all_whitespace_becomes_empty() { + char str[] = " \t\r\n\" "; + _trim(str); + TEST_ASSERT_EQUAL_STRING("", str); +} +static void test_trim_empty_string() { + char str[] = ""; + _trim(str); + TEST_ASSERT_EQUAL_STRING("", str); +} + +/********************************************************************* + * toBoolean + ********************************************************************/ +static void test_toBoolean_missing_uses_default() { + TEST_ASSERT_TRUE(toBoolean(nullptr, true)); + TEST_ASSERT_FALSE(toBoolean(nullptr, false)); + TEST_ASSERT_TRUE(toBoolean("", true)); + TEST_ASSERT_FALSE(toBoolean("", false)); +} +static void test_toBoolean_true_values() { + TEST_ASSERT_TRUE(toBoolean("true", false)); + TEST_ASSERT_TRUE(toBoolean("True", false)); + TEST_ASSERT_TRUE(toBoolean("1", false)); +} +static void test_toBoolean_false_values() { + TEST_ASSERT_FALSE(toBoolean("false", true)); + TEST_ASSERT_FALSE(toBoolean("False", true)); + TEST_ASSERT_FALSE(toBoolean("0", true)); +} +static void test_toBoolean_unknown_uses_default() { + TEST_ASSERT_TRUE(toBoolean("yes", true)); + TEST_ASSERT_FALSE(toBoolean("yes", false)); +} + +/********************************************************************* + * SETCHARPROP + ********************************************************************/ +static void test_setcharprop_copies_short_value() { + char prop[8]; + SETCHARPROP(prop, "abc", sizeof(prop)); + TEST_ASSERT_EQUAL_STRING("abc", prop); +} +static void test_setcharprop_truncates_long_value() { + char prop[5]; + SETCHARPROP(prop, "abcdefgh", sizeof(prop)); + TEST_ASSERT_EQUAL_STRING("abcd", prop); +} + +/********************************************************************* + * sortArray / sortArrayReverse + ********************************************************************/ +static bool absLarger(int a, int b) { return abs(a) > abs(b); } + +static void test_sortArray_ascending() { + int arr[] = {5, -1, 3, 0, 3}; + const int expected[] = {-1, 0, 3, 3, 5}; + sortArray(arr, 5); + TEST_ASSERT_EQUAL_INT_ARRAY(expected, arr, 5); +} +static void test_sortArrayReverse_descending() { + int arr[] = {5, -1, 3, 0, 3}; + const int expected[] = {5, 3, 3, 0, -1}; + sortArrayReverse(arr, 5); + TEST_ASSERT_EQUAL_INT_ARRAY(expected, arr, 5); +} +static void test_sortArray_custom_comparator() { + int arr[] = {-4, 1, 3, -2}; + const int expected[] = {1, -2, 3, -4}; + sortArray(arr, 4, absLarger); + TEST_ASSERT_EQUAL_INT_ARRAY(expected, arr, 4); + const int reversed[] = {-4, 3, -2, 1}; + sortArrayReverse(arr, 4, absLarger); + TEST_ASSERT_EQUAL_INT_ARRAY(reversed, arr, 4); +} +static void test_sortArray_empty_and_single() { + int one[] = {42}; + sortArray(one, 1); + TEST_ASSERT_EQUAL_INT(42, one[0]); + int none[] = {7}; + sortArray(none, 0); + TEST_ASSERT_EQUAL_INT(7, none[0]); +} +static void test_sortArray_moves_free_ids_last() { + // Free shade id slots are 255 and must end up after the used ones. + uint8_t ids[] = {3, 255, 1, 255, 2}; + const uint8_t expected[] = {1, 2, 3, 255, 255}; + sortArray(ids, sizeof(ids)); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, ids, sizeof(ids)); +} + +/********************************************************************* + * Timestamp + ********************************************************************/ +static struct tm sampleTm() { + struct tm dt = {}; + dt.tm_year = 2024 - 1900; + dt.tm_mon = 2; + dt.tm_mday = 5; + dt.tm_hour = 7; + dt.tm_min = 8; + dt.tm_sec = 9; + return dt; +} +static void test_formatISO_offsets() { + Timestamp ts; + struct tm dt = sampleTm(); + TEST_ASSERT_EQUAL_STRING("2024-03-05T07:08:09.000+0000", ts.formatISO(&dt, 0)); + TEST_ASSERT_EQUAL_STRING("2024-03-05T07:08:09.000-0500", ts.formatISO(&dt, -500)); + TEST_ASSERT_EQUAL_STRING("2024-03-05T07:08:09.000+0530", ts.formatISO(&dt, 530)); +} +static void test_calcTZOffset_whole_hours() { + time_t t = SAMPLE_UTC; + setTZ("UTC0"); + TEST_ASSERT_EQUAL_INT(0, Timestamp::calcTZOffset(&t)); + setTZ("EST5"); + TEST_ASSERT_EQUAL_INT(-500, Timestamp::calcTZOffset(&t)); +} +static void test_calcTZOffset_half_hour_east() { + time_t t = SAMPLE_UTC; + setTZ("IST-5:30"); + TEST_ASSERT_EQUAL_INT(530, Timestamp::calcTZOffset(&t)); +} +static void test_calcTZOffset_half_hour_west() { + time_t t = SAMPLE_UTC; + setTZ("NST3:30"); + TEST_ASSERT_EQUAL_INT(-330, Timestamp::calcTZOffset(&t)); +} +static void test_parseUTCTime_in_utc() { + setTZ("UTC0"); + TEST_ASSERT_EQUAL_INT64((int64_t)SAMPLE_UTC, (int64_t)Timestamp::parseUTCTime("2024-03-05T07:08:09Z")); +} +static void test_parseUTCTime_ignores_local_zone() { + setTZ("IST-5:30"); + TEST_ASSERT_EQUAL_INT64((int64_t)SAMPLE_UTC, (int64_t)Timestamp::parseUTCTime("2024-03-05T07:08:09Z")); +} + +int main(int argc, char **argv) { + (void)argc; + (void)argv; + UNITY_BEGIN(); + RUN_TEST(test_trim_strips_whitespace_and_quotes); + RUN_TEST(test_trim_keeps_clean_string); + RUN_TEST(test_trim_all_whitespace_becomes_empty); + RUN_TEST(test_trim_empty_string); + RUN_TEST(test_toBoolean_missing_uses_default); + RUN_TEST(test_toBoolean_true_values); + RUN_TEST(test_toBoolean_false_values); + RUN_TEST(test_toBoolean_unknown_uses_default); + RUN_TEST(test_setcharprop_copies_short_value); + RUN_TEST(test_setcharprop_truncates_long_value); + RUN_TEST(test_sortArray_ascending); + RUN_TEST(test_sortArrayReverse_descending); + RUN_TEST(test_sortArray_custom_comparator); + RUN_TEST(test_sortArray_empty_and_single); + RUN_TEST(test_sortArray_moves_free_ids_last); + RUN_TEST(test_formatISO_offsets); + RUN_TEST(test_calcTZOffset_whole_hours); + RUN_TEST(test_calcTZOffset_half_hour_east); + RUN_TEST(test_calcTZOffset_half_hour_west); + RUN_TEST(test_parseUTCTime_in_utc); + RUN_TEST(test_parseUTCTime_ignores_local_zone); + return UNITY_END(); +}