#!/usr/bin/env python3 """Exercise real contact handles, cache policy and persistence under sanitizers.""" from pathlib import Path import re 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_empty_path_pool_has_no_flash_load_image(self): # The 390-handle pool must occupy zero-initialized storage, not a # multi-KiB initialized-data image containing repeated empty markers. # Compile a real static instance; source-shape checks cannot establish # whether the compiler emits a flash initializer for it. with tempfile.TemporaryDirectory(prefix="mesh-path-pool-image-") as directory: source = Path(directory) / "pool.cpp" obj = Path(directory) / "pool.o" source.write_text('''#include mesh::ContactPathStorage& testPathStorage() { static mesh::ContactPathPool<390> pool; return pool; } ''', encoding="ascii") compiled = subprocess.run([ "c++", "-std=c++17", "-Os", "-fno-threadsafe-statics", "-fno-rtti", "-fno-exceptions", "-I", str(ROOT / "src"), "-c", str(source), "-o", str(obj), ], capture_output=True, text=True) self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr) symbols = subprocess.run(["nm", "-S", "-C", "--defined-only", str(obj)], capture_output=True, text=True) self.assertEqual(symbols.returncode, 0, symbols.stdout + symbols.stderr) match = re.search(r"^\S+\s+([0-9a-fA-F]+)\s+(\w) testPathStorage\(\)::pool$", symbols.stdout, re.MULTILINE) self.assertIsNotNone(match, symbols.stdout) self.assertGreater(int(match[1], 16), 5000) self.assertEqual(match[2].lower(), "b", symbols.stdout) def test_default_policy_covers_high_contact_nrf52_without_full(self): cases = ( ({"NRF52_PLATFORM": 1, "MAX_CONTACTS": 350}, 1), ({"NRF52_PLATFORM": 1, "MAX_CONTACTS": 301}, 1), ({"NRF52_PLATFORM": 1, "MAX_CONTACTS": 300}, 0), ({"NRF52_PLATFORM": 1}, 0), ({"ESP32_PLATFORM": 1, "MAX_CONTACTS": 350}, 0), ({"NRF52_PLATFORM": 1, "COMPANION_RADIO_FULL": 1}, 1), ({"ESP32_PLATFORM": 1, "COMPANION_RADIO_FULL": 1}, 1), ({"NRF52_PLATFORM": 1, "MAX_CONTACTS": 350, "BOARD_HAS_PSRAM": 1}, 0), ({"ESP32_PLATFORM": 1, "COMPANION_RADIO_FULL": 1, "BOARD_HAS_PSRAM": 1}, 0), ({"NRF52_PLATFORM": 1, "MAX_CONTACTS": 350, "MESH_CONTACT_CACHE": 0}, 0), ({"NRF52_PLATFORM": 1, "MAX_CONTACTS": 100, "MESH_CONTACT_CACHE": 1}, 1), ) for defines, expected in cases: with self.subTest(defines=defines): result = subprocess.run([ "c++", "-E", "-P", "-x", "c++", "-", "-I", str(ROOT / "src"), *("-D%s=%s" % item for item in defines.items()), ], input='#include \nMESH_CONTACT_CACHE\n', text=True, capture_output=True, check=True) self.assertEqual(result.stdout.strip(), str(expected)) 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_standard_nrf52_policy_preserves_migration_and_snapshots(self): # No Full or explicit cache flag: exercise the ordinary 350-contact # recipe's default through the same real cache/persistence harness. self.run_cache_test("NRF52_PLATFORM", standard_nrf52=True) 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 test_esp32_unconditional_contact_reset_is_detected(self): self.run_cache_test("ESP32_PLATFORM", disable_raw_recovery=True) def test_nrf52_unconditional_contact_reset_is_detected(self): self.run_cache_test("NRF52_PLATFORM", disable_raw_recovery=True) def test_nrf52_unsafe_legacy_discovery_recovery_is_detected(self): self.run_cache_test("NRF52_PLATFORM", unsafe_legacy_discovery=True) def test_nrf52_unsafe_page_discovery_recovery_is_detected(self): self.run_cache_test("NRF52_PLATFORM", unsafe_page_discovery=True) def run_cache_test(self, platform, disable_crc_batch=False, disable_recovery=False, disable_raw_recovery=False, unsafe_legacy_discovery=False, unsafe_page_discovery=False, fixture_transform=None, expected_failure=None, expected_marker='PASS: 114 shared request/login payload and routing traces', standard_nrf52=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 disable_raw_recovery: # Restore the exact pre-fix unconditional host-reset path, # leaving fault classification and bounded fallback unchanged. raw_retry = method(store, "if (_contact_recovery & ContactRecoveryRawOnly) {") self.assertEqual(implementation.count(raw_retry), 1) implementation = implementation.replace(raw_retry, "") if unsafe_legacy_discovery: discovery = method(store, 'if (!contactPathPresence(contacts_fs, "/contacts3", legacy_exists))') self.assertEqual(discovery.count("queueContactRecovery();"), 1) self.assertEqual(implementation.count(discovery), 1) implementation = implementation.replace(discovery, discovery.replace( "queueContactRecovery();", "queueContactRecovery(true);")) if unsafe_page_discovery: discovery = method(store, "if (!contactPathPresence(contacts_fs, path, page_exists))") self.assertNotIn("_unread_contact_pages.mark(page);", discovery) self.assertEqual(discovery.count("queueContactRecovery();"), 1) self.assertEqual(implementation.count(discovery), 1) implementation = implementation.replace(discovery, discovery.replace( "queueContactRecovery();", "_unread_contact_pages.mark(page);\n queueContactRecovery();")) 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") companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text() text_helpers = (ROOT / "src/helpers/TxtDataHelpers.cpp").read_text() (temp / "contact_frame_under_test.h").write_text( method(text_helpers, "void StrHelper::strzcpy(") + "\n" + method(companion, "bool MyMesh::writeContactRespFrame(")) 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" fixture = ROOT / "test/fixtures/contact_cache/test_contact_cache.cpp" if fixture_transform is not None: fixture = fixture_transform(temp) result = subprocess.run([ "c++", "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-fsanitize=address,undefined", "-fno-omit-frame-pointer", *([] if standard_nrf52 else ["-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(fixture), 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 expected_failure is not None: self.assertNotEqual(result.returncode, 0) self.assertIn(expected_failure, result.stdout + result.stderr) elif disable_raw_recovery: self.assertNotEqual(result.returncode, 0) self.assertIn("raw_read_recovery_preserves_accepted_ram_state", result.stderr) self.assertIn("f.host.recovery_resets == 0", result.stderr) elif unsafe_page_discovery: self.assertNotEqual(result.returncode, 0) self.assertIn("nrf52_unknown_page_stat_preserves_contact_authority", result.stderr) self.assertIn("f.host.contacts.size()", result.stderr) elif unsafe_legacy_discovery: self.assertNotEqual(result.returncode, 0) self.assertIn("nrf52_unknown_legacy_stat_preserves_contact_authority", result.stderr) self.assertIn("SPIFFS.files == durable", result.stderr) elif 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(expected_marker, result.stdout) if __name__ == "__main__": unittest.main()