Verify fallback namespace moves before accepting publication or rollback. Reload readable contact images at a safe host boundary, discard only unreadable contact data after bounded retries, and permit volatile operation when storage cannot recover. Preserve unrelated stores and fail explicit durability requests truthfully. Keep existing flash, RAM, OTA and capability limits by streaming nRF52 preferences through the validated wire table and sharing identical contact deadline resets. Add fault-injection, sanitizer, legacy-image parity and CLI lifecycle regressions.
175 lines
9.5 KiB
Python
175 lines
9.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Exercise real contact handles, cache policy and persistence under sanitizers."""
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from test_t096_full_memory import method
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def crypto_objects(temp):
|
|
objects = []
|
|
for name in ("fe", "ge", "sc", "sha512", "keypair", "key_exchange"):
|
|
obj = temp / (name + ".o")
|
|
subprocess.run([
|
|
"cc", "-O1", "-g", "-fsanitize=address", "-fwrapv", "-c",
|
|
str(ROOT / "lib/ed25519" / (name + ".c")), "-o", str(obj),
|
|
], capture_output=True, text=True, check=True)
|
|
objects.append(str(obj))
|
|
return objects
|
|
|
|
|
|
class ContactCacheTest(unittest.TestCase):
|
|
def test_esp32_misses_recalculate_without_flash_io(self):
|
|
with tempfile.TemporaryDirectory(prefix="mesh-secret-power-") as temp:
|
|
temp = Path(temp)
|
|
binary = temp / "test"
|
|
result = subprocess.run([
|
|
"c++", "-std=c++17", "-O1", "-g", "-fsanitize=address,undefined",
|
|
"-DESP32_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1",
|
|
"-I", str(ROOT / "test/fixtures/contact_cache/mocks"),
|
|
"-I", str(ROOT / "lib/ed25519"), "-I", str(ROOT / "src"),
|
|
str(ROOT / "test/fixtures/contact_cache/test_secret_power.cpp"),
|
|
*crypto_objects(temp), "-lcrypto", "-o", str(binary),
|
|
], capture_output=True, text=True)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
result = subprocess.run([str(binary)], capture_output=True, text=True)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
|
|
def test_eviction_snapshots_transactions_and_identity_changes(self):
|
|
self.run_cache_test("ESP32_PLATFORM")
|
|
|
|
def test_nrf52_paged_store_migration_and_snapshots(self):
|
|
self.run_cache_test("NRF52_PLATFORM")
|
|
|
|
def test_esp32_default_crc_cadence_is_detected(self):
|
|
self.run_cache_test("ESP32_PLATFORM", disable_crc_batch=True)
|
|
|
|
def test_esp32_disabled_contact_recovery_is_detected(self):
|
|
self.run_cache_test("ESP32_PLATFORM", disable_recovery=True)
|
|
|
|
def run_cache_test(self, platform, disable_crc_batch=False, disable_recovery=False):
|
|
with tempfile.TemporaryDirectory(prefix="mesh-contact-cache-") as temp:
|
|
temp = Path(temp)
|
|
store = (ROOT / "examples/companion_radio/DataStore.cpp").read_text()
|
|
functions = [
|
|
"static bool serializeContactRecord(",
|
|
"static bool deserializeContactRecord(",
|
|
"void DataStore::clearContactLoadFailure(",
|
|
"void DataStore::recoverContacts(",
|
|
"bool DataStore::hasIncompleteContactLoad() const",
|
|
"void DataStore::loadContacts(",
|
|
"bool DataStore::saveContacts(",
|
|
"bool DataStore::flushContactWrites(",
|
|
"bool DataStore::readStoredPath(",
|
|
"bool DataStore::flushCachedPaths(",
|
|
"uint16_t DataStore::secretSlot(",
|
|
"bool DataStore::readSavedSecret(",
|
|
"bool DataStore::saveSecret(",
|
|
]
|
|
helper_start = store.index("namespace {\nbool cachedContactFilter")
|
|
helper_end = store.index("bool DataStore::readStoredPath(", helper_start)
|
|
implementation = store[helper_start:helper_end]
|
|
header = (ROOT / "examples/companion_radio/DataStore.h").read_text()
|
|
recovery_start = header.index(" // Contact-only recovery")
|
|
recovery_end = header.index("#if defined(ESP32_PLATFORM)", recovery_start)
|
|
(temp / "contact_recovery_state_under_test.h").write_text(
|
|
header[recovery_start:recovery_end])
|
|
(temp / "contact_recovery_api_under_test.h").write_text(
|
|
method(header, " bool contactRecoveryPending() const") + "\n" +
|
|
method(header, " bool contactsAreVolatile() const"))
|
|
state_start = header.index(" mesh::ContactFileTransaction* _contact_write")
|
|
state_end = header.index("\n#endif", state_start)
|
|
(temp / "contact_write_state_under_test.h").write_text(
|
|
header[state_start:state_end])
|
|
functions.append("DataStore::~DataStore()")
|
|
if platform == "ESP32_PLATFORM":
|
|
functions.extend((
|
|
"static bool readableContactFile(",
|
|
"const char* DataStore::selectContactFile(",
|
|
"bool DataStore::prepareContactSave(",
|
|
"void DataStore::cancelContactWrite(",
|
|
"bool DataStore::cancelCooperativeWrite(",
|
|
"bool DataStore::serviceContactWrite(",
|
|
"bool DataStore::markContactDirty(",
|
|
"bool DataStore::releaseContact(",
|
|
"bool DataStore::serviceContactWrites(",
|
|
"bool DataStore::hasPendingContactWrites() const",
|
|
"void DataStore::begin()",
|
|
"void DataStore::disableSecondaryFS(",
|
|
))
|
|
if platform == "NRF52_PLATFORM":
|
|
implementation += method(store, "static void makeContactPagePath(") + "\n"
|
|
implementation += method(store, "static void discardInvalidContactPage(") + "\n"
|
|
functions.append("bool DataStore::writeContactPage(")
|
|
functions.append("bool DataStore::loadContactPages(")
|
|
functions.append("void DataStore::resetContactPageState(")
|
|
implementation += "\n".join(method(store, signature) for signature in functions)
|
|
if disable_recovery:
|
|
recovery = method(store, "void DataStore::recoverContacts(")
|
|
self.assertIn(recovery, implementation)
|
|
implementation = implementation.replace(
|
|
recovery, "void DataStore::recoverContacts(DataStoreHost*) {}")
|
|
if platform == "NRF52_PLATFORM":
|
|
# Fault only the page snapshot allocation; keep production
|
|
# loading/recovery logic and unrelated crypto allocations.
|
|
allocation = "(uint8_t*)malloc(mesh::storage::CONTACT_PAGE_PAYLOAD_SIZE)"
|
|
self.assertEqual(implementation.count(allocation), 1)
|
|
implementation = implementation.replace(
|
|
allocation,
|
|
"(uint8_t*)allocateContactPagePayload(mesh::storage::CONTACT_PAGE_PAYLOAD_SIZE)")
|
|
if disable_crc_batch:
|
|
self.assertIn("_contact_write->serviceCommit(true, 8, true)", implementation)
|
|
implementation = implementation.replace(
|
|
"_contact_write->serviceCommit(true, 8, true)",
|
|
"_contact_write->serviceCommit(true, 1, true)")
|
|
(temp / "store_under_test.h").write_text(implementation)
|
|
packet = (ROOT / "src/Packet.cpp").read_text()
|
|
(temp / "packet_under_test.h").write_text("namespace mesh {\n" + "\n".join(
|
|
method(packet, signature) for signature in (
|
|
"Packet::Packet()", "int Packet::getRawLength() const",
|
|
"bool Packet::isValidPathLen(", "size_t Packet::writePath(",
|
|
"uint8_t Packet::copyPath(")) + "\n}\n")
|
|
chat = (ROOT / "src/helpers/BaseChatMesh.cpp").read_text()
|
|
mesh = (ROOT / "src/Mesh.cpp").read_text()
|
|
(temp / "send_under_test.h").write_text(
|
|
"namespace mesh {\n" + method(mesh, "bool Mesh::sendDirect(") + "\n}\n"
|
|
+ "\n".join(method(chat, signature) for signature in (
|
|
"int BaseChatMesh::sendContactPacket(",
|
|
"int BaseChatMesh::sendLogin(", "int BaseChatMesh::sendAnonReq(",
|
|
"bool BaseChatMesh::allocateRequestTag(",
|
|
"int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t*",
|
|
"int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t"))
|
|
)
|
|
binary = temp / "test"
|
|
result = subprocess.run([
|
|
"c++", "-std=c++17", "-O1", "-g", "-Wall", "-Wextra",
|
|
"-fsanitize=address,undefined", "-fno-omit-frame-pointer",
|
|
"-DMESH_CONTACT_CACHE=1", "-DCOMPANION_RADIO_FULL=1",
|
|
"-DMESH_CONTACT_SECRET_FLASH_CACHE=1",
|
|
"-D" + platform + "=1", "-DMAX_CONTACTS=350",
|
|
"-I", str(ROOT / "test/fixtures/contact_cache/mocks"),
|
|
"-I", str(ROOT / "lib/ed25519"),
|
|
"-I", str(ROOT / "src"), "-I", str(temp),
|
|
str(ROOT / "test/fixtures/contact_cache/test_contact_cache.cpp"),
|
|
str(ROOT / "src/helpers/ContactInfo.cpp"), *crypto_objects(temp),
|
|
"-lcrypto", "-o", str(binary),
|
|
], capture_output=True, text=True)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
result = subprocess.run([str(binary)], capture_output=True, text=True)
|
|
if disable_recovery:
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("!f.store.contactRecoveryPending()", result.stderr)
|
|
elif disable_crc_batch:
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("crc_read_passes == 104", result.stderr)
|
|
else:
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
self.assertIn('PASS: 114 shared request/login payload and routing traces', result.stdout)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|