Automatically recover internal Companion ExtraFS on boot

This commit is contained in:
mikecarper committed 2026-09-03 15:15:23 -07:00
1 parent 1c91f82ec0
commit 73f10f3fc3
7 files changed
+379 -70

No files matched your search

+68
View File
@@ -0,0 +1,68 @@
# nRF52 Companion automatic ExtraFS recovery
Companion builds using internal ExtraFS reserve 100 KiB at
`0xD4000..0xED000`. Identity and preferences remain in the separate 28 KiB
primary store at `0xED000..0xF4000`. This applies to internal-ExtraFS Companion
builds, including Full, not repeaters or external QSPI storage.
At startup the firmware:
1. Mounts primary storage without erasing nonblank media. Only proven-blank
primary storage may be initialized automatically.
2. Checks the exact ExtraFS address, size, block geometry, and application
linker reservation, then attempts a non-formatting secondary mount.
3. Validates primary filesystem metadata before any destructive secondary
recovery. An unavailable primary store blocks recovery and identity replacement.
4. Keeps an already mounted, metadata-valid ExtraFS unchanged. If its initial
mount or validation failed, unmounts and retries once without formatting.
5. If the retry remains unusable, automatically formats only the reserved
100 KiB region, then remounts and validates it. There is one format attempt
per boot; failure keeps the contact/channel write quarantine active.
6. Runs the existing verified migration from primary into ExtraFS. Primary
contact/channel sources are retired only after every destination is copied,
read back, and the migration transaction is committed. Identity and
preferences stay on primary.
No terminal repair command, phone intervention, or bootloader update is needed
for this recovery path when a compatible application/layout is already used.
After successful recovery, the Companion storage API reports 100 KiB total
rather than the 28 KiB primary fallback.
## Data-loss boundary
Automatic rebuilding prioritizes a usable contact/channel store over retaining
an unreadable secondary filesystem. Data stored only in that damaged region
can be lost. Surviving primary files are migrated, but may not contain the
latest secondary-only changes. An API backup remains advisable before flashing.
Primary storage is never formatted by this recovery path.
This is mount/metadata recovery, not forensic file recovery. An invalid
migration journal, unreadable individual contact page, primary-source error,
full filesystem, or failed migration cleanup does **not** on its own trigger a
format of a mountable, metadata-valid secondary store. Those cases retain the
existing validation, retry, and incomplete-load protections.
## Regression coverage
`test/test_internal_secondary_fs_repair` exercises healthy/no-format startup,
successful remount, failed remount/traversal followed by one repair, and
format/remount/validation failure. `test/test_nrf52_extrafs_contract.py` checks
primary-first ordering, internal-only geometry guards, quarantine handling,
and the separation between migration failures and destructive recovery.
## T1000-E hardware qualification (2026-09-03)
A device reporting 25/28 KiB primary fallback and contact enumeration error 5
was flashed with the recovery build. With no explicit repair command, startup
activated 100 KiB ExtraFS and loaded all 125 contacts and 40 channel slots.
Identity, preferences, radio settings, and channels matched the pre-update API
snapshot. Add, edit, and delete of an owned temporary contact each survived a
separate normal reboot; the device ended with the original 125 contacts and
27/100 KiB usage. The 161 offline/native regression tests and target build passed.
This was not lossless recovery of every historical RAM value: 21 contacts had
older advertisement/local-modification timestamps, including one older advertised
location. No contact was missing, and no computer-backup restoration was used.
A subsequent read-only scan found no readable cached advert for any of the 125
contacts. Without a surviving newer copy, firmware cannot infer those earlier
values; receiving new verified advertisements can refresh them normally.
+55 -3
View File
@@ -105,13 +105,18 @@ void DataStore::begin() {
_fsExtra = nullptr;
return;
}
#if defined(EXTRAFS) && !defined(QSPIFLASH)
// Validate primary first: automatic secondary recovery must never hide a
// primary/identity fault. Retry or rebuild before loading any RAM state.
recoverInternalExtraFSOnBoot();
#else
if (_fsExtra != nullptr && !validateLfsFilesystem(_fsExtra)) {
// A traversal-failed filesystem may still be recoverable offline. Keep
// both removable QSPI and managed on-chip ExtraFS intact until an explicit
// local repair/factory-reset operation authorizes erasing it.
// Automatic destructive recovery is restricted to reserved internal
// ExtraFS. Removable/external QSPI still requires an explicit repair.
MESH_DEBUG_PRINTLN("DataStore: secondary LittleFS metadata is corrupt; preserving it and using primary storage");
disableSecondaryFS(true);
}
#endif
if (primary_ready) cleanupAtomicTempFiles(_fs);
if (_fsExtra != nullptr) cleanupAtomicTempFiles(_fsExtra);
#endif
@@ -210,6 +215,53 @@ static void cleanupAtomicTempFiles(FILESYSTEM* fs) {
}
#if defined(EXTRAFS) && !defined(QSPIFLASH)
bool DataStore::recoverInternalExtraFSOnBoot() {
if (_configuredFsExtra == nullptr) return false;
CustomLFS* extra = static_cast<CustomLFS*>(_configuredFsExtra);
if (_primary_storage_unavailable
|| !mesh::storage::isExpectedInternalExtraFsGeometry(
extra->getFlashAddr(), extra->getFlashSize(), extra->getBlockSize())
|| !mesh::storage::isInternalExtraFsReservedByApplication(
(uint32_t)(uintptr_t)__flash_arduino_end)) {
disableSecondaryFS(true);
MESH_DEBUG_PRINTLN("DataStore: refusing automatic ExtraFS recovery outside reserved 100 KiB region");
return false;
}
const mesh::storage::InternalSecondaryFsRecoveryResult result =
mesh::storage::recoverInternalSecondaryFilesystem(
[this]() -> bool {
return _fsExtra == _configuredFsExtra
&& validateLfsFilesystem(_fsExtra);
},
[this, extra]() -> bool {
MESH_DEBUG_PRINTLN("DataStore: retrying internal ExtraFS mount before recovery");
_fsExtra = nullptr;
extra->end();
if (!extra->Adafruit_LittleFS::begin()) return false;
_fsExtra = _configuredFsExtra;
return true;
},
[this]() -> bool {
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS remains unusable; rebuilding 100 KiB (secondary-only data may be lost)");
return reinitializeInternalExtraFS();
});
if (result == mesh::storage::InternalSecondaryFsRecoveryResult::Failed) {
disableSecondaryFS(true);
return false;
}
// This runs only before migration and user-data loading. Clear the initial
// mount quarantine, not errors from a later incomplete contact/prefs load.
// Migration below establishes authority again and can re-latch any error.
_secondary_authority_unknown = false;
_contact_load_incomplete = false;
_identity_creation_blocked = false;
_prefs_load_incomplete = false;
return true;
}
bool DataStore::reinitializeInternalExtraFS() {
if (_configuredFsExtra == nullptr) return false;
+3 -2
View File
@@ -19,8 +19,8 @@ class DataStore {
FILESYSTEM* _fs;
FILESYSTEM* _fsExtra;
// Keep the configured secondary even when normal I/O falls back to the
// primary filesystem. An explicit local repair/factory reset must still be
// able to address the inactive on-chip ExtraFS.
// primary filesystem. Boot recovery and explicit local repair must still
// be able to address the inactive on-chip ExtraFS.
FILESYSTEM* _configuredFsExtra;
mesh::RTCClock* _clock;
IdentityStore identity_store;
@@ -47,6 +47,7 @@ class DataStore {
bool truncateLegacyContacts(uint16_t remaining_contacts);
void resetContactPageState(bool clear_incomplete = false);
#if defined(EXTRAFS) && !defined(QSPIFLASH)
bool recoverInternalExtraFSOnBoot();
bool reinitializeInternalExtraFS();
#endif
#endif
+5 -35
View File
@@ -2147,46 +2147,16 @@ void setup() {
ExtraFS.getBlockSize())
&& mesh::storage::isInternalExtraFsReservedByApplication(
(uint32_t)(uintptr_t)__flash_arduino_end);
mesh::storage::InternalSecondaryFsBootResult extra_fs_boot =
mesh::storage::InternalSecondaryFsBootResult::InitializationFailed;
if (extra_fs_geometry_valid) {
extra_fs_boot = mesh::storage::prepareInternalSecondaryFilesystem(
[]() -> bool {
// Bypass CustomLFS::begin(), whose mount-failure path erases
// nonblank media before DataStore can quarantine it.
return ExtraFS.Adafruit_LittleFS::begin();
},
[]() -> bool {
const volatile uint32_t* word =
reinterpret_cast<const volatile uint32_t*>(
mesh::storage::INTERNAL_EXTRAFS_START);
for (uint32_t offset = 0;
offset < mesh::storage::INTERNAL_EXTRAFS_SIZE;
offset += sizeof(uint32_t), word++) {
if (*word != 0xFFFFFFFFUL) return false;
}
return true;
},
[]() -> bool {
ExtraFS.end();
return ExtraFS.format();
});
}
// Initial mount is always non-destructive, even for blank media.
// DataStore validates primary first, then retries/rebuilds ExtraFS.
if (!extra_fs_geometry_valid
|| extra_fs_boot == mesh::storage::InternalSecondaryFsBootResult::PreservedNonBlank
|| extra_fs_boot == mesh::storage::InternalSecondaryFsBootResult::InitializationFailed) {
|| !ExtraFS.Adafruit_LittleFS::begin()) {
if (!extra_fs_geometry_valid) {
MESH_DEBUG_PRINTLN("CustomLFS: internal ExtraFS geometry is not 100 KiB; refusing to mount or erase it");
} else if (extra_fs_boot
== mesh::storage::InternalSecondaryFsBootResult::PreservedNonBlank) {
MESH_DEBUG_PRINTLN("CustomLFS: internal ExtraFS mount failed; preserving nonblank media and using primary storage");
} else {
MESH_DEBUG_PRINTLN("CustomLFS: blank internal ExtraFS initialization failed; using primary storage");
MESH_DEBUG_PRINTLN("CustomLFS: internal ExtraFS mount failed; deferring automatic recovery until primary validation");
}
const bool secondary_authority_unknown = !extra_fs_geometry_valid
|| extra_fs_boot
== mesh::storage::InternalSecondaryFsBootResult::PreservedNonBlank;
store.disableSecondaryFS(secondary_authority_unknown);
store.disableSecondaryFS(true);
}
#endif
#endif
+23 -3
View File
@@ -63,9 +63,8 @@ enum class InternalSecondaryFsRepairResult {
ValidationFailed,
};
// Repair is deliberately separate from the normal boot path. A filesystem
// which mounts but fails a full metadata traversal may still be forensically
// recoverable, so erasing it requires an explicit local operator action.
// The caller must authorize and bound the erase. In particular, automatic
// recovery is only for the reserved secondary region, never identity storage.
template <typename Format, typename Mount, typename Validate>
InternalSecondaryFsRepairResult repairInternalSecondaryFilesystem(
Format format, Mount mount, Validate validate) {
@@ -75,5 +74,26 @@ InternalSecondaryFsRepairResult repairInternalSecondaryFilesystem(
return InternalSecondaryFsRepairResult::Repaired;
}
enum class InternalSecondaryFsRecoveryResult {
Ready,
Remounted,
Reinitialized,
Failed,
};
// A failed initial mount/traversal may be transient. Retry non-destructively
// before rebuilding, and attempt the destructive repair at most once. Repair
// must include its own mount and validation before reporting success.
template <typename IsReady, typename Remount, typename Repair>
InternalSecondaryFsRecoveryResult recoverInternalSecondaryFilesystem(
IsReady is_ready, Remount remount, Repair repair) {
if (is_ready()) return InternalSecondaryFsRecoveryResult::Ready;
if (remount() && is_ready()) {
return InternalSecondaryFsRecoveryResult::Remounted;
}
if (repair()) return InternalSecondaryFsRecoveryResult::Reinitialized;
return InternalSecondaryFsRecoveryResult::Failed;
}
} // namespace storage
} // namespace mesh
@@ -4,17 +4,23 @@
using mesh::storage::InternalSecondaryFsRepairResult;
using mesh::storage::InternalSecondaryFsBootResult;
using mesh::storage::InternalSecondaryFsRecoveryResult;
using mesh::storage::isInternalExtraFsReservedByApplication;
using mesh::storage::isErasedFlashRange;
using mesh::storage::isExpectedInternalExtraFsGeometry;
using mesh::storage::prepareInternalSecondaryFilesystem;
using mesh::storage::repairInternalSecondaryFilesystem;
using mesh::storage::recoverInternalSecondaryFilesystem;
TEST(InternalSecondaryFsBoot, RequiresExact100KiBGeometry) {
EXPECT_TRUE(isExpectedInternalExtraFsGeometry(0xD4000, 0x19000, 128));
EXPECT_FALSE(isExpectedInternalExtraFsGeometry(0xED000, 0x7000, 128));
EXPECT_FALSE(isExpectedInternalExtraFsGeometry(0xD4000, 0x7000, 128));
EXPECT_FALSE(isExpectedInternalExtraFsGeometry(0xD4000, 0x19000, 4096));
EXPECT_FALSE(isExpectedInternalExtraFsGeometry(0xD3000, 0x1A000, 128));
EXPECT_FALSE(isExpectedInternalExtraFsGeometry(0xD4000, 0x19001, 128));
EXPECT_FALSE(isExpectedInternalExtraFsGeometry(0xD4000, 0x18FFF, 128));
EXPECT_FALSE(isExpectedInternalExtraFsGeometry(0xD4000, 0, 128));
EXPECT_TRUE(isInternalExtraFsReservedByApplication(0xD4000));
EXPECT_TRUE(isInternalExtraFsReservedByApplication(0xC0000));
EXPECT_FALSE(isInternalExtraFsReservedByApplication(0xD4001));
@@ -171,6 +177,131 @@ TEST(InternalSecondaryFsRepair, RejectsFilesystemWhichRemainsInvalid) {
EXPECT_EQ(result, InternalSecondaryFsRepairResult::ValidationFailed);
}
TEST(InternalSecondaryFsRecovery, HealthyFilesystemNeverRemountsOrRepairs) {
int ready_calls = 0;
int remount_calls = 0;
int repair_calls = 0;
const auto result = recoverInternalSecondaryFilesystem(
[&]() { ready_calls++; return true; },
[&]() { remount_calls++; return true; },
[&]() { repair_calls++; return true; });
EXPECT_EQ(result, InternalSecondaryFsRecoveryResult::Ready);
EXPECT_EQ(ready_calls, 1);
EXPECT_EQ(remount_calls, 0);
EXPECT_EQ(repair_calls, 0);
}
TEST(InternalSecondaryFsRecovery, SuccessfulRetryIsValidatedWithoutRepair) {
int sequence = 0;
int ready_calls = 0;
int repair_calls = 0;
const auto result = recoverInternalSecondaryFilesystem(
[&]() {
ready_calls++;
EXPECT_EQ(++sequence, ready_calls == 1 ? 1 : 3);
return ready_calls == 2;
},
[&]() { EXPECT_EQ(++sequence, 2); return true; },
[&]() { repair_calls++; return true; });
EXPECT_EQ(result, InternalSecondaryFsRecoveryResult::Remounted);
EXPECT_EQ(ready_calls, 2);
EXPECT_EQ(repair_calls, 0);
EXPECT_EQ(sequence, 3);
}
TEST(InternalSecondaryFsRecovery, FailedRemountSkipsTraversalAndRepairsOnce) {
int ready_calls = 0;
int remount_calls = 0;
int repair_calls = 0;
const auto result = recoverInternalSecondaryFilesystem(
[&]() { ready_calls++; return false; },
[&]() { remount_calls++; return false; },
[&]() {
EXPECT_EQ(ready_calls, 1);
EXPECT_EQ(remount_calls, 1);
repair_calls++;
return true;
});
EXPECT_EQ(result, InternalSecondaryFsRecoveryResult::Reinitialized);
EXPECT_EQ(ready_calls, 1);
EXPECT_EQ(remount_calls, 1);
EXPECT_EQ(repair_calls, 1);
}
TEST(InternalSecondaryFsRecovery, MountedButStillCorruptFilesystemIsRepairedOnce) {
int ready_calls = 0;
int remount_calls = 0;
int repair_calls = 0;
const auto result = recoverInternalSecondaryFilesystem(
[&]() { ready_calls++; return false; },
[&]() { remount_calls++; return true; },
[&]() {
EXPECT_EQ(ready_calls, 2);
repair_calls++;
return true;
});
EXPECT_EQ(result, InternalSecondaryFsRecoveryResult::Reinitialized);
EXPECT_EQ(ready_calls, 2);
EXPECT_EQ(remount_calls, 1);
EXPECT_EQ(repair_calls, 1);
}
TEST(InternalSecondaryFsRecovery, RepairFailureRemainsFailedWithoutRetryLoop) {
int ready_calls = 0;
int remount_calls = 0;
int repair_calls = 0;
const auto result = recoverInternalSecondaryFilesystem(
[&]() { ready_calls++; return false; },
[&]() { remount_calls++; return false; },
[&]() { repair_calls++; return false; });
EXPECT_EQ(result, InternalSecondaryFsRecoveryResult::Failed);
EXPECT_EQ(ready_calls, 1);
EXPECT_EQ(remount_calls, 1);
EXPECT_EQ(repair_calls, 1);
}
TEST(InternalSecondaryFsRecovery, ReinitializationRequiresFormatMountAndValidation) {
// The recovery policy delegates destructive work, but the real repair
// callback must never report success after only formatting or mounting.
for (int failing_step = 1; failing_step <= 3; failing_step++) {
int sequence = 0;
const auto result = recoverInternalSecondaryFilesystem(
[]() { return false; },
[]() { return false; },
[&]() {
const auto repaired = repairInternalSecondaryFilesystem(
[&]() { return ++sequence != failing_step; },
[&]() { return ++sequence != failing_step; },
[&]() { return ++sequence != failing_step; });
return repaired == InternalSecondaryFsRepairResult::Repaired;
});
EXPECT_EQ(result, InternalSecondaryFsRecoveryResult::Failed);
EXPECT_EQ(sequence, failing_step);
}
}
TEST(InternalSecondaryFsRecovery, VerifiedReinitializationCompletesExactlyOnce) {
int sequence = 0;
const auto result = recoverInternalSecondaryFilesystem(
[]() { return false; },
[]() { return false; },
[&]() {
const auto repaired = repairInternalSecondaryFilesystem(
[&]() { EXPECT_EQ(++sequence, 1); return true; },
[&]() { EXPECT_EQ(++sequence, 2); return true; },
[&]() { EXPECT_EQ(++sequence, 3); return true; });
return repaired == InternalSecondaryFsRepairResult::Repaired;
});
EXPECT_EQ(result, InternalSecondaryFsRecoveryResult::Reinitialized);
EXPECT_EQ(sequence, 3);
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
+94 -27
View File
@@ -365,30 +365,50 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
self.assertEqual(size, 100 * 1024)
self.assertEqual(start + size, 0xED000)
def test_boot_mount_is_non_destructive_and_checks_runtime_geometry(self):
def test_initial_secondary_mount_defers_repair_until_primary_validation(self):
main = MAIN.read_text(encoding="utf-8")
setup = function_body(main, "void setup()")
self.assertIn("isExpectedInternalExtraFsGeometry(", setup)
self.assertIn("isInternalExtraFsReservedByApplication(", setup)
self.assertIn("uint32_t __flash_arduino_end[]", main)
self.assertIn("(uintptr_t)__flash_arduino_end", setup)
self.assertIn("prepareInternalSecondaryFilesystem(", setup)
self.assertIn(
"ExtraFS.Adafruit_LittleFS::begin()", setup
)
geometry_at = setup.index("isExpectedInternalExtraFsGeometry(")
store_begin_at = setup.index("store.begin()", geometry_at)
secondary_mount = setup[geometry_at:store_begin_at]
self.assertIn("ExtraFS.Adafruit_LittleFS::begin()", secondary_mount)
self.assertNotIn("ExtraFS.begin()", setup)
self.assertIn("PreservedNonBlank", setup)
self.assertIn(
"store.disableSecondaryFS(secondary_authority_unknown);", setup
)
self.assertLess(
setup.index("prepareInternalSecondaryFilesystem("),
setup.index("store.begin()"),
)
self.assertNotIn("prepareInternalSecondaryFilesystem(", secondary_mount)
self.assertNotIn("ExtraFS.format()", secondary_mount)
self.assertNotIn("isErasedFlashRange(", secondary_mount)
self.assertIn("store.disableSecondaryFS(true);", secondary_mount)
def test_corrupt_secondary_is_preserved_until_explicit_repair(self):
def test_boot_recovery_runs_only_for_internal_secondary_after_primary_validation(self):
store = STORE.read_text(encoding="utf-8")
begin = function_body(store, "void DataStore::begin()")
primary_failure = function_body(begin, "if (!primary_ready)")
self.assertIn("return;", primary_failure)
self.assertNotIn("recoverInternalExtraFSOnBoot", primary_failure)
self.assertLess(
begin.index("if (!primary_ready)"),
begin.index("recoverInternalExtraFSOnBoot()"),
)
internal = re.search(
r"#if defined\(EXTRAFS\) && !defined\(QSPIFLASH\)(.*?)#else",
begin,
re.DOTALL,
)
self.assertIsNotNone(internal)
self.assertIn("recoverInternalExtraFSOnBoot()", internal.group(1))
self.assertLess(
begin.index("recoverInternalExtraFSOnBoot()"),
begin.index("cleanupAtomicTempFiles(_fs)"),
)
migration_at = begin.index("migrateToSecondaryFS()")
self.assertLess(begin.index("recoverInternalExtraFSOnBoot()"), migration_at)
self.assertNotIn("reinitializeInternalExtraFS", begin[migration_at:])
self.assertNotIn("recoverInternalExtraFSOnBoot", begin[migration_at:])
# QSPI/removable secondary storage retains the non-destructive path.
invalid = re.search(
r"if\s*\(_fsExtra\s*!=\s*nullptr\s*&&\s*"
r"!validateLfsFilesystem\(_fsExtra\)\)\s*\{(.*?)\}",
@@ -400,6 +420,23 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
self.assertNotIn("format", invalid.group(1))
self.assertNotIn("reinitializeInternalExtraFS", invalid.group(1))
def test_boot_recovery_retains_configured_secondary_and_checks_exact_region(self):
store = STORE.read_text(encoding="utf-8")
recover = function_body(store, "bool DataStore::recoverInternalExtraFSOnBoot()")
self.assertIn("_configuredFsExtra == nullptr", recover)
policy_at = recover.index("recoverInternalSecondaryFilesystem(")
geometry_at = recover.index("isExpectedInternalExtraFsGeometry(")
reservation_at = recover.index("isInternalExtraFsReservedByApplication(")
self.assertLess(geometry_at, policy_at)
self.assertLess(reservation_at, policy_at)
self.assertIn("(uintptr_t)__flash_arduino_end", recover)
self.assertIn("extra->getFlashAddr()", recover)
self.assertIn("extra->getFlashSize()", recover)
self.assertIn("extra->getBlockSize()", recover)
self.assertIn("return false;", recover[geometry_at:policy_at])
self.assertNotIn("->format(", recover)
self.assertNotIn("_fs->", recover)
header = STORE_HEADER.read_text(encoding="utf-8")
self.assertIn("FILESYSTEM* _configuredFsExtra;", header)
extra_constructor = re.search(
@@ -410,6 +447,40 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
self.assertIsNotNone(extra_constructor)
self.assertIn("_configuredFsExtra(&fsExtra)", extra_constructor.group(0))
def test_boot_recovery_retries_nonformatting_mount_before_guarded_reinitialization(self):
store = STORE.read_text(encoding="utf-8")
recover = function_body(store, "bool DataStore::recoverInternalExtraFSOnBoot()")
policy_at = recover.index("recoverInternalSecondaryFilesystem(")
ready_at = recover.index("validateLfsFilesystem(_fsExtra)", policy_at)
unmount_at = recover.index("extra->end()", ready_at)
remount_at = recover.index("extra->Adafruit_LittleFS::begin()", unmount_at)
repair_at = recover.index("reinitializeInternalExtraFS()", remount_at)
self.assertLess(ready_at, unmount_at)
self.assertLess(unmount_at, remount_at)
self.assertLess(remount_at, repair_at)
self.assertIn("_fsExtra = _configuredFsExtra", recover[remount_at:repair_at])
self.assertNotIn("extra->begin()", recover)
self.assertNotIn("migrateToSecondaryFS()", recover)
def test_boot_recovery_only_clears_authority_latches_after_verified_success(self):
store = STORE.read_text(encoding="utf-8")
recover = function_body(store, "bool DataStore::recoverInternalExtraFSOnBoot()")
failure_at = recover.index("InternalSecondaryFsRecoveryResult::Failed")
clear_at = recover.index("_secondary_authority_unknown = false;")
failure = recover[failure_at:clear_at]
self.assertIn("disableSecondaryFS(true);", failure)
self.assertIn("return false;", failure)
success = recover[clear_at:]
for flag in (
"_secondary_authority_unknown",
"_identity_creation_blocked",
"_contact_load_incomplete",
"_prefs_load_incomplete",
):
self.assertIn(f"{flag} = false;", success)
self.assertNotIn("_primary_storage_unavailable = false", recover)
self.assertIn("return true;", success)
def test_repair_is_internal_only_and_revalidates_before_activation(self):
store = STORE.read_text(encoding="utf-8")
repair = function_body(store, "bool DataStore::repairInternalExtraFS()")
@@ -865,21 +936,17 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
self.assertEqual(setup.count("InternalFS.begin();"), 1)
self.assertGreater(setup.index("#else", primary_policy), primary_format)
self.assertIn("store.disableSecondaryFS(true);", setup)
authority = setup.index(
"const bool secondary_authority_unknown = !extra_fs_geometry_valid"
secondary_geometry = setup.index(
"const bool extra_fs_geometry_valid ="
)
disable_call = setup.index(
"store.disableSecondaryFS(secondary_authority_unknown)", authority
)
authority_expression = setup[authority:disable_call]
self.assertIn(
"InternalSecondaryFsBootResult::PreservedNonBlank",
authority_expression,
)
self.assertNotIn(
"InternalSecondaryFsBootResult::InitializationFailed",
authority_expression,
secondary_disable = setup.index(
"store.disableSecondaryFS(true);", secondary_geometry
)
secondary_guard = setup[secondary_geometry:secondary_disable]
self.assertIn("if (!extra_fs_geometry_valid", secondary_guard)
self.assertIn("!ExtraFS.Adafruit_LittleFS::begin()", secondary_guard)
self.assertNotIn("ExtraFS.format()", secondary_guard)
self.assertNotIn("isErasedFlashRange(", secondary_guard)
begin = function_body(mesh, "void MyMesh::begin(")
load_at = begin.index("const bool identity_loaded =")