mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 12:22:26 +00:00
Keep the deployed SD target and layout, select qualified internal delta storage when no card is usable, and retain exact SD bootloader-update gates. Add storage, capability, transfer, recipe, and release-selection regression coverage. Also preserve mandatory runtime packet logging in canonical XIAO images when verbose build debugging is disabled, fixing the four stopped release-matrix qualification failures.
441 lines
20 KiB
Python
441 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
"""Run the MeshTower selector with peripheral stores and the real manifest parser."""
|
|
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
OTA = ROOT / "src/helpers/ota"
|
|
FLAGS = ["-DNRF52_PLATFORM=1", "-DHELTEC_TOWER_V2_SDCARD=1",
|
|
"-DOTA_SD_STORE=1", "-DOTA_FLASH_STORE=1", "-DOTA_TOWER_AUTO_STORE=1"]
|
|
|
|
CAPS = r'''
|
|
#pragma once
|
|
#include <stdint.h>
|
|
namespace mesh { namespace ota {
|
|
static const uint8_t OTA_BL_STORAGE_SD = 1;
|
|
static const uint8_t OTA_BL_STORAGE_STAGE_CEILING = 2;
|
|
struct OtaBlCaps {
|
|
bool present = false;
|
|
uint16_t apply_abi = 3, codec_mask = 5;
|
|
uint8_t storage_flags = 9, optional_app_storage = 2;
|
|
};
|
|
extern OtaBlCaps installed;
|
|
extern int cap_reads;
|
|
inline OtaBlCaps ota_bootloader_app_caps() { ++cap_reads; return installed; }
|
|
} }
|
|
'''
|
|
|
|
STORES = r'''
|
|
#pragma once
|
|
#include "OtaStore.h"
|
|
#include <string.h>
|
|
namespace mesh {
|
|
class MainBoard {};
|
|
namespace ota {
|
|
struct Medium {
|
|
int constructors = 0, destructors = 0, probes = 0;
|
|
int begin_calls = 0, plan_calls = 0, read_calls = 0, write_calls = 0;
|
|
int clear_calls = 0, discard_calls = 0, reopen_calls = 0;
|
|
int meta_calls = 0, finalize_calls = 0, checkpoints = 0;
|
|
int format_calls = 0, erase_calls = 0, space_calls = 0, list_calls = 0;
|
|
bool present = true, mountable = true, begin_ok = true, layout_ok = true;
|
|
bool io_ok = true, discard_ok = true, staged = false;
|
|
uint32_t total = 0, last_target = 0;
|
|
const uint8_t* last_mid = nullptr;
|
|
uint8_t bytes[512] = {0};
|
|
};
|
|
extern Medium sd_medium, flash_medium;
|
|
extern int auth_clears;
|
|
class FakeStore : public OtaStore {
|
|
protected:
|
|
Medium& media;
|
|
uint32_t total = 0;
|
|
public:
|
|
explicit FakeStore(Medium& m) : media(m) { ++media.constructors; }
|
|
~FakeStore() override { ++media.destructors; }
|
|
bool begin(uint32_t n) override {
|
|
++media.begin_calls;
|
|
total = 0;
|
|
if (!media.present || !media.begin_ok) return false;
|
|
total = media.total = n; media.staged = true; return true;
|
|
}
|
|
bool write(uint32_t p, const uint8_t* d, uint32_t n) override {
|
|
++media.write_calls;
|
|
if (!media.present || !media.io_ok || p + n > total || p + n > sizeof(media.bytes)) return false;
|
|
memcpy(media.bytes + p, d, n); return true;
|
|
}
|
|
bool read(uint32_t p, uint8_t* d, uint32_t n) const override {
|
|
++media.read_calls;
|
|
if (!media.present || !media.io_ok || p + n > total || p + n > sizeof(media.bytes)) return false;
|
|
memcpy(d, media.bytes + p, n); return true;
|
|
}
|
|
uint32_t capacity() const override { return media.present ? 8192u : 0u; }
|
|
uint32_t staged_size() const override { return total; }
|
|
void clear() override { ++media.clear_calls; total = 0; }
|
|
bool discard() override {
|
|
++media.discard_calls; total = 0;
|
|
if (!media.present || !media.discard_ok) return false;
|
|
media.staged = false; media.total = 0; return true;
|
|
}
|
|
bool set_meta_size(uint32_t) override { ++media.meta_calls; return media.io_ok; }
|
|
bool finalize() override { ++media.finalize_calls; return media.present && media.io_ok; }
|
|
void checkpoint() override { ++media.checkpoints; }
|
|
bool reopen() override {
|
|
++media.reopen_calls;
|
|
total = media.present && media.staged ? media.total : 0;
|
|
return total != 0;
|
|
}
|
|
bool reopenFor(const uint8_t* mid, uint32_t target) override {
|
|
media.last_mid = mid; media.last_target = target; return reopen();
|
|
}
|
|
bool plan_layout(bool, uint32_t, uint32_t, uint32_t, bool) override {
|
|
++media.plan_calls; return media.layout_ok;
|
|
}
|
|
};
|
|
class OtaStoreSdNrf52 : public FakeStore {
|
|
public:
|
|
OtaStoreSdNrf52() : FakeStore(sd_medium) { ++auth_clears; }
|
|
bool probeMedia() { ++media.probes; return media.present && media.mountable; }
|
|
void clear() override { ++media.clear_calls; total = 0; media.staged = false; }
|
|
const char* last_error() const { return "SD peripheral error"; }
|
|
bool formatCard(MainBoard&) { ++media.format_calls; return media.present; }
|
|
bool eraseCard(MainBoard&) { ++media.erase_calls; return media.present; }
|
|
bool getSpace(MainBoard&, uint64_t& used, uint64_t& free) {
|
|
++media.space_calls; used = 1; free = 2; return media.present;
|
|
}
|
|
bool listFiles(MainBoard&, uint16_t, char*, size_t) { ++media.list_calls; return media.present; }
|
|
};
|
|
class OtaStoreFlashNrf52 : public FakeStore {
|
|
public:
|
|
OtaStoreFlashNrf52() : FakeStore(flash_medium) { ++auth_clears; }
|
|
};
|
|
} }
|
|
'''
|
|
|
|
DRIVER = r'''
|
|
#include "OtaStoreTowerNrf52.h"
|
|
#include "OtaByteIO.h"
|
|
#include <assert.h>
|
|
#include <string.h>
|
|
using namespace mesh::ota;
|
|
namespace mesh { namespace ota {
|
|
OtaBlCaps installed;
|
|
Medium sd_medium, flash_medium;
|
|
int cap_reads = 0, auth_clears = 0;
|
|
} }
|
|
|
|
static void defaults() {
|
|
installed.present = true;
|
|
installed.storage_flags = 9;
|
|
installed.optional_app_storage = 2;
|
|
}
|
|
static void manifest(Medium& m, uint8_t flags = 0) {
|
|
m.staged = true;
|
|
memset(m.bytes, 0, sizeof(m.bytes));
|
|
uint8_t* p = m.bytes + 8;
|
|
const bool boot = flags & MFLAG_BOOTLOADER;
|
|
p[0] = boot ? MOTA_BOOT_FORMAT_VER : MOTA_APP_FORMAT_VER;
|
|
p[1] = flags; p[2] = HASH_ALGO_SHA256;
|
|
wr_u32le(p + 3, 11); wr_u32le(p + 7, 1);
|
|
wr_u32le(p + 11, boot ? 0xA000u : 1024u);
|
|
wr_u32le(p + 15, boot ? 0xA000u : 64u);
|
|
p[19] = 10;
|
|
p[20] = 1; p[21] = 2; p[22] = 3; p[23] = 4;
|
|
p[56] = flags & MFLAG_FULL ? CODEC_FULL : CODEC_DETOOLS_INPLACE;
|
|
m.total = boot ? 41330u : 278u;
|
|
}
|
|
static void startup() {
|
|
OtaStoreTowerNrf52 store;
|
|
assert(sd_medium.constructors == 1 && flash_medium.constructors == 0 && auth_clears == 1);
|
|
OtaStoreSdNrf52* owner = &store.sdStore();
|
|
assert(owner == &store.externalStore());
|
|
assert(!store.usesExternalStore() && !store.usesExternal() && !store.usesInternal());
|
|
assert(!store.compatible() && store.capacity() == 0 && store.staged_size() == 0);
|
|
uint8_t byte = 0;
|
|
assert(!store.read(0, &byte, 1) && !store.write(0, &byte, 1));
|
|
assert(!store.set_meta_size(1) && !store.finalize());
|
|
store.checkpoint(); store.clear(); store.resetSelection();
|
|
assert(owner == &store.sdStore());
|
|
assert(sd_medium.probes == 0 && cap_reads == 0 && flash_medium.constructors == 0);
|
|
assert(sd_medium.clear_calls == 0 && sd_medium.reopen_calls == 0);
|
|
}
|
|
static void primary() {
|
|
defaults(); OtaStoreTowerNrf52 store;
|
|
assert(store.selectStorage() && store.usesExternalStore() && store.compatible());
|
|
assert(sd_medium.probes == 1 && sd_medium.reopen_calls == 0 && flash_medium.constructors == 0);
|
|
assert(store.plan_layout(true, 1024, 209, 64, false));
|
|
assert(store.begin(278));
|
|
uint8_t data[4] = {1, 2, 3, 4}, got[4] = {0};
|
|
assert(store.write(0, data, 4) && store.read(0, got, 4) && memcmp(data, got, 4) == 0);
|
|
assert(store.set_meta_size(16) && store.finalize()); store.checkpoint();
|
|
assert(store.staged_size() == 278 && store.capacity() == 8192);
|
|
assert(store.selectStorage() && sd_medium.probes == 1 && cap_reads == 1);
|
|
assert(sd_medium.begin_calls == 1 && sd_medium.meta_calls == 1 && sd_medium.finalize_calls == 1);
|
|
assert(sd_medium.checkpoints == 1 && flash_medium.begin_calls == 0 && auth_clears == 1);
|
|
assert(store.plan_layout(true, 0xA000, 365, 0xA000, true));
|
|
}
|
|
static void fallback(bool unmountable) {
|
|
defaults(); sd_medium.present = unmountable; sd_medium.mountable = false;
|
|
OtaStoreTowerNrf52 store;
|
|
assert(store.selectStorage() && store.usesInternal() && !store.usesExternal());
|
|
assert(flash_medium.constructors == 1 && auth_clears == 2);
|
|
assert(!store.begin(278) && flash_medium.begin_calls == 0);
|
|
assert(!store.plan_layout(true, 1024, 209, 64, false));
|
|
assert(!store.begin(278) && flash_medium.begin_calls == 0);
|
|
assert(!store.plan_layout(true, 0xA000, 365, 0xA000, true));
|
|
assert(!store.begin(41330) && flash_medium.begin_calls == 0);
|
|
assert(store.plan_layout(false, 1024, 209, 64, false) && store.begin(278));
|
|
assert(flash_medium.plan_calls == 1 && flash_medium.begin_calls == 1);
|
|
uint8_t data = 7, got = 0;
|
|
assert(store.write(0, &data, 1) && store.read(0, &got, 1) && got == data);
|
|
assert(store.set_meta_size(16) && store.finalize()); store.checkpoint();
|
|
sd_medium.present = sd_medium.mountable = true;
|
|
assert(store.selectStorage() && store.usesInternal() && sd_medium.probes == 1);
|
|
assert(sd_medium.begin_calls == 0 && sd_medium.plan_calls == 0);
|
|
store.clear();
|
|
assert(store.usesInternal() && !store.begin(278) && flash_medium.begin_calls == 1);
|
|
assert(store.plan_layout(false, 1024, 209, 64, false) && store.begin(278));
|
|
}
|
|
static void old_or_unqualified() {
|
|
defaults(); sd_medium.present = false;
|
|
for (uint8_t option : {0u, 1u, 4u, 6u}) {
|
|
installed.optional_app_storage = option;
|
|
OtaStoreTowerNrf52 store;
|
|
assert(!store.selectStorage() && !store.usesInternal() && !store.usesExternal());
|
|
assert(!store.begin(278) && store.capacity() == 0);
|
|
}
|
|
installed.optional_app_storage = 2; installed.storage_flags = 2;
|
|
{ OtaStoreTowerNrf52 store; assert(!store.selectStorage()); }
|
|
installed.storage_flags = 9; installed.present = false;
|
|
{ OtaStoreTowerNrf52 store; assert(!store.selectStorage()); }
|
|
assert(flash_medium.constructors == 0 && flash_medium.begin_calls == 0);
|
|
installed.present = true; installed.optional_app_storage = 0; sd_medium.present = true;
|
|
{ OtaStoreTowerNrf52 store; assert(store.selectStorage() && store.usesExternal()); }
|
|
installed.present = false;
|
|
{ OtaStoreTowerNrf52 store;
|
|
assert(!store.selectStorage() && store.usesExternal() && !store.compatible());
|
|
assert(!store.begin(278)); }
|
|
}
|
|
static void failures() {
|
|
defaults(); OtaStoreTowerNrf52 store;
|
|
assert(store.selectStorage());
|
|
sd_medium.layout_ok = false;
|
|
assert(!store.plan_layout(false, 1024, 209, 64, false) && !store.begin(278));
|
|
assert(sd_medium.begin_calls == 0 && store.usesExternal() && flash_medium.constructors == 0);
|
|
sd_medium.layout_ok = true; sd_medium.begin_ok = false;
|
|
assert(store.plan_layout(false, 1024, 209, 64, false) && !store.begin(278));
|
|
assert(sd_medium.begin_calls == 1 && flash_medium.constructors == 0);
|
|
sd_medium.begin_ok = true; assert(store.begin(278));
|
|
sd_medium.present = false; uint8_t byte = 0;
|
|
assert(!store.write(0, &byte, 1) && !store.read(0, &byte, 1) && !store.finalize());
|
|
assert(store.usesExternal() && store.selectStorage() && sd_medium.probes == 1);
|
|
assert(flash_medium.constructors == 0 && !store.discard());
|
|
store.resetSelection();
|
|
assert(store.selectStorage() && store.usesInternal() && flash_medium.constructors == 1);
|
|
}
|
|
static void resume(bool internal) {
|
|
defaults(); sd_medium.present = !internal;
|
|
Medium& m = internal ? flash_medium : sd_medium;
|
|
manifest(m);
|
|
OtaStoreTowerNrf52 store;
|
|
uint8_t good[4] = {1, 2, 3, 4}, wrong[4] = {9, 9, 9, 9};
|
|
assert(!store.reopenFor(wrong, 11) && store.staged_size() == 0);
|
|
assert(m.staged && m.clear_calls == 0 && m.discard_calls == 0);
|
|
assert(!store.reopenFor(nullptr, 22) && store.staged_size() == 0);
|
|
assert(store.reopenFor(good, 11) && store.staged_size() == m.total);
|
|
if (internal) assert(m.last_mid == good && m.last_target == 11);
|
|
assert(store.reopen() && store.staged_size() == m.total);
|
|
manifest(m, MFLAG_FULL);
|
|
assert(store.reopen() == !internal);
|
|
manifest(m, MFLAG_FULL | MFLAG_SIGNED | MFLAG_BOOTLOADER);
|
|
assert(store.reopen() == !internal);
|
|
m.bytes[8] = 99;
|
|
assert(!store.reopen());
|
|
assert(m.staged && m.clear_calls == 0 && m.discard_calls == 0);
|
|
assert(sd_medium.probes == 1);
|
|
}
|
|
static void cancellation() {
|
|
defaults(); sd_medium.present = false;
|
|
OtaStoreTowerNrf52 store; OtaStoreSdNrf52* owner = &store.sdStore();
|
|
assert(store.plan_layout(false, 1024, 209, 64, false) && store.begin(278));
|
|
store.clear(); assert(store.staged_size() == 0 && flash_medium.staged);
|
|
manifest(flash_medium); assert(store.reopen());
|
|
flash_medium.discard_ok = false; assert(!store.discard() && flash_medium.staged);
|
|
flash_medium.discard_ok = true; assert(store.discard() && !flash_medium.staged);
|
|
sd_medium.present = true; store.resetSelection();
|
|
assert(flash_medium.destructors == 1 && owner == &store.sdStore());
|
|
assert(store.capacity() == 0 && store.selectStorage() && store.usesExternal());
|
|
assert(store.begin(278)); store.clear();
|
|
assert(store.usesExternal() && !sd_medium.staged && sd_medium.clear_calls == 1);
|
|
assert(sd_medium.probes == 2);
|
|
}
|
|
static void management() {
|
|
defaults(); sd_medium.present = false; OtaStoreTowerNrf52 store;
|
|
assert(store.selectStorage() && store.usesInternal());
|
|
mesh::MainBoard board; uint64_t used = 0, free = 0; char out[20];
|
|
assert(!store.formatCard(board) && !store.eraseCard(board));
|
|
assert(!store.getSpace(board, used, free) && !store.listFiles(board, 0, out, sizeof(out)));
|
|
assert(sd_medium.format_calls == 1 && sd_medium.erase_calls == 1);
|
|
assert(sd_medium.space_calls == 1 && sd_medium.list_calls == 1);
|
|
sd_medium.present = true;
|
|
assert(store.formatCard(board) && store.eraseCard(board));
|
|
assert(store.getSpace(board, used, free) && used == 1 && free == 2);
|
|
assert(store.listFiles(board, 1, out, sizeof(out)) && store.usesInternal());
|
|
assert(flash_medium.begin_calls == 0 && sd_medium.probes == 1);
|
|
}
|
|
int main(int argc, char** argv) {
|
|
assert(argc == 2);
|
|
if (!strcmp(argv[1], "startup")) startup();
|
|
else if (!strcmp(argv[1], "primary")) primary();
|
|
else if (!strcmp(argv[1], "missing")) fallback(false);
|
|
else if (!strcmp(argv[1], "unmountable")) fallback(true);
|
|
else if (!strcmp(argv[1], "legacy")) old_or_unqualified();
|
|
else if (!strcmp(argv[1], "failures")) failures();
|
|
else if (!strcmp(argv[1], "resume-sd")) resume(false);
|
|
else if (!strcmp(argv[1], "resume-internal")) resume(true);
|
|
else if (!strcmp(argv[1], "cancel")) cancellation();
|
|
else if (!strcmp(argv[1], "management")) management();
|
|
else return 2;
|
|
}
|
|
'''
|
|
|
|
PROBE = r'''
|
|
#include "OtaStoreSdNrf52.h"
|
|
#include <assert.h>
|
|
#include <stdlib.h>
|
|
using namespace mesh::ota;
|
|
static bool mountable;
|
|
static unsigned mounts, capacity_calls;
|
|
static uint32_t sectors;
|
|
namespace mesh { namespace ota {
|
|
OtaStoreSdNrf52::OtaStoreSdNrf52() {}
|
|
OtaStoreSdNrf52::~OtaStoreSdNrf52() {}
|
|
bool OtaStoreSdNrf52::mount() { ++mounts; return mountable; }
|
|
uint32_t OtaStoreSdNrf52::capacity() const { ++capacity_calls; return sectors * 512u; }
|
|
bool OtaStoreSdNrf52::begin(uint32_t) { abort(); }
|
|
bool OtaStoreSdNrf52::write(uint32_t, const uint8_t*, uint32_t) { abort(); }
|
|
bool OtaStoreSdNrf52::read(uint32_t, uint8_t*, uint32_t) const { abort(); }
|
|
void OtaStoreSdNrf52::clear() { abort(); }
|
|
bool OtaStoreSdNrf52::discard() { abort(); }
|
|
bool OtaStoreSdNrf52::set_meta_size(uint32_t) { abort(); }
|
|
bool OtaStoreSdNrf52::finalize() { abort(); }
|
|
void OtaStoreSdNrf52::checkpoint() { abort(); }
|
|
bool OtaStoreSdNrf52::reopen() { abort(); }
|
|
bool OtaStoreSdNrf52::plan_layout(bool, uint32_t, uint32_t, uint32_t, bool) { abort(); }
|
|
} }
|
|
int main() {
|
|
OtaStoreSdNrf52 store;
|
|
assert(mounts == 0 && capacity_calls == 0);
|
|
mountable = false; sectors = 10;
|
|
assert(!store.probeMedia() && mounts == 1 && capacity_calls == 0);
|
|
mountable = true; sectors = 0;
|
|
assert(!store.probeMedia() && mounts == 2 && capacity_calls == 1);
|
|
sectors = 10;
|
|
assert(store.probeMedia() && mounts == 3 && capacity_calls == 2);
|
|
}
|
|
'''
|
|
|
|
|
|
class TowerAutoStorageTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.compiler = shutil.which("c++")
|
|
if not cls.compiler:
|
|
raise unittest.SkipTest("C++ compiler unavailable")
|
|
cls.temp = tempfile.TemporaryDirectory(prefix="tower-auto-storage-")
|
|
cls.work = Path(cls.temp.name)
|
|
for name in ("OtaStore.h", "OtaFormat.h", "MotaContainer.h", "OtaByteIO.h"):
|
|
shutil.copyfile(OTA / name, cls.work / name)
|
|
(cls.work / "OtaBlInfo.h").write_text(CAPS)
|
|
(cls.work / "stores.h").write_text(STORES)
|
|
for name in ("OtaStoreFlashNrf52.h", "OtaStoreSdNrf52.h"):
|
|
(cls.work / name).write_text('#include "stores.h"\n')
|
|
(cls.work / "Utils.h").write_text("""
|
|
#pragma once
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
namespace mesh { class Utils { public:
|
|
static void sha256(uint8_t*, size_t, const uint8_t*, int);
|
|
static void sha256(uint8_t*, size_t, const uint8_t*, int, const uint8_t*, int);
|
|
}; }
|
|
""")
|
|
(cls.work / "test.cpp").write_text('#include <initializer_list>\n' + DRIVER)
|
|
cls.header = (OTA / "OtaStoreTowerNrf52.h").read_text()
|
|
cls.exe = cls.compile(cls.header, "selector")
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.temp.cleanup()
|
|
|
|
@classmethod
|
|
def compile(cls, header, name):
|
|
(cls.work / "OtaStoreTowerNrf52.h").write_text(header)
|
|
exe = cls.work / name
|
|
subprocess.run([cls.compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror",
|
|
"-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections",
|
|
*FLAGS, "-I", str(cls.work), "-I", str(OTA),
|
|
str(cls.work / "test.cpp"), str(OTA / "MotaContainer.cpp"),
|
|
"-o", str(exe)], check=True, capture_output=True, text=True)
|
|
return exe
|
|
|
|
def test_selection_and_transfer_boundaries(self):
|
|
for scenario in ("startup", "primary", "missing", "unmountable", "legacy",
|
|
"failures", "resume-sd", "resume-internal", "cancel", "management"):
|
|
with self.subTest(scenario=scenario):
|
|
subprocess.run([str(self.exe), scenario], check=True, capture_output=True)
|
|
|
|
def test_exact_real_sd_probe_never_opens_or_removes_files(self):
|
|
# Keep the source away from the selector's peripheral doubles so its
|
|
# quoted include resolves to the real production SD header.
|
|
probe_dir = self.work / "real-probe"
|
|
probe_dir.mkdir()
|
|
source = probe_dir / "probe.cpp"
|
|
source.write_text(PROBE)
|
|
exe = self.work / "probe"
|
|
subprocess.run([self.compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror",
|
|
"-DNRF52_PLATFORM=1", "-DOTA_SD_STORE=1", "-I", str(OTA),
|
|
str(source), "-o", str(exe)], check=True, capture_output=True, text=True)
|
|
subprocess.run([str(exe)], check=True, capture_output=True)
|
|
|
|
def test_unsupported_recipe_cannot_enable_tower_selection(self):
|
|
(self.work / "OtaStoreTowerNrf52.h").write_text(self.header)
|
|
source = self.work / "guard.cpp"
|
|
source.write_text('#include "OtaStoreTowerNrf52.h"\n')
|
|
for missing in FLAGS[:-1]:
|
|
with self.subTest(missing=missing):
|
|
flags = [flag for flag in FLAGS if flag != missing]
|
|
result = subprocess.run([self.compiler, "-std=c++11", "-fsyntax-only",
|
|
*flags, "-I", str(self.work), str(source)],
|
|
capture_output=True, text=True)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("requires the qualified nRF52 SD primary", result.stderr)
|
|
|
|
def test_negative_controls_expose_unsafe_selection_and_resume(self):
|
|
mutations = [
|
|
("startup-probe", "OtaStoreTowerNrf52() = default;",
|
|
"OtaStoreTowerNrf52() { selectStorage(); }", "startup"),
|
|
("legacy-fallback", "sd_profile && caps.optional_app_storage == OTA_BL_STORAGE_STAGE_CEILING",
|
|
"sd_profile", "legacy"),
|
|
("internal-full", "_mode == INTERNAL_MODE && (full || bootloader)",
|
|
"false", "missing"),
|
|
("missing-layout", "_mode == INTERNAL_MODE && !_layout_accepted",
|
|
"false", "missing"),
|
|
("resume-identity", "reopened && resumeMatches(mid, target)",
|
|
"reopened", "resume-sd"),
|
|
]
|
|
for name, old, new, scenario in mutations:
|
|
with self.subTest(mutation=name):
|
|
self.assertEqual(self.header.count(old), 1)
|
|
exe = self.compile(self.header.replace(old, new), name)
|
|
result = subprocess.run([str(exe), scenario], capture_output=True)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|