Share nRF52 checked-record reads and command-family lookups without changing storage formats, error protection, fleet authorization, or delegated scopes. Use the existing bounded Merkle proof workspace for qualified ESP32 repeaters while retaining source capacity, wire format, and every enabled feature. Restore advert tab/CRLF delimiters and expose nested qualification build logs. Validation: all 1932 native CI test cases; exact canonical RAK4631 Full, Wio-E5 Mini, Station G3 Full MQTT, and Wireless Tracker Full wrappers; linked RAK crypto/workspace checks and retained Reader code equivalence; focused persistence, authorization, source-proof, and RAM policy regressions.
172 lines
7.8 KiB
Python
172 lines
7.8 KiB
Python
"""Exercise production OTA speed CLI/persistence without an OTA workspace."""
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
HARNESS = r'''
|
|
#include <cassert>
|
|
#include <cstring>
|
|
#include <helpers/ota/OtaSpeedConfig.h>
|
|
using namespace mesh::ota;
|
|
int main() {
|
|
MemoryFS fs;
|
|
char reply[160];
|
|
auto command = [&](const char* text, const char* prefix) {
|
|
memset(reply, 0, sizeof reply);
|
|
assert(handleSpeedCommand(text, reply, sizeof reply));
|
|
assert(!strncmp(reply, prefix, strlen(prefix)));
|
|
};
|
|
beginSpeedConfig(&fs);
|
|
command("get ota.speed", "> ota.speed=1x packet=1x");
|
|
assert(handleSpeedCommand("get ota.speed", reply, sizeof reply, 0.25f));
|
|
assert(!strcmp(reply, "> ota.speed=1x packet=0.25x"));
|
|
assert(effectivePacketPace(0.25f) == 0.25f);
|
|
assert(effectivePacketPace(1.0f) == 1.0f);
|
|
for (const auto* bad : {"", "0", "-1", "0.049", "3.01", "nan", "inf", "1e0", "1abc", "1 2"}) {
|
|
const std::string text = std::string("set ota.speed ") + bad;
|
|
command(text.c_str(), "ERR");
|
|
assert(speedFactor() == 1.0f && fs.files.empty());
|
|
}
|
|
command("set ota.speed .05", "OK ota.speed=0.05x (saved) packet=0.05x");
|
|
assert(handleSpeedCommand("get ota.speed", reply, sizeof reply, 0.25f));
|
|
assert(!strcmp(reply, "> ota.speed=0.05x packet=0.05x"));
|
|
assert(validSpeed(speedFactor()));
|
|
beginSpeedConfig(&fs);
|
|
command("get ota.speed", "> ota.speed=0.05x packet=0.05x");
|
|
command("ota config speed 3", "OK ota.speed=3x (saved) packet=1x");
|
|
command("ota config speed", "> ota.speed=3x packet=1x");
|
|
command("ota cfg speed 0.5", "OK ota.speed=0.5x (saved) packet=0.5x");
|
|
command("ota speed", "> ota.speed=0.5x packet=0.5x");
|
|
command("get ota.speed 1", "ERR");
|
|
assert(!handleSpeedCommand("get ota.speed.extra", reply, sizeof reply));
|
|
assert(!handleSpeedCommand("ota config advert 10", reply, sizeof reply));
|
|
const auto good = fs.files["/ota_speed"];
|
|
fs.fail_write = true;
|
|
command("set ota.speed 2", "ERR");
|
|
assert(speedFactor() == 0.5f && fs.files["/ota_speed"] == good);
|
|
fs.fail_write = false;
|
|
fs.fail_rename = 2; // cannot promote temp; restore primary from backup
|
|
command("set ota.speed 2", "ERR");
|
|
assert(speedFactor() == 0.5f && fs.files["/ota_speed"] == good);
|
|
fs.files["/ota_speed.bak"] = good;
|
|
fs.files.erase("/ota_speed"); // power cut in rename gap
|
|
beginSpeedConfig(&fs);
|
|
assert(speedFactor() == 0.5f && fs.files["/ota_speed"] == good);
|
|
fs.files["/ota_speed.bak"] = good;
|
|
fs.files.erase("/ota_speed");
|
|
fs.fail_rename = 1; // valid backup remains authoritative if repair fails
|
|
beginSpeedConfig(&fs);
|
|
assert(speedFactor() == 0.5f);
|
|
command("set ota.speed 2", "ERR");
|
|
assert(fs.files["/ota_speed.bak"] == good);
|
|
beginSpeedConfig(&fs);
|
|
assert(speedFactor() == 0.5f && fs.files["/ota_speed"] == good);
|
|
fs.files["/ota_speed"][4] ^= 1; // damaged image is not partly adopted
|
|
beginSpeedConfig(&fs);
|
|
assert(speedFactor() == 1.0f);
|
|
command("set ota.speed 2", "OK ota.speed=2x (saved)");
|
|
assert(speedFactor() == 2.0f);
|
|
beginSpeedConfig(&fs);
|
|
assert(speedFactor() == 2.0f);
|
|
fs.files.clear();
|
|
beginSpeedConfig(&fs);
|
|
command("ota set speed 1.125", "OK ota.speed=1.125x");
|
|
beginSpeedConfig(&fs);
|
|
assert(speedFactor() == 1.125f);
|
|
}
|
|
'''
|
|
|
|
|
|
class OtaSpeedTest(unittest.TestCase):
|
|
def test_commands_range_storage_and_failure_recovery(self):
|
|
compiler = shutil.which("g++") or shutil.which("clang++")
|
|
self.assertIsNotNone(compiler)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
source = Path(directory) / "speed.cpp"
|
|
source.write_text(HARNESS)
|
|
binary = Path(directory) / "speed"
|
|
result = subprocess.run([
|
|
compiler, "-std=c++17", "-Wall", "-Wextra", "-DENABLE_OTA=1",
|
|
"-I", str(ROOT / "test/fixtures/radio_profiles/mocks"),
|
|
"-I", str(ROOT / "test/mocks"), "-I", str(ROOT / "src"),
|
|
str(ROOT / "src/helpers/ota/OtaSpeedConfig.cpp"), str(source),
|
|
"-o", str(binary),
|
|
], text=True, capture_output=True)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
subprocess.run([str(binary)], check=True)
|
|
|
|
def test_nrf52_metadata_open_and_short_reads_preserve_committed_speed(self):
|
|
mock = (ROOT / "test/fixtures/radio_profiles/mocks/helpers/IdentityStore.h").read_text()
|
|
mock = mock.replace("bool fail_write = false;", "bool stat_error=false;\n bool fail_write = false;")
|
|
mock = mock.replace("bool mkdir(const char*)", "void _lockFS() {}\n void _unlockFS() {}\n"
|
|
" MemoryFS* _getFS() { return this; }\n"
|
|
" bool mkdir(const char*)")
|
|
mock += ("\nstruct lfs_info {};\nconstexpr int LFS_ERR_NOENT=-2;\n"
|
|
"inline int lfs_stat(MemoryFS* fs, const char* path, lfs_info*) {\n"
|
|
" if (fs->stat_error) return -5;\n"
|
|
" return fs->files.count(path) ? 0 : LFS_ERR_NOENT;\n}\n")
|
|
faults = r'''
|
|
const auto committed = fs.files;
|
|
for (unsigned fault = 0; fault < 3; ++fault) {
|
|
fs.stat_error = fault == 0;
|
|
fs.fail_read_open = fault == 1;
|
|
fs.fail_read_after = fault == 2 ? 4 : -1;
|
|
beginSpeedConfig(&fs);
|
|
assert(speedFactor() == 1.0f); // Unreadable is not a corrupt image to replace.
|
|
command("set ota.speed 2", "ERR");
|
|
assert(fs.files == committed);
|
|
fs.stat_error = fs.fail_read_open = false;
|
|
fs.fail_read_after = -1;
|
|
beginSpeedConfig(&fs);
|
|
assert(speedFactor() == 1.125f);
|
|
}
|
|
fs.files["/ota_speed"].pop_back(); // A complete, wrong-sized image is invalid.
|
|
beginSpeedConfig(&fs);
|
|
assert(speedFactor() == 1.0f);
|
|
command("set ota.speed 2", "OK ota.speed=2x (saved)");
|
|
beginSpeedConfig(&fs);
|
|
assert(speedFactor() == 2.0f);
|
|
'''
|
|
source = HARNESS.replace(" assert(speedFactor() == 1.125f);\n}",
|
|
" assert(speedFactor() == 1.125f);\n" + faults + "}")
|
|
compiler = shutil.which("g++") or shutil.which("clang++")
|
|
self.assertIsNotNone(compiler)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
work = Path(directory)
|
|
(work / "helpers").mkdir()
|
|
(work / "helpers/IdentityStore.h").write_text(mock)
|
|
cpp = work / "speed.cpp"
|
|
cpp.write_text(source)
|
|
binary = work / "speed"
|
|
result = subprocess.run([
|
|
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-Wno-sign-compare",
|
|
"-DENABLE_OTA=1", "-DNRF52_PLATFORM=1", "-I", directory,
|
|
"-I", str(ROOT / "test/fixtures/radio_profiles/mocks"),
|
|
"-I", str(ROOT / "test/mocks"), "-I", str(ROOT / "src"),
|
|
str(ROOT / "src/helpers/ota/OtaSpeedConfig.cpp"), str(cpp),
|
|
"-o", str(binary),
|
|
], text=True, capture_output=True)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
subprocess.run([str(binary)], check=True)
|
|
|
|
def test_shared_role_wiring_and_idle_context_access(self):
|
|
common = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
|
|
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
|
for text in (common, companion):
|
|
self.assertIn("mesh::ota::beginSpeedConfig(", text)
|
|
self.assertIn("mesh::ota::handleSpeedCommand(command, reply,", text)
|
|
cli = (ROOT / "src/helpers/ota/OtaCli.cpp").read_text()
|
|
self.assertLess(cli.index("handleSpeedCommand(command"), cli.index("ota_acquire_context(reply"))
|
|
mesh = (ROOT / "src/Mesh.cpp").read_text()
|
|
self.assertIn("syncOtaTiming(context->manager)", mesh)
|
|
self.assertIn("syncOtaTiming(ota::ota_ctx().manager)", mesh)
|
|
self.assertIn("manager.set_speed(getOtaSpeedFactor())", mesh)
|
|
self.assertIn("_ota_announce_timer.arm(_ms->getMillis(), gap, ota_speed)", mesh)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|