138 lines
7.1 KiB
Python
138 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Preserve OTA capacities while removing padding and oversized page bitmaps."""
|
|
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_replay_reset_integration import extract_braced
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
class OtaMemoryLayoutTest(unittest.TestCase):
|
|
def test_real_headers_preserve_natural_alignment_and_descriptor_initialization(self):
|
|
source = r'''
|
|
#include <helpers/ota/OtaManager.h>
|
|
#include <helpers/ota/OtaStoreQspiNrf52.h>
|
|
#include <assert.h>
|
|
#include <stddef.h>
|
|
using namespace mesh::ota;
|
|
static_assert(sizeof(OtaManager::ServeEntry) == (sizeof(void*) == 4 ? 60u : 64u),
|
|
"source entries must retain every field without old padding");
|
|
static_assert(alignof(OtaManager::ServeEntry) >= alignof(MotaSource*), "unaligned source pointer");
|
|
static_assert(offsetof(OtaManager::ServeEntry, desc) % alignof(MotaDesc) == 0, "unaligned descriptor");
|
|
static_assert(offsetof(OtaManager::ServeEntry, target_id) % alignof(uint32_t) == 0, "unaligned word");
|
|
static_assert(sizeof(OtaStoreQspiNrf52) > 8192u, "both staging page buffers must remain");
|
|
int main() {
|
|
OtaManager::ServeEntry row;
|
|
assert(row.desc.target_id == 0 && row.desc.fw_version == 0 && row.desc.source_caps == 0);
|
|
assert(row.desc.mid[0] == 0 && row.desc.mid[3] == 0 && row.desc.total_size == 0);
|
|
row.mid[0] = 17; row.target_id = 123; row.fw_version = 456;
|
|
row.have_count = 789; row.src = nullptr; row.src_idx = 255;
|
|
row.codec_id = 2; row.flags = 3; row.is_self = true;
|
|
row.desc.target_id = 987; row.desc.payload_size = 654;
|
|
const OtaManager::ServeEntry copy = row;
|
|
assert(copy.mid[0] == 17 && copy.target_id == 123 && copy.fw_version == 456);
|
|
assert(copy.have_count == 789 && copy.src == nullptr && copy.src_idx == 255);
|
|
assert(copy.codec_id == 2 && copy.flags == 3 && copy.is_self);
|
|
assert(copy.desc.target_id == 987 && copy.desc.payload_size == 654);
|
|
}
|
|
'''
|
|
with tempfile.TemporaryDirectory(prefix="ota-layout-") as directory:
|
|
work = Path(directory)
|
|
fixture = work / "layout.cpp"
|
|
fixture.write_text(source, encoding="ascii")
|
|
flags = ["-std=c++17", "-DNRF52_PLATFORM=1", "-DOTA_QSPI_STORE=1",
|
|
"-DOTA_QSPI_EXPECTED_SIZE=2097152UL", "-I", str(ROOT / "src")]
|
|
compiler = shutil.which("c++") or shutil.which("g++")
|
|
self.assertIsNotNone(compiler)
|
|
binary = work / "layout"
|
|
result = subprocess.run([compiler, *flags, "-fsanitize=address,undefined", "-fno-pie", "-no-pie",
|
|
str(fixture), "-o", str(binary)], capture_output=True, text=True)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
subprocess.run([str(binary)], check=True, timeout=10)
|
|
arm = shutil.which("arm-none-eabi-g++")
|
|
if arm:
|
|
result = subprocess.run([arm, *flags, "-mcpu=cortex-m4", "-mthumb", "-c", str(fixture),
|
|
"-o", str(work / "layout-arm.o")], capture_output=True,
|
|
text=True, timeout=30)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
|
|
def test_production_bitmap_geometry_and_boundary_accesses(self):
|
|
header = (ROOT / "src/helpers/ota/OtaStoreQspiNrf52.h").read_text()
|
|
source = (ROOT / "src/helpers/ota/OtaStoreQspiNrf52.cpp").read_text()
|
|
constants = header.split(" static const uint32_t PAGE =", 1)[1]
|
|
constants = " static const uint32_t PAGE =" + constants.split(
|
|
" static const uint32_t INVALID_PAGE", 1)[0]
|
|
member = next(line.strip() for line in header.splitlines() if "uint8_t _known_pages[" in line)
|
|
methods = "\n".join(extract_braced(source, signature) for signature in (
|
|
"bool OtaStoreQspiNrf52::pageKnown(uint32_t page) const",
|
|
"void OtaStoreQspiNrf52::setPageKnown(uint32_t page)"))
|
|
fixture = """
|
|
#include <stdint.h>
|
|
#include <assert.h>
|
|
#include <string.h>
|
|
class Bitmap {
|
|
public:
|
|
CONSTANTS
|
|
uint8_t before[8];
|
|
MEMBER
|
|
uint8_t after[8];
|
|
bool pageKnown(uint32_t page) const;
|
|
void setPageKnown(uint32_t page);
|
|
};
|
|
METHODS
|
|
int main() {
|
|
Bitmap value;
|
|
static_assert(sizeof(value._known_pages) == EXPECT_BITMAP_BYTES, "bitmap was not sized to accepted capacity");
|
|
static_assert(sizeof(value._known_pages) > 0u, "zero-length bitmap");
|
|
memset(value.before, 0xA7, sizeof(value.before));
|
|
memset(value.after, 0xD3, sizeof(value.after));
|
|
memset(value._known_pages, 0, sizeof(value._known_pages));
|
|
for (uint32_t page = 0; page < Bitmap::MAX_PAGES; ++page) {
|
|
assert(!value.pageKnown(page)); value.setPageKnown(page); assert(value.pageKnown(page));
|
|
if (page + 1 < Bitmap::MAX_PAGES) assert(!value.pageKnown(page + 1));
|
|
}
|
|
uint8_t snapshot[sizeof(value._known_pages)];
|
|
memcpy(snapshot, value._known_pages, sizeof(snapshot));
|
|
const uint32_t invalid[] = {Bitmap::MAX_PAGES, Bitmap::MAX_PAGES + 1u, UINT32_MAX};
|
|
for (uint32_t page : invalid) { assert(!value.pageKnown(page)); value.setPageKnown(page); }
|
|
assert(memcmp(snapshot, value._known_pages, sizeof(snapshot)) == 0);
|
|
for (uint8_t byte : value.before) assert(byte == 0xA7);
|
|
for (uint8_t byte : value.after) assert(byte == 0xD3);
|
|
}
|
|
""".replace("CONSTANTS", constants).replace("MEMBER", member).replace(
|
|
"METHODS", methods.replace("OtaStoreQspiNrf52::", "Bitmap::"))
|
|
compiler = shutil.which("c++") or shutil.which("g++")
|
|
self.assertIsNotNone(compiler)
|
|
with tempfile.TemporaryDirectory(prefix="ota-bitmap-") as directory:
|
|
work = Path(directory)
|
|
path = work / "bitmap.cpp"
|
|
path.write_text(fixture, encoding="ascii")
|
|
for mib in (None, 1, 2, 4, 8, 16):
|
|
with self.subTest(expected_mib=mib):
|
|
binary = work / ("generic" if mib is None else str(mib))
|
|
flags = [] if mib is None else [f"-DOTA_QSPI_EXPECTED_SIZE={mib << 20}UL"]
|
|
expected = (16 if mib is None else mib) * 32
|
|
subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
|
"-fsanitize=address,undefined", "-fno-pie", "-no-pie", *flags,
|
|
f"-DEXPECT_BITMAP_BYTES={expected}", str(path), "-o", str(binary)],
|
|
check=True, capture_output=True, text=True, timeout=30)
|
|
subprocess.run([str(binary)], check=True, timeout=10)
|
|
for invalid in (0, 1, 4096, 1048575, 3145728, 33554432, (1 << 32) + (2 << 20)):
|
|
with self.subTest(invalid=invalid):
|
|
result = subprocess.run([compiler, "-std=c++17", "-c", str(path),
|
|
f"-DOTA_QSPI_EXPECTED_SIZE={invalid}UL",
|
|
"-DEXPECT_BITMAP_BYTES=1", "-o", str(work / "invalid.o")],
|
|
capture_output=True, text=True, timeout=30)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("expected QSPI capacity must match", result.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|