Cache verified nRF52 metadata for OTA status displays
This commit is contained in:
1 parent
a14a0ad81d
commit
841571a7d8
6 files changed
+468
-5
No files matched your search
@@ -213,6 +213,7 @@ jobs:
|
||||
python3 -B test/test_ota_heap_context.py
|
||||
python3 -B test/test_ota_self_serve_policy.py -v
|
||||
python3 -B test/test_ota_self_metadata_cache.py -v
|
||||
python3 -B test/test_ota_nrf52_display_cache.py -v
|
||||
python3 -B test/test_ota_device_deflate_encoder.py -v
|
||||
python3 -B test/test_ota_device_deflate_role.py -v
|
||||
python3 -B test/test_client_acl_spiffs.py
|
||||
|
||||
@@ -223,7 +223,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
c.folder_active ? c.folder_dest_info : "not connected",
|
||||
(unsigned)c.manager.servedCount(), dighx);
|
||||
#else
|
||||
SelfFwInfo fi; bool s = ota_self_firmware(fi);
|
||||
SelfFwInfo fi; bool s = ota_self_firmware_for_display(fi);
|
||||
char selfhx[9]; if (s && fi.valid) mesh::Utils::toHex(selfhx, fi.body_hash, 4); else strcpy(selfhx, "?");
|
||||
OtaManager::FetchState fs = c.manager.fetchState();
|
||||
char dl[80];
|
||||
@@ -303,7 +303,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
const OtaManager::ServeEntry* e = c.manager.servedEntry(i);
|
||||
if (e && e->is_self) { self = e; break; }
|
||||
}
|
||||
SelfFwInfo fi; bool sok = ota_self_firmware(fi);
|
||||
SelfFwInfo fi; bool sok = ota_self_firmware_for_display(fi);
|
||||
char midhx[9], bodyhx[9], verbuf[20], dighx[9];
|
||||
if (self) mesh::Utils::toHex(midhx, self->mid, 4); else strcpy(midhx, "?");
|
||||
if (sok && fi.valid) mesh::Utils::toHex(bodyhx, fi.body_hash, 4); else strcpy(bodyhx, "?");
|
||||
|
||||
@@ -106,6 +106,49 @@ bool ota_self_firmware(SelfFwInfo& out) {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool ota_self_firmware_for_display(SelfFwInfo& out) {
|
||||
#if defined(NRF52_PLATFORM)
|
||||
struct DisplayCache {
|
||||
bool valid = false;
|
||||
uint32_t app_base = 0;
|
||||
uint32_t stage_ceiling = 0;
|
||||
SelfFwInfo info;
|
||||
};
|
||||
static DisplayCache cache;
|
||||
out = SelfFwInfo();
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
const uint32_t stage_ceiling =
|
||||
#if defined(OTA_SD_STORE)
|
||||
MOTA_NRF52_APP_END;
|
||||
#else
|
||||
mota_nrf52_layout_stage_ceiling();
|
||||
#endif
|
||||
if (!mota_nrf52_layout_valid(app_base, stage_ceiling)) {
|
||||
cache.valid = false;
|
||||
return false;
|
||||
}
|
||||
if (cache.valid && cache.app_base == app_base && cache.stage_ceiling == stage_ceiling) {
|
||||
out = cache.info;
|
||||
return true;
|
||||
}
|
||||
// App updates reboot, so only diagnostic metadata needs a per-boot snapshot. Keep all explicit
|
||||
// verification and apply/base/headroom checks on the fresh accessor: a display hit proves nothing
|
||||
// about later flash mutations. Never cache failure or retain metadata from a different layout.
|
||||
cache.valid = false;
|
||||
if (!ota_self_firmware(out) || !out.valid) {
|
||||
out = SelfFwInfo();
|
||||
return false;
|
||||
}
|
||||
cache.app_base = app_base;
|
||||
cache.stage_ceiling = stage_ceiling;
|
||||
cache.info = out;
|
||||
cache.valid = true;
|
||||
return true;
|
||||
#else
|
||||
return ota_self_firmware(out);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(ESP32_PLATFORM)
|
||||
bool ota_self_read(uint32_t off, uint8_t* buf, uint32_t len) {
|
||||
const esp_partition_t* p = esp_ota_get_running_partition();
|
||||
|
||||
@@ -15,6 +15,11 @@ namespace ota {
|
||||
// missing partitions, changed geometry and read failures are never cached. OTA writes inactive flash.
|
||||
bool ota_self_firmware(SelfFwInfo& out);
|
||||
|
||||
// Display-only snapshot for status/stats polling. nRF52 caches successful, body-verified metadata
|
||||
// for this boot while the app base and staging ceiling match; invalid geometry or failed scans are
|
||||
// never cached. Use ota_self_firmware(), not this accessor, for explicit verification or OTA safety.
|
||||
bool ota_self_firmware_for_display(SelfFwInfo& out);
|
||||
|
||||
// Read `len` bytes of the running firmware image at offset `off` (ESP32: running partition via
|
||||
// esp_partition_read; nRF52: memory-mapped app region). false on unsupported platforms.
|
||||
bool ota_self_read(uint32_t off, uint8_t* buf, uint32_t len);
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise real nRF52 self verification and status/stats without device headers."""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_ota_self_metadata_cache import method
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
HARNESS = r'''
|
||||
#include <helpers/ota/FirmwareInfo.h>
|
||||
#include <helpers/ota/OtaByteIO.h>
|
||||
#include <SHA256.h>
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace mesh {
|
||||
struct Utils {
|
||||
static void toHex(char* out, const uint8_t* data, size_t len) {
|
||||
const char* digits = "0123456789abcdef";
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
out[i * 2] = digits[data[i] >> 4]; out[i * 2 + 1] = digits[data[i] & 15];
|
||||
}
|
||||
out[len * 2] = 0;
|
||||
}
|
||||
};
|
||||
namespace ota {
|
||||
static const uint32_t MOTA_NRF52_STAGE_CEILING_LEGACY = 0xD4000;
|
||||
static const uint32_t MOTA_NRF52_STAGE_CEILING_EXPANDED = 0xED000;
|
||||
static const uint32_t MOTA_NRF52_APP_END = 0xED000;
|
||||
static const uint32_t MOTA_NRF52_FLASH_PAGE = 4096;
|
||||
static uint32_t app_base = 0x26000, stage_ceiling = 0xD4000;
|
||||
static std::vector<uint8_t> flash;
|
||||
static unsigned scans = 0, hashes = 0;
|
||||
static uint64_t hashed_bytes = 0;
|
||||
static uint32_t clock_ms = 1000;
|
||||
uint32_t mota_nrf52_app_base() { return app_base; }
|
||||
uint32_t mota_nrf52_layout_stage_ceiling() { return stage_ceiling; }
|
||||
@GEOMETRY@
|
||||
|
||||
static uint32_t effective_ceiling() {
|
||||
#if defined(OTA_SD_STORE)
|
||||
return MOTA_NRF52_APP_END;
|
||||
#else
|
||||
return stage_ceiling;
|
||||
#endif
|
||||
}
|
||||
void mh8(uint8_t out[8], const uint8_t* data, size_t len) {
|
||||
++hashes; hashed_bytes += len;
|
||||
SHA256 sha; sha.update(data, len); sha.finalize(out, 8);
|
||||
}
|
||||
@SCANNER@
|
||||
// Translate the real accessor's memory-mapped address at the scanner boundary, without changing
|
||||
// either production function. This keeps the harness portable and compatible with sanitizers.
|
||||
bool find_self_firmware(const uint8_t* region, uint32_t len, SelfFwInfo& out, bool verify_body) {
|
||||
++scans;
|
||||
assert(uintptr_t(region) == app_base && len == effective_ceiling() - app_base);
|
||||
assert(len <= flash.size());
|
||||
return scan_flash(flash.data(), len, out, verify_body);
|
||||
}
|
||||
@FRESH@
|
||||
@DISPLAY@
|
||||
|
||||
struct OtaManager {
|
||||
enum FetchState { IDLE, WANT_MANIFEST, WANT_LEAVES, VERIFYING_STAGED, FETCHING,
|
||||
COMPLETE, FAILED, PAUSED };
|
||||
enum FetchError { FETCH_ERROR_NONE, FETCH_ERROR_MANIFEST, FETCH_ERROR_HASH_ALGO,
|
||||
FETCH_ERROR_VERSION, FETCH_ERROR_CODEC, FETCH_ERROR_GEOMETRY, FETCH_ERROR_TOO_LARGE,
|
||||
FETCH_ERROR_STORAGE, FETCH_ERROR_INTEGRITY, FETCH_ERROR_MANIFEST_TIMEOUT,
|
||||
FETCH_ERROR_LEAVES_TIMEOUT };
|
||||
enum { AUTOFETCH_ANY = 2, AUTOFETCH_SIGNED = 1 };
|
||||
struct ServeEntry { bool is_self; uint8_t mid[4]; uint32_t fw_version, have_count; };
|
||||
FetchState state = IDLE;
|
||||
FetchError error = FETCH_ERROR_NONE;
|
||||
unsigned have = 0, total = 26;
|
||||
uint8_t mid[4] = {1, 2, 3, 4};
|
||||
FetchState fetchState() const { return state; }
|
||||
FetchError fetchError() const { return error; }
|
||||
unsigned blocksHave() const { return have; }
|
||||
unsigned blocksTotal() const { return total; }
|
||||
const uint8_t* fetchManifestId() const { return mid; }
|
||||
bool fetched_is_bootloader() const { return false; }
|
||||
uint32_t target() const { return 0x12345678; }
|
||||
unsigned servedCount() const { return 0; }
|
||||
const ServeEntry* servedEntry(unsigned) const { return nullptr; }
|
||||
void servedDigest(uint8_t* out) const { memset(out, 0, 4); }
|
||||
uint8_t autofetch() const { return 0; }
|
||||
unsigned max_hops() const { return 2; }
|
||||
};
|
||||
struct OtaBlCaps { bool present = true; uint8_t storage_flags = 1; };
|
||||
static const uint8_t OTA_BL_STORAGE_SD = 1;
|
||||
struct Context {
|
||||
OtaManager manager;
|
||||
uint32_t session_started_ms = 1000;
|
||||
char hw_id[33] = "test";
|
||||
bool serving = false;
|
||||
struct Allow { unsigned count() const { return 0; } } allow;
|
||||
OtaBlCaps caps;
|
||||
const OtaBlCaps& bootloaderAppCaps() const { return caps; }
|
||||
};
|
||||
uint32_t millis() { return clock_ms; }
|
||||
unsigned ota_max_block_capability() { return 2048; }
|
||||
const char* ota_target_env_name(uint32_t) { return "test"; }
|
||||
uint8_t ota_bootloader_last_rc() { return 0xB8; }
|
||||
bool ota_nrf52_boot_update_result(uint8_t) { return false; }
|
||||
@CLI_HELPERS@
|
||||
void status(Context& c, char* reply) { @STATUS@ }
|
||||
void stats(Context& c, char* reply) { @STATS@ }
|
||||
|
||||
static SelfFwInfo image(uint32_t body, uint8_t tag) {
|
||||
assert(body + ENDF_LEN <= effective_ceiling() - app_base);
|
||||
flash.assign(effective_ceiling() - app_base, uint8_t(0xA0 + tag));
|
||||
uint8_t* trailer = flash.data() + body;
|
||||
memcpy(trailer, ENDF_MAGIC, 4);
|
||||
wr_u32le(trailer + 4, body);
|
||||
SHA256 sha; sha.update(flash.data(), body); sha.finalize(trailer + 8, 8);
|
||||
wr_u32le(trailer + 16, 0x01110100u + tag);
|
||||
wr_u32le(trailer + 20, 0xA0000000u + tag);
|
||||
memset(trailer + 24, 'A' + tag, 32);
|
||||
SelfFwInfo expected;
|
||||
assert(scan_flash(flash.data(), flash.size(), expected, false));
|
||||
return expected;
|
||||
}
|
||||
static void expect_info(const SelfFwInfo& actual, const SelfFwInfo& expected) {
|
||||
assert(actual.valid && actual.body_len == expected.body_len);
|
||||
assert(actual.image_len == expected.image_len && actual.endf_offset == expected.endf_offset);
|
||||
assert(actual.fw_version == expected.fw_version && actual.target_id == expected.target_id);
|
||||
assert(memcmp(actual.body_hash, expected.body_hash, 8) == 0);
|
||||
assert(memcmp(actual.hw_id, expected.hw_id, 33) == 0);
|
||||
}
|
||||
static void expect_empty(const SelfFwInfo& info) {
|
||||
assert(!info.valid && !info.body_len && !info.image_len && !info.endf_offset);
|
||||
assert(!info.fw_version && !info.target_id);
|
||||
for (uint8_t value : info.body_hash) assert(value == 0);
|
||||
for (char value : info.hw_id) assert(value == 0);
|
||||
}
|
||||
} }
|
||||
using namespace mesh::ota;
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
assert(argc == 2);
|
||||
const std::string which = argv[1];
|
||||
SelfFwInfo out;
|
||||
if (which == "production") {
|
||||
const SelfFwInfo expected = image(489u * 1024u, 1);
|
||||
assert(ota_self_firmware_for_display(out));
|
||||
expect_info(out, expected);
|
||||
assert(scans == 1 && hashes == 1 && hashed_bytes == expected.body_len);
|
||||
for (unsigned i = 0; i < 50; ++i) {
|
||||
out = SelfFwInfo();
|
||||
assert(ota_self_firmware_for_display(out));
|
||||
expect_info(out, expected);
|
||||
}
|
||||
assert(scans == 1 && hashes == 1 && hashed_bytes == expected.body_len);
|
||||
// Every caller receives a copy, not a mutable view of the cache.
|
||||
out.valid = false; out.body_hash[0] ^= 1; out.hw_id[0] = '?';
|
||||
assert(ota_self_firmware_for_display(out)); expect_info(out, expected);
|
||||
std::cout << "489-KiB body verified once; zero scans or hashes for 50 display polls\n";
|
||||
} else if (which == "base") {
|
||||
image(4096, 1);
|
||||
assert(ota_self_firmware_for_display(out));
|
||||
app_base = 0x27000;
|
||||
SelfFwInfo expected = image(8192, 2);
|
||||
assert(ota_self_firmware_for_display(out)); expect_info(out, expected);
|
||||
assert(scans == 2 && hashes == 2);
|
||||
app_base = 0x26000;
|
||||
expected = image(1024, 3);
|
||||
assert(ota_self_firmware_for_display(out)); expect_info(out, expected);
|
||||
assert(scans == 3 && hashes == 3);
|
||||
} else if (which == "ceiling") {
|
||||
SelfFwInfo expected = image(4096, 1);
|
||||
assert(ota_self_firmware_for_display(out));
|
||||
stage_ceiling = MOTA_NRF52_STAGE_CEILING_EXPANDED;
|
||||
#if defined(OTA_SD_STORE)
|
||||
// SD always scans through APP_END, irrespective of internal staging geometry.
|
||||
assert(ota_self_firmware_for_display(out)); expect_info(out, expected);
|
||||
stage_ceiling = 123;
|
||||
assert(ota_self_firmware_for_display(out)); expect_info(out, expected);
|
||||
assert(scans == 1 && hashes == 1);
|
||||
#else
|
||||
expected = image(8192, 2);
|
||||
assert(ota_self_firmware_for_display(out)); expect_info(out, expected);
|
||||
assert(scans == 2 && hashes == 2);
|
||||
stage_ceiling = MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
expected = image(1024, 3);
|
||||
assert(ota_self_firmware_for_display(out)); expect_info(out, expected);
|
||||
assert(scans == 3 && hashes == 3);
|
||||
stage_ceiling = 123;
|
||||
assert(!ota_self_firmware_for_display(out)); expect_empty(out);
|
||||
assert(scans == 3 && hashes == 3);
|
||||
stage_ceiling = MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
expected = image(512, 4);
|
||||
assert(ota_self_firmware_for_display(out)); expect_info(out, expected);
|
||||
assert(scans == 4 && hashes == 4);
|
||||
#endif
|
||||
} else if (which == "invalid") {
|
||||
image(4096, 1);
|
||||
assert(ota_self_firmware_for_display(out));
|
||||
++app_base; // Alignment failure must clear the previously successful snapshot.
|
||||
assert(!ota_self_firmware_for_display(out)); expect_empty(out);
|
||||
assert(scans == 1 && hashes == 1);
|
||||
--app_base;
|
||||
SelfFwInfo expected = image(1024, 2);
|
||||
assert(ota_self_firmware_for_display(out)); expect_info(out, expected);
|
||||
assert(scans == 2 && hashes == 2);
|
||||
app_base = MOTA_NRF52_APP_END; // Empty/inverted application regions are invalid too.
|
||||
assert(!ota_self_firmware_for_display(out)); expect_empty(out);
|
||||
assert(scans == 2 && hashes == 2);
|
||||
} else if (which == "retry") {
|
||||
image(4096, 1);
|
||||
flash[0] ^= 1;
|
||||
assert(!ota_self_firmware_for_display(out)); expect_empty(out);
|
||||
assert(!ota_self_firmware_for_display(out)); expect_empty(out);
|
||||
assert(scans == 2 && hashes == 2);
|
||||
SelfFwInfo expected = image(4096, 2);
|
||||
assert(ota_self_firmware_for_display(out)); expect_info(out, expected);
|
||||
assert(scans == 3 && hashes == 3);
|
||||
app_base = 0x27000; // A failed new layout must never resurrect the old success.
|
||||
image(4096, 3);
|
||||
wr_u32le(flash.data() + 4096 + 4, 4095);
|
||||
assert(!ota_self_firmware_for_display(out)); expect_empty(out);
|
||||
assert(!ota_self_firmware_for_display(out)); expect_empty(out);
|
||||
assert(scans == 5 && hashes == 3);
|
||||
expected = image(1024, 4);
|
||||
assert(ota_self_firmware_for_display(out)); expect_info(out, expected);
|
||||
assert(scans == 6 && hashes == 4);
|
||||
} else if (which == "integrity") {
|
||||
const SelfFwInfo expected = image(4096, 1);
|
||||
assert(ota_self_firmware_for_display(out));
|
||||
flash[0] ^= 1; // A cached display must never weaken the fresh safety accessor.
|
||||
assert(!ota_self_firmware(out)); expect_empty(out);
|
||||
assert(scans == 2 && hashes == 2);
|
||||
assert(ota_self_firmware_for_display(out)); expect_info(out, expected);
|
||||
assert(scans == 2 && hashes == 2);
|
||||
assert(!ota_self_firmware(out)); expect_empty(out);
|
||||
assert(scans == 3 && hashes == 3);
|
||||
} else if (which == "cli") {
|
||||
image(489u * 1024u, 1);
|
||||
Context c;
|
||||
char reply[160];
|
||||
status(c, reply); assert(strstr(reply, "no download"));
|
||||
stats(c, reply); assert(strstr(reply, "fetch idle"));
|
||||
assert(scans == 1 && hashes == 1);
|
||||
for (unsigned i = 1; i <= 25; ++i) {
|
||||
c.manager.state = OtaManager::FETCHING; c.manager.have = i;
|
||||
clock_ms = 1000 + i * 1000;
|
||||
char progress[20], age[20];
|
||||
snprintf(progress, sizeof progress, "%u/26", i);
|
||||
snprintf(age, sizeof age, "%us", i);
|
||||
status(c, reply);
|
||||
assert(strstr(reply, progress) && strstr(reply, age) && strstr(reply, "id=01020304"));
|
||||
stats(c, reply);
|
||||
assert(strstr(reply, progress) && strstr(reply, age) && strstr(reply, "id=01020304"));
|
||||
}
|
||||
c.manager.mid[0] = 10;
|
||||
c.manager.state = OtaManager::FAILED; c.manager.error = OtaManager::FETCH_ERROR_INTEGRITY;
|
||||
status(c, reply); assert(strstr(reply, "failed (integrity check)") && strstr(reply, "id=0a020304"));
|
||||
stats(c, reply); assert(strstr(reply, "failed:integrity check") && strstr(reply, "id=0a020304"));
|
||||
c.manager.state = OtaManager::COMPLETE; c.manager.have = 26;
|
||||
status(c, reply); assert(strstr(reply, "ready to install 26/26"));
|
||||
stats(c, reply); assert(strstr(reply, "fetch done 26/26"));
|
||||
assert(scans == 1 && hashes == 1);
|
||||
std::cout << "Live status/stats progress, state, MID and errors update without rescanning\n";
|
||||
} else {
|
||||
assert(false && "unknown test case");
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class OtaNrf52DisplayCacheTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.temp = tempfile.TemporaryDirectory(prefix="meshcore-nrf52-display-")
|
||||
cls.addClassCleanup(cls.temp.cleanup)
|
||||
self_source = (ROOT / "src/helpers/ota/OtaSelf.cpp").read_text(encoding="utf-8")
|
||||
nrf_source = self_source[self_source.index("#elif defined(NRF52_PLATFORM)\n// nRF52 internal flash"):]
|
||||
scanner = method((ROOT / "src/helpers/ota/FirmwareInfo.cpp").read_text(encoding="utf-8"),
|
||||
"bool find_self_firmware(").replace("find_self_firmware(", "scan_flash(", 1)
|
||||
layout = (ROOT / "src/helpers/ota/OtaFlashLayout_nrf52.h").read_text(encoding="utf-8")
|
||||
geometry = method(layout, "inline bool mota_nrf52_stage_ceiling_valid(") + "\n" + method(
|
||||
layout, "inline bool mota_nrf52_layout_valid(uint32_t app_base, uint32_t stage_ceiling)")
|
||||
cli = (ROOT / "src/helpers/ota/OtaCli.cpp").read_text(encoding="utf-8")
|
||||
status = method(cli, '} else if (*a == 0 || is_cmd(a, "status|st", &rest))')
|
||||
stats = method(cli, '} else if (is_cmd(a, "stats", &rest))')
|
||||
helpers = "\n".join(method(cli, signature) for signature in (
|
||||
"static const char* state_word(", "static const char* state_short(",
|
||||
"static const char* fetch_error_word(", "static void ver_str("))
|
||||
source = HARNESS
|
||||
for token, value in {
|
||||
"GEOMETRY": geometry, "SCANNER": scanner,
|
||||
"FRESH": method(nrf_source, "bool ota_self_firmware(SelfFwInfo& out)"),
|
||||
"DISPLAY": method(self_source, "bool ota_self_firmware_for_display(SelfFwInfo& out)"),
|
||||
"CLI_HELPERS": helpers,
|
||||
"STATUS": status[status.index("{") + 1:-1],
|
||||
"STATS": stats[stats.index("{") + 1:-1],
|
||||
}.items():
|
||||
source = source.replace("@" + token + "@", value)
|
||||
cls.binaries = {}
|
||||
for name, defines in (("internal", ()), ("sd", ("OTA_SD_STORE=1",))):
|
||||
binary = Path(cls.temp.name) / (name + (".exe" if os.name == "nt" else ""))
|
||||
sanitizers = [] if os.name == "nt" else ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
|
||||
built = subprocess.run([
|
||||
# The real CLI deliberately truncates descriptive tails to its 160-byte reply cap.
|
||||
"c++", "-std=c++11", "-Wall", "-Wextra", *sanitizers,
|
||||
"-DNRF52_PLATFORM=1", *["-D" + define for define in defines],
|
||||
"-I", str(ROOT / "src"), "-I", str(ROOT / "test/mocks"),
|
||||
"-x", "c++", "-", "-o", str(binary),
|
||||
], input=source, text=True, capture_output=True)
|
||||
if built.returncode:
|
||||
raise AssertionError(built.stderr)
|
||||
cls.binaries[name] = binary
|
||||
|
||||
def run_case(self, case):
|
||||
for storage, binary in self.binaries.items():
|
||||
with self.subTest(storage=storage):
|
||||
result = subprocess.run([str(binary), case], text=True, capture_output=True)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_production_body_is_verified_once_for_fifty_polls(self):
|
||||
self.run_case("production")
|
||||
|
||||
def test_application_base_changes_force_fresh_verification(self):
|
||||
self.run_case("base")
|
||||
|
||||
def test_exact_storage_ceiling_is_used_for_cache_invalidation(self):
|
||||
self.run_case("ceiling")
|
||||
|
||||
def test_invalid_geometry_clears_previous_success(self):
|
||||
self.run_case("invalid")
|
||||
|
||||
def test_body_or_trailer_failures_remain_retryable(self):
|
||||
self.run_case("retry")
|
||||
|
||||
def test_fresh_verification_still_detects_body_mutation_after_display_hit(self):
|
||||
self.run_case("integrity")
|
||||
|
||||
def test_real_status_and_stats_poll_live_progress_without_hashing_again(self):
|
||||
self.run_case("cli")
|
||||
|
||||
def test_unsupported_platform_display_delegates_empty_result(self):
|
||||
self_source = (ROOT / "src/helpers/ota/OtaSelf.cpp").read_text(encoding="utf-8")
|
||||
unsupported = self_source[self_source.index("#else\nbool ota_self_firmware(SelfFwInfo& out)"):]
|
||||
source = r'''
|
||||
#include <helpers/ota/OtaSelf.h>
|
||||
#include <cassert>
|
||||
namespace mesh { namespace ota {
|
||||
@FRESH@
|
||||
@DISPLAY@
|
||||
} }
|
||||
int main() {
|
||||
mesh::ota::SelfFwInfo out;
|
||||
out.valid = true; out.body_len = 42; out.hw_id[0] = 'X';
|
||||
assert(!mesh::ota::ota_self_firmware_for_display(out));
|
||||
assert(!out.valid && !out.body_len && !out.hw_id[0]);
|
||||
}
|
||||
'''
|
||||
source = source.replace("@FRESH@", method(unsupported, "bool ota_self_firmware(SelfFwInfo& out)"))
|
||||
source = source.replace("@DISPLAY@", method(self_source, "bool ota_self_firmware_for_display(SelfFwInfo& out)"))
|
||||
sanitizers = [] if os.name == "nt" else ["-fsanitize=address,undefined"]
|
||||
for platform in (None, "STM32_PLATFORM=1", "RP2040_PLATFORM=1"):
|
||||
with self.subTest(platform=platform):
|
||||
binary = Path(self.temp.name) / "unsupported.exe"
|
||||
built = subprocess.run([
|
||||
"c++", "-std=c++11", "-Wall", "-Wextra", "-Werror", *sanitizers,
|
||||
*(["-D" + platform] if platform else []), "-I", str(ROOT / "src"),
|
||||
"-x", "c++", "-", "-o", str(binary),
|
||||
], input=source, text=True, capture_output=True)
|
||||
self.assertEqual(built.returncode, 0, built.stderr)
|
||||
result = subprocess.run([str(binary)], text=True, capture_output=True)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -57,6 +57,7 @@ int esp_partition_read(const esp_partition_t* p, uint32_t off, void* dst, uint32
|
||||
|
||||
namespace mesh { namespace ota {
|
||||
@ACCESSOR@
|
||||
@DISPLAY@
|
||||
} }
|
||||
using namespace mesh::ota;
|
||||
|
||||
@@ -223,6 +224,29 @@ int main(int argc, char** argv) {
|
||||
const unsigned complete = reads;
|
||||
assert(ota_self_firmware(out) && reads == complete);
|
||||
}
|
||||
} else if (which == "display") {
|
||||
image(4096, 1);
|
||||
assert(ota_self_firmware_for_display(out));
|
||||
expect_info(out, 4096, 1);
|
||||
const unsigned first = reads;
|
||||
for (unsigned i = 0; i < 50; ++i) {
|
||||
assert(ota_self_firmware_for_display(out));
|
||||
expect_info(out, 4096, 1);
|
||||
}
|
||||
assert(reads == first);
|
||||
running.address = 0x410000;
|
||||
image(8192, 2);
|
||||
assert(ota_self_firmware_for_display(out));
|
||||
expect_info(out, 8192, 2);
|
||||
assert(reads > first);
|
||||
present = false;
|
||||
assert(!ota_self_firmware_for_display(out)); expect_empty(out);
|
||||
present = true;
|
||||
image(1024, 3);
|
||||
fail_call = reads + 1;
|
||||
assert(!ota_self_firmware_for_display(out)); expect_empty(out);
|
||||
fail_call = 0;
|
||||
assert(ota_self_firmware_for_display(out)); expect_info(out, 1024, 3);
|
||||
} else {
|
||||
assert(false && "unknown test case");
|
||||
}
|
||||
@@ -236,10 +260,12 @@ class OtaSelfMetadataCacheTest(unittest.TestCase):
|
||||
cls.temp = tempfile.TemporaryDirectory(prefix="meshcore-self-metadata-")
|
||||
cls.addClassCleanup(cls.temp.cleanup)
|
||||
cls.binary = Path(cls.temp.name) / ("self_metadata.exe" if os.name == "nt" else "self_metadata")
|
||||
accessor = method((ROOT / "src/helpers/ota/OtaSelf.cpp").read_text(encoding="utf-8"),
|
||||
"bool ota_self_firmware(SelfFwInfo& out)")
|
||||
source = HARNESS.replace("@ACCESSOR@", accessor)
|
||||
self_source = (ROOT / "src/helpers/ota/OtaSelf.cpp").read_text(encoding="utf-8")
|
||||
accessor = method(self_source, "bool ota_self_firmware(SelfFwInfo& out)")
|
||||
display = method(self_source, "bool ota_self_firmware_for_display(SelfFwInfo& out)")
|
||||
source = HARNESS.replace("@ACCESSOR@", accessor).replace("@DISPLAY@", display)
|
||||
built = subprocess.run(["c++", "-std=c++11", "-Wall", "-Wextra", "-Werror",
|
||||
"-DESP32_PLATFORM=1",
|
||||
"-I", str(ROOT / "src"), "-x", "c++", "-", "-o", str(cls.binary)],
|
||||
input=source, text=True, capture_output=True)
|
||||
if built.returncode:
|
||||
@@ -277,6 +303,9 @@ class OtaSelfMetadataCacheTest(unittest.TestCase):
|
||||
def test_chunk_overlap_and_partition_boundary(self):
|
||||
self.run_case("edges")
|
||||
|
||||
def test_display_accessor_delegates_esp32_cache_and_retry_semantics(self):
|
||||
self.run_case("display")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user