mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-14 14:45:39 +00:00
683 lines
28 KiB
C++
683 lines
28 KiB
C++
#include <gtest/gtest.h>
|
|
#include <cstring>
|
|
|
|
#include "helpers/ota/OtaFlashLayout_nrf52.h"
|
|
#include "helpers/ota/OtaFlashLayout_esp32.h"
|
|
#include "helpers/ota/OtaStoreQspiNrf52.h"
|
|
|
|
using namespace mesh::ota;
|
|
|
|
// These lock down the nRF52 single-slot staging geometry that OtaStoreFlashNrf52::begin()/
|
|
// reopen() rely on. A received `.mota` is placed bottom-aligned below the filesystem region - ExtraFS
|
|
// (0xD4000) / InternalFS (0xED000), where the node's user preferences live - and above the running image.
|
|
// The prefs region is assumed IMMUTABLE (its bytes are outside the served/hashed self-image), so staging
|
|
// or an in-place apply must never reach into it. If a layout constant or the placement math is edited
|
|
// inconsistently, these fail here instead of silently corrupting prefs / the app on real hardware.
|
|
|
|
static constexpr uint32_t APP_V6 = MOTA_NRF52_APP_BASE_S140_V6;
|
|
static constexpr uint32_t APP_V7 = MOTA_NRF52_APP_BASE_S140_V7;
|
|
static constexpr uint32_t LEGACY = MOTA_NRF52_STAGE_CEILING_LEGACY;
|
|
static constexpr uint32_t EXPANDED = MOTA_NRF52_STAGE_CEILING_EXPANDED;
|
|
static constexpr uint32_t SHARED_BOOT_START = MOTA_NRF52_SHARED_BOOT_STAGE_START;
|
|
static constexpr uint32_t APP_IMAGE_SIZE = 520u * 1024u;
|
|
static constexpr uint32_t APP_END_V6 = APP_V6 + APP_IMAGE_SIZE;
|
|
static constexpr uint32_t APP_END_V7 = APP_V7 + APP_IMAGE_SIZE;
|
|
static constexpr uint32_t CAP_V6 = LEGACY - APP_END_V6;
|
|
static constexpr uint32_t CAP_V7 = LEGACY - APP_END_V7;
|
|
static constexpr uint32_t CAP_V6_EXPANDED = EXPANDED - APP_END_V6;
|
|
static constexpr uint32_t CAP_V7_EXPANDED = EXPANDED - APP_END_V7;
|
|
|
|
TEST(OtaQspiTiming, PreservesDeepPowerDownEntryAndWakeGuards) {
|
|
// MX25R1635F requires 10 us to enter DPD plus 30 us before another command;
|
|
// its release latency can reach 45 us. These constants are consumed by the
|
|
// real HAL path, so a future power-saving edit cannot restore the live
|
|
// plan_layout()->begin() race without failing the native suite.
|
|
EXPECT_GE(MOTA_QSPI_DPD_ENTRY_GUARD_US, 50u);
|
|
EXPECT_GE(MOTA_QSPI_DPD_WAKE_GUARD_US, 45u);
|
|
}
|
|
|
|
TEST(OtaQspiWake, ShiftsReleaseCommandMostSignificantBitFirst) {
|
|
// This byte is emitted over GPIO before TASKS_ACTIVATE. If activation is
|
|
// attempted first, a sleeping NOR ignores it and READY never arrives.
|
|
const bool expected[] = {true, false, true, false, true, false, true, true};
|
|
EXPECT_EQ(MOTA_QSPI_RELEASE_FROM_DPD_OPCODE, 0xABu);
|
|
for (uint8_t bit = 0; bit < 8u; bit++) {
|
|
EXPECT_EQ(mota_qspi_release_from_dpd_bit(bit), expected[bit]);
|
|
}
|
|
EXPECT_FALSE(mota_qspi_release_from_dpd_bit(8u));
|
|
}
|
|
|
|
TEST(OtaQspiStatus, TreatsOnlyTheNorWriteInProgressBitAsBusy) {
|
|
// Nordic READY is not flash completion. The hardware path polls RDSR until
|
|
// this predicate clears after every program and erase operation.
|
|
EXPECT_FALSE(mota_qspi_status_busy(0x00));
|
|
EXPECT_FALSE(mota_qspi_status_busy(0xFC));
|
|
EXPECT_TRUE(mota_qspi_status_busy(0x01));
|
|
EXPECT_TRUE(mota_qspi_status_busy(0xFF));
|
|
}
|
|
|
|
TEST(OtaQspiDiagnostics, ExposesStableFailureStageNames) {
|
|
EXPECT_STREQ(mota_qspi_stage_name(OtaQspiStage::PROGRAM), "program");
|
|
EXPECT_STREQ(mota_qspi_stage_name(OtaQspiStage::PROGRAM_BUSY), "program-busy");
|
|
EXPECT_STREQ(mota_qspi_stage_name(OtaQspiStage::ERASE_BUSY), "erase-busy");
|
|
EXPECT_STREQ(mota_qspi_stage_name(OtaQspiStage::INVALIDATE_VERIFY), "invalidate-verify");
|
|
}
|
|
|
|
TEST(OtaFlashPlan, SelectsCeilingFromLinkedLayoutAndStorage) {
|
|
// Actual internal secondary storage is authoritative regardless of linker selection.
|
|
EXPECT_EQ(mota_nrf52_stage_ceiling_for_layout(EXPANDED, true), LEGACY);
|
|
EXPECT_EQ(mota_nrf52_stage_ceiling_for_layout(LEGACY, true), LEGACY);
|
|
// Standard or ExtraFS linker without an internal secondary filesystem reclaims the unused 100 KiB.
|
|
EXPECT_EQ(mota_nrf52_stage_ceiling_for_layout(EXPANDED, false), EXPANDED);
|
|
EXPECT_EQ(mota_nrf52_stage_ceiling_for_layout(LEGACY, false), EXPANDED);
|
|
// The explicit XIAO boot-update linker leaves 40 KiB scratch at 0xE0000 and still stages externally.
|
|
EXPECT_EQ(mota_nrf52_stage_ceiling_for_layout(MOTA_NRF52_BOOT_SCRATCH_START, false), EXPANDED);
|
|
// An unrecognized linker region is never permission to erase a larger window.
|
|
EXPECT_EQ(mota_nrf52_stage_ceiling_for_layout(0xE1000u, false), LEGACY);
|
|
}
|
|
|
|
TEST(OtaFlashPlan, ValidatesExternalInplacePatchGeometryBeforeHandoff) {
|
|
const uint32_t workspace = EXPANDED - APP_V7;
|
|
const uint32_t running = 620000u;
|
|
const uint32_t target = 633984u;
|
|
ASSERT_TRUE(mota_nrf52_external_patch_geometry_valid(
|
|
workspace, MOTA_NRF52_FLASH_PAGE, 0, running, target,
|
|
workspace, running, target));
|
|
|
|
EXPECT_FALSE(mota_nrf52_external_patch_geometry_valid(
|
|
workspace + 1, MOTA_NRF52_FLASH_PAGE, 0, running, target,
|
|
workspace, running, target));
|
|
EXPECT_FALSE(mota_nrf52_external_patch_geometry_valid(
|
|
workspace, 2048, 0, running, target, workspace, running, target));
|
|
EXPECT_FALSE(mota_nrf52_external_patch_geometry_valid(
|
|
workspace, MOTA_NRF52_FLASH_PAGE, 1, running, target,
|
|
workspace, running, target));
|
|
EXPECT_FALSE(mota_nrf52_external_patch_geometry_valid(
|
|
workspace, MOTA_NRF52_FLASH_PAGE, 0, running - 1, target,
|
|
workspace, running, target));
|
|
EXPECT_FALSE(mota_nrf52_external_patch_geometry_valid(
|
|
workspace, MOTA_NRF52_FLASH_PAGE, 0, running, target + 1,
|
|
workspace, running, target));
|
|
}
|
|
|
|
// A typical running image (~520 KB) leaves room; the container lands strictly above it and below ExtraFS.
|
|
TEST(OtaFlashPlan, StagesBelowFilesystemAndAboveApp) {
|
|
uint32_t start = 0xDEADBEEF;
|
|
ASSERT_TRUE(mota_nrf52_stage_plan(64u * 1024u, APP_V6, APP_END_V6, LEGACY, start));
|
|
EXPECT_GE(start, APP_END_V6); // never overlaps the running image
|
|
EXPECT_LE(start + 64u * 1024u, LEGACY); // never reaches into ExtraFS/prefs
|
|
EXPECT_EQ(start % MOTA_NRF52_FLASH_PAGE, 0u); // page-aligned (the flash erase unit)
|
|
}
|
|
|
|
// Bottom-aligned: start is the page-aligned FS_START - total_size, so the trailer sits within the
|
|
// highest page below the ceiling where the bootloader's downward scan finds it.
|
|
TEST(OtaFlashPlan, BottomAlignedBelowCeiling) {
|
|
uint32_t start = 0;
|
|
uint32_t total = 60000;
|
|
ASSERT_TRUE(mota_nrf52_stage_plan(total, APP_V6, APP_V6, LEGACY, start));
|
|
EXPECT_EQ(start, (LEGACY - total) & ~(MOTA_NRF52_FLASH_PAGE - 1));
|
|
EXPECT_LE(start + total, LEGACY);
|
|
EXPECT_GT(start + total, LEGACY - MOTA_NRF52_FLASH_PAGE); // within one page of the ceiling
|
|
}
|
|
|
|
// An exactly-capacity container fills the page-aligned space above the app; one byte more never fits.
|
|
TEST(OtaFlashPlan, RejectsOversizedContainer) {
|
|
uint32_t start = 0;
|
|
ASSERT_TRUE(mota_nrf52_stage_plan(CAP_V6, APP_V6, APP_END_V6, LEGACY, start));
|
|
EXPECT_EQ(start, APP_END_V6);
|
|
EXPECT_EQ(start + CAP_V6, LEGACY);
|
|
EXPECT_FALSE(mota_nrf52_stage_plan(CAP_V6 + 1, APP_V6, APP_END_V6, LEGACY, start));
|
|
}
|
|
|
|
// The package carries its own checked memory_size, so a valid app may exceed the old 608 KiB fallback.
|
|
TEST(OtaFlashPlan, AcceptsAppLargerThanFallbackWhenPackageFits) {
|
|
const uint32_t app_end = APP_V6 + MOTA_NRF52_FALLBACK_INPLACE_MEMORY + 32u * 1024u;
|
|
uint32_t start = 0;
|
|
ASSERT_TRUE(mota_nrf52_stage_plan(16u * 1024u, APP_V6, app_end, LEGACY, start));
|
|
EXPECT_GE(start, app_end);
|
|
}
|
|
|
|
// Minimum container is header(8)+trailer(5)=13 bytes; anything smaller is not a container.
|
|
TEST(OtaFlashPlan, RejectsUndersizedContainer) {
|
|
uint32_t start = 0;
|
|
EXPECT_FALSE(mota_nrf52_stage_plan(12, APP_V6, APP_V6, LEGACY, start));
|
|
EXPECT_TRUE(mota_nrf52_stage_plan(13, APP_V6, APP_V6, LEGACY, start));
|
|
}
|
|
|
|
// The user-preferences filesystems (ExtraFS @ 0xD4000, InternalFS @ 0xED000) are entirely ABOVE any
|
|
// staged container. Hard-code the FS addresses here (independent of the layout header) so a future edit
|
|
// that drifts into a filesystem is caught.
|
|
TEST(OtaFlashPlan, PrefsRegionNeverStaged) {
|
|
const uint32_t EXTRAFS_START = 0xD4000u; // companion ExtraFS (CustomLFS(0xD4000, 0x19000))
|
|
const uint32_t INTERNALFS_START = 0xED000u; // primary LittleFS (holds /com_prefs)
|
|
EXPECT_EQ(LEGACY, EXTRAFS_START);
|
|
EXPECT_EQ(EXPANDED, INTERNALFS_START);
|
|
EXPECT_LT(EXTRAFS_START, INTERNALFS_START);
|
|
// the largest possible staged container still ends at the ceiling, never into a filesystem
|
|
uint32_t start = 0;
|
|
ASSERT_TRUE(mota_nrf52_stage_plan(CAP_V6, APP_V6, APP_END_V6, LEGACY, start));
|
|
EXPECT_LE(start + CAP_V6, EXTRAFS_START);
|
|
}
|
|
|
|
// S140 v7 moves the app start by one page. Runtime linker-base discovery must leave a correspondingly
|
|
// smaller but still safe staging region rather than scanning the v6 address and missing EndF.
|
|
TEST(OtaFlashPlan, SupportsS140V7RuntimeBase) {
|
|
EXPECT_TRUE(mota_nrf52_layout_valid(APP_V7, LEGACY));
|
|
EXPECT_EQ(mota_nrf52_stage_capacity(APP_V7, APP_END_V7, LEGACY), CAP_V7);
|
|
uint32_t start = 0;
|
|
ASSERT_TRUE(mota_nrf52_stage_plan(CAP_V7, APP_V7, APP_END_V7, LEGACY, start));
|
|
EXPECT_EQ(start, APP_END_V7);
|
|
EXPECT_EQ(start + CAP_V7, LEGACY);
|
|
}
|
|
|
|
TEST(OtaFlashPlan, ExpandedCeilingAddsExactly100KiBForV6AndV7) {
|
|
EXPECT_EQ(EXPANDED - LEGACY, 100u * 1024u);
|
|
EXPECT_EQ(CAP_V6_EXPANDED - CAP_V6, 100u * 1024u);
|
|
EXPECT_EQ(CAP_V7_EXPANDED - CAP_V7, 100u * 1024u);
|
|
EXPECT_EQ(mota_nrf52_stage_capacity(APP_V6, APP_END_V6, EXPANDED), CAP_V6_EXPANDED);
|
|
EXPECT_EQ(mota_nrf52_stage_capacity(APP_V7, APP_END_V7, EXPANDED), CAP_V7_EXPANDED);
|
|
|
|
uint32_t start = 0;
|
|
ASSERT_TRUE(mota_nrf52_stage_plan(CAP_V7_EXPANDED, APP_V7, APP_END_V7, EXPANDED, start));
|
|
EXPECT_EQ(start, APP_END_V7);
|
|
EXPECT_EQ(start + CAP_V7_EXPANDED, EXPANDED);
|
|
}
|
|
|
|
TEST(OtaFlashPlan, HybridUsesTheFrozenFlashChargeAndRamSuffix) {
|
|
uint32_t start = 0, flash = 0, ram = 0;
|
|
const uint32_t page = MOTA_NRF52_FLASH_PAGE;
|
|
ASSERT_TRUE(mota_nrf52_hybrid_stage_plan(
|
|
page + 1u, APP_V6, APP_END_V6, EXPANDED, start, flash, ram));
|
|
EXPECT_EQ(start, EXPANDED - page);
|
|
EXPECT_EQ(flash, page);
|
|
EXPECT_EQ(ram, 1u);
|
|
|
|
ASSERT_TRUE(mota_nrf52_hybrid_stage_plan(
|
|
17u * page, APP_V6, APP_END_V6, EXPANDED, start, flash, ram));
|
|
EXPECT_EQ(start, EXPANDED - page);
|
|
EXPECT_EQ(flash, page);
|
|
EXPECT_EQ(ram, MOTA_NRF52_HYBRID_RAM_SIZE);
|
|
|
|
ASSERT_TRUE(mota_nrf52_hybrid_stage_plan(
|
|
17u * page + 1u, APP_V6, APP_END_V6,
|
|
EXPANDED, start, flash, ram));
|
|
EXPECT_EQ(start, EXPANDED - 2u * page);
|
|
EXPECT_EQ(flash, 2u * page);
|
|
EXPECT_EQ(ram, 15u * page + 1u);
|
|
|
|
ASSERT_TRUE(mota_nrf52_hybrid_stage_plan(
|
|
17u * page, APP_V7, APP_END_V7,
|
|
EXPANDED, start, flash, ram));
|
|
EXPECT_EQ(start, EXPANDED - page);
|
|
EXPECT_EQ(flash, page);
|
|
EXPECT_EQ(ram, MOTA_NRF52_HYBRID_RAM_SIZE);
|
|
}
|
|
|
|
TEST(OtaFlashPlan, HybridFailsClosedOutsideItsExactProfile) {
|
|
uint32_t start = 0xAAAAAAAAu, flash = 0xBBBBBBBBu, ram = 0xCCCCCCCCu;
|
|
const uint32_t page = MOTA_NRF52_FLASH_PAGE;
|
|
EXPECT_FALSE(mota_nrf52_hybrid_stage_plan(
|
|
MOTA_NRF52_CONTAINER_MIN_SIZE, APP_V6, APP_END_V6,
|
|
EXPANDED, start, flash, ram));
|
|
EXPECT_FALSE(mota_nrf52_hybrid_stage_plan(
|
|
page, APP_V6, APP_END_V6, EXPANDED, start, flash, ram));
|
|
EXPECT_FALSE(mota_nrf52_hybrid_stage_plan(
|
|
17u * page, APP_V6, APP_END_V6, LEGACY, start, flash, ram));
|
|
EXPECT_FALSE(mota_nrf52_hybrid_stage_plan(
|
|
17u * page + 1u, APP_V6, EXPANDED - page,
|
|
EXPANDED, start, flash, ram));
|
|
EXPECT_FALSE(mota_nrf52_hybrid_stage_plan(
|
|
17u * page, 0x00028000u, 0x00028000u,
|
|
EXPANDED, start, flash, ram));
|
|
EXPECT_FALSE(mota_nrf52_hybrid_stage_plan(
|
|
UINT32_MAX, APP_V6, APP_END_V6,
|
|
EXPANDED, start, flash, ram));
|
|
EXPECT_EQ(start, 0xAAAAAAAAu);
|
|
EXPECT_EQ(flash, 0xBBBBBBBBu);
|
|
EXPECT_EQ(ram, 0xCCCCCCCCu);
|
|
}
|
|
|
|
TEST(OtaFlashPlan, RejectsAppOutsideSelectedRegion) {
|
|
uint32_t start = 0x1234ABCD;
|
|
EXPECT_FALSE(mota_nrf52_stage_plan(4096, APP_V6, LEGACY + 1, LEGACY, start));
|
|
EXPECT_EQ(start, 0x1234ABCDu);
|
|
}
|
|
|
|
TEST(OtaFlashPlan, RejectsUnknownCeiling) {
|
|
uint32_t start = 0x1234ABCD;
|
|
EXPECT_FALSE(mota_nrf52_stage_plan(4096, APP_V6, APP_V6, 0xE1000u, start));
|
|
EXPECT_EQ(start, 0x1234ABCDu);
|
|
}
|
|
|
|
TEST(OtaFlashPlan, BootPackageUsesOneDynamicSharedInternalSlot) {
|
|
// v3 = header + fixed manifest + forty leaves + 40 KiB image + trailer.
|
|
const uint32_t total = 8u + 197u + 40u * 4u + 40u * 1024u + 5u;
|
|
ASSERT_EQ(total, MOTA_NRF52_BOOT_CONTAINER_SIZE);
|
|
uint32_t start = 0;
|
|
ASSERT_TRUE(mota_nrf52_shared_boot_stage_plan(
|
|
total, APP_V6, true, SHARED_BOOT_START, start));
|
|
EXPECT_EQ(start, SHARED_BOOT_START);
|
|
EXPECT_EQ(start, (EXPANDED - total) & ~(MOTA_NRF52_FLASH_PAGE - 1u));
|
|
|
|
// OTAFIX compacts payload offset +365 forward inside these same eleven
|
|
// pages; no independent scratch bank participates in the internal path.
|
|
const uint32_t payload_offset = 8u + 197u + 40u * 4u;
|
|
EXPECT_EQ(payload_offset, 365u);
|
|
EXPECT_LE(start + payload_offset + 40u * 1024u, EXPANDED);
|
|
EXPECT_LE(start + 40u * 1024u, EXPANDED);
|
|
|
|
// Runtime headroom, not a special linker, is authoritative.
|
|
EXPECT_FALSE(mota_nrf52_shared_boot_stage_plan(
|
|
total, APP_V6, true, SHARED_BOOT_START + 1u, start));
|
|
EXPECT_FALSE(mota_nrf52_shared_boot_stage_plan(
|
|
total - 1u, APP_V6, true, APP_V6, start));
|
|
EXPECT_FALSE(mota_nrf52_shared_boot_stage_plan(
|
|
total + 1u, APP_V6, true, APP_V6, start));
|
|
}
|
|
|
|
TEST(OtaFlashPlan, SharedInternalStoreKeepsNormalApplicationCeilingAndHandoff) {
|
|
EXPECT_TRUE(mota_nrf52_target_image_fits(
|
|
APP_V6, EXPANDED - APP_V6, EXPANDED));
|
|
EXPECT_FALSE(mota_nrf52_target_image_fits(
|
|
APP_V6, EXPANDED - APP_V6 + 1u, EXPANDED));
|
|
EXPECT_EQ(mota_nrf52_flash_stage_handoff(EXPANDED),
|
|
GPREGRET2_OTA_STAGE_EXPANDED);
|
|
EXPECT_EQ(mota_nrf52_flash_stage_handoff(LEGACY),
|
|
GPREGRET2_OTA_STAGE_LEGACY);
|
|
}
|
|
|
|
TEST(OtaFlashPlan, InternalOrdinaryDeltaUsesTheSameEd000Store) {
|
|
const uint32_t running_end = APP_V6 + 500u * 1024u;
|
|
const uint32_t delta_container = 28u * 1024u;
|
|
uint32_t staged = 0;
|
|
ASSERT_TRUE(mota_nrf52_stage_plan(delta_container, APP_V6, running_end,
|
|
EXPANDED, staged));
|
|
EXPECT_GE(staged, running_end);
|
|
EXPECT_LE(staged + delta_container, EXPANDED);
|
|
EXPECT_EQ(mota_nrf52_flash_stage_handoff(EXPANDED), 0xEDu);
|
|
}
|
|
|
|
TEST(OtaFlashPlan, LargeInternalOrdinaryDeltaMayStageBelowSharedBootStart) {
|
|
// An ordinary delta container can exceed the boot package's eleven pages.
|
|
// It bottom-aligns below E2000 and remains valid when detools'
|
|
// encoded workspace ends before the actual container start.
|
|
const uint32_t running_end = APP_V6 + 600u * 1024u;
|
|
const uint32_t delta_container = 80u * 1024u;
|
|
uint32_t staged = 0;
|
|
ASSERT_TRUE(mota_nrf52_stage_plan(delta_container, APP_V6, running_end,
|
|
EXPANDED, staged));
|
|
EXPECT_LT(staged, SHARED_BOOT_START);
|
|
EXPECT_GE(staged, running_end);
|
|
|
|
const uint32_t workspace = staged - APP_V6;
|
|
const uint32_t target_size = workspace;
|
|
EXPECT_TRUE(mota_nrf52_internal_patch_workspace_valid(
|
|
workspace, APP_V6, staged, target_size, EXPANDED));
|
|
EXPECT_FALSE(mota_nrf52_internal_patch_workspace_valid(
|
|
workspace + 1u, APP_V6, staged, target_size, EXPANDED));
|
|
EXPECT_FALSE(mota_nrf52_internal_patch_workspace_valid(
|
|
workspace, APP_V6, staged, EXPANDED - APP_V6 + 1u, EXPANDED));
|
|
}
|
|
|
|
TEST(OtaFlashPlan, MissingEndfRejectsEverySharedInternalPackageBeforeErase) {
|
|
uint32_t protected_end = 0;
|
|
ASSERT_TRUE(mota_nrf52_protected_app_end(
|
|
APP_V6, EXPANDED, false, 0, false, protected_end));
|
|
EXPECT_EQ(protected_end, APP_V6 + MOTA_NRF52_FALLBACK_INPLACE_MEMORY);
|
|
|
|
// A shared-internal-update build is linked through ED000, so the old 608 KiB
|
|
// estimate is not a safe erase floor for either package kind. Missing EndF
|
|
// disables all internal staging before begin()/reopen can erase a page.
|
|
EXPECT_FALSE(mota_nrf52_protected_app_end(
|
|
APP_V6, EXPANDED, false, 0, true, protected_end));
|
|
|
|
// The old rescue estimate would also admit this ordinary 80 KiB delta at
|
|
// D9000 even though a legal ED000-linked image can have live bytes there.
|
|
// A privileged shared-internal build therefore never calls stage_plan with
|
|
// that estimate: protected_app_end() above returns false/capacity zero.
|
|
const uint32_t ordinary_container = 80u * 1024u;
|
|
uint32_t unsafe_start = 0;
|
|
const uint32_t legacy_estimate =
|
|
APP_V6 + MOTA_NRF52_FALLBACK_INPLACE_MEMORY;
|
|
ASSERT_TRUE(mota_nrf52_stage_plan(
|
|
ordinary_container, APP_V6, legacy_estimate, EXPANDED, unsafe_start));
|
|
const uint32_t legal_live_tail = unsafe_start + MOTA_NRF52_FLASH_PAGE;
|
|
ASSERT_GT(legal_live_tail, legacy_estimate);
|
|
ASSERT_LT(legal_live_tail, EXPANDED);
|
|
EXPECT_FALSE(mota_nrf52_stage_plan(
|
|
ordinary_container, APP_V6, legal_live_tail, EXPANDED, protected_end));
|
|
|
|
// Model a legal linked image whose tail extends beyond the old 608 KiB
|
|
// estimate. The privileged planner rejects missing EndF even if the generic
|
|
// rescue fallback would appear to leave enough room.
|
|
const uint32_t real_tail = APP_V6 + MOTA_NRF52_FALLBACK_INPLACE_MEMORY + 0x8000u;
|
|
ASSERT_LT(real_tail, SHARED_BOOT_START);
|
|
uint32_t staged = 0;
|
|
EXPECT_FALSE(mota_nrf52_shared_boot_stage_plan(
|
|
MOTA_NRF52_BOOT_CONTAINER_SIZE, APP_V6, false, protected_end, staged));
|
|
ASSERT_TRUE(mota_nrf52_shared_boot_stage_plan(
|
|
MOTA_NRF52_BOOT_CONTAINER_SIZE, APP_V6, true, real_tail, staged));
|
|
EXPECT_EQ(staged, SHARED_BOOT_START);
|
|
}
|
|
|
|
TEST(OtaFlashPlan, ReopenBoundsUntrustedTotalBeforeManifestRead) {
|
|
const uint32_t start = SHARED_BOOT_START;
|
|
EXPECT_TRUE(mota_nrf52_container_span_valid(
|
|
start, EXPANDED, MOTA_NRF52_BOOT_CONTAINER_SIZE, 8u + 197u + 5u));
|
|
EXPECT_FALSE(mota_nrf52_container_span_valid(
|
|
start, EXPANDED, UINT32_MAX, 8u + 197u + 5u));
|
|
EXPECT_FALSE(mota_nrf52_container_span_valid(
|
|
start, EXPANDED, 8u + 197u + 4u, 8u + 197u + 5u));
|
|
EXPECT_FALSE(mota_nrf52_container_span_valid(
|
|
EXPANDED + 1u, EXPANDED, MOTA_NRF52_BOOT_CONTAINER_SIZE,
|
|
8u + 197u + 5u));
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct FakeStagedHeader {
|
|
uint32_t address;
|
|
uint32_t total;
|
|
bool present;
|
|
bool invalidate_ok;
|
|
bool invalidated;
|
|
};
|
|
|
|
struct FakeStagedFlash {
|
|
FakeStagedHeader* headers;
|
|
size_t count;
|
|
uint32_t invalidate_calls;
|
|
};
|
|
|
|
bool fake_read_staged_header(void* context, uint32_t address,
|
|
uint32_t& total) {
|
|
FakeStagedFlash* flash = static_cast<FakeStagedFlash*>(context);
|
|
for (size_t i = 0; i < flash->count; ++i) {
|
|
FakeStagedHeader& header = flash->headers[i];
|
|
if (header.address == address && header.present) {
|
|
total = header.total;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool fake_invalidate_staged_header(void* context, uint32_t address) {
|
|
FakeStagedFlash* flash = static_cast<FakeStagedFlash*>(context);
|
|
++flash->invalidate_calls;
|
|
for (size_t i = 0; i < flash->count; ++i) {
|
|
FakeStagedHeader& header = flash->headers[i];
|
|
if (header.address != address || !header.present) continue;
|
|
if (!header.invalidate_ok) return false;
|
|
header.present = false;
|
|
header.invalidated = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST(OtaFlashDiscard, FreshIdleObjectStillInvalidatesPersistedHeader) {
|
|
// The callback context models flash only: there is deliberately no live
|
|
// OtaStore session or staged_size. Cancellation after reboot must still
|
|
// find the bottom-aligned persistent header.
|
|
FakeStagedHeader headers[] = {
|
|
{0xE2000u, EXPANDED - 0xE2000u, true, true, false},
|
|
};
|
|
FakeStagedFlash flash{headers, 1u, 0u};
|
|
uint32_t invalidated = 99u;
|
|
|
|
EXPECT_TRUE(mota_nrf52_discard_staged_headers(
|
|
APP_V6, APP_END_V6, EXPANDED, &flash, fake_read_staged_header,
|
|
fake_invalidate_staged_header, &invalidated));
|
|
EXPECT_EQ(invalidated, 1u);
|
|
EXPECT_EQ(flash.invalidate_calls, 1u);
|
|
EXPECT_TRUE(headers[0].invalidated);
|
|
|
|
// Durable discard is idempotent: a new scan cannot reopen or re-invalidate
|
|
// the header that the first call consumed.
|
|
invalidated = 99u;
|
|
EXPECT_TRUE(mota_nrf52_discard_staged_headers(
|
|
APP_V6, APP_END_V6, EXPANDED, &flash, fake_read_staged_header,
|
|
fake_invalidate_staged_header, &invalidated));
|
|
EXPECT_EQ(invalidated, 0u);
|
|
EXPECT_EQ(flash.invalidate_calls, 1u);
|
|
}
|
|
|
|
TEST(OtaFlashDiscard, InvalidatesEveryCurrentAndLegacyLayoutHeader) {
|
|
FakeStagedHeader headers[] = {
|
|
// Two expanded-ceiling containers from transfers of different sizes.
|
|
{0xE2000u, EXPANDED - 0xE2000u, true, true, false},
|
|
{0xD8000u, EXPANDED - 0xD8000u, true, true, false},
|
|
// A retained container from the older D4000 ceiling geometry.
|
|
{0xC0000u, LEGACY - 0xC0000u, true, true, false},
|
|
// Magic at a page whose total does not bottom-align there is not a
|
|
// reopenable store header and must remain untouched.
|
|
{0xD9000u, 0x8000u, true, true, false},
|
|
};
|
|
FakeStagedFlash flash{headers, 4u, 0u};
|
|
uint32_t invalidated = 0;
|
|
|
|
EXPECT_TRUE(mota_nrf52_discard_staged_headers(
|
|
APP_V6, APP_END_V6, EXPANDED, &flash, fake_read_staged_header,
|
|
fake_invalidate_staged_header, &invalidated));
|
|
EXPECT_EQ(invalidated, 3u);
|
|
EXPECT_EQ(flash.invalidate_calls, 3u);
|
|
EXPECT_TRUE(headers[0].invalidated);
|
|
EXPECT_TRUE(headers[1].invalidated);
|
|
EXPECT_TRUE(headers[2].invalidated);
|
|
EXPECT_TRUE(headers[3].present);
|
|
EXPECT_FALSE(headers[3].invalidated);
|
|
}
|
|
|
|
TEST(OtaFlashDiscard, ReportsFailureButContinuesInvalidatingOtherHeaders) {
|
|
FakeStagedHeader headers[] = {
|
|
{0xE2000u, EXPANDED - 0xE2000u, true, false, false},
|
|
{0xD8000u, EXPANDED - 0xD8000u, true, true, false},
|
|
};
|
|
FakeStagedFlash flash{headers, 2u, 0u};
|
|
uint32_t invalidated = 0;
|
|
|
|
EXPECT_FALSE(mota_nrf52_discard_staged_headers(
|
|
APP_V6, APP_END_V6, EXPANDED, &flash, fake_read_staged_header,
|
|
fake_invalidate_staged_header, &invalidated));
|
|
EXPECT_EQ(invalidated, 1u);
|
|
EXPECT_EQ(flash.invalidate_calls, 2u);
|
|
EXPECT_TRUE(headers[0].present);
|
|
EXPECT_TRUE(headers[1].invalidated);
|
|
}
|
|
|
|
TEST(OtaFlashDiscard, UnsafeBoundsFailBeforeAnyFlashCallback) {
|
|
FakeStagedHeader headers[] = {
|
|
{0xE2000u, EXPANDED - 0xE2000u, true, true, false},
|
|
};
|
|
FakeStagedFlash flash{headers, 1u, 0u};
|
|
uint32_t invalidated = 99u;
|
|
|
|
EXPECT_FALSE(mota_nrf52_discard_staged_headers(
|
|
APP_V6, EXPANDED + 1u, EXPANDED, &flash, fake_read_staged_header,
|
|
fake_invalidate_staged_header, &invalidated));
|
|
EXPECT_EQ(invalidated, 0u);
|
|
EXPECT_EQ(flash.invalidate_calls, 0u);
|
|
EXPECT_TRUE(headers[0].present);
|
|
|
|
EXPECT_FALSE(mota_nrf52_discard_staged_headers(
|
|
APP_V6, APP_END_V6, EXPANDED, &flash, nullptr,
|
|
fake_invalidate_staged_header, &invalidated));
|
|
EXPECT_EQ(flash.invalidate_calls, 0u);
|
|
}
|
|
|
|
TEST(OtaEsp32FlashPlan, FullAndDeltaPlacementStaySectorAligned) {
|
|
constexpr uint32_t partition = 2u * 1024u * 1024u;
|
|
constexpr uint32_t sector = 4096u;
|
|
constexpr uint32_t meta_capacity = 65536u;
|
|
MotaEsp32StageLayout full;
|
|
ASSERT_TRUE(mota_esp32_stage_layout(
|
|
partition, sector, meta_capacity, true, 1024u * 1024u, 40000u,
|
|
1024u * 1024u, full));
|
|
EXPECT_EQ(full.total, 40000u + 1024u * 1024u + 5u);
|
|
EXPECT_EQ(full.meta_flush, 40960u);
|
|
EXPECT_EQ(full.meta_part, partition - full.meta_flush);
|
|
EXPECT_EQ(full.meta_part % sector, 0u);
|
|
EXPECT_EQ(full.pay_part0, 0u);
|
|
|
|
MotaEsp32StageLayout delta;
|
|
ASSERT_TRUE(mota_esp32_stage_layout(
|
|
partition, sector, meta_capacity, false, 900000u, 1234u,
|
|
100000u, delta));
|
|
EXPECT_EQ(delta.total, 101239u);
|
|
EXPECT_EQ(delta.meta_span, sector);
|
|
EXPECT_EQ(delta.meta_part, delta.write_start);
|
|
EXPECT_EQ(delta.write_start % sector, 0u);
|
|
EXPECT_EQ(delta.pay_part0, delta.write_start + sector);
|
|
}
|
|
|
|
TEST(OtaEsp32FlashPlan, RejectsOverflowAndUnfittableMetadata) {
|
|
MotaEsp32StageLayout layout;
|
|
EXPECT_FALSE(mota_esp32_stage_layout(
|
|
4097u, 4096u, 65536u, true, 1u, 200u, 1u, layout));
|
|
EXPECT_FALSE(mota_esp32_stage_layout(
|
|
2u * 1024u * 1024u, 4096u, 65536u, true, 1u, UINT32_MAX,
|
|
1u, layout));
|
|
EXPECT_FALSE(mota_esp32_stage_layout(
|
|
32768u, 4096u, 65536u, true, 1u, 40000u, 1u, layout));
|
|
EXPECT_FALSE(mota_esp32_stage_layout(
|
|
2u * 1024u * 1024u, 4096u, 65536u, false,
|
|
2u * 1024u * 1024u, 1000u, 1000u, layout));
|
|
}
|
|
|
|
TEST(OtaEsp32FlashPlan, RejectsMalformedOrOversizedFullPayloadBeforeErase) {
|
|
constexpr uint32_t partition = 2u * 1024u * 1024u;
|
|
constexpr uint32_t sector = 4096u;
|
|
constexpr uint32_t meta_capacity = 65536u;
|
|
MotaEsp32StageLayout layout;
|
|
|
|
// FULL means that the payload is exactly the final application image.
|
|
EXPECT_FALSE(mota_esp32_stage_layout(
|
|
partition, sector, meta_capacity, true, 100000u, 2000u,
|
|
99999u, layout));
|
|
EXPECT_FALSE(mota_esp32_stage_layout(
|
|
partition, sector, meta_capacity, true, 100000u, 2000u,
|
|
100001u, layout));
|
|
|
|
// The logical container and rounded metadata/payload regions must both fit
|
|
// wholly inside the inactive slot before OtaStoreFlashEsp32::begin().
|
|
EXPECT_FALSE(mota_esp32_stage_layout(
|
|
partition, sector, meta_capacity, true, partition, 2000u,
|
|
partition, layout));
|
|
EXPECT_FALSE(mota_esp32_stage_layout(
|
|
8192u, sector, meta_capacity, true, 4097u, 1u, 4097u,
|
|
layout));
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct FakeEspStagedHeader {
|
|
uint32_t offset;
|
|
bool reopenable;
|
|
bool probe_ok;
|
|
bool invalidate_ok;
|
|
bool invalidated;
|
|
};
|
|
|
|
struct FakeEspPartition {
|
|
FakeEspStagedHeader* headers;
|
|
size_t count;
|
|
uint32_t probe_calls;
|
|
uint32_t invalidate_calls;
|
|
};
|
|
|
|
bool fake_probe_esp_header(void* context, uint32_t offset,
|
|
bool& reopenable) {
|
|
FakeEspPartition* partition = static_cast<FakeEspPartition*>(context);
|
|
++partition->probe_calls;
|
|
reopenable = false;
|
|
for (size_t i = 0; i < partition->count; ++i) {
|
|
FakeEspStagedHeader& header = partition->headers[i];
|
|
if (header.offset != offset || header.invalidated) continue;
|
|
reopenable = header.reopenable;
|
|
return header.probe_ok;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool fake_invalidate_esp_header(void* context, uint32_t offset) {
|
|
FakeEspPartition* partition = static_cast<FakeEspPartition*>(context);
|
|
++partition->invalidate_calls;
|
|
for (size_t i = 0; i < partition->count; ++i) {
|
|
FakeEspStagedHeader& header = partition->headers[i];
|
|
if (header.offset != offset || header.invalidated) continue;
|
|
if (!header.invalidate_ok) return false;
|
|
header.invalidated = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST(OtaEsp32FlashDiscard, FreshIdleScanInvalidatesEveryReopenableHeader) {
|
|
constexpr uint32_t sector = 4096u;
|
|
constexpr uint32_t partition_size = 6u * sector;
|
|
FakeEspStagedHeader headers[] = {
|
|
{5u * sector, true, true, true, false},
|
|
{3u * sector, false, true, true, false},
|
|
{1u * sector, true, true, true, false},
|
|
};
|
|
FakeEspPartition partition{headers, 3u, 0u, 0u};
|
|
uint32_t invalidated = 99u;
|
|
|
|
EXPECT_TRUE(mota_esp32_discard_staged_headers(
|
|
partition_size, sector, &partition, fake_probe_esp_header,
|
|
fake_invalidate_esp_header, &invalidated));
|
|
EXPECT_EQ(partition.probe_calls, 6u);
|
|
EXPECT_EQ(partition.invalidate_calls, 2u);
|
|
EXPECT_EQ(invalidated, 2u);
|
|
EXPECT_TRUE(headers[0].invalidated);
|
|
EXPECT_FALSE(headers[1].invalidated);
|
|
EXPECT_TRUE(headers[2].invalidated);
|
|
}
|
|
|
|
TEST(OtaEsp32FlashDiscard, IoAndInvalidateFailuresAreReportedAfterFullScan) {
|
|
constexpr uint32_t sector = 4096u;
|
|
constexpr uint32_t partition_size = 6u * sector;
|
|
FakeEspStagedHeader headers[] = {
|
|
{4u * sector, false, false, true, false},
|
|
{3u * sector, true, true, false, false},
|
|
{1u * sector, true, true, true, false},
|
|
};
|
|
FakeEspPartition partition{headers, 3u, 0u, 0u};
|
|
uint32_t invalidated = 0u;
|
|
|
|
EXPECT_FALSE(mota_esp32_discard_staged_headers(
|
|
partition_size, sector, &partition, fake_probe_esp_header,
|
|
fake_invalidate_esp_header, &invalidated));
|
|
EXPECT_EQ(partition.probe_calls, 6u);
|
|
EXPECT_EQ(partition.invalidate_calls, 2u);
|
|
EXPECT_EQ(invalidated, 1u);
|
|
EXPECT_FALSE(headers[1].invalidated);
|
|
EXPECT_TRUE(headers[2].invalidated);
|
|
}
|
|
|
|
TEST(OtaEsp32FlashDiscard, InvalidScanArgumentsFailWithoutCallbacks) {
|
|
FakeEspPartition partition{nullptr, 0u, 0u, 0u};
|
|
uint32_t invalidated = 99u;
|
|
EXPECT_FALSE(mota_esp32_discard_staged_headers(
|
|
4095u, 4096u, &partition, fake_probe_esp_header,
|
|
fake_invalidate_esp_header, &invalidated));
|
|
EXPECT_FALSE(mota_esp32_discard_staged_headers(
|
|
4097u, 4096u, &partition, fake_probe_esp_header,
|
|
fake_invalidate_esp_header, &invalidated));
|
|
EXPECT_EQ(invalidated, 0u);
|
|
EXPECT_EQ(partition.probe_calls, 0u);
|
|
EXPECT_EQ(partition.invalidate_calls, 0u);
|
|
}
|
|
|
|
// out_start is only written on success - a rejected plan must not clobber the caller's variable.
|
|
TEST(OtaFlashPlan, LeavesOutputUntouchedOnReject) {
|
|
uint32_t start = 0x1234ABCD;
|
|
EXPECT_FALSE(mota_nrf52_stage_plan(CAP_V6 + 1, APP_V6, APP_END_V6, LEGACY, start));
|
|
EXPECT_EQ(start, 0x1234ABCDu);
|
|
}
|