test(firmware): broaden native-test coverage + gcov instrumentation
Extract the pre-first-image retry from normal_operation() into a templated bootstrap_loop() helper in operation.h so the loop body becomes unit-testable. Add four tests against it: two that verify the X-Panel-Id header is sent on every poll and matches the compile-time PANEL_ID (a silent server-side mis-routing risk if dropped), and two that exercise the loop's exit-on-deep-sleep vs. iterate-while-204 behaviour. Wire --coverage into env:native-test (compile + link via a post-script) so `gcovr -r . --filter src/` produces a real number, and ignore the stray *.gcov files gcovr drops at the repo root. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1,2 @@
|
||||
.pio/
|
||||
*.gcov
|
||||
|
||||
+8
-1
@@ -150,9 +150,16 @@ build_flags =
|
||||
-DBOARD_HAS_PSRAM
|
||||
|
||||
; ── Native unit tests — no hardware, uses test/mocks/ ──
|
||||
; --coverage instruments gcov on both compile and link. After a test run,
|
||||
; .gcda files land in .pio/build/native-test/test/... — gcovr aggregates
|
||||
; them against src/. To regenerate the number:
|
||||
; pio test -e native-test
|
||||
; gcovr -r . --filter src/ --print-summary
|
||||
[env:native-test]
|
||||
platform = native
|
||||
lib_deps =
|
||||
throwtheswitch/Unity@^2.6
|
||||
build_flags = -DUNIT_TEST -std=c++17 -iquote test/mocks -iquote test -Itest/mocks -Itest
|
||||
build_flags = -DUNIT_TEST -std=c++17 -iquote test/mocks -iquote test -Itest/mocks -Itest --coverage -O0 -g
|
||||
build_unflags = -O2
|
||||
test_build_src = no
|
||||
extra_scripts = post:scripts/native_coverage_link.py
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Adds --coverage to the link step for env:native-test, so gcov's runtime
|
||||
# (libgcov) gets linked alongside the instrumented objects. build_flags
|
||||
# already pushes --coverage through compile; the linker needs it too or
|
||||
# the program fails with undefined references to __gcov_init / etc.
|
||||
Import("env")
|
||||
env.Append(LINKFLAGS=["--coverage"])
|
||||
+4
-4
@@ -292,13 +292,13 @@ static void normal_operation(const String& mac) {
|
||||
// we've received our first image. While in the pre-image window it
|
||||
// returns instead, so we keep WiFi up and retry on a short interval —
|
||||
// way faster end-to-end than waiting through a deep-sleep + reconnect
|
||||
// for every "no image yet" poll.
|
||||
while (true) {
|
||||
// for every "no image yet" poll. The loop body is extracted into
|
||||
// bootstrap_loop() (operation.h) so it's unit-testable.
|
||||
bootstrap_loop([&]() {
|
||||
HTTPClient http;
|
||||
http.begin(client, url);
|
||||
normal_operation_impl(mac, http, url, prefs);
|
||||
delay(BOOTSTRAP_RETRY_INTERVAL_MS);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Setup ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -356,3 +356,23 @@ void normal_operation_impl(const String& mac, HTTP& http, const String& url, Pre
|
||||
esp_sleep_enable_ext0_wakeup((gpio_num_t)PIN_BTN_RESET, 0);
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
|
||||
// ── Bootstrap-stay-awake loop ────────────────────────────────────────────────
|
||||
// Wraps normal_operation_impl in a retry loop for the pre-first-image window.
|
||||
// Once an image arrives, impl calls esp_deep_sleep_start() and never returns —
|
||||
// in production that's literal silicon-level halt; in unit tests the mock just
|
||||
// sets g_deep_sleep_started and returns, so we check that flag to break out.
|
||||
// Caller passes a callable that runs one poll iteration (instantiates a fresh
|
||||
// HTTPClient bound to the WiFiClient, calls normal_operation_impl, etc.).
|
||||
template<typename PollOnce>
|
||||
inline void bootstrap_loop(PollOnce poll_once) {
|
||||
while (true) {
|
||||
poll_once();
|
||||
#ifdef UNIT_TEST
|
||||
// Production: esp_deep_sleep_start never returns. Tests: it sets the
|
||||
// flag and returns, so without this guard the loop spins forever.
|
||||
if (g_deep_sleep_started) return;
|
||||
#endif
|
||||
delay(BOOTSTRAP_RETRY_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#define SLEEP_CLAMP_MIN_MS 30000ULL
|
||||
#define SLEEP_CLAMP_MAX_MS (25ULL * 60ULL * 60ULL * 1000ULL)
|
||||
#define FIRST_IMAGE_POLL_INTERVAL_MS 15000ULL
|
||||
#define BOOTSTRAP_RETRY_INTERVAL_MS 2000ULL
|
||||
#define WIFI_TIMEOUT_MS 30000
|
||||
#define RESET_HOLD_MS 5000
|
||||
#define AP_IP "192.168.4.1"
|
||||
|
||||
@@ -534,6 +534,80 @@ void test_fw17b_missing_sha256_header_skips_verification() {
|
||||
TEST_ASSERT_EQUAL(0, prefs.getInt(NVS_KEY_ERR_BORDER, -1));
|
||||
}
|
||||
|
||||
// FW-PANEL-A: X-Panel-Id header sent on every poll. V2 (13.3") shipped with
|
||||
// this header but no test covered it; if a future refactor accidentally
|
||||
// drops the addHeader call, the server's DeviceModel routing breaks silently
|
||||
// (a 13.3" frame would be served 7.3" images cropped to 800x480).
|
||||
void test_fw_panel_id_header_sent_on_every_poll() {
|
||||
g_http_response_headers["X-Image-Id"] = "1";
|
||||
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
g_http_request_headers.find("X-Panel-Id") != g_http_request_headers.end(),
|
||||
"X-Panel-Id header must be present on every poll");
|
||||
}
|
||||
|
||||
// FW-PANEL-B: Header value matches the compile-time PANEL_ID. In the native-
|
||||
// test build PANEL_ID is "unknown" (no env -D flag) — that's the fallback
|
||||
// path; the real production envs set the panel-specific id via build_flags.
|
||||
void test_fw_panel_id_header_value_matches_macro() {
|
||||
g_http_response_headers["X-Image-Id"] = "1";
|
||||
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
||||
TEST_ASSERT_EQUAL_STRING(PANEL_ID, g_http_request_headers["X-Panel-Id"].c_str());
|
||||
}
|
||||
|
||||
// FW-BOOT-LOOP-A: bootstrap_loop exits once the wrapped poll deep-sleeps
|
||||
// (i.e. an image has arrived and impl entered esp_deep_sleep_start). In
|
||||
// production esp_deep_sleep_start never returns; the test mock just sets
|
||||
// g_deep_sleep_started and returns. Without the in-test break the loop
|
||||
// would spin forever.
|
||||
void test_fw_bootstrap_loop_exits_after_deep_sleep() {
|
||||
g_http_response_headers["X-Image-Id"] = "1";
|
||||
g_http_response_headers["X-Interval-Ms"] = "300000";
|
||||
g_http_body = "BINDATA";
|
||||
|
||||
int iterations = 0;
|
||||
bootstrap_loop([&]() {
|
||||
iterations++;
|
||||
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
||||
});
|
||||
|
||||
// One poll succeeded → deep_sleep flagged → loop returned. If the loop
|
||||
// didn't honor the flag we'd never reach this assertion (test timeout).
|
||||
TEST_ASSERT_EQUAL(1, iterations);
|
||||
TEST_ASSERT_TRUE(g_deep_sleep_started);
|
||||
}
|
||||
|
||||
// FW-BOOT-LOOP-B: pre-image-arrived path — impl returns WITHOUT deep_sleep,
|
||||
// so the loop would iterate in production. In the test we drive the
|
||||
// iteration count via a side channel: the callable bumps a counter and
|
||||
// flips the response code mid-run, exercising the retry behaviour without
|
||||
// trapping the test forever.
|
||||
void test_fw_bootstrap_loop_iterates_when_no_image() {
|
||||
g_http_get_code = 204; // no image available yet
|
||||
// No img_id in NVS, so impl returns without arming deep sleep.
|
||||
|
||||
int iterations = 0;
|
||||
bootstrap_loop([&]() {
|
||||
iterations++;
|
||||
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
||||
// After the third no-image poll, simulate the image arriving so the
|
||||
// loop can exit cleanly. In production the loop runs until the
|
||||
// server flips to 200 — we just compress the timeline.
|
||||
if (iterations == 3) {
|
||||
g_http_get_code = 200;
|
||||
g_http_response_headers["X-Image-Id"] = "7";
|
||||
g_http_response_headers["X-Interval-Ms"] = "300000";
|
||||
g_http_body = "BINDATA";
|
||||
}
|
||||
});
|
||||
|
||||
// Three 204 spins + one 200 spin = 4 total iterations before the loop
|
||||
// saw the deep-sleep flag and broke out. Locks in that the loop does
|
||||
// NOT exit on a no-image return (would freeze the device on the QR).
|
||||
TEST_ASSERT_EQUAL(4, iterations);
|
||||
TEST_ASSERT_TRUE(g_deep_sleep_started);
|
||||
}
|
||||
|
||||
// FW-12/13: AP SSID derivation via ap_ssid_from_mac()
|
||||
void test_fw12_ap_ssid_from_mac_aabbcc() {
|
||||
String ssid = ap_ssid_from_mac(String("AA:BB:CC:DD:EE:FF"));
|
||||
@@ -583,5 +657,9 @@ int main(int argc, char** argv) {
|
||||
RUN_TEST(test_fw16_304_with_draw_needed_redraws);
|
||||
RUN_TEST(test_fw17a_sha256_mismatch_skips_draw_and_keeps_old_img_id);
|
||||
RUN_TEST(test_fw17b_missing_sha256_header_skips_verification);
|
||||
RUN_TEST(test_fw_panel_id_header_sent_on_every_poll);
|
||||
RUN_TEST(test_fw_panel_id_header_value_matches_macro);
|
||||
RUN_TEST(test_fw_bootstrap_loop_exits_after_deep_sleep);
|
||||
RUN_TEST(test_fw_bootstrap_loop_iterates_when_no_image);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user