Keep ESP contact saves responsive on PSRAM boards

Extend the cooperative contact transaction to uncached ESP32 storage,
including the PSRAM policy used by the XIAO and V4. Preserve owned contact
copies, the 152-byte disk format, revision cancellation and synchronous
durability operations. Keep contact capacities unchanged.

Avoid SDK error logs when optional blob or transaction cleanup files are
absent, while preserving metadata and deletion failure handling.

Validate cached ESP/nRF and uncached PSRAM paths under ASan/UBSan,
including bounded writes, mutation cancellation, faults, recovery and
reboot readback. Update the existing uncached production fixture.
This commit is contained in:
mikecarper committed 2026-10-04 04:01:40 -07:00
1 parent 49a02f59d7
commit acfcfb6008
12 files changed
+713 -175

No files matched your search

+70 -18
View File
@@ -55,7 +55,7 @@ DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra
}
DataStore::~DataStore() {
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
#if defined(ESP32_PLATFORM)
cancelContactWrite();
#endif
}
@@ -144,12 +144,14 @@ static bool contactPathPresence(FILESYSTEM* fs, const char* path,
#endif
void DataStore::begin() {
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
#if defined(ESP32_PLATFORM)
// Reinitialization must not retain a streaming transaction or an inode
// opened through an earlier filesystem route. Dirty mutations stay pending.
cancelContactWrite();
#if MESH_CONTACT_CACHE
_contact_path_reader.close();
#endif
#endif
#if defined(RP2040_PLATFORM)
identity_store.begin();
#endif
@@ -561,9 +563,11 @@ void DataStore::useVolatilePrimaryFS(FILESYSTEM& fs) {
#endif
void DataStore::disableSecondaryFS(bool authority_unknown) {
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
#if defined(ESP32_PLATFORM)
cancelContactWrite();
#if MESH_CONTACT_CACHE
_contact_path_reader.close();
#endif
#endif
_fsExtra = nullptr;
#if defined(NRF52_PLATFORM)
@@ -724,10 +728,12 @@ bool DataStore::removeFile(FILESYSTEM* fs, const char* filename) {
}
bool DataStore::formatFileSystem() {
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
#if defined(ESP32_PLATFORM)
cancelContactWrite();
#if MESH_CONTACT_CACHE
_contact_path_reader.close();
#endif
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#if defined(NRF52_PLATFORM)
resetContactPageState();
@@ -795,9 +801,9 @@ bool DataStore::formatFileSystem() {
_channel_load_incomplete = false;
_prefs_recovery_source = nullptr;
_channel_recovery_source = nullptr;
_contact_write_requested = false;
#if MESH_CONTACT_CACHE
_cache_load_incomplete = false;
_contact_write_requested = false;
#else
_uncached_contact_load_incomplete = false;
#endif
@@ -1919,7 +1925,7 @@ bool DataStore::writeContactPage(DataStoreHost* host, uint8_t page,
#endif
void DataStore::loadContacts(DataStoreHost* host) {
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
#if defined(ESP32_PLATFORM)
cancelContactWrite();
#endif
#if !defined(NRF52_PLATFORM) && !MESH_CONTACT_CACHE
@@ -2229,7 +2235,7 @@ void DataStore::loadContacts(DataStoreHost* host) {
}
bool DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c)) {
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
#if defined(ESP32_PLATFORM)
cancelContactWrite();
#endif
if (hasIncompleteContactLoad()) return false;
@@ -2242,10 +2248,11 @@ bool DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactIn
success = markContactDirty(*contact) && success;
}
return flushContactWrites(host, filter) && success;
#elif MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
#elif defined(ESP32_PLATFORM)
// Explicit save/flush callers require a fully durable current table. An
// unfinished lazy transaction may contain an earlier revision: discard it
// before synchronously writing the current contacts through the same format.
#if MESH_CONTACT_CACHE
auto& paths = mesh::contactPathStorage();
paths.beginCommit();
for (uint32_t i = 0;; ++i) {
@@ -2258,6 +2265,7 @@ bool DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactIn
_contact_write_requested = true;
return false;
}
#endif
// The cold-path reader reuses one SPIFFS File during this streaming write.
// Close it before replacing the original name so later reads reopen the
// committed file. No complete contact-table copy is needed in RAM.
@@ -2266,14 +2274,23 @@ bool DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactIn
bool success = writer;
uint8_t record[mesh::storage::CONTACT_RECORD_SIZE];
for (uint32_t i = 0; success; ++i) {
#if MESH_CONTACT_CACHE
auto* c = host->getContactForStore(i);
if (!c) break;
#else
ContactInfo snapshot;
if (!host->getContactForSave(i, snapshot)) break;
const auto* c = &snapshot;
#endif
if (filter && !filter(*c)) continue;
success = serializeContactRecord(*c, record)
&& writer.write(record, sizeof(record)) == sizeof(record);
}
#if MESH_CONTACT_CACHE
_contact_path_reader.close();
#endif
success = writer.commit(success);
#if MESH_CONTACT_CACHE
if (success) {
uint16_t index = 0;
for (uint32_t i = 0;; ++i) {
@@ -2284,6 +2301,7 @@ bool DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactIn
}
}
paths.endCommit(success);
#endif
_contact_write_requested = !success;
return success;
#else
@@ -2340,7 +2358,7 @@ bool DataStore::markContactDirty(const ContactInfo& contact) {
return _dirty_contact_pages.mark(slot / mesh::storage::CONTACTS_PER_PAGE);
#else
(void)contact;
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
#if defined(ESP32_PLATFORM)
++_contact_write_revision;
_contact_write_requested = true;
#endif
@@ -2357,7 +2375,7 @@ bool DataStore::releaseContact(const ContactInfo& contact) {
return _dirty_contact_pages.mark(slot / mesh::storage::CONTACTS_PER_PAGE);
#else
(void)contact;
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
#if defined(ESP32_PLATFORM)
++_contact_write_revision;
_contact_write_requested = true;
#endif
@@ -2428,17 +2446,20 @@ bool DataStore::truncateLegacyContacts(uint16_t remaining_contacts) {
}
#endif
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
#if defined(ESP32_PLATFORM)
void DataStore::cancelContactWrite() {
if (_contact_write == nullptr) return;
delete _contact_write;
_contact_write = nullptr;
#if MESH_CONTACT_CACHE
_contact_path_reader.close();
mesh::contactPathStorage().endCommit(false);
#endif
_contact_write_host = nullptr;
_contact_write_filter = nullptr;
}
#if MESH_CONTACT_CACHE
bool DataStore::cancelCooperativeWrite() {
// Cache pressure inside serialization/snapshot preservation must keep the
// reentrancy veto. Only an app/radio path update between service passes can
@@ -2447,9 +2468,10 @@ bool DataStore::cancelCooperativeWrite() {
cancelContactWrite();
return true;
}
#endif
bool DataStore::serviceCachedContactWrite(DataStoreHost* host,
bool (*filter)(const ContactInfo&)) {
bool DataStore::serviceContactWrite(DataStoreHost* host,
bool (*filter)(const ContactInfo&)) {
struct ServiceGuard {
bool& active;
explicit ServiceGuard(bool& value) : active(value) { active = true; }
@@ -2469,8 +2491,11 @@ bool DataStore::serviceCachedContactWrite(DataStoreHost* host,
cancelContactWrite();
return true;
}
#if MESH_CONTACT_CACHE
auto& paths = mesh::contactPathStorage();
#endif
if (_contact_write == nullptr) {
#if MESH_CONTACT_CACHE
paths.beginCommit();
for (uint32_t i = 0;; ++i) {
auto* contact = host->getContactForStore(i);
@@ -2481,12 +2506,15 @@ bool DataStore::serviceCachedContactWrite(DataStoreHost* host,
paths.endCommit(false);
return false;
}
#endif
_contact_write = new (std::nothrow) mesh::ContactFileTransaction(
_getContactsChannelsFS(), "/contacts3", companionPathPresence);
if (_contact_write == nullptr || !static_cast<bool>(*_contact_write)) {
delete _contact_write;
_contact_write = nullptr;
#if MESH_CONTACT_CACHE
paths.endCommit(false);
#endif
return false;
}
_contact_write_host = host;
@@ -2497,10 +2525,19 @@ bool DataStore::serviceCachedContactWrite(DataStoreHost* host,
return true;
}
if (!_contact_write_verifying) {
#if MESH_CONTACT_CACHE
ContactInfo* contact;
do {
contact = host->getContactForStore(_contact_write_index++);
} while (contact != nullptr && filter && !filter(*contact));
#else
ContactInfo snapshot;
bool available;
do {
available = host->getContactForSave(_contact_write_index++, snapshot);
} while (available && filter && !filter(snapshot));
const ContactInfo* contact = available ? &snapshot : nullptr;
#endif
if (contact != nullptr) {
uint8_t record[mesh::storage::CONTACT_RECORD_SIZE];
if (serializeContactRecord(*contact, record)
@@ -2510,10 +2547,13 @@ bool DataStore::serviceCachedContactWrite(DataStoreHost* host,
cancelContactWrite();
return false;
}
#if MESH_CONTACT_CACHE
_contact_path_reader.close();
#endif
_contact_write_verifying = true;
return true;
}
#if MESH_CONTACT_CACHE
if (_contact_write->readyToPublish()
&& !paths.preserveSnapshots(0, 0x8000)) {
cancelContactWrite();
@@ -2524,6 +2564,7 @@ bool DataStore::serviceCachedContactWrite(DataStoreHost* host,
// inode during verification. Close it at the actual publication boundary.
_contact_path_reader.close();
}
#endif
const auto progress = _contact_write->serviceCommit();
if (progress == mesh::ContactFileTransaction::CommitProgress::Pending) {
return true;
@@ -2532,6 +2573,7 @@ bool DataStore::serviceCachedContactWrite(DataStoreHost* host,
cancelContactWrite();
return false;
}
#if MESH_CONTACT_CACHE
// serviceCommit's final name replacement and this handle publication are
// one non-yielding section. Every later cold read sees the corresponding
// committed file and source indices, including retained ContactInfo copies.
@@ -2542,6 +2584,7 @@ bool DataStore::serviceCachedContactWrite(DataStoreHost* host,
if (!filter || filter(*contact)) paths.publish(contact->path_ref.handle(), index++);
}
paths.endCommit(true);
#endif
delete _contact_write;
_contact_write = nullptr;
_contact_write_host = nullptr;
@@ -2604,8 +2647,8 @@ bool DataStore::serviceContactWrites(DataStoreHost* host,
if (!writeContactPage(host, (uint8_t)page, filter)) return false;
_dirty_contact_pages.clear((uint8_t)page);
return true;
#elif MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
return serviceCachedContactWrite(host, filter);
#elif defined(ESP32_PLATFORM)
return serviceContactWrite(host, filter);
#else
return saveContacts(host, filter);
#endif
@@ -2613,7 +2656,7 @@ bool DataStore::serviceContactWrites(DataStoreHost* host,
bool DataStore::flushContactWrites(DataStoreHost* host,
bool (*filter)(const ContactInfo& c)) {
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
#if defined(ESP32_PLATFORM)
cancelContactWrite();
#endif
if (hasIncompleteContactLoad()) return false;
@@ -2631,7 +2674,7 @@ bool DataStore::hasPendingContactWrites() const {
#if defined(NRF52_PLATFORM)
return !hasIncompleteContactLoad()
&& (!_dirty_contact_pages.empty() || _legacy_contacts_pending_cleanup);
#elif MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
#elif defined(ESP32_PLATFORM)
return _contact_write_requested || _contact_write != nullptr;
#else
return false;
@@ -2906,7 +2949,7 @@ void DataStore::checkAdvBlobFile() {
}
bool DataStore::migrateToSecondaryFS() {
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
#if defined(ESP32_PLATFORM)
cancelContactWrite();
#endif
if (_fsExtra == nullptr) return false;
@@ -3741,9 +3784,18 @@ bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) {
char path[64];
makeBlobPath(key, key_len, path, sizeof(path));
#if defined(ESP32_PLATFORM)
// VFS remove() logs an error for normal absence, contaminating the binary
// USB stream. Distinguish absence from metadata failure before calling it;
// a real deletion failure must preserve the live contact for rollback.
bool present = false;
if (!companionPathPresence(_fs, path, present)) return false;
return !present || _fs->remove(path);
#else
_fs->remove(path);
return true; // return true even if file did not exist
#endif
}
#endif
+13 -13
View File
@@ -9,7 +9,7 @@
#include <helpers/ContactSecretCache.h>
#endif
#include <helpers/CompanionReaderConfig.h>
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
#if defined(ESP32_PLATFORM)
namespace mesh { class ContactFileTransaction; }
#endif
#if COMPANION_FEATURE_READER
@@ -47,6 +47,18 @@ class DataStore
#if defined(ESP32_PLATFORM)
const char* _prefs_recovery_source = nullptr;
const char* _channel_recovery_source = nullptr;
mesh::ContactFileTransaction* _contact_write = nullptr;
DataStoreHost* _contact_write_host = nullptr;
bool (*_contact_write_filter)(const ContactInfo&) = nullptr;
uint32_t _contact_write_revision = 0;
uint32_t _contact_write_active_revision = 0;
uint32_t _contact_write_index = 0;
bool _contact_write_verifying = false;
bool _contact_write_requested = false;
bool _contact_write_servicing = false;
void cancelContactWrite();
bool serviceContactWrite(DataStoreHost* host,
bool (*filter)(const ContactInfo&));
#endif
#if !defined(NRF52_PLATFORM)
bool _channel_load_incomplete = false;
@@ -59,18 +71,6 @@ class DataStore
bool _cache_load_incomplete = false;
#if defined(ESP32_PLATFORM)
File _contact_path_reader;
mesh::ContactFileTransaction* _contact_write = nullptr;
DataStoreHost* _contact_write_host = nullptr;
bool (*_contact_write_filter)(const ContactInfo&) = nullptr;
uint32_t _contact_write_revision = 0;
uint32_t _contact_write_active_revision = 0;
uint32_t _contact_write_index = 0;
bool _contact_write_verifying = false;
bool _contact_write_requested = false;
bool _contact_write_servicing = false;
void cancelContactWrite();
bool serviceCachedContactWrite(DataStoreHost* host,
bool (*filter)(const ContactInfo&));
#endif
bool readStoredPath(uint16_t source, uint8_t path[64]) override;
bool flushCachedPaths() override;
+2 -2
View File
@@ -225,8 +225,8 @@ static constexpr uint8_t DEFAULT_FEM_RX_GAIN = 1;
#define DIRECT_SEND_PERHOP_FACTOR 6.0f
#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250
#define LAZY_CONTACTS_WRITE_DELAY 5000
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
// A cached ESP save streams one record/CRC chunk per pass. Keep the full
#if defined(ESP32_PLATFORM)
// An ESP save streams one record/CRC chunk per pass. Keep the full
// mesh loop responsive between steps without adding seconds of page delay.
#define CONTACT_PAGE_WRITE_GAP 1
#else
+11 -4
View File
@@ -34,6 +34,11 @@ private:
present = fs->exists(path);
return true;
}
static bool removeIfPresent(FILESYSTEM* fs, const char* path,
PresenceProbe presence) {
bool present = false;
return probe(fs, path, present, presence) && (!present || fs->remove(path));
}
public:
enum class CommitProgress : uint8_t { Pending, Succeeded, Failed };
static bool recover(FILESYSTEM* fs, const char* target,
@@ -52,7 +57,7 @@ public:
snprintf(_temp, sizeof(_temp), "%s.tmp", target);
snprintf(_backup, sizeof(_backup), "%s.bak", target);
if (!recover(fs, target, presence)) return;
if (fs->exists(_temp) && !fs->remove(_temp)) return;
if (!removeIfPresent(fs, _temp, presence)) return;
#if defined(RP2040_PLATFORM)
_file = fs->open(_temp, "w");
#else
@@ -63,7 +68,7 @@ public:
~ContactFileTransaction() {
if (_file) _file.close();
if (_verify) _verify.close();
if (!_finished) _fs->remove(_temp);
if (!_finished) removeIfPresent(_fs, _temp, _presence);
}
operator bool() const { return _ok; }
bool readyToPublish() const {
@@ -132,8 +137,10 @@ public:
}
if (ok) ok = _fs->rename(_temp, _target);
if (!ok && backed_up) _fs->rename(_backup, _target);
if (ok) _fs->remove(_backup);
else _fs->remove(_temp);
// Cleanup remains best effort after the publish/rollback result, but an
// absent file must not trigger the SDK's error log on the shared USB port.
if (ok) removeIfPresent(_fs, _backup, _presence);
else removeIfPresent(_fs, _temp, _presence);
_finished = true;
return ok ? CommitProgress::Succeeded : CommitProgress::Failed;
}
+11
View File
@@ -160,6 +160,9 @@ class DataStore {
bool _identity_creation_blocked = false;
bool _channel_load_incomplete = false;
bool _uncached_contact_load_incomplete = false;
#if defined(ESP32_PLATFORM)
#include "contact_write_state_under_test.h"
#endif
struct IdentityAdapter {
bool recover(const char*) { return true; }
bool load(const char*, mesh::LocalIdentity&) {
@@ -173,6 +176,14 @@ class DataStore {
}
} identity_store;
public:
#if defined(ESP32_PLATFORM)
~DataStore();
bool markContactDirty(const ContactInfo&);
bool releaseContact(const ContactInfo&);
bool serviceContactWrites(DataStoreHost*, bool (*filter)(const ContactInfo&) = nullptr);
bool flushContactWrites(DataStoreHost*, bool (*filter)(const ContactInfo&) = nullptr);
bool hasPendingContactWrites() const;
#endif
FakeFilesystem* _getContactsChannelsFS() { return _fs; }
File openRead(FakeFilesystem* fs, const char* path) { return fs->open(path); }
bool loadMainIdentity(mesh::LocalIdentity&);
+144
View File
@@ -0,0 +1,144 @@
#pragma once
#include <algorithm>
#include <array>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <limits>
#include <map>
#include <string>
#include <vector>
#include <Arduino.h>
enum { FILE_O_READ = 0, FILE_O_WRITE = 1 };
class FakeFilesystem;
class File {
FakeFilesystem* _fs = nullptr;
std::string _path;
size_t _position = 0;
bool _write = false;
unsigned _read_generation = 0;
std::vector<uint8_t> _read_inode;
public:
File() = default;
explicit File(FakeFilesystem& fs) : _fs(&fs) {}
File(FakeFilesystem* fs, const char* path, bool write);
bool open(const char* path, uint8_t mode);
explicit operator bool() const { return _fs != nullptr; }
size_t read(uint8_t* bytes, size_t length);
size_t write(const uint8_t* bytes, size_t length);
size_t size() const;
bool seek(size_t position);
void flush() {}
void close() { _fs = nullptr; }
};
class FakeFilesystem {
struct Config { uint32_t block_count = 256, block_size = 4096; } _config;
struct Lfs { const Config* cfg; } _lfs{&_config};
public:
using Files = std::map<std::string, std::vector<uint8_t>>;
Files files;
size_t capacity = 1024 * 1024;
size_t max_write = std::numeric_limits<size_t>::max();
size_t largest_read = 0, largest_write = 0;
std::string fail_read;
bool metadata_error = false, fail_create = false, fail_remove = false;
unsigned removes = 0, missing_remove_logs = 0;
unsigned fail_rename = 0, renames = 0, writes = 0, reads = 0, opens = 0;
std::vector<Files> rename_snapshots;
std::map<std::string, unsigned> generations;
bool exists(const char* path) const { return files.count(path) != 0; }
bool mkdir(const char*) { return true; }
bool remove(const char* path) {
++removes;
if (!exists(path)) { ++missing_remove_logs; return false; }
if (fail_remove) return false;
++generations[path];
return files.erase(path) != 0;
}
File open(const char* path, const char* mode = "r", bool = false) {
++opens;
if (*mode != 'r') {
if (fail_create) return File();
files[path].clear();
return File(this, path, true);
}
if (fail_read == path || !exists(path)) return File();
return File(this, path, false);
}
bool rename(const char* from, const char* to) {
++renames;
if (renames == fail_rename || !exists(from)) return false;
#if defined(ESP32_PLATFORM)
if (exists(to)) return false;
#endif
files[to] = files.at(from);
files.erase(from);
++generations[from]; ++generations[to];
rename_snapshots.push_back(files);
return true;
}
size_t totalBytes() const { return capacity; }
void setBlockCount(uint32_t count) { _config.block_count = count; }
Lfs* _getFS() { _lfs.cfg = &_config; return &_lfs; }
size_t usedBytes() const {
size_t total = 0;
for (const auto& f : files) total += f.second.size();
return total;
}
} SPIFFS;
int _getLfsUsedBlockCount(FakeFilesystem* fs) {
unsigned used = 2; // directory metadata pair
for (const auto& file : fs->files) used += (file.second.size() + 4095) / 4096;
return used;
}
File::File(FakeFilesystem* fs, const char* path, bool write)
: _fs(fs), _path(path), _write(write) {
if (!write) {
_read_generation = fs->generations[path];
_read_inode = fs->files.at(path);
}
}
bool File::open(const char* path, uint8_t mode) {
if (!_fs) return false;
*this = _fs->open(path, mode == FILE_O_WRITE ? "w" : "r");
return static_cast<bool>(*this);
}
size_t File::read(uint8_t* bytes, size_t length) {
if (!_fs || _write || _fs->fail_read == _path) return 0;
++_fs->reads;
_fs->largest_read = std::max(_fs->largest_read, length);
// An open file keeps its old inode across a rename/unlink. A later File
// opened on the same name observes the replacement, as SPIFFS/POSIX do.
auto& data = _fs->generations[_path] == _read_generation
? _fs->files.at(_path) : _read_inode;
const size_t count = std::min(length, data.size() - std::min(data.size(), _position));
if (count) memcpy(bytes, data.data() + _position, count);
_position += count;
return count;
}
size_t File::write(const uint8_t* bytes, size_t length) {
if (!_fs || !_write) return 0;
++_fs->writes;
_fs->largest_write = std::max(_fs->largest_write, length);
const size_t count = std::min(length, _fs->max_write);
auto& data = _fs->files[_path];
data.resize(_position + count);
memcpy(data.data() + _position, bytes, count);
_position += count;
return count;
}
size_t File::size() const {
if (!_fs) return 0;
return !_write && _fs->generations[_path] != _read_generation
? _read_inode.size() : _fs->files.at(_path).size();
}
bool File::seek(size_t pos) {
if (!_fs || pos > size()) return false;
_position = pos;
return true;
}
+1 -1
View File
@@ -8,6 +8,7 @@
namespace mesh {
class Utils {
public:
static void toHex(char* out, const uint8_t* bytes, size_t length);
static void sha256(uint8_t* out, size_t size, const uint8_t* data, int length) {
uint8_t digest[32];
SHA256(data, length, digest);
@@ -15,4 +16,3 @@ public:
}
};
}
+2 -133
View File
@@ -1,136 +1,4 @@
#include <algorithm>
#include <array>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <limits>
#include <map>
#include <string>
#include <vector>
#include <Arduino.h>
enum { FILE_O_READ = 0, FILE_O_WRITE = 1 };
class FakeFilesystem;
class File {
FakeFilesystem* _fs = nullptr;
std::string _path;
size_t _position = 0;
bool _write = false;
unsigned _read_generation = 0;
std::vector<uint8_t> _read_inode;
public:
File() = default;
explicit File(FakeFilesystem& fs) : _fs(&fs) {}
File(FakeFilesystem* fs, const char* path, bool write);
bool open(const char* path, uint8_t mode);
explicit operator bool() const { return _fs != nullptr; }
size_t read(uint8_t* bytes, size_t length);
size_t write(const uint8_t* bytes, size_t length);
size_t size() const;
bool seek(size_t position);
void flush() {}
void close() { _fs = nullptr; }
};
class FakeFilesystem {
struct Config { uint32_t block_count = 256, block_size = 4096; } _config;
struct Lfs { const Config* cfg; } _lfs{&_config};
public:
using Files = std::map<std::string, std::vector<uint8_t>>;
Files files;
size_t capacity = 1024 * 1024;
size_t max_write = std::numeric_limits<size_t>::max();
std::string fail_read;
unsigned fail_rename = 0, renames = 0, writes = 0, reads = 0, opens = 0;
std::vector<Files> rename_snapshots;
std::map<std::string, unsigned> generations;
bool exists(const char* path) const { return files.count(path) != 0; }
bool mkdir(const char*) { return true; }
bool remove(const char* path) {
++generations[path];
return files.erase(path) != 0;
}
File open(const char* path, const char* mode = "r", bool = false) {
++opens;
if (*mode != 'r') {
files[path].clear();
return File(this, path, true);
}
if (fail_read == path || !exists(path)) return File();
return File(this, path, false);
}
bool rename(const char* from, const char* to) {
++renames;
if (renames == fail_rename || !exists(from)) return false;
#if defined(ESP32_PLATFORM)
if (exists(to)) return false;
#endif
files[to] = files.at(from);
files.erase(from);
++generations[from]; ++generations[to];
rename_snapshots.push_back(files);
return true;
}
size_t totalBytes() const { return capacity; }
void setBlockCount(uint32_t count) { _config.block_count = count; }
Lfs* _getFS() { _lfs.cfg = &_config; return &_lfs; }
size_t usedBytes() const {
size_t total = 0;
for (const auto& f : files) total += f.second.size();
return total;
}
} SPIFFS;
int _getLfsUsedBlockCount(FakeFilesystem* fs) {
unsigned used = 2; // directory metadata pair
for (const auto& file : fs->files) used += (file.second.size() + 4095) / 4096;
return used;
}
File::File(FakeFilesystem* fs, const char* path, bool write)
: _fs(fs), _path(path), _write(write) {
if (!write) {
_read_generation = fs->generations[path];
_read_inode = fs->files.at(path);
}
}
bool File::open(const char* path, uint8_t mode) {
if (!_fs) return false;
*this = _fs->open(path, mode == FILE_O_WRITE ? "w" : "r");
return static_cast<bool>(*this);
}
size_t File::read(uint8_t* bytes, size_t length) {
if (!_fs || _write || _fs->fail_read == _path) return 0;
++_fs->reads;
// An open file keeps its old inode across a rename/unlink. A later File
// opened on the same name observes the replacement, as SPIFFS/POSIX do.
auto& data = _fs->generations[_path] == _read_generation
? _fs->files.at(_path) : _read_inode;
const size_t count = std::min(length, data.size() - std::min(data.size(), _position));
if (count) memcpy(bytes, data.data() + _position, count);
_position += count;
return count;
}
size_t File::write(const uint8_t* bytes, size_t length) {
if (!_fs || !_write) return 0;
++_fs->writes;
const size_t count = std::min(length, _fs->max_write);
auto& data = _fs->files[_path];
data.resize(_position + count);
memcpy(data.data() + _position, bytes, count);
_position += count;
return count;
}
size_t File::size() const {
if (!_fs) return 0;
return !_write && _fs->generations[_path] != _read_generation
? _read_inode.size() : _fs->files.at(_path).size();
}
bool File::seek(size_t pos) {
if (!_fs || pos > size()) return false;
_position = pos;
return true;
}
#include "FakeFilesystem.h"
#define FILESYSTEM FakeFilesystem
#include <helpers/ContactInfo.h>
@@ -179,6 +47,7 @@ class DataStore : public mesh::ContactPathBackend, public mesh::ContactSecretBac
DataStoreHost* _cache_host;
bool _cache_load_incomplete = false;
#if defined(ESP32_PLATFORM)
File _contact_path_reader;
#include "contact_write_state_under_test.h"
FakeFilesystem* _fs = &SPIFFS;
FakeFilesystem* _fsExtra = nullptr;
@@ -0,0 +1,353 @@
#include "FakeFilesystem.h"
#include <cerrno>
#include <sys/stat.h>
#define FILESYSTEM FakeFilesystem
#include <helpers/ContactInfo.h>
#include <helpers/ContactFileTransaction.h>
#include "packet_under_test.h"
static_assert(MESH_CONTACT_CACHE == 0,
"This fixture must exercise PSRAM/explicit inline-path storage");
// 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; }
assert(strncmp(vfs_path, "/spiffs", 7) == 0);
if (!fs->exists(vfs_path + 7)) { errno = ENOENT; return -1; }
return 0;
}
#include "presence_under_test.h"
struct Host {
std::vector<ContactInfo> contacts;
unsigned copies = 0;
size_t capacity = 350;
bool onContactLoaded(const ContactInfo& c) {
if (contacts.size() >= capacity) return false;
contacts.push_back(c);
return true;
}
bool getContactForSave(uint32_t index, ContactInfo& contact) {
++copies;
if (index >= contacts.size()) return false;
contact = contacts[index];
return true;
}
ContactInfo* getContactForStore(uint32_t) {
assert(false && "Uncached ESP must keep getContactForSave copy semantics");
return nullptr;
}
};
using DataStoreHost = Host;
// Hardware-only constructor/FS seams are supplied here. The state fields and
// all save/load/dirty/transaction bodies below come from production sources.
class DataStore {
FakeFilesystem* _fs;
bool _channel_load_incomplete = false;
bool _uncached_contact_load_incomplete = false;
#include "contact_write_state_under_test.h"
public:
explicit DataStore(FakeFilesystem& fs) : _fs(&fs) {}
~DataStore();
FakeFilesystem* _getContactsChannelsFS() const { return _fs; }
File openRead(FakeFilesystem* fs, const char* path) { return fs->open(path); }
bool hasIncompleteContactLoad() const;
void loadContacts(DataStoreHost*);
bool saveContacts(DataStoreHost*, bool (*)(const ContactInfo&));
bool flushContactWrites(DataStoreHost*, bool (*)(const ContactInfo&));
bool markContactDirty(const ContactInfo&);
bool releaseContact(const ContactInfo&);
bool serviceContactWrites(DataStoreHost*, bool (*)(const ContactInfo&));
bool hasPendingContactWrites() const;
bool deleteBlobByKey(const uint8_t[], int);
bool verifying() const { return _contact_write_verifying; }
bool readyToPublish() const {
return _contact_write && _contact_write->readyToPublish();
}
};
#include "store_under_test.h"
ContactInfo contact(unsigned number) {
ContactInfo c;
for (unsigned i = 0; i < 32; ++i) c.id.pub_key[i] = (number * 13 + i * 17) & 255;
memcpy(c.id.pub_key, &number, sizeof(number));
snprintf(c.name, sizeof(c.name), "Fixture %u", number);
c.type = 1;
c.flags = number & 31;
c.tx_radio = number % 5;
c.sync_since = 1000 + number;
c.last_advert_timestamp = 2000 + number;
c.lastmod = 3000 + number;
c.gps_lat = 4000 + number;
c.gps_lon = -5000 - number;
uint8_t path[9];
for (unsigned i = 0; i < sizeof(path); ++i) path[i] = number + i;
assert(c.setPath(path, 0x83));
return c;
}
Host table(unsigned count = 32) {
Host h;
for (unsigned i = 0; i < count; ++i) h.contacts.push_back(contact(i));
return h;
}
bool keepContact(const ContactInfo& c) { return c.type != 0; }
bool keepEven(const ContactInfo& c) { return (c.id.pub_key[0] & 1) == 0; }
using Filter = bool (*)(const ContactInfo&);
std::vector<uint8_t> image(const Host& h, Filter filter = nullptr) {
std::vector<uint8_t> bytes;
for (const auto& c : h.contacts) {
if (filter && !filter(c)) continue;
uint8_t record[mesh::storage::CONTACT_RECORD_SIZE];
assert(serializeContactRecord(c, record));
bytes.insert(bytes.end(), record, record + sizeof(record));
}
return bytes;
}
unsigned drain(DataStore& store, Host& h, Filter filter = nullptr) {
const bool existed = SPIFFS.exists("/contacts3");
const auto old = existed ? SPIFFS.files.at("/contacts3") : std::vector<uint8_t>();
unsigned passes = 0;
while (store.hasPendingContactWrites()) {
const unsigned writes = SPIFFS.writes, reads = SPIFFS.reads;
assert(++passes < 5000);
assert(store.serviceContactWrites(&h, filter));
assert(SPIFFS.writes - writes <= 1);
assert(SPIFFS.reads - reads <= 1);
// Transport service runs here, between every production pass. It must
// never observe an unpublished replacement or a target-name gap.
if (store.hasPendingContactWrites()) {
assert(SPIFFS.exists("/contacts3") == existed);
if (existed) assert(SPIFFS.files.at("/contacts3") == old);
}
}
assert(SPIFFS.files.at("/contacts3") == image(h, filter));
assert(!SPIFFS.exists("/contacts3.tmp"));
assert(SPIFFS.missing_remove_logs == 0);
return passes;
}
void bootCheck(const Host& expected, Filter filter = nullptr) {
Host boot;
DataStore after_reset(SPIFFS);
after_reset.loadContacts(&boot);
assert(!after_reset.hasIncompleteContactLoad());
assert(image(boot) == image(expected, filter));
assert(!after_reset.hasPendingContactWrites());
}
void boundedFullTableAndFirstSave() {
SPIFFS = FakeFilesystem();
Host h = table(350);
for (unsigned i = 0; i < h.contacts.size(); i += 7) h.contacts[i].type = 0;
DataStore store(SPIFFS);
assert(store.markContactDirty(h.contacts[0]));
const auto expected = image(h, keepContact);
const auto passes = drain(store, h, keepContact);
assert(passes > expected.size() / 64);
assert(SPIFFS.largest_read <= 64);
assert(SPIFFS.largest_write == mesh::storage::CONTACT_RECORD_SIZE);
assert(SPIFFS.removes == 0); // first publication has no backup/temp to delete
assert(h.copies >= h.contacts.size());
bootCheck(h, keepContact);
}
void mutationRestart(unsigned phase) {
SPIFFS = FakeFilesystem();
Host h = table();
SPIFFS.files["/contacts3"] = image(h);
const auto old = SPIFFS.files.at("/contacts3");
DataStore store(SPIFFS);
assert(store.markContactDirty(h.contacts[0]));
unsigned passes = 0;
while ((phase == 0 && passes < 5)
|| (phase == 1 && (!store.verifying() || SPIFFS.reads == 0))
|| (phase == 2 && !store.readyToPublish())) {
assert(++passes < 1000);
assert(store.serviceContactWrites(&h, nullptr));
}
const auto removed = h.contacts[4];
h.contacts.erase(h.contacts.begin() + 4);
assert(store.releaseContact(removed));
std::reverse(h.contacts.begin(), h.contacts.end());
h.contacts[0] = contact(777);
assert(store.markContactDirty(h.contacts[0]));
assert(store.serviceContactWrites(&h, nullptr)); // discard obsolete job
assert(store.hasPendingContactWrites());
assert(!SPIFFS.exists("/contacts3.tmp"));
assert(SPIFFS.files.at("/contacts3") == old);
drain(store, h);
bootCheck(h);
}
void faultAndRetry(unsigned fault) {
SPIFFS = FakeFilesystem();
Host h = table();
SPIFFS.files["/contacts3"] = image(h);
const auto old = SPIFFS.files.at("/contacts3");
DataStore store(SPIFFS);
h.contacts[2] = contact(888);
assert(store.markContactDirty(h.contacts[2]));
if (fault == 0) SPIFFS.max_write = 50;
if (fault == 1) SPIFFS.fail_create = true;
if (fault == 7) SPIFFS.metadata_error = true;
bool injected = fault == 0 || fault == 1 || fault == 7;
bool success = true;
for (unsigned pass = 0; pass < 1000 && success; ++pass) {
if (!injected && store.verifying()) {
if (fault == 2) SPIFFS.fail_read = "/contacts3.tmp";
if (fault == 3) SPIFFS.files.at("/contacts3.tmp")[20] ^= 1;
if (fault == 4) SPIFFS.files.at("/contacts3.tmp").pop_back();
if (fault <= 4) injected = true;
}
if (!injected && store.readyToPublish()) {
SPIFFS.fail_rename = SPIFFS.renames + (fault == 5 ? 1 : 2);
injected = true;
}
success = store.serviceContactWrites(&h, nullptr);
}
assert(injected && !success);
assert(store.hasPendingContactWrites());
assert(SPIFFS.files.at("/contacts3") == old);
assert(!SPIFFS.exists("/contacts3.tmp"));
assert(SPIFFS.missing_remove_logs == 0);
SPIFFS.max_write = std::numeric_limits<size_t>::max();
SPIFFS.fail_create = SPIFFS.metadata_error = false;
SPIFFS.fail_read.clear();
SPIFFS.fail_rename = 0;
drain(store, h);
bootCheck(h);
}
void syncFlushAndFilterChange() {
SPIFFS = FakeFilesystem();
Host h = table();
SPIFFS.files["/contacts3"] = image(h);
DataStore store(SPIFFS);
assert(store.markContactDirty(h.contacts[0]));
for (unsigned i = 0; i < 10; ++i) assert(store.serviceContactWrites(&h, nullptr));
h.contacts[0] = contact(901);
assert(store.markContactDirty(h.contacts[0]));
assert(store.flushContactWrites(&h, keepEven));
assert(!store.hasPendingContactWrites());
assert(SPIFFS.files.at("/contacts3") == image(h, keepEven));
bootCheck(h, keepEven);
assert(store.markContactDirty(h.contacts[0]));
for (unsigned i = 0; i < 10; ++i) assert(store.serviceContactWrites(&h, keepEven));
assert(store.serviceContactWrites(&h, keepContact)); // changed filter cancels
assert(store.hasPendingContactWrites() && !SPIFFS.exists("/contacts3.tmp"));
drain(store, h, keepContact);
bootCheck(h, keepContact);
Host replacement = table(17);
assert(store.markContactDirty(h.contacts[0]));
for (unsigned i = 0; i < 10; ++i) assert(store.serviceContactWrites(&h, nullptr));
assert(store.serviceContactWrites(&replacement, nullptr)); // changed host cancels
assert(store.hasPendingContactWrites() && !SPIFFS.exists("/contacts3.tmp"));
drain(store, replacement);
bootCheck(replacement);
}
void realBackupCleanupFailure() {
SPIFFS = FakeFilesystem();
Host h = table();
const auto old = image(h);
SPIFFS.files["/contacts3"] = old;
SPIFFS.files["/contacts3.bak"] = {42};
DataStore store(SPIFFS);
h.contacts[1] = contact(987);
assert(store.markContactDirty(h.contacts[1]));
SPIFFS.fail_remove = true;
bool success = true;
for (unsigned i = 0; i < 1000 && success; ++i) {
success = store.serviceContactWrites(&h, nullptr);
}
assert(!success && store.hasPendingContactWrites());
assert(SPIFFS.files.at("/contacts3") == old);
assert(SPIFFS.files.at("/contacts3.bak") == std::vector<uint8_t>({42}));
// Best-effort temp cleanup cannot hide the primary remove failure.
assert(SPIFFS.exists("/contacts3.tmp") && SPIFFS.missing_remove_logs == 0);
SPIFFS.fail_remove = false;
drain(store, h);
bootCheck(h);
}
void rebootAbortRecoveryAndDurableRemoval() {
SPIFFS = FakeFilesystem();
Host h = table();
const auto old = image(h);
SPIFFS.files["/contacts3"] = old;
{
DataStore store(SPIFFS);
assert(store.markContactDirty(h.contacts[0]));
for (unsigned i = 0; i < 7; ++i) assert(store.serviceContactWrites(&h, nullptr));
} // CPU reset/destruction must abandon an incomplete lazy image.
assert(SPIFFS.files.at("/contacts3") == old);
assert(!SPIFFS.exists("/contacts3.tmp"));
bootCheck(h);
assert(SPIFFS.rename("/contacts3", "/contacts3.bak")); // interrupted publication
bootCheck(h); // production load recovers the prior complete table
{
DataStore store(SPIFFS);
const auto removed = h.contacts[8];
h.contacts.erase(h.contacts.begin() + 8);
assert(store.releaseContact(removed));
drain(store, h);
}
bootCheck(h); // committed removal survives a new DataStore instance
SPIFFS.fail_read = "/contacts3";
Host incomplete;
DataStore failed_boot(SPIFFS);
failed_boot.loadContacts(&incomplete);
assert(failed_boot.hasIncompleteContactLoad());
const auto before = SPIFFS.files;
assert(!failed_boot.serviceContactWrites(&incomplete, nullptr));
assert(!failed_boot.flushContactWrites(&incomplete, nullptr));
assert(SPIFFS.files == before);
}
void expectedAbsenceAndRealDeleteFailures() {
SPIFFS = FakeFilesystem();
DataStore store(SPIFFS);
uint8_t key[32] = {1, 2, 3};
char path[64];
makeBlobPath(key, sizeof(key), path, sizeof(path));
assert(store.deleteBlobByKey(key, sizeof(key)));
assert(SPIFFS.removes == 0 && SPIFFS.missing_remove_logs == 0);
SPIFFS.metadata_error = true;
assert(!store.deleteBlobByKey(key, sizeof(key)));
assert(SPIFFS.removes == 0);
SPIFFS.metadata_error = false;
SPIFFS.files[path] = {7, 8};
SPIFFS.fail_remove = true;
assert(!store.deleteBlobByKey(key, sizeof(key)));
assert(SPIFFS.exists(path));
SPIFFS.fail_remove = false;
assert(store.deleteBlobByKey(key, sizeof(key)));
assert(!SPIFFS.exists(path));
assert(store.deleteBlobByKey(key, sizeof(key)));
assert(SPIFFS.removes == 2 && SPIFFS.missing_remove_logs == 0);
// Failed construction/destruction must not remove a temp that never existed.
SPIFFS.fail_create = true;
{ mesh::ContactFileTransaction writer(&SPIFFS, "/empty", companionPathPresence);
assert(!writer); }
assert(SPIFFS.removes == 2 && SPIFFS.missing_remove_logs == 0);
SPIFFS.metadata_error = true;
{ mesh::ContactFileTransaction writer(&SPIFFS, "/unreadable", companionPathPresence);
assert(!writer); }
assert(SPIFFS.removes == 2 && SPIFFS.missing_remove_logs == 0);
}
int main() {
boundedFullTableAndFirstSave();
for (unsigned phase = 0; phase < 3; ++phase) mutationRestart(phase);
for (unsigned fault = 0; fault < 8; ++fault) faultAndRetry(fault);
syncFlushAndFilterChange();
realBackupCleanupFailure();
rebootAbortRecoveryAndDurableRemoval();
expectedAbsenceAndRealDeleteFailures();
}
+21 -2
View File
@@ -22,7 +22,7 @@ class CompanionUncachedStorageTest(unittest.TestCase):
def run_platform(self, platform):
source = (ROOT / "examples/companion_radio/DataStore.cpp").read_text()
signatures = (
signatures = [
"static bool companionPathPresence(",
"bool DataStore::loadMainIdentity(",
"bool DataStore::canCreateMainIdentity() const",
@@ -31,7 +31,22 @@ class CompanionUncachedStorageTest(unittest.TestCase):
"void DataStore::loadContacts(",
"bool DataStore::saveContacts(",
"bool DataStore::hasIncompleteContactLoad() const",
)
]
if platform == "ESP32_PLATFORM":
signatures.extend((
"static bool serializeContactRecord(",
"DataStore::~DataStore()",
"void DataStore::cancelContactWrite(",
"bool DataStore::serviceContactWrite(",
"bool DataStore::markContactDirty(",
"bool DataStore::releaseContact(",
"bool DataStore::serviceContactWrites(",
"bool DataStore::flushContactWrites(",
"bool DataStore::hasPendingContactWrites() const",
))
# The serializer must be declared before save/service uses it.
signatures.insert(0, signatures.pop(signatures.index(
"static bool serializeContactRecord(")))
packet = (ROOT / "src/Packet.cpp").read_text()
generated = "namespace mesh {\n" + extract_braced(
packet, "bool Packet::isValidPathLen(") + "\n}\n"
@@ -39,6 +54,10 @@ class CompanionUncachedStorageTest(unittest.TestCase):
with tempfile.TemporaryDirectory(prefix="mesh-uncached-store-") as temp:
temp = Path(temp)
(temp / "store_under_test.h").write_text(generated)
header = (ROOT / "examples/companion_radio/DataStore.h").read_text()
start = header.index(" mesh::ContactFileTransaction* _contact_write")
end = header.index("\n#endif", start)
(temp / "contact_write_state_under_test.h").write_text(header[start:end])
# IdentityStore.h only needs the platform filesystem declaration;
# the concrete mock below implements the same file API.
(temp / "FS.h").write_text(
+2 -2
View File
@@ -64,7 +64,7 @@ class ContactCacheTest(unittest.TestCase):
helper_end = store.index("bool DataStore::readStoredPath(", helper_start)
implementation = store[helper_start:helper_end]
header = (ROOT / "examples/companion_radio/DataStore.h").read_text()
state_start = header.index(" File _contact_path_reader;")
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])
@@ -73,7 +73,7 @@ class ContactCacheTest(unittest.TestCase):
functions.extend((
"void DataStore::cancelContactWrite(",
"bool DataStore::cancelCooperativeWrite(",
"bool DataStore::serviceCachedContactWrite(",
"bool DataStore::serviceContactWrite(",
"bool DataStore::markContactDirty(",
"bool DataStore::releaseContact(",
"bool DataStore::serviceContactWrites(",
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Run production ESP contact transactions with inline paths, including PSRAM."""
from pathlib import Path
import subprocess
import tempfile
import unittest
from test_t096_full_memory import method
ROOT = Path(__file__).resolve().parents[1]
class UncachedESPContactsTest(unittest.TestCase):
def test_actual_psram_cache_policy_and_cooperative_durability(self):
self.run_fixture(["-DBOARD_HAS_PSRAM=1"])
def test_explicit_uncached_qualification_configuration(self):
self.run_fixture(["-DMESH_CONTACT_CACHE=0"])
def run_fixture(self, policy):
with tempfile.TemporaryDirectory(prefix="mesh-esp-inline-contacts-") as directory:
temp = Path(directory)
source = (ROOT / "examples/companion_radio/DataStore.cpp").read_text()
header = (ROOT / "examples/companion_radio/DataStore.h").read_text()
start = header.index(" mesh::ContactFileTransaction* _contact_write")
end = header.index("\n#endif", start)
(temp / "contact_write_state_under_test.h").write_text(header[start:end])
signatures = (
"static bool serializeContactRecord(",
"static bool deserializeContactRecord(",
"DataStore::~DataStore()",
"void DataStore::cancelContactWrite(",
"bool DataStore::serviceContactWrite(",
"bool DataStore::markContactDirty(",
"bool DataStore::releaseContact(",
"bool DataStore::serviceContactWrites(",
"bool DataStore::saveContacts(",
"bool DataStore::flushContactWrites(",
"bool DataStore::hasPendingContactWrites() const",
"bool DataStore::hasIncompleteContactLoad() const",
"void DataStore::loadContacts(",
"inline void makeBlobPath(",
)
implementation = "\n".join(method(source, signature) for signature in signatures)
# Only the native SDK stat seam is substituted. The production
# presence logic still distinguishes ENOENT from metadata failure.
presence = method(source, "static bool companionPathPresence(")
presence = presence.replace("::stat(vfs_path, &info)",
"fixtureStat(fs, vfs_path, &info)")
(temp / "presence_under_test.h").write_text(presence)
# NRF/STM and ESP implementations have the same public signature.
delete_start = source.rindex("bool DataStore::deleteBlobByKey(")
implementation += "\n" + method(source[delete_start:],
"bool DataStore::deleteBlobByKey(")
utils = (ROOT / "src/Utils.cpp").read_text()
implementation += "\nnamespace mesh {\n"
implementation += 'static const char hex_chars[] = "0123456789ABCDEF";\n'
implementation += method(utils, "void Utils::toHex(") + "\n}\n"
(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 (
"bool Packet::isValidPathLen(", "size_t Packet::writePath(",
"uint8_t Packet::copyPath("))
+ "\n}\n")
binary = temp / "test"
result = subprocess.run([
"c++", "-std=c++17", "-O1", "-g", "-Wall", "-Wextra",
"-fsanitize=address,undefined", "-fno-omit-frame-pointer",
"-DESP32_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", *policy,
"-I", str(ROOT / "test/fixtures/contact_cache/mocks"),
"-I", str(ROOT / "test/fixtures/contact_cache"),
"-I", str(ROOT / "lib/ed25519"), "-I", str(ROOT / "src"),
"-I", str(temp),
str(ROOT / "test/fixtures/contact_persistence/test_esp32_uncached.cpp"),
"-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)
if __name__ == "__main__":
unittest.main()