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:
2026-05-14 16:25:00 -04:00
parent 013e49d859
commit e9f2ec0629
7 changed files with 118 additions and 5 deletions
+78
View File
@@ -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();
}