Files
MeshCore/test/test_ota_heap_context.py
T
mikecarper fa010196b1 Merge PR #7 with reviewed ExpressLRS power and memory fixes
Integrate ExpressLRS TX module support and its stacked ESP32 heap changes.
Use the approved Linkflow calibration (17-30 dBm), preserve the PA drive
and output path after radio recovery, and keep LoRa OTA enabled.

Preserve stored ACLs and filters on allocation failure, release owned
client/filter buffers, service heap OTA contexts on Companions, release
self-serving workspaces after TempRadio, and reset staged-resume state
when a context is released. Keep manual staging and active operations
alive. Account for all moved allocations in the runtime RAM gate.

Validation: 1,393 native cases; radio-power, heap-context, ACL persistence,
shared-queue transfer, display/inbox, radio receive, and memory regressions.
Firmware builds passed for Linkflow, Heltec V2 Companion, T-Beam MQTT
repeater, Heltec V4 R8 MQTT repeater, RAK4631 repeater, and Indicator Full.
Physical verification awaits access to the currently offline lab Pi.
2026-09-10 22:06:16 -07:00

140 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""Run the real heap-backed OTA context and its lifetime boundaries on host."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
from test_t096_full_memory import method
ROOT = Path(__file__).resolve().parents[1]
class OtaHeapTest(unittest.TestCase):
def test_build_policy_only_selects_classic_esp32_and_respects_storage_owner(self):
class Env(dict):
def BoardConfig(self):
return {"build.mcu": self["mcu"]}
def Append(self, **values):
for key, value in values.items():
self.setdefault(key, []).extend(value)
script = (ROOT / "scripts/esp32_ota_heap_context.py").read_text()
for mcu in ("esp32", "esp32s3", "esp32s2", "esp32c3", "nrf52840"):
env = Env(mcu=mcu)
exec(script, {"env": env, "Import": lambda _: None})
self.assertEqual(env.get("CPPDEFINES", []),
[("OTA_HEAP_CONTEXT", 1)] if mcu == "esp32" else [])
for flags in ("-DOTA_SHARED_COMPANION_QUEUE=1", ["-D", "OTA_SHARED_COMPANION_QUEUE=1"],
["-DOTA_HEAP_CONTEXT=1"]):
env = Env(mcu="esp32", BUILD_FLAGS=flags)
exec(script, {"env": env, "Import": lambda _: None})
self.assertNotIn("CPPDEFINES", env)
env = Env(mcu="esp32", CPPDEFINES=[("OTA_SHARED_COMPANION_QUEUE", 1)])
exec(script, {"env": env, "Import": lambda _: None})
self.assertEqual(env["CPPDEFINES"], [("OTA_SHARED_COMPANION_QUEUE", 1)])
def test_context_releases_self_serve_but_preserves_live_operations(self):
with tempfile.TemporaryDirectory() as temp:
path = Path(temp)
source = path / "test.cpp"
absent_context = method((ROOT / "src/Mesh.cpp").read_text(),
"if (!ota::ota_context_if_active())")
# Exercise the production absence guard with install support enabled,
# independently of the source-only storage used by this host fixture.
absent_context = absent_context.replace("#if !defined(OTA_SEEDER_ONLY)", "#if 1")
(path / "mesh_absence.h").write_text(
"namespace ota = mesh::ota;\nstruct MeshMaintenance {\n"
"bool _ota_temp_was_active = true, _ota_resumed = true, _ota_autoinstall_tried = true;\n"
"void service() {\n" + absent_context + "\n}\n};\n")
source.write_text(r'''
#include <helpers/ota/OtaContext.h>
#include <cassert>
#include <new>
#include "mesh_absence.h"
using namespace mesh::ota;
static bool fail_allocation = false;
void* operator new(std::size_t size, const std::nothrow_t&) noexcept {
return fail_allocation ? nullptr : ::operator new(size);
}
namespace mesh { namespace ota {
bool ota_self_firmware(SelfFwInfo& info) { info = SelfFwInfo(); return false; }
} }
static bool send(void*, const uint8_t*, uint16_t, bool) { return true; }
int main() {
MeshMaintenance maintenance;
maintenance.service();
assert(!maintenance._ota_temp_was_active && !maintenance._ota_resumed
&& !maintenance._ota_autoinstall_tried);
char reply[160] = {};
assert(!ota_acquire_context(reply, sizeof(reply)));
ota_begin_context(123, send, nullptr, "test", nullptr);
fail_allocation = true;
assert(!ota_acquire_context(reply, sizeof(reply)));
assert(strstr(reply, "out of memory") && !ota_context_if_active());
fail_allocation = false;
for (int cycle = 0; cycle < 16; ++cycle) {
ota_service_temp_radio_context(true);
assert(ota_context_if_active());
auto& c = ota_ctx();
c.manager.set_max_hops(7);
c.autoinstall = OtaContext::AUTOINSTALL_TRUSTED;
c.serving = true;
c.serve_self_leaves = static_cast<uint8_t*>(malloc(64));
c.serve_self_proof = static_cast<uint8_t*>(malloc(64));
assert(c.ensureServeBuffer());
ota_service_temp_radio_context(true);
assert(ota_context_if_active() == &c);
ota_service_temp_radio_context(false);
assert(!ota_context_if_active());
assert(ota_hop_limit() == 7);
}
assert(ota_acquire_context(reply, sizeof(reply)));
assert(ota_ctx().autoinstall == OtaContext::AUTOINSTALL_TRUSTED);
assert(ota_ctx().manager.max_hops() == 7);
ota_ctx().apply_pending = true;
ota_service_temp_radio_context(false);
assert(ota_context_if_active());
ota_ctx().apply_pending = false;
ota_ctx().folder_active = true;
ota_service_temp_radio_context(false);
assert(ota_context_if_active());
ota_ctx().folder_active = false;
ota_ctx().folder_dest = reinterpret_cast<FolderMotaStore*>(1);
ota_service_temp_radio_context(false);
assert(ota_context_if_active());
ota_ctx().folder_dest = nullptr;
// Manual staging is a series of CLI commands, even outside TempRadio.
ota_ctx().serve_expected = 100;
assert(ota_ctx().ensureServeBuffer());
ota_ctx().serve_buf[0] = 0x42;
ota_service_temp_radio_context(false);
assert(ota_context_if_active() && ota_ctx().serve_buf[0] == 0x42);
ota_ctx().serve_expected = 0;
ota_service_temp_radio_context(false);
assert(!ota_context_if_active());
}
''')
flags = [] if os.name == "nt" else ["-fsanitize=address,undefined"]
tinf = path / "tinf.o"
subprocess.run([shutil.which("cc") or "gcc", "-DENABLE_OTA=1", *flags, "-c",
str(ROOT / "src/helpers/ota/OtaTinf.c"), "-o", str(tinf)], check=True)
sources = ["OtaContext.cpp", "OtaManager.cpp", "OtaProtocol.cpp",
"MotaContainer.cpp", "MerkleTree.cpp", "OtaDeflate.cpp"]
binary = path / "heap.exe"
result = subprocess.run([
"c++", "-std=c++17", *flags, "-DENABLE_OTA=1",
"-DOTA_HEAP_CONTEXT=1", "-DESP32_PLATFORM=1", "-DOTA_SEEDER_ONLY=1",
"-I", str(ROOT / "src"), "-I", str(ROOT / "test/mocks"),
str(source), *[str(ROOT / "src/helpers/ota" / name) for name in sources],
str(ROOT / "src/Utils.cpp"), str(tinf), "-o", str(binary),
], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
subprocess.run([str(binary)], check=True)
if __name__ == "__main__":
unittest.main()