test: add portal OTA A/B contract coverage

This commit is contained in:
2026-09-17 12:10:48 +10:00
parent 7305ea8827
commit 228e1252f4
21 changed files with 954 additions and 33 deletions
+6
View File
@@ -30,3 +30,9 @@ build_flags =
-DWM_LOG_LEVEL=3
lib_deps =
${common.lib_deps}
; Clean-consumer validation against the maintained ESP32 platform lane. This
; is intentionally separate from the temporary 3.0.5 compatibility target.
[env:esp32_core_3_3_11]
extends = env:esp32
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
+22
View File
@@ -14,3 +14,25 @@ Use it through the repository runner so a secondary Wi-Fi adapter is explicitly
The ESP8266 portal SSID is `WM Contract ESP8266`; the ESP32 SSID is `WM Contract 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.
## A/B portal OTA fixture
The same fixture has dedicated `*_ota_a` and `*_ota_b` PlatformIO environments
for the physical portal HTTP OTA contract. A and B differ only by a compiled
marker served from the fixture-only `/api/test/firmware-marker` endpoint. That
proves a B boot without trusting saved portal values, EEPROM, or a filename.
```bash
./tools/portal-hardware ota \
--platform esp32 \
--port /dev/serial/by-id/... \
--client-interface wlx...
```
ESP8266 explicitly uses `eagle.flash.4m1m.ld`. ESP32 uses the tracked two-slot
`partitions/esp32_ota_4m_no_fs.csv` layout on the maintained Arduino-ESP32
3.3.11 fixture lane. Both are 4 MB layouts. The runner builds and preserves A
and B before it touches the board, validates the matching ESP32 slots (or the
live ESP8266 updater capacity), then erases the explicitly selected test board
before serial-flashing A. A successful run leaves B installed in the
portal-only fixture.
@@ -0,0 +1,12 @@
# 4 MB ESP32 portal-harness OTA test layout.
#
# The fixture compiles portal assets into the application image, so it uses no
# filesystem. app0 and app1 are equal 0x1F0000-byte slots. The portal OTA
# runner validates B against that inactive-slot size before it flashes A or
# submits the browser upload.
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x1F0000,
app1, app, ota_1, 0x200000, 0x1F0000,
coredump, data, coredump,0x3F0000, 0x10000,
1 # 4 MB ESP32 portal-harness OTA test layout.
2 #
3 # The fixture compiles portal assets into the application image, so it uses no
4 # filesystem. app0 and app1 are equal 0x1F0000-byte slots. The portal OTA
5 # runner validates B against that inactive-slot size before it flashes A or
6 # submits the browser upload.
7 # Name, Type, SubType, Offset, Size, Flags
8 nvs, data, nvs, 0x9000, 0x5000,
9 otadata, data, ota, 0xe000, 0x2000,
10 app0, app, ota_0, 0x10000, 0x1F0000,
11 app1, app, ota_1, 0x200000, 0x1F0000,
12 coredump, data, coredump,0x3F0000, 0x10000,
+39
View File
@@ -26,3 +26,42 @@ build_flags =
-DSOC_WIFI_SUPPORTED=1
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
-DWM_LOG_LEVEL=4
; The maintained ESP32 validation lane. Keep this named rather than relying on
; whatever framework/toolchain pair happens to be installed in PlatformIO's
; global package cache.
[env:esp32_core_3_3_11]
extends = env:esp32
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
; OTA contract images deliberately differ only by their compile-time marker.
; ESP8266 needs an OTA-capable linker layout; ESP32 needs a tracked two-slot
; partition table. The hardware runner always flashes A over serial then
; uploads B through the real portal form.
[env:esp8266_ota_a]
extends = env:esp8266
board_build.ldscript = eagle.flash.4m1m.ld
build_flags =
${env:esp8266.build_flags}
-DWM_OTA_TEST_IMAGE=\"A\"
[env:esp8266_ota_b]
extends = env:esp8266
board_build.ldscript = eagle.flash.4m1m.ld
build_flags =
${env:esp8266.build_flags}
-DWM_OTA_TEST_IMAGE=\"B\"
[env:esp32_ota_a]
extends = env:esp32_core_3_3_11
board_build.partitions = partitions/esp32_ota_4m_no_fs.csv
build_flags =
${env:esp32_core_3_3_11.build_flags}
-DWM_OTA_TEST_IMAGE=\"A\"
[env:esp32_ota_b]
extends = env:esp32_core_3_3_11
board_build.partitions = partitions/esp32_ota_4m_no_fs.csv
build_flags =
${env:esp32_core_3_3_11.build_flags}
-DWM_OTA_TEST_IMAGE=\"B\"
+31
View File
@@ -3,6 +3,14 @@
namespace {
// These markers belong only to the portal hardware fixture. They are compiled
// into the image rather than stored in Wi-FiManager settings, so an A -> B
// assertion proves that the new firmware booted after the updater restarted
// the board. Normal portal-contract builds retain a descriptive fixture value.
#ifndef WM_OTA_TEST_IMAGE
#define WM_OTA_TEST_IMAGE "portal-contract"
#endif
#if defined(ESP8266)
constexpr char kPortalSsid[] = "WM Contract ESP8266";
#else
@@ -57,6 +65,28 @@ WiFiManagerParameter* const kPortalParameters[] = {
&kNotes,
};
void registerOtaTestMarker() {
// setWebServerCallback runs after WiFiManager creates its server and
// before it registers built-in routes. This private fixture endpoint is
// intentionally not a WiFiManager product API.
wifi.setWebServerCallback([]() {
AsyncWebServer* const server = wifi.getServer();
if (server == nullptr) {
return;
}
server->on("/api/test/firmware-marker", HTTP_GET,
[](AsyncWebServerRequest* request) {
String response = F("{\"marker\":\"");
response += WM_OTA_TEST_IMAGE;
response += F("\",\"freeSketchSpace\":");
response += String(ESP.getFreeSketchSpace());
response += F("}");
request->send(200, "application/json", response);
});
});
}
} // namespace
void setup() {
@@ -75,6 +105,7 @@ void setup() {
IPAddress(192, 168, 4, 1),
IPAddress(192, 168, 4, 1),
IPAddress(255, 255, 255, 0));
registerOtaTestMarker();
// Keep custom parameters on their own native Save parameters page. This
// lets the browser contract exercise a parameter-only submit without
// starting a station connection as part of the regression test.