utils.h
1 #pragma once 2 3 #ifdef PIO_UNIT_TESTING 4 5 #include <unity.h> 6 #include <string.h> 7 #include <config.h> 8 #include <Arduino.h> 9 #include <Wire.h> 10 #include <Preferences.h> 11 #include <i2c.h> 12 13 // ───────────────────────────────────────────────────────────────────────────── 14 // BDD narration macros 15 // 16 // These are thin wrappers over Unity's TEST_MESSAGE. The Python test runner 17 // (tests/test_custom_runner.py) parses the [KEYWORD] prefix to produce 18 // Ward-inspired BDD output with semantic colors and progressive indentation. 19 // 20 // What we use from Unity: 21 // TEST_MESSAGE / TEST_PRINTF — narrative output (TEST_PRINTF for formatted) 22 // CAVEAT: TEST_PRINTF does NOT support width/precision (%-16s, %.2f). 23 // Those cause va_arg misalignment → ESP32 LoadProhibited crash. 24 // Use snprintf + TEST_MESSAGE for format strings with modifiers. 25 // TEST_ASSERT_EQUAL_STRING — prefer over strcmp-based assertions 26 // TEST_ASSERT_EQUAL_MEMORY — prefer over memcmp for struct roundtrips 27 // TEST_ASSERT_FLOAT_WITHIN — prefer for sensor range checks 28 // TEST_ASSERT_FLOAT_IS_DETERMINATE — add to sensor reads to catch NaN/Inf 29 // TEST_ASSERT_NOT_EMPTY — prefer for "is this string set?" checks 30 // UNITY_INCLUDE_EXEC_TIME — per-test timing (enabled in unity_config.h) 31 // UNITY_INCLUDE_PRINT_FORMATTED — enables TEST_PRINTF (enabled in unity_config.h) 32 // 33 // What we DON'T use, and why: 34 // Unity Fixture (extras/fixture/) 35 // TEST_GROUP / TEST / RUN_TEST_CASE — provides grouping + CLI filtering, 36 // but (a) Fixture's name format "TEST(Group, Name)" breaks PlatformIO's 37 // parse_test_case regex ([^\s]+ can't handle the space), (b) Fixture's 38 // CLI filtering (-g, -n) requires argv which doesn't exist on ESP32 39 // (Arduino's setup() has no argc/argv), (c) Fixture auto-includes 40 // unity_memory.h which wraps stdlib malloc/free — wrong on ESP32 with 41 // FreeRTOS (needs pvPortMalloc/vPortFree). Our MODULE() macro serves the 42 // rendering purpose without any of these compatibility issues. 43 // 44 // Unity auto/ scripts (generate_test_runner.rb, stylize_as_junit.py) 45 // PlatformIO handles test wiring and already provides --junit-output-path 46 // and --json-output-path built-in. Manual test() functions give us MODULE 47 // grouping control that auto-generation would lose. 48 // 49 // Unity BDD (extras/bdd/unity_bdd.h) 50 // The upstream GIVEN/WHEN/THEN are if(0){printf}else — pure documentation 51 // scaffolding that emits NO output. We need actual output for the Python 52 // runner to parse, so our macros use TEST_MESSAGE instead. 53 // ───────────────────────────────────────────────────────────────────────────── 54 55 #define GIVEN(desc) TEST_MESSAGE("[GIVEN] " desc) 56 #define WHEN(desc) TEST_MESSAGE("[WHEN] " desc) 57 #define THEN(desc) TEST_MESSAGE("[THEN] " desc) 58 #define AND(desc) TEST_MESSAGE("[AND] " desc) 59 #define MODULE(name) TEST_MESSAGE("[MODULE] " name) 60 61 // ───────────────────────────────────────────────────────────────────────────── 62 // I2C helpers 63 // ───────────────────────────────────────────────────────────────────────────── 64 65 static inline void test_ensure_wire0(void) { 66 Wire.begin(config::i2c::BUS_0.sda_gpio, config::i2c::BUS_0.scl_gpio, 67 config::i2c::FREQUENCY_KHZ * 1000); 68 Wire.setTimeOut(100); 69 } 70 71 static inline void test_ensure_wire1(void) { 72 Wire1.begin(config::i2c::BUS_1.sda_gpio, config::i2c::BUS_1.scl_gpio, 73 config::i2c::FREQUENCY_KHZ * 1000); 74 Wire1.setTimeOut(100); 75 } 76 77 static inline void test_ensure_wire1_with_power(void) { 78 hardware::i2c::enable(); 79 test_ensure_wire1(); 80 } 81 82 // ───────────────────────────────────────────────────────────────────────────── 83 // NVS snapshot/restore helpers 84 // ───────────────────────────────────────────────────────────────────────────── 85 86 struct NvsStringSnapshot { 87 char value[128]; 88 bool exists; 89 }; 90 91 struct NvsBoolSnapshot { 92 bool value; 93 bool exists; 94 }; 95 96 static inline void nvs_snapshot_string(Preferences &prefs, const char *key, 97 NvsStringSnapshot *snap) { 98 snap->exists = prefs.isKey(key); 99 if (snap->exists) { 100 prefs.getString(key, snap->value, sizeof(snap->value)); 101 } else { 102 snap->value[0] = '\0'; 103 } 104 } 105 106 static inline void nvs_snapshot_bool(Preferences &prefs, const char *key, 107 NvsBoolSnapshot *snap, bool default_val) { 108 snap->exists = prefs.isKey(key); 109 snap->value = prefs.getBool(key, default_val); 110 } 111 112 static inline void nvs_restore_string(Preferences &prefs, const char *key, 113 const NvsStringSnapshot *snap) { 114 if (snap->exists) prefs.putString(key, snap->value); 115 } 116 117 static inline void nvs_restore_bool(Preferences &prefs, const char *key, 118 const NvsBoolSnapshot *snap) { 119 if (snap->exists) prefs.putBool(key, snap->value); 120 } 121 122 struct WifiNvsSnapshot { 123 NvsStringSnapshot sta_ssid; 124 NvsStringSnapshot sta_pass; 125 NvsStringSnapshot ap_ssid; 126 NvsStringSnapshot ap_pass; 127 NvsBoolSnapshot ap_on; 128 }; 129 130 static inline void wifi_nvs_save(WifiNvsSnapshot *snap) { 131 Preferences prefs; 132 if (!prefs.begin(config::wifi::NVS_NAMESPACE, true)) return; 133 nvs_snapshot_string(prefs, "sta_ssid", &snap->sta_ssid); 134 nvs_snapshot_string(prefs, "sta_pass", &snap->sta_pass); 135 nvs_snapshot_string(prefs, "ap_ssid", &snap->ap_ssid); 136 nvs_snapshot_string(prefs, "ap_pass", &snap->ap_pass); 137 nvs_snapshot_bool(prefs, "ap_on", &snap->ap_on, true); 138 prefs.end(); 139 } 140 141 static inline void wifi_nvs_restore(const WifiNvsSnapshot *snap) { 142 Preferences prefs; 143 if (!prefs.begin(config::wifi::NVS_NAMESPACE, false)) return; 144 prefs.clear(); 145 nvs_restore_string(prefs, "sta_ssid", &snap->sta_ssid); 146 nvs_restore_string(prefs, "sta_pass", &snap->sta_pass); 147 nvs_restore_string(prefs, "ap_ssid", &snap->ap_ssid); 148 nvs_restore_string(prefs, "ap_pass", &snap->ap_pass); 149 nvs_restore_bool(prefs, "ap_on", &snap->ap_on); 150 prefs.end(); 151 } 152 153 #endif