mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-17 08:31:31 +10:00
test: add portal hardware lifecycle coverage
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
# Portal browser harness
|
||||
|
||||
This test fixture starts only the WiFiManager captive portal. It intentionally has no station credentials, MQTT configuration, DeviceFramework dependency, or application logic.
|
||||
|
||||
Use it through the repository runner so a secondary Wi-Fi adapter is explicitly selected and protected from becoming the host default route:
|
||||
|
||||
```bash
|
||||
./tools/test-portal-browser.sh \
|
||||
--platform esp8266 \
|
||||
--port /dev/serial/by-id/... \
|
||||
--wifi-interface wlx... \
|
||||
--output /tmp/wifimanager-browser
|
||||
```
|
||||
|
||||
The ESP8266 portal SSID is `WM Browser ESP8266`; the ESP32 SSID is `WM Browser ESP32`. Both use `default1` exclusively for local development tests.
|
||||
|
||||
The runner cleans up only the temporary connection it creates on the named secondary interface. It refuses to run if that interface is the system default route.
|
||||
@@ -0,0 +1,28 @@
|
||||
[platformio]
|
||||
default_envs = esp8266
|
||||
|
||||
[common]
|
||||
framework = arduino
|
||||
lib_ldf_mode = deep+
|
||||
lib_deps =
|
||||
WiFiManager=symlink://../..
|
||||
|
||||
[env:esp8266]
|
||||
extends = common
|
||||
platform = espressif8266
|
||||
board = d1_mini
|
||||
platform_packages =
|
||||
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
|
||||
build_flags =
|
||||
-DWM_LOG_LEVEL=4
|
||||
|
||||
[env:esp32]
|
||||
extends = common
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||
board = esp32dev
|
||||
build_unflags = -std=gnu++11
|
||||
build_flags =
|
||||
-std=gnu++14
|
||||
-DSOC_WIFI_SUPPORTED=1
|
||||
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
|
||||
-DWM_LOG_LEVEL=4
|
||||
@@ -0,0 +1,33 @@
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
|
||||
namespace {
|
||||
|
||||
#if defined(ESP8266)
|
||||
constexpr char kPortalSsid[] = "WM Browser ESP8266";
|
||||
#else
|
||||
constexpr char kPortalSsid[] = "WM Browser ESP32";
|
||||
#endif
|
||||
constexpr char kPortalPassword[] = "default1";
|
||||
|
||||
WiFiManager wifi;
|
||||
|
||||
} // namespace
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(300);
|
||||
|
||||
// This harness is deliberately portal-only: it has no saved station
|
||||
// credentials and never exercises a consuming application.
|
||||
wifi.setConfigPortalTimeout(0);
|
||||
wifi.setAPStaticIPConfig(
|
||||
IPAddress(192, 168, 4, 1),
|
||||
IPAddress(192, 168, 4, 1),
|
||||
IPAddress(255, 255, 255, 0));
|
||||
wifi.startConfigPortal(kPortalSsid, kPortalPassword);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
wifi.process();
|
||||
}
|
||||
@@ -105,6 +105,7 @@ TestCase tests[] = {
|
||||
TEST_ENTRY(test_scan_cancels_when_lifecycle_blocked),
|
||||
TEST_ENTRY(test_scan_generation_invalidated_on_reset),
|
||||
|
||||
TEST_ENTRY(test_real_async_scan_completes),
|
||||
// Template rendering tests
|
||||
TEST_ENTRY(test_shell_template_renders_core_placeholders),
|
||||
TEST_ENTRY(test_shell_template_renders_dynamic_theme_with_percent_values),
|
||||
@@ -128,6 +129,8 @@ TestCase tests[] = {
|
||||
TEST_ENTRY(test_rapid_portal_start_stop),
|
||||
TEST_ENTRY(test_multiple_parameters_stress),
|
||||
TEST_ENTRY(test_portal_with_timeout_stress),
|
||||
TEST_ENTRY(test_portal_resource_recovery),
|
||||
TEST_ENTRY(test_scan_result_storage_released_when_portal_closes),
|
||||
};
|
||||
|
||||
const size_t TEST_COUNT = sizeof(tests) / sizeof(TestCase);
|
||||
|
||||
@@ -107,6 +107,7 @@ void test_scan_completion_wait();
|
||||
void test_scan_cancels_when_connect_pending();
|
||||
void test_scan_cancels_when_lifecycle_blocked();
|
||||
void test_scan_generation_invalidated_on_reset();
|
||||
void test_real_async_scan_completes();
|
||||
|
||||
// Template rendering tests
|
||||
void test_shell_template_renders_core_placeholders();
|
||||
@@ -141,6 +142,8 @@ void test_long_running_portal();
|
||||
void test_rapid_portal_start_stop();
|
||||
void test_multiple_parameters_stress();
|
||||
void test_portal_with_timeout_stress();
|
||||
void test_portal_resource_recovery();
|
||||
void test_scan_result_storage_released_when_portal_closes();
|
||||
|
||||
#endif // TEST_MAIN_H
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user