Files
HaloKeymind/test/fixtures/advert_cache/test.cpp
T

315 lines
14 KiB
C++

#include "FakeFilesystem.h"
#include <array>
#include <helpers/LazyPersistence.h>
#define FILESYSTEM FakeFilesystem
namespace mesh { struct Utils {
static void toHex(char* dest, const uint8_t* src, int length) {
for (int i=0; i<length; ++i) sprintf(dest+2*i, "%02X", src[i]);
dest[2*length]=0;
}
}; }
struct ContactInfo {};
class MyMesh;
using DataStoreHost = MyMesh;
class DataStore {
public:
FakeFilesystem* _fs;
bool _identity_creation_blocked=false, _prefs_load_incomplete=false;
bool _channel_load_incomplete=false, _contact_write_requested=false;
bool _uncached_contact_load_incomplete=false, _cache_load_incomplete=false;
const char* _prefs_recovery_source=nullptr;
const char* _channel_recovery_source=nullptr;
File _contact_path_reader;
bool contact_pending=false;
unsigned contact_passes=0;
#include "advert_state.h"
explicit DataStore(FakeFilesystem& fs):_fs(&fs) {}
File openRead(FakeFilesystem* fs,const char* path){return fs->open(path);}
void cancelContactWrite(){}
void begin();
bool formatFileSystem();
bool queueAdvertByKey(const uint8_t[],int,const uint8_t[],uint8_t);
bool serviceAdvertWrites(uint32_t);
bool flushAdvertWrites(uint32_t now=0);
bool hasPendingAdvertWrites() const;
bool isAdvertWriteDue(uint32_t) const;
bool consumeSynchronousAdvertIO();
uint8_t getBlobByKey(const uint8_t[],int,uint8_t[]);
bool putBlobByKey(const uint8_t[],int,const uint8_t[],uint8_t);
bool deleteBlobByKey(const uint8_t[],int);
bool hasPendingContactWrites() const{return contact_pending;}
bool serviceContactWrites(DataStoreHost*,bool(*)(const ContactInfo&)) {
++_fs->operations; ++contact_passes; return true;
}
bool flushContactWrites(DataStoreHost*,bool(*)(const ContactInfo&)) {
++_fs->operations; ++contact_passes; contact_pending=false; return true;
}
};
#include "presence.h"
#include "store.h"
constexpr uint32_t CONTACT_PAGE_WRITE_GAP=1;
struct Clock { uint32_t now=10000; uint32_t getMillis() const{return now;} };
struct RadioDriver {
bool isWatchdogObserving() const{return false;}
bool isCalibratingNoiseFloor() const{return false;}
} radio_driver;
struct SerialInterface {
bool hasPendingIO() const{return false;}
bool isReplyRouteAvailable(const SerialInterface*) const{return false;}
};
struct DelayedReplies { bool hasBinaryTrace() const{return false;} };
class MyMesh {
public:
DataStore* _store;
Clock* _ms;
bool _advert_write_next=true;
unsigned long dirty_contacts_expiry=0;
uint8_t dirty_contacts_failures=0;
bool(*save_filter)(const ContactInfo&)=nullptr;
bool _radio_available=false, _iter_started=false;
bool command_radio_apply_pending=false, saved_radio_apply_pending=false;
uint32_t _scheduled_reboot_at=0, radio_apply_retry_at=0;
uint32_t emergency_client_repeat_send_at=0;
void* sign_data=nullptr;
void* emergency_client_repeat_packet=nullptr;
SerialInterface* _serial=nullptr;
SerialInterface* _iter_reply_route=nullptr;
DelayedReplies _delayed_replies;
bool putBlobByKey(const uint8_t[],int,const uint8_t[],int);
void servicePersistence();
bool flushContactsBeforeReboot();
bool hasPendingWork() const;
bool canRecoverUsbLogging() const;
bool isDualRadioActive() const{return false;}
bool hasOutbound() const{return false;}
bool isAnyTempRadioActive() const{return false;}
bool hasPendingOtaApply() const{return false;}
bool hasPendingReqs() const{return false;}
bool hasQueuedWorkDue() const{return false;}
bool hasRetryWorkDue() const{return false;}
bool millisHasNowPassed(uint32_t when) const{return _ms->now>=when;}
bool isContactWriteDue() const{return _store->contact_pending;}
void scheduleContactWriteRetry(){++dirty_contacts_failures;}
unsigned long futureMillis(uint32_t delay) const{return _ms->now+delay;}
};
#include "mesh.h"
using Key=std::array<uint8_t,32>;
using Blob=std::vector<uint8_t>;
using Stage=DataStore::AdvertWriteState::Stage;
Key key(unsigned n){Key k{};k[0]=n;return k;}
Blob blob(unsigned n, unsigned len=255){return Blob(len,n);}
std::string path(const Key& key){char path[64];makeBlobPath(key.data(),32,path,sizeof(path));return path;}
Blob get(DataStore& store,const Key& k){uint8_t bytes[255];auto n=store.getBlobByKey(k.data(),32,bytes);return Blob(bytes,bytes+n);}
void enqueue(DataStore& store,const Key& k,const Blob& b){assert(store.queueAdvertByKey(k.data(),32,b.data(),b.size()));}
void step(DataStore& store,FakeFilesystem& fs,uint32_t now=10000) {
auto before=fs.operations, backend=fs.backend_writes;
fs.max_backend_write=0;
store.serviceAdvertWrites(now);
assert(fs.operations-before<=1 && "one storage operation per cooperative pass");
assert(fs.backend_writes-backend<=1);
assert(fs.max_backend_write<=64);
}
void drain(DataStore& store,FakeFilesystem& fs) {
unsigned n=0;
while(store.hasPendingAdvertWrites()){assert(++n<1000);step(store,fs,10000+n*1000);}
assert(fs.handles==0);
}
void until(DataStore& store,FakeFilesystem& fs,Stage target) {
unsigned n=0;
do {assert(++n<1000);step(store,fs,10000+n*1000);}
while(store._advert_write.stage!=target);
}
void rxRouteAndScheduling() {
FakeFilesystem fs;DataStore store(fs);Clock clock;MyMesh mesh{&store,&clock};
auto k=key(1);auto b=blob(1);
const auto before=fs.operations;
assert(mesh.putBlobByKey(k.data(),32,b.data(),b.size()));
assert(fs.operations==before && "advert_rx_no_fs");
assert(get(store,k)==b && fs.operations==before);
store.contact_pending=true;
unsigned blob_passes=0, serial_services=0;
while(store.hasPendingAdvertWrites()) {
++serial_services;
auto old=fs.operations, contacts=store.contact_passes;
mesh.servicePersistence();
assert(fs.operations-old<=1);
if(store.contact_passes==contacts)++blob_passes;
++clock.now;
}
assert(serial_services>20 && blob_passes>10 && store.contact_passes>10);
assert(fs.files.at(path(k))==b && !fs.exists("/advert.tmp"));
// Explicit durability work must not be followed by another automatic job.
enqueue(store,k,blob(2));assert(store.putBlobByKey(k.data(),32,b.data(),b.size()));
auto old=fs.operations;mesh.servicePersistence();assert(old==fs.operations);
// Disk share/export work consumes the same pass; a RAM share does not.
store.consumeSynchronousAdvertIO();
assert(get(store,k)==b);enqueue(store,key(2),blob(2));
old=fs.operations;mesh.servicePersistence();assert(old==fs.operations);
assert(get(store,key(2))==blob(2));
old=fs.operations;mesh.servicePersistence();assert(fs.operations==old+1);
store.contact_pending=false;drain(store,fs);
}
void coalesceOverflowAndDeleteEveryStage() {
for(unsigned stage=0;stage<24;++stage) {
FakeFilesystem fs;DataStore store(fs);auto k=key(1), other=key(2);auto old=blob(1), fresh=blob(2);
fs.files[path(k)]=old;enqueue(store,k,fresh);
for(unsigned i=0;i<stage && store.hasPendingAdvertWrites();++i)step(store,fs);
enqueue(store,k,blob(3));assert(get(store,k)==blob(3));
drain(store,fs);assert(fs.files.at(path(k))==blob(3));
enqueue(store,k,old);
for(unsigned i=0;i<stage && store.hasPendingAdvertWrites();++i)step(store,fs);
assert(store.deleteBlobByKey(k.data(),32));
// Reusing the active slot must not turn its old temp into another key.
enqueue(store,other,fresh);drain(store,fs);
assert(get(store,k).empty() && fs.files.at(path(other))==fresh);
}
FakeFilesystem fs;DataStore store(fs);
for(unsigned i=1;i<=4;++i)enqueue(store,key(i),blob(i));
auto before=fs.operations;
auto fifth=key(5);auto b=blob(5);
assert(!store.queueAdvertByKey(fifth.data(),32,b.data(),b.size()));
assert(fs.operations==before);enqueue(store,key(2),blob(22));
Key same_prefix=key(2);same_prefix[31]=99;
assert(get(store,same_prefix)==blob(22));
drain(store,fs);assert(fs.files.at(path(key(2)))==blob(22));
}
void crashBoundaries() {
for(unsigned stop=0;stop<26;++stop) {
FakeFilesystem fs;auto k=key(1);auto old=blob(1), fresh=blob(2);
fs.files[path(k)]=old;
{
DataStore store(fs);enqueue(store,k,fresh);
for(unsigned i=0;i<stop && store.hasPendingAdvertWrites();++i)step(store,fs);
}
assert(fs.handles==0);
DataStore boot(fs);auto value=get(boot,k);
assert(value==old || value==fresh);
assert(!boot.hasPendingAdvertWrites());
enqueue(boot,k,blob(3));drain(boot,fs);
assert(get(boot,k)==blob(3));
assert(!fs.exists((path(k)+".bak").c_str()));
}
}
void synchronousMutationAndRollback() {
for(unsigned stage=0;stage<24;++stage) {
FakeFilesystem fs;DataStore store(fs);auto k=key(1);auto old=blob(1), queued=blob(2), explicit_blob=blob(3);
fs.files[path(k)]=old;enqueue(store,k,queued);
for(unsigned i=0;i<stage && store.hasPendingAdvertWrites();++i)step(store,fs);
assert(store.putBlobByKey(k.data(),32,explicit_blob.data(),explicit_blob.size()));
drain(store,fs);assert(get(store,k)==explicit_blob);
enqueue(store,k,queued);until(store,fs,Stage::Publish);
fs.short_write=true;
assert(!store.putBlobByKey(k.data(),32,explicit_blob.data(),explicit_blob.size()));
assert(get(store,k)==queued);fs.short_write=false;drain(store,fs);assert(get(store,k)==queued);
enqueue(store,k,old);until(store,fs,Stage::Publish);
fs.fail_remove=fs.remove_calls+1;
assert(!store.deleteBlobByKey(k.data(),32));
assert(get(store,k)==old);fs.fail_remove=0;drain(store,fs);assert(get(store,k)==old);
}
FakeFilesystem fs;DataStore store(fs);auto k=key(1);auto old=blob(1), fresh=blob(2);
fs.files[path(k)]=old;fs.files[path(k)+".bak"]=blob(8);enqueue(store,k,fresh);
fs.fail_remove=fs.remove_calls+1;
assert(!store.deleteBlobByKey(k.data(),32));assert(fs.files.at(path(k))==old);
fs.fail_remove=0;fs.fail_metadata=true;
auto removes=fs.remove_calls;assert(!store.deleteBlobByKey(k.data(),32));assert(fs.remove_calls==removes);
fs.fail_metadata=false;drain(store,fs);assert(get(store,k)==fresh);
fs.files[path(k)]=old;fs.files[path(k)+".bak"]=blob(8);enqueue(store,k,fresh);
fs.fail_metadata_at=fs.metadata_calls+2;
removes=fs.remove_calls;assert(!store.deleteBlobByKey(k.data(),32));
assert(fs.remove_calls==removes && fs.files.at(path(k))==old);
fs.fail_metadata_at=0;fs.fail_remove=fs.remove_calls+2;
assert(!store.deleteBlobByKey(k.data(),32));
assert(fs.files.at(path(k))==old && !fs.exists((path(k)+".bak").c_str()));
fs.fail_remove=0;drain(store,fs);assert(get(store,k)==fresh);
}
void failuresAndLifecycle() {
for(unsigned fault=0;fault<9;++fault) {
FakeFilesystem fs;DataStore store(fs);auto k=key(1);auto old=blob(1), fresh=blob(2);
fs.files[path(k)]=old;enqueue(store,k,fresh);
switch(fault) {
case 0:fs.fail_metadata=true;break;
case 1:fs.fail_open=true;fs.partial_open=true;break;
case 2:fs.fail_config=1;break;
case 3:fs.short_write=true;break;
case 4:fs.fail_close=true;break;
case 5:fs.fail_read=true;break;
case 6:fs.fail_rename=1;break;
case 7:fs.fail_rename=2;break;
case 8:fs.fail_config=2;break;
}
assert(!store.flushAdvertWrites(10000));
assert(fs.handles==0 && get(store,k)==fresh);
assert(!store.isAdvertWriteDue(10999));
fs.fail_metadata=fs.fail_open=fs.partial_open=fs.short_write=fs.fail_close=fs.fail_read=false;
fs.fail_config=fs.fail_rename=0;drain(store,fs);
assert(get(store,k)==fresh && !fs.exists("/advert.tmp"));
}
{
FakeFilesystem fs;DataStore store(fs);auto k=key(1);auto old=blob(1), fresh=blob(2);
fs.files[path(k)]=old;enqueue(store,k,fresh);until(store,fs,Stage::OpenVerify);
fs.files.at("/advert.tmp")[73]^=1;
assert(!store.flushAdvertWrites(10000));
assert(fs.files.at(path(k))==old && get(store,k)==fresh && fs.handles==0);
drain(store,fs);assert(get(store,k)==fresh);
}
FakeFilesystem fs;DataStore store(fs);auto k=key(1);auto b=blob(1);
enqueue(store,k,b);until(store,fs,Stage::Write);assert(fs.handles==1);
store.begin();assert(fs.handles==0 && !store.hasPendingAdvertWrites());
enqueue(store,k,b);until(store,fs,Stage::Write);fs.fail_format=true;
assert(!store.formatFileSystem());assert(fs.handles==0 && get(store,k)==b);
fs.fail_format=false;drain(store,fs);enqueue(store,k,blob(2));
assert(store.formatFileSystem());assert(!store.hasPendingAdvertWrites() && get(store,k).empty());
// Reboot and identity-import paths use the real MyMesh durability gate.
Clock clock;MyMesh mesh{&store,&clock};enqueue(store,k,b);
assert(mesh.flushContactsBeforeReboot());assert(get(store,k)==b && fs.handles==0);
enqueue(store,k,blob(2));fs.fail_open=true;
assert(!mesh.flushContactsBeforeReboot());assert(fs.handles==0);
auto before=fs.operations;assert(!mesh.flushContactsBeforeReboot());assert(fs.operations==before);
// Persistent errors use the shared bounded exponential retry policy;
// received coalescing adverts cannot postpone or cancel the backoff.
FakeFilesystem broken_fs;DataStore broken(broken_fs);enqueue(broken,k,b);
broken_fs.fail_metadata=true;uint32_t now=10000;
for(unsigned failure=0;failure<12;++failure) {
while(broken.serviceAdvertWrites(now)) ++now;
uint32_t delay=broken._advert_write.retry_at-now;
assert(delay==std::min(1000u<<failure,300000u));
enqueue(broken,k,blob(2));
auto ops=broken_fs.operations;broken.serviceAdvertWrites(now+delay-1);
assert(ops==broken_fs.operations && get(broken,k)==blob(2));
now+=delay;
}
}
void pendingWorkAndRecovery() {
FakeFilesystem fs;DataStore store(fs);Clock clock;MyMesh mesh{&store,&clock};
const auto k=key(1);const auto b=blob(1);
assert(!mesh.hasPendingWork() && mesh.canRecoverUsbLogging());
enqueue(store,k,b);
assert(mesh.hasPendingWork() && !mesh.canRecoverUsbLogging());
fs.fail_open=true;
assert(!store.flushAdvertWrites(clock.now));
assert(!store.isAdvertWriteDue(clock.now));
// An accepted RAM packet awaiting retry must not be abandoned by sleep or
// discarded when the USB logger's recovery path resets the transport.
assert(mesh.hasPendingWork() && !mesh.canRecoverUsbLogging());
fs.fail_open=false;clock.now+=1000;until(store,fs,Stage::Write);
assert(store.deleteBlobByKey(k.data(),32));
assert(get(store,k).empty());
// Successful deletion retires RAM immediately, while the old file handle
// and reserved temp still need cooperative cancellation and cleanup.
assert(mesh.hasPendingWork() && !mesh.canRecoverUsbLogging());
drain(store,fs);
assert(!mesh.hasPendingWork() && mesh.canRecoverUsbLogging());
}
int main() {
rxRouteAndScheduling();coalesceOverflowAndDeleteEveryStage();crashBoundaries();
synchronousMutationAndRollback();failuresAndLifecycle();pendingWorkAndRecovery();
puts("PASS: actual ESP queue/route/scheduler, latest RAM, crash/rollback, faults and lifecycle");
}