/* * test_hw_button.c — Unit tests for hw_button debounce/gesture driver (Issue #682). * * Build (host, no hardware): * gcc -I include -I test/stubs -DTEST_HOST \ * -o /tmp/test_hw_button src/hw_button.c test/test_hw_button.c * * All tests use hw_button_inject() to drive the simulated GPIO pin. * All timing is driven through the now_ms argument passed to hw_button_tick(). */ /* ---- Stubs: prevent STM32 HAL from being included in non-TEST_HOST path ---- */ #define STM32F7XX_HAL_H #include #include /* ---- Include implementation directly ---- */ #include "../src/hw_button.c" /* ---- Test framework ---- */ #include #include static int g_pass = 0; static int g_fail = 0; #define ASSERT(cond, msg) \ do { \ if (cond) { g_pass++; } \ else { g_fail++; printf("FAIL [%s:%d] %s\n", __FILE__, __LINE__, msg); } \ } while (0) /* ---- Helpers ---- */ /* Advance time by `ms` ticks with the pin held at `pressed`. * Returns the last non-NONE event seen, or BTN_EVENT_NONE. */ static hw_btn_event_t _tick_n(uint32_t *now, uint32_t ms, bool pressed) { hw_btn_event_t last = BTN_EVENT_NONE; hw_button_inject(pressed); for (uint32_t i = 0; i < ms; i++) { (*now)++; hw_btn_event_t ev = hw_button_tick(*now); if (ev != BTN_EVENT_NONE) last = ev; } return last; } /* Reset driver state between tests. */ static void _reset(uint32_t *now) { *now = 0; hw_button_inject(false); hw_button_init(); /* Drain any pending sequence state */ for (int i = 0; i < 100; i++) hw_button_tick((*now)++); } /* ---- Tests ---- */ static void test_idle_no_event(void) { uint32_t now = 0; hw_button_init(); hw_button_inject(false); for (int i = 0; i < 1000; i++) { hw_btn_event_t ev = hw_button_tick(now++); ASSERT(ev == BTN_EVENT_NONE, "idle: no event while pin released"); } } static void test_debounce_bounce_ignored(void) { uint32_t now = 0; hw_button_init(); /* Press for only 10 ms (< BTN_DEBOUNCE_MS=20) then release */ hw_btn_event_t ev = _tick_n(&now, 10, true); ASSERT(ev == BTN_EVENT_NONE, "bounce < debounce: no event during press"); ev = _tick_n(&now, BTN_COMMIT_MS + 50, false); ASSERT(ev == BTN_EVENT_NONE, "bounce < debounce: no event after release"); } static void test_debounce_confirmed(void) { uint32_t now = 0; hw_button_init(); /* Press for BTN_DEBOUNCE_MS + 1 ms — debounce should confirm */ _tick_n(&now, BTN_DEBOUNCE_MS + 1, true); ASSERT(hw_button_is_pressed(), "debounce passed: is_pressed() == true"); } static void test_short_press_park(void) { uint32_t now = 0; hw_button_init(); /* Press for BTN_DEBOUNCE_MS + 10 ms (short: well < BTN_LONG_MIN_MS) */ _tick_n(&now, BTN_DEBOUNCE_MS + 10, true); /* Release */ _tick_n(&now, 1, false); ASSERT(!hw_button_is_pressed(), "short press: released"); /* Wait BTN_COMMIT_MS quiet -> should fire PARK */ hw_btn_event_t ev = BTN_EVENT_NONE; hw_button_inject(false); for (uint32_t i = 0; i < BTN_COMMIT_MS + 10; i++) { now++; hw_btn_event_t e = hw_button_tick(now); if (e != BTN_EVENT_NONE) ev = e; } ASSERT(ev == BTN_EVENT_PARK, "short press + quiet: BTN_EVENT_PARK"); } static void test_long_press_no_park(void) { uint32_t now = 0; hw_button_init(); /* Press for BTN_LONG_MIN_MS + 100 ms (long press) */ _tick_n(&now, BTN_DEBOUNCE_MS + BTN_LONG_MIN_MS + 100, true); /* Release and wait */ hw_btn_event_t ev = _tick_n(&now, BTN_COMMIT_MS + 10, false); /* Lone LONG press has no defined gesture */ ASSERT(ev == BTN_EVENT_NONE, "lone long press: no event"); } static void test_rearm_combo_fires(void) { uint32_t now = 0; hw_button_init(); /* Press 1: SHORT (30 ms hold + release) */ _tick_n(&now, BTN_DEBOUNCE_MS + 10, true); _tick_n(&now, 1, false); /* Brief gap between presses (< BTN_COMMIT_MS so no PARK fires) */ _tick_n(&now, 100, false); /* Press 2: SHORT */ _tick_n(&now, BTN_DEBOUNCE_MS + 10, true); _tick_n(&now, 1, false); _tick_n(&now, 100, false); /* Press 3: LONG — combo fires on release */ _tick_n(&now, BTN_DEBOUNCE_MS + BTN_LONG_MIN_MS + 50, true); hw_btn_event_t ev = BTN_EVENT_NONE; hw_button_inject(false); for (int i = 0; i < 5; i++) { hw_btn_event_t e = hw_button_tick(now++); if (e != BTN_EVENT_NONE) ev = e; } ASSERT(ev == BTN_EVENT_REARM_COMBO, "SHORT+SHORT+LONG: BTN_EVENT_REARM_COMBO"); } static void test_rearm_combo_resets_buffer(void) { uint32_t now = 0; hw_button_init(); /* Fire combo once */ _tick_n(&now, BTN_DEBOUNCE_MS + 10, true); _tick_n(&now, 100, false); _tick_n(&now, BTN_DEBOUNCE_MS + 10, true); _tick_n(&now, 100, false); _tick_n(&now, BTN_DEBOUNCE_MS + BTN_LONG_MIN_MS + 50, true); hw_button_inject(false); hw_btn_event_t ev = BTN_EVENT_NONE; for (int i = 0; i < 5; i++) { hw_btn_event_t e = hw_button_tick(now++); if (e != BTN_EVENT_NONE) ev = e; } ASSERT(ev == BTN_EVENT_REARM_COMBO, "first combo fires"); /* After combo, a lone short press should produce PARK */ _tick_n(&now, BTN_DEBOUNCE_MS + 10, true); _tick_n(&now, 1, false); ev = BTN_EVENT_NONE; hw_button_inject(false); for (uint32_t i = 0; i < BTN_COMMIT_MS + 10; i++) { hw_btn_event_t e = hw_button_tick(now++); if (e != BTN_EVENT_NONE) ev = e; } ASSERT(ev == BTN_EVENT_PARK, "after combo: short press fires PARK"); } static void test_seq_timeout_clears_buffer(void) { uint32_t now = 0; hw_button_init(); /* TWO short presses — seq_len=2 (no PARK since it's not a lone SHORT). * Then wait BTN_SEQ_TIMEOUT_MS — buffer should be abandoned silently. */ _tick_n(&now, BTN_DEBOUNCE_MS + 10, true); _tick_n(&now, 1, false); _tick_n(&now, 100, false); /* gap between presses (< BTN_COMMIT_MS) */ _tick_n(&now, BTN_DEBOUNCE_MS + 10, true); _tick_n(&now, 1, false); /* Wait BTN_SEQ_TIMEOUT_MS — seq_len=2 buffer expires silently */ hw_btn_event_t ev = _tick_n(&now, BTN_SEQ_TIMEOUT_MS + 10, false); ASSERT(ev == BTN_EVENT_NONE, "seq timeout (seq_len=2): buffer cleared, no event"); /* Fresh lone short press should produce PARK as a new sequence */ _tick_n(&now, BTN_DEBOUNCE_MS + 10, true); _tick_n(&now, 1, false); ev = BTN_EVENT_NONE; hw_button_inject(false); for (uint32_t i = 0; i < BTN_COMMIT_MS + 10; i++) { hw_btn_event_t e = hw_button_tick(now++); if (e != BTN_EVENT_NONE) ev = e; } ASSERT(ev == BTN_EVENT_PARK, "after timeout: fresh short press fires PARK"); } static void test_is_pressed_tracks_held(void) { uint32_t now = 0; hw_button_init(); ASSERT(!hw_button_is_pressed(), "initially not pressed"); _tick_n(&now, BTN_DEBOUNCE_MS + 1, true); ASSERT(hw_button_is_pressed(), "after debounce: is_pressed true"); _tick_n(&now, 1, false); ASSERT(!hw_button_is_pressed(), "after release: is_pressed false"); } static void test_short_press_before_commit_no_park(void) { uint32_t now = 0; hw_button_init(); /* Press and release */ _tick_n(&now, BTN_DEBOUNCE_MS + 10, true); _tick_n(&now, 1, false); /* Wait less than BTN_COMMIT_MS — no PARK yet */ hw_btn_event_t ev = BTN_EVENT_NONE; hw_button_inject(false); for (uint32_t i = 0; i < BTN_COMMIT_MS - 50; i++) { hw_btn_event_t e = hw_button_tick(now++); if (e != BTN_EVENT_NONE) ev = e; } ASSERT(ev == BTN_EVENT_NONE, "before commit window: no PARK yet"); } static void test_two_shorts_no_park(void) { uint32_t now = 0; hw_button_init(); /* Two short presses close together — seq_len=2 does not trigger PARK */ _tick_n(&now, BTN_DEBOUNCE_MS + 10, true); _tick_n(&now, 1, false); _tick_n(&now, 100, false); _tick_n(&now, BTN_DEBOUNCE_MS + 10, true); _tick_n(&now, 1, false); hw_btn_event_t ev = BTN_EVENT_NONE; hw_button_inject(false); for (uint32_t i = 0; i < BTN_COMMIT_MS + 10; i++) { hw_btn_event_t e = hw_button_tick(now++); if (e != BTN_EVENT_NONE) ev = e; } ASSERT(ev == BTN_EVENT_NONE, "two shorts + quiet: no PARK (seq_len=2, not lone SHORT)"); } /* ---- main ---- */ int main(void) { printf("=== test_hw_button ===\n"); test_idle_no_event(); test_debounce_bounce_ignored(); test_debounce_confirmed(); test_short_press_park(); test_long_press_no_park(); test_rearm_combo_fires(); test_rearm_combo_resets_buffer(); test_seq_timeout_clears_buffer(); test_is_pressed_tracks_held(); test_short_press_before_commit_no_park(); test_two_shorts_no_park(); printf("\nResults: %d passed, %d failed\n", g_pass, g_fail); return g_fail ? 1 : 0; }