From 0a3187ac30930f3c7522311db1ad7a06bb79ce78 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Sun, 4 Oct 2026 05:27:09 -0700 Subject: [PATCH] Stage ESP contact transaction startup between loop passes Heltec V4 timing measured 2.19 seconds for recovery checks, temporary-file cleanup, open and buffer configuration combined in one lazy-save pass. Defer those startup operations across guarded companion loops while explicit durability callers still initialize synchronously. Track ownership only after attempting to create the temporary file, preserve unowned stale files on early cancellation, and reject premature writes or commits. Cover startup faults, interruption, mutation, cache pressure and synchronous-construction negative controls with native sanitizer tests. --- examples/companion_radio/DataStore.cpp | 17 +- src/helpers/ContactFileTransaction.h | 96 +++++++++- test/fixtures/contact_cache/FakeFilesystem.h | 10 +- .../contact_cache/test_contact_cache.cpp | 40 ++++- .../test_esp32_uncached.cpp | 165 +++++++++++++++++- test/test_esp32_cooperative_contacts.py | 19 +- 6 files changed, 326 insertions(+), 21 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 62025a24..8e43bdb2 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -2508,10 +2508,8 @@ bool DataStore::serviceContactWrite(DataStoreHost* host, } #endif _contact_write = new (std::nothrow) mesh::ContactFileTransaction( - _getContactsChannelsFS(), "/contacts3", companionPathPresence); - if (_contact_write == nullptr || !static_cast(*_contact_write)) { - delete _contact_write; - _contact_write = nullptr; + _getContactsChannelsFS(), "/contacts3", companionPathPresence, true); + if (_contact_write == nullptr) { #if MESH_CONTACT_CACHE paths.endCommit(false); #endif @@ -2524,6 +2522,17 @@ bool DataStore::serviceContactWrite(DataStoreHost* host, _contact_write_verifying = false; return true; } + if (!static_cast(*_contact_write)) { + // The revision/session guard above also applies to every setup step. + // Keep initialization separate from serialization, including its final + // buffer configuration, so transports are serviced between operations. + const auto progress = _contact_write->serviceBegin(); + if (progress == mesh::ContactFileTransaction::BeginProgress::Failed) { + cancelContactWrite(); + return false; + } + return true; + } if (!_contact_write_verifying) { #if MESH_CONTACT_CACHE ContactInfo* contact; diff --git a/src/helpers/ContactFileTransaction.h b/src/helpers/ContactFileTransaction.h index 6ddec2ca..b0b050a3 100644 --- a/src/helpers/ContactFileTransaction.h +++ b/src/helpers/ContactFileTransaction.h @@ -28,6 +28,16 @@ private: uint32_t _verify_crc = 0xffffffff; size_t _verify_remaining = 0; PresenceProbe _presence; +#if defined(ESP32_PLATFORM) + enum class BeginStage : uint8_t { + Target, Backup, Recover, Temp, Remove, Open, Configure, Ready, Failed + }; + BeginStage _begin_stage = BeginStage::Target; + bool _begin_target_exists = false; + bool _begin_backup_exists = false; + bool _begin_temp_exists = false; + bool _owns_temp = false; +#endif static bool probe(FILESYSTEM* fs, const char* path, bool& present, PresenceProbe presence) { if (presence) return presence(fs, path, present); @@ -52,10 +62,21 @@ public: return !backup_exists || fs->rename(backup, target); } ContactFileTransaction(FILESYSTEM* fs, const char* target, - PresenceProbe presence = nullptr) + PresenceProbe presence = nullptr +#if defined(ESP32_PLATFORM) + , bool defer_begin = false +#endif + ) : _fs(fs), _target(target), _presence(presence) { snprintf(_temp, sizeof(_temp), "%s.tmp", target); snprintf(_backup, sizeof(_backup), "%s.bak", target); +#if defined(ESP32_PLATFORM) + // Explicit durability callers retain synchronous construction. The lazy + // contact writer requests one setup operation per transport-serviced pass. + if (!defer_begin) { + while (serviceBegin() == BeginProgress::Pending) {} + } +#else if (!recover(fs, target, presence)) return; if (!removeIfPresent(fs, _temp, presence)) return; #if defined(RP2040_PLATFORM) @@ -64,19 +85,75 @@ public: _file = fs->open(_temp, "w", true); #endif _ok = static_cast(_file); -#if defined(ESP32_PLATFORM) - // Arduino's default 4 KiB stdio buffer hides large SPIFFS writes inside a - // single small write() pass. Configure this newly opened stream before - // its first I/O, keeping one contact record as the largest buffered batch. - _ok = _ok && _file.setBufferSize(storage::CONTACT_RECORD_SIZE); #endif } ~ContactFileTransaction() { if (_file) _file.close(); if (_verify) _verify.close(); - if (!_finished) removeIfPresent(_fs, _temp, _presence); + if (!_finished +#if defined(ESP32_PLATFORM) + && _owns_temp +#endif + ) removeIfPresent(_fs, _temp, _presence); } operator bool() const { return _ok; } +#if defined(ESP32_PLATFORM) + enum class BeginProgress : uint8_t { Pending, Ready, Failed }; + BeginProgress serviceBegin() { + bool ok = true; + switch (_begin_stage) { + case BeginStage::Target: + ok = probe(_fs, _target, _begin_target_exists, _presence); + if (ok) _begin_stage = BeginStage::Backup; + break; + case BeginStage::Backup: + ok = probe(_fs, _backup, _begin_backup_exists, _presence); + if (ok) _begin_stage = BeginStage::Recover; + break; + case BeginStage::Recover: + if (!_begin_target_exists && _begin_backup_exists) + ok = _fs->rename(_backup, _target); + if (ok) _begin_stage = BeginStage::Temp; + break; + case BeginStage::Temp: + ok = probe(_fs, _temp, _begin_temp_exists, _presence); + if (ok) _begin_stage = BeginStage::Remove; + break; + case BeginStage::Remove: + if (_begin_temp_exists) ok = _fs->remove(_temp); + if (ok) _begin_stage = BeginStage::Open; + break; + case BeginStage::Open: + // The previous stages proved this name absent. Even an unsuccessful + // create/open can leave a partial inode belonging to this attempt. + // Before this point cancellation must leave an unowned stale temp. + _owns_temp = true; + _file = _fs->open(_temp, "w", true); + ok = static_cast(_file); + if (ok) _begin_stage = BeginStage::Configure; + break; + case BeginStage::Configure: + // Arduino otherwise batches small writes into a 4 KiB stdio buffer. + // Configure the newly opened stream before its first I/O. + ok = _file.setBufferSize(storage::CONTACT_RECORD_SIZE); + if (ok) { + _ok = true; + _begin_stage = BeginStage::Ready; + return BeginProgress::Ready; + } + break; + case BeginStage::Ready: + return BeginProgress::Ready; + case BeginStage::Failed: + return BeginProgress::Failed; + } + if (!ok) { + _begin_stage = BeginStage::Failed; + return BeginProgress::Failed; + } + return BeginProgress::Pending; + } +#endif bool readyToPublish() const { return !_finished && _commit_stage == CommitStage::Publish; } @@ -92,6 +169,11 @@ public: // service its transports between passes without exposing the rename gap. // A synchronous durability operation uses commit() to drain the same steps. CommitProgress serviceCommit(bool valid = true) { +#if defined(ESP32_PLATFORM) + // A premature commit must not publish, clean up, or implicitly aggregate + // the deferred setup operations inside this call. + if (_begin_stage != BeginStage::Ready) return CommitProgress::Failed; +#endif if (_finished) return CommitProgress::Failed; bool ok = _ok && valid; if (ok && _commit_stage == CommitStage::Flush) { diff --git a/test/fixtures/contact_cache/FakeFilesystem.h b/test/fixtures/contact_cache/FakeFilesystem.h index a17385d5..d5508ef6 100644 --- a/test/fixtures/contact_cache/FakeFilesystem.h +++ b/test/fixtures/contact_cache/FakeFilesystem.h @@ -57,6 +57,8 @@ public: unsigned buffer_config_calls = 0, fail_buffer_config = 0; std::string fail_read; bool metadata_error = false, fail_create = false, fail_remove = false; + bool partial_create_failure = false; + unsigned metadata_probes = 0, fail_metadata_probe = 0; unsigned removes = 0, missing_remove_logs = 0; unsigned fail_rename = 0, renames = 0, writes = 0, reads = 0, opens = 0; std::vector rename_snapshots; @@ -73,13 +75,19 @@ public: File open(const char* path, const char* mode = "r", bool = false) { ++opens; if (*mode != 'r') { - if (fail_create) return File(); + if (fail_create) { + if (partial_create_failure) files[path].clear(); + return File(); + } files[path].clear(); return File(this, path, true); } if (fail_read == path || !exists(path)) return File(); return File(this, path, false); } + unsigned startupOperations() const { + return metadata_probes + opens + removes + renames + buffer_config_calls; + } bool rename(const char* from, const char* to) { ++renames; if (renames == fail_rename || !exists(from)) return false; diff --git a/test/fixtures/contact_cache/test_contact_cache.cpp b/test/fixtures/contact_cache/test_contact_cache.cpp index ef993c93..c346c875 100644 --- a/test/fixtures/contact_cache/test_contact_cache.cpp +++ b/test/fixtures/contact_cache/test_contact_cache.cpp @@ -103,6 +103,13 @@ public: bool releaseContact(const ContactInfo&); bool hasPendingContactWrites() const; bool serviceContactWrites(DataStoreHost*, bool (*filter)(const ContactInfo&)); + bool starting() const { + return _contact_write != nullptr && !static_cast(*_contact_write); + } + bool activeJob() const { return _contact_write != nullptr; } + bool readyToPublish() const { + return _contact_write && _contact_write->readyToPublish(); + } #endif #if defined(NRF52_PLATFORM) bool loadContactPages(DataStoreHost*, uint16_t, uint32_t); @@ -460,11 +467,13 @@ static void cooperative_save_services_commands_and_preserves_snapshots() { } static void cooperative_mutations_never_publish_obsolete_indices() { - for (unsigned mutation_step : {4u, 46u, 110u, 140u}) { + for (unsigned mutation_step : {4u, 46u, 110u, 0u}) { Fixture f(40); const auto old = SPIFFS.files.at("/contacts3"); assert(f.store.markContactDirty(f.host.contacts[0])); - for (unsigned step = 0; step < mutation_step; ++step) { + unsigned step = 0; + while (mutation_step == 0 ? !f.store.readyToPublish() : step < mutation_step) { + assert(++step < 1000); assert(f.store.serviceContactWrites(&f.host, cachedContactFilter)); assert(SPIFFS.files.at("/contacts3") == old); } @@ -507,6 +516,7 @@ static void cooperative_failures_keep_the_old_file_and_pending_mutations() { failed = !f.store.serviceContactWrites(&f.host, cachedContactFilter); } assert(failed && f.store.hasPendingContactWrites()); + assert(!f.store.activeJob()); assert(SPIFFS.files.at("/contacts3") == old); assert(!SPIFFS.exists("/contacts3.tmp")); f.check(0, route(993)); @@ -555,6 +565,31 @@ static void interleaved_path_pressure_preserves_guarded_eviction() { f.check(i, route(i < 26 ? 1200 + i : i)); } +static void pending_startup_pressure_preserves_guarded_eviction() { + for (unsigned completed : {0u, 3u, 6u}) { + Fixture f(40); + for (unsigned i = 0; i < 16; ++i) { + assert(f.host.contacts[i].setRawPath(route(1600 + i).data())); + assert(f.store.markContactDirty(f.host.contacts[i])); + } + assert(f.host.cache_flushes == 0); + SPIFFS.files["/contacts3.tmp"] = {8, 9}; + assert(f.store.serviceContactWrites(&f.host, cachedContactFilter)); // allocate + for (unsigned pass = 0; pass < completed; ++pass) + assert(f.store.serviceContactWrites(&f.host, cachedContactFilter)); + assert(f.store.starting()); + // The seventeenth resident update must cancel a pending begin, release + // _committing, then run the existing guarded synchronous eviction flush. + assert(f.host.contacts[16].setRawPath(route(1616).data())); + assert(f.host.cache_flushes == 1); + assert(!f.store.activeJob()); + assert(f.store.markContactDirty(f.host.contacts[16])); + drain_cooperative_save(f); + for (unsigned i = 0; i < 40; ++i) + f.check(i, route(i < 17 ? 1600 + i : i)); + } +} + static void lifecycle_cancels_jobs_and_closes_prior_filesystem_readers() { { Fixture f(30); @@ -755,6 +790,7 @@ int main() { cooperative_failures_keep_the_old_file_and_pending_mutations(); synchronous_flush_cancels_pending_jobs_and_stays_durable(); interleaved_path_pressure_preserves_guarded_eviction(); + pending_startup_pressure_preserves_guarded_eviction(); lifecycle_cancels_jobs_and_closes_prior_filesystem_readers(); routes_survive_eviction_and_full_sync(); snapshot_rollback_and_dirty_eviction(); diff --git a/test/fixtures/contact_persistence/test_esp32_uncached.cpp b/test/fixtures/contact_persistence/test_esp32_uncached.cpp index a5e40664..42c55eb1 100644 --- a/test/fixtures/contact_persistence/test_esp32_uncached.cpp +++ b/test/fixtures/contact_persistence/test_esp32_uncached.cpp @@ -12,7 +12,10 @@ static_assert(MESH_CONTACT_CACHE == 0, // Replace only the SDK metadata syscall; the production presence/error logic // and transaction implementation are compiled unchanged. int fixtureStat(FakeFilesystem* fs, const char* vfs_path, struct stat*) { - if (fs->metadata_error) { errno = EIO; return -1; } + ++fs->metadata_probes; + if (fs->metadata_error || fs->metadata_probes == fs->fail_metadata_probe) { + errno = EIO; return -1; + } assert(strncmp(vfs_path, "/spiffs", 7) == 0); if (!fs->exists(vfs_path + 7)) { errno = ENOENT; return -1; } return 0; @@ -63,6 +66,10 @@ public: bool hasPendingContactWrites() const; bool deleteBlobByKey(const uint8_t[], int); bool verifying() const { return _contact_write_verifying; } + bool starting() const { + return _contact_write == nullptr || !static_cast(*_contact_write); + } + bool activeJob() const { return _contact_write != nullptr; } bool readyToPublish() const { return _contact_write && _contact_write->readyToPublish(); } @@ -116,8 +123,12 @@ unsigned drain(DataStore& store, Host& h, Filter filter = nullptr) { while (store.hasPendingContactWrites()) { const unsigned writes = SPIFFS.writes, reads = SPIFFS.reads; const unsigned backend_writes = SPIFFS.backend_writes, backend_reads = SPIFFS.backend_reads; + const bool starting = store.starting(); + const unsigned startup_before = SPIFFS.startupOperations(); assert(++passes < 5000); assert(store.serviceContactWrites(&h, filter)); + const unsigned startup_operations = SPIFFS.startupOperations() - startup_before; + if (starting) assert(startup_operations <= 1); assert(SPIFFS.writes - writes <= 1); assert(SPIFFS.reads - reads <= 1); assert(SPIFFS.backend_writes - backend_writes <= 1); @@ -163,6 +174,152 @@ void boundedFullTableAndFirstSave() { bootCheck(h, keepContact); } +void deferredBeginCancellationAndCrashRecovery() { + using Writer = mesh::ContactFileTransaction; + // Observe/destruct at every startup boundary, including before any recovery + // and after open but before setBufferSize. A stale temp is never authoritative. + for (unsigned completed = 0; completed <= 7; ++completed) { + SPIFFS = FakeFilesystem(); + SPIFFS.emulate_stdio = true; + const Host h = table(1); + const auto old = image(h); + const std::vector stale = {9, 8, 7}; + SPIFFS.files["/contacts3"] = old; + SPIFFS.files["/contacts3.tmp"] = stale; + unsigned before_destructor = 0; + { + Writer writer(&SPIFFS, "/contacts3", companionPathPresence, true); + assert(SPIFFS.startupOperations() == 0 && !writer); + for (unsigned pass = 0; pass < completed; ++pass) { + const unsigned before = SPIFFS.startupOperations(); + const auto progress = writer.serviceBegin(); + assert(progress != Writer::BeginProgress::Failed); + const unsigned startup_operations = SPIFFS.startupOperations() - before; + assert(startup_operations <= 1); + assert(SPIFFS.files.at("/contacts3") == old); + // A physical reset at this boundary sees the prior complete image. + FakeFilesystem rebooted; + rebooted.files = SPIFFS.files; + assert(Writer::recover(&rebooted, "/contacts3", companionPathPresence)); + assert(rebooted.files.at("/contacts3") == old); + } + if (completed < 7) { + const unsigned before = SPIFFS.startupOperations(); + assert(!writer); + assert(writer.write(old.data(), old.size()) == 0); + assert(writer.serviceCommit() == Writer::CommitProgress::Failed); + assert(!writer.commit()); + assert(SPIFFS.startupOperations() == before); // no implicit startup/cleanup + } else { + assert(writer); + } + before_destructor = SPIFFS.startupOperations(); + } + if (completed < 6) { + assert(SPIFFS.startupOperations() == before_destructor); // never owned a temp + } + if (completed < 5) assert(SPIFFS.files.at("/contacts3.tmp") == stale); + else assert(!SPIFFS.exists("/contacts3.tmp")); + assert(SPIFFS.files.at("/contacts3") == old && SPIFFS.missing_remove_logs == 0); + bootCheck(h); + } + // An interrupted prior publication recovers identically with deferred setup. + SPIFFS = FakeFilesystem(); + const Host h = table(1); + const auto old = image(h); + SPIFFS.files["/contacts3.bak"] = old; + { + Writer writer(&SPIFFS, "/contacts3", companionPathPresence, true); + for (unsigned pass = 0; pass < 7; ++pass) { + assert(writer.serviceBegin() != Writer::BeginProgress::Failed); + FakeFilesystem rebooted; + rebooted.files = SPIFFS.files; + assert(Writer::recover(&rebooted, "/contacts3", companionPathPresence)); + assert(rebooted.files.at("/contacts3") == old); + } + assert(writer && SPIFFS.files.at("/contacts3") == old); + } + bootCheck(h); +} + +void deferredBeginFaultsAndSynchronousDefaults() { + using Writer = mesh::ContactFileTransaction; + for (unsigned fault = 0; fault < 8; ++fault) { + SPIFFS = FakeFilesystem(); + SPIFFS.emulate_stdio = true; + const Host h = table(1); + const auto old = image(h); + const std::vector stale = {1, 2, 3}; + SPIFFS.files["/contacts3"] = old; + SPIFFS.files["/contacts3.tmp"] = stale; + if (fault == 0) SPIFFS.fail_metadata_probe = 1; // target + if (fault == 1) SPIFFS.fail_metadata_probe = 2; // backup + if (fault == 2) { + SPIFFS.files.erase("/contacts3"); + SPIFFS.files["/contacts3.bak"] = old; + SPIFFS.fail_rename = 1; // failed interrupted-publication recovery + } + if (fault == 3) SPIFFS.fail_metadata_probe = 3; // stale temp + if (fault == 4) SPIFFS.fail_remove = true; + if (fault == 5 || fault == 6) SPIFFS.fail_create = true; + if (fault == 6) SPIFFS.partial_create_failure = true; + if (fault == 7) SPIFFS.fail_buffer_config = 1; + { + Writer writer(&SPIFFS, "/contacts3", companionPathPresence, true); + auto progress = Writer::BeginProgress::Pending; + for (unsigned pass = 0; pass < 8 && progress == Writer::BeginProgress::Pending; ++pass) + progress = writer.serviceBegin(); + assert(progress == Writer::BeginProgress::Failed && !writer); + assert(writer.write(old.data(), old.size()) == 0 && !writer.commit()); + } + if (fault <= 4) assert(SPIFFS.files.at("/contacts3.tmp") == stale); + else assert(!SPIFFS.exists("/contacts3.tmp")); // includes a failed partial create + if (fault == 2) assert(SPIFFS.files.at("/contacts3.bak") == old); + else assert(SPIFFS.files.at("/contacts3") == old); + assert(SPIFFS.missing_remove_logs == 0); + SPIFFS.fail_metadata_probe = SPIFFS.fail_rename = SPIFFS.fail_buffer_config = 0; + SPIFFS.fail_create = SPIFFS.fail_remove = false; + // All pre-existing callers still finish initialization inside construction. + Writer synchronous(&SPIFFS, "/contacts3", companionPathPresence); + assert(synchronous && SPIFFS.files.at("/contacts3") == old); + assert(synchronous.write(old.data(), old.size()) == old.size()); + assert(synchronous.commit()); + assert(SPIFFS.files.at("/contacts3") == old); + bootCheck(h); + } +} + +void mutationDuringEveryBeginStage() { + for (unsigned completed = 0; completed <= 7; ++completed) { + SPIFFS = FakeFilesystem(); + SPIFFS.emulate_stdio = true; + Host h = table(); + const auto old = image(h); + const std::vector stale = {4, 5, 6}; + SPIFFS.files["/contacts3"] = old; + SPIFFS.files["/contacts3.tmp"] = stale; + DataStore store(SPIFFS); + assert(store.markContactDirty(h.contacts[0])); + assert(store.serviceContactWrites(&h, nullptr)); // allocate only + assert(SPIFFS.startupOperations() == 0); + for (unsigned pass = 0; pass < completed; ++pass) + assert(store.serviceContactWrites(&h, nullptr)); + const unsigned opens = SPIFFS.opens, configurations = SPIFFS.buffer_config_calls; + const unsigned before_cancel = SPIFFS.startupOperations(); + h.contacts[2] = contact(909); + assert(store.markContactDirty(h.contacts[2])); + assert(store.serviceContactWrites(&h, nullptr)); // guard precedes every begin step + assert(store.hasPendingContactWrites()); + assert(SPIFFS.opens == opens && SPIFFS.buffer_config_calls == configurations); + if (completed < 6) assert(SPIFFS.startupOperations() == before_cancel); + if (completed < 5) assert(SPIFFS.files.at("/contacts3.tmp") == stale); + else assert(!SPIFFS.exists("/contacts3.tmp")); + assert(SPIFFS.files.at("/contacts3") == old); + drain(store, h); + bootCheck(h); + } +} + void mutationRestart(unsigned phase) { SPIFFS = FakeFilesystem(); SPIFFS.emulate_stdio = true; @@ -172,7 +329,7 @@ void mutationRestart(unsigned phase) { DataStore store(SPIFFS); assert(store.markContactDirty(h.contacts[0])); unsigned passes = 0; - while ((phase == 0 && passes < 5) + while ((phase == 0 && SPIFFS.writes < 5) || (phase == 1 && (!store.verifying() || SPIFFS.reads == 0)) || (phase == 2 && !store.readyToPublish())) { assert(++passes < 1000); @@ -222,6 +379,7 @@ void faultAndRetry(unsigned fault) { success = store.serviceContactWrites(&h, nullptr); } assert(injected && !success); + assert(!store.activeJob()); // no Ready/_ok=false job can spin in initialization assert(store.hasPendingContactWrites()); assert(SPIFFS.files.at("/contacts3") == old); assert(!SPIFFS.exists("/contacts3.tmp")); @@ -359,6 +517,9 @@ void expectedAbsenceAndRealDeleteFailures() { int main() { boundedFullTableAndFirstSave(); + deferredBeginCancellationAndCrashRecovery(); + deferredBeginFaultsAndSynchronousDefaults(); + mutationDuringEveryBeginStage(); for (unsigned phase = 0; phase < 3; ++phase) mutationRestart(phase); for (unsigned fault = 0; fault < 10; ++fault) faultAndRetry(fault); syncFlushAndFilterChange(); diff --git a/test/test_esp32_cooperative_contacts.py b/test/test_esp32_cooperative_contacts.py index 6acb7bdb..81890adf 100644 --- a/test/test_esp32_cooperative_contacts.py +++ b/test/test_esp32_cooperative_contacts.py @@ -22,6 +22,9 @@ class UncachedESPContactsTest(unittest.TestCase): def test_default_stdio_reader_readahead_is_detected(self): self.run_fixture(["-DBOARD_HAS_PSRAM=1"], disable="reader") + def test_synchronous_startup_batch_is_detected(self): + self.run_fixture(["-DBOARD_HAS_PSRAM=1"], disable="begin") + def run_fixture(self, policy, disable=None): with tempfile.TemporaryDirectory(prefix="mesh-esp-inline-contacts-") as directory: temp = Path(directory) @@ -47,6 +50,11 @@ class UncachedESPContactsTest(unittest.TestCase): "inline void makeBlobPath(", ) implementation = "\n".join(method(source, signature) for signature in signatures) + if disable == "begin": + self.assertIn('"/contacts3", companionPathPresence, true)', implementation) + implementation = implementation.replace( + '"/contacts3", companionPathPresence, true)', + '"/contacts3", companionPathPresence, false)') # Only the native SDK stat seam is substituted. The production # presence logic still distinguishes ENOENT from metadata failure. presence = method(source, "static bool companionPathPresence(") @@ -64,10 +72,10 @@ class UncachedESPContactsTest(unittest.TestCase): (temp / "store_under_test.h").write_text(implementation) transaction = (ROOT / "src/helpers/ContactFileTransaction.h").read_text() if disable == "writer": - self.assertIn("_ok = _ok && _file.setBufferSize(storage::CONTACT_RECORD_SIZE);", transaction) + self.assertIn("ok = _file.setBufferSize(storage::CONTACT_RECORD_SIZE);", transaction) transaction = transaction.replace( - "_ok = _ok && _file.setBufferSize(storage::CONTACT_RECORD_SIZE);", - "// Negative control: retain the SDK's default writer buffer.") + "ok = _file.setBufferSize(storage::CONTACT_RECORD_SIZE);", + "ok = true; // Negative control: retain default writer buffer.") elif disable == "reader": self.assertIn("ok = ok && _verify.setBufferSize(64);", transaction) transaction = transaction.replace("ok = ok && _verify.setBufferSize(64);", @@ -105,8 +113,9 @@ class UncachedESPContactsTest(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) else: self.assertNotEqual(result.returncode, 0) - self.assertIn("largest_backend_" + ("write" if disable == "writer" else "read"), - result.stderr) + marker = "startup_operations" if disable == "begin" else ( + "largest_backend_" + ("write" if disable == "writer" else "read")) + self.assertIn(marker, result.stderr) if __name__ == "__main__":