87af8cb030
Three bugs fixed: - NVS img_id now written before epd_init/draw; new draw_needed flag in NVS survives power-loss mid-refresh so next boot re-draws from LittleFS instead of showing stale content - epd_sleep() now only called when display was initialized this cycle, preventing a 60 s wait_busy() timeout on every 304 poll - esp_task_wdt_reset() added to wait_busy() loop so the ~20 s 6-color refresh no longer triggers the task watchdog Also extracts normal_operation into operation.h template and adds a native PlatformIO test suite (16 tests) covering the full response matrix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
249 lines
9.2 KiB
C++
249 lines
9.2 KiB
C++
#include <unity.h>
|
|
#include <map>
|
|
#include <string>
|
|
#include <cstdint>
|
|
#include <cctype>
|
|
|
|
// Include mocks first — they shadow system/Arduino headers.
|
|
// -iquote test/mocks ensures quoted includes from test_main find mocks first.
|
|
// operation.h uses #ifdef UNIT_TEST to pick epd_mock.h and esp_sleep.h.
|
|
#include "Arduino.h"
|
|
#include "WiFi.h"
|
|
#include "WiFiClientSecure.h"
|
|
#include "Preferences.h"
|
|
#include "LittleFS.h"
|
|
#include "epd_mock.h"
|
|
#include "esp_sleep.h"
|
|
#include "HTTPClient.h"
|
|
#include "SPI.h"
|
|
#include "WebServer.h"
|
|
#include "DNSServer.h"
|
|
#include "qrcode.h"
|
|
#include "config.h"
|
|
|
|
// Define globals referenced as extern in the mock headers
|
|
int g_http_get_code;
|
|
std::map<std::string, std::string> g_http_response_headers;
|
|
std::map<std::string, std::string> g_http_request_headers;
|
|
bool g_http_end_called;
|
|
std::string g_http_body;
|
|
|
|
int g_epd_init_count, g_epd_sleep_count, g_epd_draw_image_count;
|
|
int g_epd_fill_count, g_epd_fill_last_color, g_epd_draw_setup_count;
|
|
|
|
uint64_t g_sleep_us;
|
|
bool g_deep_sleep_started;
|
|
|
|
// Globals for new mocks
|
|
int g_show_setup_qr_count;
|
|
uint32_t g_millis_value;
|
|
int g_digital_read_value;
|
|
int g_wifi_status;
|
|
|
|
// Ordering / sequencing globals (shared with Preferences.h and epd_mock.h)
|
|
int g_call_seq = 0;
|
|
int g_prefs_putint_seq = -1;
|
|
int g_epd_draw_seq = -1;
|
|
|
|
// Include the template under test AFTER all mocks are defined.
|
|
// operation.h with UNIT_TEST defined will include "epd_mock.h" and "esp_sleep.h"
|
|
// via -iquote test/mocks path (real src/epd.h is never pulled in).
|
|
#include "../../src/operation.h"
|
|
|
|
// Test fixtures
|
|
static Preferences prefs;
|
|
static MockHTTPClient http;
|
|
|
|
void reset_state() {
|
|
g_http_get_code = 200;
|
|
g_http_response_headers.clear();
|
|
g_http_request_headers.clear();
|
|
g_http_end_called = false;
|
|
g_http_body = "TESTDATA";
|
|
g_epd_init_count = g_epd_sleep_count = g_epd_draw_image_count = 0;
|
|
g_epd_fill_count = g_epd_fill_last_color = g_epd_draw_setup_count = 0;
|
|
g_sleep_us = 0;
|
|
g_deep_sleep_started = false;
|
|
g_show_setup_qr_count = 0;
|
|
g_millis_value = 0;
|
|
g_digital_read_value = HIGH; // button not pressed by default
|
|
g_wifi_status = WL_CONNECTED; // connected by default
|
|
prefs.clear();
|
|
LittleFS.files.clear();
|
|
http._ended = false;
|
|
g_call_seq = 0;
|
|
g_prefs_putint_seq = -1;
|
|
g_epd_draw_seq = -1;
|
|
}
|
|
|
|
void setUp() { reset_state(); }
|
|
void tearDown() {}
|
|
|
|
// FW-01: 200 response — file written, epd_draw called, NVS saved, deep sleep started
|
|
void test_fw01_200_response_happy_path() {
|
|
// Use an interval < FETCH_INTERVAL_MS so server value is honored
|
|
g_http_response_headers["X-Image-Id"] = "42";
|
|
g_http_response_headers["X-Interval-Ms"] = "30000";
|
|
g_http_body = "BINDATA";
|
|
|
|
normal_operation_impl(String("1C:C3:AB:D1:91:F8"), http, String("https://test/api/device/mac/image"), prefs);
|
|
|
|
TEST_ASSERT_EQUAL(1, g_epd_draw_image_count);
|
|
TEST_ASSERT_EQUAL(42, prefs.getInt("img_id", -1));
|
|
TEST_ASSERT_EQUAL_UINT64(30000ULL * 1000ULL, g_sleep_us);
|
|
TEST_ASSERT_TRUE(g_deep_sleep_started);
|
|
TEST_ASSERT_FALSE(LittleFS.files[IMAGE_PATH].empty());
|
|
}
|
|
|
|
// FW-02: REGRESSION — headers must be read BEFORE http.end(), otherwise newId is empty
|
|
void test_fw02_headers_read_before_end_regression() {
|
|
g_http_response_headers["X-Image-Id"] = "99";
|
|
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
|
// If newId was read after end(), NVS img_id would remain -1
|
|
TEST_ASSERT_EQUAL(99, prefs.getInt("img_id", -1));
|
|
}
|
|
|
|
// FW-03: 304 — no epd draw, no init, deep sleep started
|
|
void test_fw03_304_no_redraw() {
|
|
g_http_get_code = 304;
|
|
// Use an interval < FETCH_INTERVAL_MS so server value is honored
|
|
g_http_response_headers["X-Interval-Ms"] = "30000";
|
|
|
|
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
|
|
|
TEST_ASSERT_EQUAL(0, g_epd_init_count);
|
|
TEST_ASSERT_EQUAL(0, g_epd_draw_image_count);
|
|
TEST_ASSERT_TRUE(g_deep_sleep_started);
|
|
TEST_ASSERT_EQUAL_UINT64(30000ULL * 1000ULL, g_sleep_us);
|
|
}
|
|
|
|
// FW-04: 204 — show_setup_qr called exactly once
|
|
void test_fw04_204_shows_setup_qr() {
|
|
g_http_get_code = 204;
|
|
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
|
TEST_ASSERT_EQUAL(1, g_show_setup_qr_count);
|
|
TEST_ASSERT_EQUAL(0, g_epd_draw_image_count);
|
|
}
|
|
|
|
// FW-05: 404 — show_setup_qr called exactly once
|
|
void test_fw05_404_shows_setup_qr() {
|
|
g_http_get_code = 404;
|
|
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
|
TEST_ASSERT_EQUAL(1, g_show_setup_qr_count);
|
|
TEST_ASSERT_EQUAL(0, g_epd_draw_image_count);
|
|
}
|
|
|
|
// FW-06: other error — epd_fill yellow
|
|
void test_fw06_error_fills_yellow() {
|
|
g_http_get_code = 500;
|
|
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
|
TEST_ASSERT_EQUAL(1, g_epd_fill_count);
|
|
TEST_ASSERT_EQUAL(COLOR_YELLOW, g_epd_fill_last_color);
|
|
}
|
|
|
|
// FW-07: NVS has saved img_id → X-Current-Image-Id header sent
|
|
void test_fw07_current_image_id_sent_when_saved() {
|
|
prefs.ints["img_id"] = 99;
|
|
g_http_response_headers["X-Image-Id"] = "99";
|
|
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
|
TEST_ASSERT_EQUAL_STRING("99", g_http_request_headers["X-Current-Image-Id"].c_str());
|
|
}
|
|
|
|
// FW-08: NVS img_id = -1 (default) → X-Current-Image-Id NOT sent
|
|
void test_fw08_no_current_image_id_when_default() {
|
|
// prefs has no img_id — getInt returns -1
|
|
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
|
TEST_ASSERT_TRUE(g_http_request_headers.find("X-Current-Image-Id") == g_http_request_headers.end());
|
|
}
|
|
|
|
// FW-09: server interval < FETCH_INTERVAL_MS → server value used
|
|
void test_fw09_server_interval_honored() {
|
|
g_http_response_headers["X-Interval-Ms"] = "30000";
|
|
g_http_response_headers["X-Image-Id"] = "1";
|
|
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
|
TEST_ASSERT_EQUAL_UINT64(30000ULL * 1000ULL, g_sleep_us);
|
|
}
|
|
|
|
// FW-10: server interval > FETCH_INTERVAL_MS → capped at ceiling
|
|
void test_fw10_server_interval_capped() {
|
|
g_http_response_headers["X-Interval-Ms"] = "999999999";
|
|
g_http_response_headers["X-Image-Id"] = "1";
|
|
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
|
TEST_ASSERT_EQUAL_UINT64(FETCH_INTERVAL_MS * 1000ULL, g_sleep_us);
|
|
}
|
|
|
|
// FW-11: no X-Interval-Ms → default ceiling used
|
|
void test_fw11_default_interval_when_absent() {
|
|
g_http_response_headers["X-Image-Id"] = "1";
|
|
// no X-Interval-Ms set
|
|
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
|
TEST_ASSERT_EQUAL_UINT64(FETCH_INTERVAL_MS * 1000ULL, g_sleep_us);
|
|
}
|
|
|
|
// FW-14: 304 — epd_sleep NOT called (display already in hardware deep sleep)
|
|
void test_fw14_304_skips_epd_sleep() {
|
|
g_http_get_code = 304;
|
|
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
|
TEST_ASSERT_EQUAL(0, g_epd_sleep_count);
|
|
TEST_ASSERT_EQUAL(0, g_epd_init_count);
|
|
}
|
|
|
|
// FW-15: 200 — NVS img_id saved BEFORE epd_draw_image_from_file; draw_needed cleared after
|
|
void test_fw15_nvs_saved_before_epd_draw_and_flag_cleared() {
|
|
g_http_response_headers["X-Image-Id"] = "42";
|
|
g_http_body = "BINDATA";
|
|
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
|
TEST_ASSERT_TRUE_MESSAGE(g_prefs_putint_seq < g_epd_draw_seq,
|
|
"NVS putInt must be called before epd_draw_image_from_file");
|
|
TEST_ASSERT_EQUAL(42, prefs.getInt("img_id", -1));
|
|
TEST_ASSERT_EQUAL(1, g_epd_draw_image_count);
|
|
TEST_ASSERT_EQUAL(0, prefs.getInt("draw", -1));
|
|
}
|
|
|
|
// FW-16: 304 with draw_needed=1 (interrupted draw) — re-draws from LittleFS and clears flag
|
|
void test_fw16_304_with_draw_needed_redraws() {
|
|
prefs.ints["img_id"] = 42;
|
|
prefs.ints["draw"] = 1;
|
|
g_http_get_code = 304;
|
|
LittleFS.files[IMAGE_PATH] = "IMGDATA";
|
|
|
|
normal_operation_impl(String("mac"), http, String("url"), prefs);
|
|
|
|
TEST_ASSERT_EQUAL(1, g_epd_init_count);
|
|
TEST_ASSERT_EQUAL(1, g_epd_draw_image_count);
|
|
TEST_ASSERT_EQUAL(1, g_epd_sleep_count);
|
|
TEST_ASSERT_EQUAL(0, prefs.getInt("draw", -1));
|
|
}
|
|
|
|
// 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"));
|
|
TEST_ASSERT_EQUAL_STRING("PictureFrame-EEFF", ssid.c_str());
|
|
}
|
|
|
|
void test_fw13_ap_ssid_from_real_mac() {
|
|
String ssid = ap_ssid_from_mac(String("1C:C3:AB:D1:91:F8"));
|
|
TEST_ASSERT_EQUAL_STRING("PictureFrame-91F8", ssid.c_str());
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_fw01_200_response_happy_path);
|
|
RUN_TEST(test_fw02_headers_read_before_end_regression);
|
|
RUN_TEST(test_fw03_304_no_redraw);
|
|
RUN_TEST(test_fw04_204_shows_setup_qr);
|
|
RUN_TEST(test_fw05_404_shows_setup_qr);
|
|
RUN_TEST(test_fw06_error_fills_yellow);
|
|
RUN_TEST(test_fw07_current_image_id_sent_when_saved);
|
|
RUN_TEST(test_fw08_no_current_image_id_when_default);
|
|
RUN_TEST(test_fw09_server_interval_honored);
|
|
RUN_TEST(test_fw10_server_interval_capped);
|
|
RUN_TEST(test_fw11_default_interval_when_absent);
|
|
RUN_TEST(test_fw12_ap_ssid_from_mac_aabbcc);
|
|
RUN_TEST(test_fw13_ap_ssid_from_real_mac);
|
|
RUN_TEST(test_fw14_304_skips_epd_sleep);
|
|
RUN_TEST(test_fw15_nvs_saved_before_epd_draw_and_flag_cleared);
|
|
RUN_TEST(test_fw16_304_with_draw_needed_redraws);
|
|
return UNITY_END();
|
|
}
|