/ firmware / include / testing / utils.h
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