test: add portal hardware lifecycle coverage

This commit is contained in:
2026-09-06 20:03:38 +10:00
parent fcd991c5a2
commit d57f6c91c5
15 changed files with 595 additions and 20 deletions
@@ -4,7 +4,8 @@
// Test process() doesn't block - verify it must be called periodically
void test_nonblocking_process() {
Serial.println("[TEST] Testing process() calls (non-blocking behavior)...");
constexpr unsigned long kMaximumProcessDurationMs = 250;
unsigned long maximumObservedDurationMs = 0;
WiFiManager wm;
@@ -18,10 +19,13 @@ void test_nonblocking_process() {
for (int i = 0; i < 10; i++) {
unsigned long start = millis();
wm.process();
unsigned long elapsed = millis() - start;
const unsigned long elapsed = millis() - start;
maximumObservedDurationMs = max(maximumObservedDurationMs, elapsed);
// Each call should be fast (< 100ms) - verifies non-blocking behavior
TEST_ASSERT_LESS_THAN(100, elapsed);
// A normal cooperative call is quick, but DNS and Wi-Fi service may
// briefly run on ESP8266. This still catches a truly blocking portal.
TEST_ASSERT_LESS_THAN_MESSAGE(kMaximumProcessDurationMs, elapsed,
"process() exceeded its bounded service time");
delay(10);
}
@@ -31,6 +35,7 @@ void test_nonblocking_process() {
wm.stopConfigPortal();
Serial.printf("[METRIC] WM_PROCESS max_elapsed_ms=%lu\n", maximumObservedDurationMs);
Serial.println("[TEST] Non-blocking process() test completed successfully");
}
@@ -0,0 +1,186 @@
#include <Arduino.h>
#include <WiFiManager.h>
#include <unity.h>
#include <vector>
#include "../test_main.h"
namespace {
struct HeapSnapshot {
uint32_t freeHeap;
uint32_t largestBlock;
uint8_t fragmentation;
};
HeapSnapshot captureHeap(const char* label) {
HeapSnapshot snapshot{
ESP.getFreeHeap(),
#if defined(ESP8266)
ESP.getMaxFreeBlockSize(),
ESP.getHeapFragmentation(),
#else
ESP.getMaxAllocHeap(),
0,
#endif
};
Serial.printf("[METRIC] WM_HEAP label=%s free=%lu largest=%lu fragmentation=%u\n",
label,
static_cast<unsigned long>(snapshot.freeHeap),
static_cast<unsigned long>(snapshot.largestBlock),
snapshot.fragmentation);
return snapshot;
}
uint32_t allowedHeapDrift() {
#if defined(ESP8266)
return 1024;
#else
return 4096;
#endif
}
uint32_t allowedLargestBlockDrift() {
#if defined(ESP8266)
return 1024;
#else
return 4096;
#endif
}
void assertRecovered(const HeapSnapshot& settled, const HeapSnapshot& final) {
const uint32_t freeFloor = settled.freeHeap > allowedHeapDrift()
? settled.freeHeap - allowedHeapDrift()
: 0;
const uint32_t blockFloor = settled.largestBlock > allowedLargestBlockDrift()
? settled.largestBlock - allowedLargestBlockDrift()
: 0;
TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(
freeFloor, final.freeHeap,
"Portal start/stop cycles retained too much heap after warm-up");
TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(
blockFloor, final.largestBlock,
"Portal start/stop cycles degraded the largest contiguous allocation");
}
void startAndStopPortal(WiFiManager& wm, const char* ssid) {
wm.startConfigPortal(ssid);
wm.process();
TEST_ASSERT_TRUE_MESSAGE(wm.getConfigPortalActive(), "Portal should start");
TEST_ASSERT_NOT_NULL_MESSAGE(wm.getServer(), "Portal server should exist");
TEST_ASSERT_NOT_NULL_MESSAGE(wm.getDNSServer(), "Portal DNS server should exist");
wm.stopConfigPortal();
wm.process();
TEST_ASSERT_FALSE_MESSAGE(wm.getConfigPortalActive(), "Portal should stop");
TEST_ASSERT_NULL_MESSAGE(wm.getServer(), "Portal server should be released");
TEST_ASSERT_NULL_MESSAGE(wm.getDNSServer(), "Portal DNS server should be released");
}
} // namespace
void test_portal_resource_recovery() {
Serial.println("[TEST] Measuring portal resource recovery...");
WiFiManager wm;
wm.setConfigPortalTimeout(20);
// Warm the Wi-Fi core once before establishing the comparison point. The
// SDK is allowed to retain its own one-time allocations; the test detects
// repeated decline after that settled point instead.
startAndStopPortal(wm, "WM-Memory-Warmup");
delay(200);
const HeapSnapshot settled = captureHeap("settled");
for (uint8_t cycle = 0; cycle < 20; ++cycle) {
const String ssid = String("WM-Memory-") + String(cycle);
wm.startConfigPortal(ssid.c_str());
wm.process();
TEST_ASSERT_TRUE_MESSAGE(wm.getConfigPortalActive(), "Portal should start during cycle");
TEST_ASSERT_NOT_NULL_MESSAGE(wm.getServer(), "Server should exist during cycle");
TEST_ASSERT_NOT_NULL_MESSAGE(wm.getDNSServer(), "DNS should exist during cycle");
if (cycle == 0) {
captureHeap("portal-active");
}
wm.stopConfigPortal();
wm.process();
TEST_ASSERT_FALSE_MESSAGE(wm.getConfigPortalActive(), "Portal should stop during cycle");
TEST_ASSERT_NULL_MESSAGE(wm.getServer(), "Server should be released during cycle");
TEST_ASSERT_NULL_MESSAGE(wm.getDNSServer(), "DNS should be released during cycle");
captureHeap("cycle-stopped");
}
delay(200);
const HeapSnapshot final = captureHeap("final");
assertRecovered(settled, final);
Serial.println("[TEST] Portal resource recovery test completed successfully");
}
void test_scan_result_storage_released_when_portal_closes() {
Serial.println("[TEST] Testing scan-result storage release on portal close...");
WiFiManager wm;
wm.setConfigPortalTimeout(20);
wm.startConfigPortal("WM-Scan-Storage");
wm.process();
#ifdef UNIT_TEST
std::vector<WiFiManager::WiFiScanNetwork> results;
for (uint8_t i = 0; i < 24; ++i) {
results.push_back({String("Network-") + String(i), -30 - i, static_cast<uint8_t>(i % 2)});
}
wm.wmTestInjectScanResults(results);
captureHeap("scan-cache-held");
TEST_ASSERT_GREATER_THAN_UINT32(0, wm.getScanResults().capacity());
#endif
wm.stopConfigPortal();
wm.process();
TEST_ASSERT_EQUAL_UINT32(0, wm.getScanResults().size());
TEST_ASSERT_EQUAL_UINT32(0, wm.getScanResults().capacity());
captureHeap("scan-storage-cleared");
Serial.println("[TEST] Scan-result storage release test completed successfully");
}
void test_real_async_scan_completes() {
Serial.println("[TEST] Testing real asynchronous Wi-Fi scan completion...");
WiFiManager wm;
wm.setConfigPortalTimeout(30);
wm.startConfigPortal("WM-Real-Scan");
wm.process();
TEST_ASSERT_TRUE_MESSAGE(wm.getConfigPortalActive(), "Portal should be active for scanning");
captureHeap("scan-portal-active");
wm.requestAsyncScan(true);
captureHeap("scan-requested");
const uint32_t deadline = millis() + 25000UL;
while (wm.isScanRunning() && millis() < deadline) {
wm.process();
delay(20);
}
const WiFiManager::WiFiScanRuntimeState scan = wm.getScanSnapshot();
Serial.printf("[METRIC] WM_SCAN state=%u result=%d count=%d elapsed=%lu\n",
static_cast<unsigned>(scan.state),
scan.lastScanResult,
static_cast<int>(wm.getScanResults().size()),
static_cast<unsigned long>(millis() - scan.startedAt));
captureHeap("scan-complete-cache");
const bool stillRunning = wm.isScanRunning();
const WiFiManager::wm_scan_state_t state = wm.getScanState();
wm.stopConfigPortal();
captureHeap("scan-storage-cleared");
TEST_ASSERT_FALSE_MESSAGE(stillRunning, "Async scan exceeded its completion deadline");
TEST_ASSERT_EQUAL_MESSAGE(WiFiManager::WM_SCAN_COMPLETE, state,
"Real scan must complete rather than enter failed/timeout state");
Serial.println("[TEST] Real asynchronous Wi-Fi scan test completed successfully");
}