#include "Hardware/TDeck/MapTileStore.h" #include #include #include #include #include #include #include #include using Hardware::TDeck::MapTileStore; using Hardware::TDeck::MapTileStorage; using Hardware::TDeck::TileKey; using Hardware::TDeck::TileStoreConfig; using Hardware::TDeck::TileStoreResult; namespace { std::size_t tests_run = 0U; void fail(const char* e, int line) { std::cerr << "line " << line << ": " << e << '\n'; std::exit(1); } #define CHECK(e) do { if (!(e)) fail(#e, __LINE__); } while (false) void beginTest() { ++tests_run; } struct File { std::string path; std::vector bytes; }; class FakeStorage : public MapTileStorage { public: bool available; bool short_write; bool fail_commit; bool fail_remove; int fail_remove_call; int fail_rename_call; int fail_rename_call2; int power_cut_after_rename_call; std::string fail_remove_path; std::vector files; std::string open_path; std::size_t position; std::size_t list_position; int rename_calls; int remove_calls; FakeStorage() : available(true), short_write(false), fail_commit(false), fail_remove(false), fail_remove_call(0), fail_rename_call(0), fail_rename_call2(0), power_cut_after_rename_call(0), position(0U), list_position(0U), rename_calls(0), remove_calls(0) {} int find(const char* path) const { for (std::size_t i = 0U; i < files.size(); ++i) if (files[i].path == path) return static_cast(i); return -1; } void add(const char* path, const std::vector& bytes) { int i = find(path); if (i >= 0) files[static_cast(i)].bytes = bytes; else files.push_back(File{path, bytes}); } virtual bool isAvailable() const { return available; } virtual TileStoreResult beginRead(const char* path, std::uint32_t& size) { if (!available) return TileStoreResult::STORAGE_UNAVAILABLE; const int i = find(path); if (i < 0) return TileStoreResult::MISS; open_path = path; position = 0U; size = static_cast(files[static_cast(i)].bytes.size()); return TileStoreResult::OK; } virtual TileStoreResult readChunk(std::uint8_t* out, std::size_t capacity, std::size_t& count) { if (!available) return TileStoreResult::STORAGE_UNAVAILABLE; const int i = find(open_path.c_str()); if (i < 0) return TileStoreResult::IO_ERROR; const std::vector& b = files[static_cast(i)].bytes; count = std::min(capacity, b.size() - position); if (count != 0U) std::memcpy(out, &b[position], count); position += count; return TileStoreResult::OK; } virtual void endRead() { open_path.clear(); } virtual TileStoreResult beginWrite(const char* path) { if (!available) return TileStoreResult::STORAGE_UNAVAILABLE; add(path, std::vector()); open_path = path; return TileStoreResult::OK; } virtual TileStoreResult writeChunk(const std::uint8_t* data, std::size_t size, std::size_t& written) { if (!available) return TileStoreResult::STORAGE_UNAVAILABLE; const int i = find(open_path.c_str()); if (i < 0) return TileStoreResult::IO_ERROR; written = (short_write && size != 0U) ? size - 1U : size; files[static_cast(i)].bytes.insert(files[static_cast(i)].bytes.end(), data, data + written); return TileStoreResult::OK; } virtual TileStoreResult commitWrite() { open_path.clear(); return !available ? TileStoreResult::STORAGE_UNAVAILABLE : (fail_commit ? TileStoreResult::IO_ERROR : TileStoreResult::OK); } virtual void abortWrite() { if (!open_path.empty()) remove(open_path.c_str()); open_path.clear(); } virtual TileStoreResult remove(const char* path) { if (!available) return TileStoreResult::STORAGE_UNAVAILABLE; ++remove_calls; if (fail_remove || (fail_remove_call == remove_calls) || fail_remove_path == path) return TileStoreResult::IO_ERROR; const int i = find(path); if (i < 0) return TileStoreResult::MISS; files.erase(files.begin() + i); return TileStoreResult::OK; } virtual TileStoreResult rename(const char* from, const char* to) { if (!available) return TileStoreResult::STORAGE_UNAVAILABLE; ++rename_calls; if (fail_rename_call == rename_calls || fail_rename_call2 == rename_calls) return TileStoreResult::IO_ERROR; const int i = find(from); if (i < 0) return TileStoreResult::MISS; remove(to); files[static_cast(i >= find(from) ? find(from) : 0)].path = to; if (power_cut_after_rename_call == rename_calls) available = false; return TileStoreResult::OK; } virtual TileStoreResult stat(const char* path, std::uint32_t& size) { if (!available) return TileStoreResult::STORAGE_UNAVAILABLE; const int i = find(path); if (i < 0) return TileStoreResult::MISS; size = static_cast(files[static_cast(i)].bytes.size()); return TileStoreResult::OK; } virtual TileStoreResult beginList() { list_position = 0U; return available ? TileStoreResult::OK : TileStoreResult::STORAGE_UNAVAILABLE; } virtual TileStoreResult nextList(char* path, std::size_t capacity, bool& done) { if (!available) return TileStoreResult::STORAGE_UNAVAILABLE; if (list_position >= files.size()) { done = true; return TileStoreResult::OK; } done = false; const std::string& p = files[list_position++].path; if (p.size() + 1U > capacity) return TileStoreResult::INDEX_MISMATCH; std::memcpy(path, p.c_str(), p.size() + 1U); return TileStoreResult::OK; } virtual void endList() {} }; std::vector png(std::size_t size = 40U, std::uint32_t w = 256U, std::uint32_t h = 256U) { std::vector b(size, 0U); const std::uint8_t sig[] = {137U,80U,78U,71U,13U,10U,26U,10U}; if (size >= 24U) { std::memcpy(&b[0], sig, 8U); b[11] = 13U; b[12]='I'; b[13]='H'; b[14]='D'; b[15]='R'; b[16]=static_cast(w>>24); b[17]=static_cast(w>>16); b[18]=static_cast(w>>8); b[19]=static_cast(w); b[20]=static_cast(h>>24); b[21]=static_cast(h>>16); b[22]=static_cast(h>>8); b[23]=static_cast(h); } return b; } void putU32(std::uint8_t* p,std::uint32_t v){p[0]=static_cast(v>>24U);p[1]=static_cast(v>>16U);p[2]=static_cast(v>>8U);p[3]=static_cast(v);} std::uint32_t crc32(const std::uint8_t* p,std::size_t n){std::uint32_t c=UINT32_C(0xffffffff);for(std::size_t i=0;i>1U)^((c&1U)?UINT32_C(0xedb88320):0U);}return ~c;} void putKey(std::uint8_t* p,const TileKey& k){p[0]=k.zoom;putU32(p+1,k.x);putU32(p+5,k.y);} std::vector evictionManifest(const TileKey& candidate,const std::vector& victims){std::vector r(21U+9U*victims.size(),0U);r[0]='P';r[1]='Y';r[2]='E';r[3]='V';r[4]=1U;r[6]=static_cast(victims.size()>>8U);r[7]=static_cast(victims.size());putKey(&r[8],candidate);for(std::size_t i=0;i& b, std::size_t split=17U) { TileStoreResult r=s.beginPut(k); if (r!=TileStoreResult::OK) return r; for (std::size_t p=0U;p bad=png(); bad[0]=0U; CHECK(put(s,TileKey{0U,0U,0U},bad)==TileStoreResult::INVALID_PNG); CHECK(put(s,TileKey{0U,0U,0U},png(20U))==TileStoreResult::INVALID_PNG); CHECK(put(s,TileKey{0U,0U,0U},png(40U,255U,256U))==TileStoreResult::INVALID_PNG); CHECK(put(s,TileKey{0U,0U,0U},png(81U))==TileStoreResult::TOO_LARGE); } void testShortWriteAbortsTemp() { beginTest(); FakeStorage fs; MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::OK); fs.short_write=true; CHECK(put(s,TileKey{0U,0U,0U},png())==TileStoreResult::IO_ERROR); CHECK(fs.files.empty()); } void testCommitFailureDoesNotAcknowledgeOrReplaceLive() { beginTest(); FakeStorage fs; MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::OK); const TileKey key={0U,0U,0U}; CHECK(put(s,key,png())==TileStoreResult::OK); fs.fail_commit=true; CHECK(put(s,key,png(50U))==TileStoreResult::IO_ERROR); fs.fail_commit=false; drain(s,key,40U); } void testExactQuotaAndLruEviction() { beginTest(); FakeStorage fs; MapTileStore s(fs,config(3U,80U,80U)); CHECK(s.initialize()==TileStoreResult::OK); CHECK(put(s,TileKey{1U,0U,0U},png())==TileStoreResult::OK); CHECK(put(s,TileKey{1U,1U,0U},png())==TileStoreResult::OK); std::uint32_t n=0U; CHECK(s.beginGet(TileKey{1U,0U,0U},n)==TileStoreResult::OK); s.endGet(); CHECK(put(s,TileKey{1U,0U,1U},png())==TileStoreResult::OK); CHECK(s.beginGet(TileKey{1U,1U,0U},n)==TileStoreResult::MISS); CHECK(s.totalBytes()==80U); } void testDuplicateAtomicReplacement() { beginTest(); FakeStorage fs; MapTileStore s(fs,config(2U,100U,80U)); CHECK(s.initialize()==TileStoreResult::OK); TileKey k={0U,0U,0U}; CHECK(put(s,k,png(40U))==TileStoreResult::OK); CHECK(put(s,k,png(60U))==TileStoreResult::OK); CHECK(s.entryCount()==1U); CHECK(s.totalBytes()==60U); drain(s,k,60U); } void testInterruptedFilesRecover() { beginTest(); FakeStorage fs; fs.add("/pyxis-map/tiles/1/0/0.png.bak",png(40U)); fs.add("/pyxis-map/tiles/1/1/0.png.tmp",png(40U)); MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::OK); CHECK(fs.find("/pyxis-map/tiles/1/0/0.png")>=0); CHECK(fs.find("/pyxis-map/tiles/1/1/0.png.tmp")<0); CHECK(s.entryCount()==1U); } void testLiveWinsRecovery() { beginTest(); FakeStorage fs; fs.add("/pyxis-map/tiles/0/0/0.png",png(40U)); fs.add("/pyxis-map/tiles/0/0/0.png.bak",png(60U)); fs.add("/pyxis-map/tiles/0/0/0.png.tmp",png(50U)); MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::OK); CHECK(fs.files.size()==1U); CHECK(s.totalBytes()==40U); } void testCorruptLiveRecoversValidBackup() { beginTest(); FakeStorage fs; std::vector bad=png(40U); bad[0]=0U; fs.add("/pyxis-map/tiles/0/0/0.png",bad); fs.add("/pyxis-map/tiles/0/0/0.png.bak",png(60U)); MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::OK); CHECK(s.entryCount()==1U); CHECK(s.totalBytes()==60U); drain(s,TileKey{0U,0U,0U},60U); } void testCorruptLiveWithoutBackupIsRemoved() { beginTest(); FakeStorage fs; std::vector bad=png(40U); bad[0]=0U; fs.add("/pyxis-map/tiles/0/0/0.png",bad); MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::OK); CHECK(s.entryCount()==0U); CHECK(fs.files.empty()); } void testStaleTempRemovalFailureAbortsPut() { beginTest(); FakeStorage fs; MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::OK); fs.add("/pyxis-map/tiles/0/0/0.png.tmp",png()); fs.fail_remove=true; CHECK(s.beginPut(TileKey{0U,0U,0U})==TileStoreResult::IO_ERROR); CHECK(fs.find("/pyxis-map/tiles/0/0/0.png.tmp")>=0); } void testRecoveryRejectsMalformedAndExhaustion() { beginTest(); FakeStorage fs; fs.add("/pyxis-map/tiles/0/0/../evil.png",png()); MapTileStore a(fs,config()); CHECK(a.initialize()==TileStoreResult::INDEX_MISMATCH); FakeStorage fs2; fs2.add("/pyxis-map/tiles/1/0/0.png",png()); fs2.add("/pyxis-map/tiles/1/1/0.png",png()); MapTileStore b(fs2,config(1U,100U,80U)); CHECK(b.initialize()==TileStoreResult::INDEX_FULL); } void testRecoveryQuotaFailsClosed() { beginTest(); FakeStorage fs; fs.add("/pyxis-map/tiles/1/0/0.png",png(60U)); fs.add("/pyxis-map/tiles/1/1/0.png",png(60U)); MapTileStore s(fs,config(3U,100U,80U)); CHECK(s.initialize()==TileStoreResult::QUOTA_EXCEEDED); } void testRenameFailureRestoresDuplicate() { beginTest(); FakeStorage fs; MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::OK); TileKey k={0U,0U,0U}; CHECK(put(s,k,png())==TileStoreResult::OK); fs.fail_rename_call=fs.rename_calls+2; CHECK(put(s,k,png(50U))==TileStoreResult::IO_ERROR); drain(s,k,40U); } void testDuplicateRollbackFailureInvalidatesStore() { beginTest(); FakeStorage fs; MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::OK); TileKey k={0U,0U,0U}; CHECK(put(s,k,png())==TileStoreResult::OK); fs.fail_rename_call=fs.rename_calls+2; fs.fail_rename_call2=fs.rename_calls+3; CHECK(put(s,k,png(50U))==TileStoreResult::IO_ERROR); std::uint32_t size=0U; CHECK(s.beginGet(k,size)==TileStoreResult::NOT_INITIALIZED); } void testPromotionFailureDoesNotEvictVictims() { beginTest(); FakeStorage fs; MapTileStore s(fs,config(3U,80U,80U)); CHECK(s.initialize()==TileStoreResult::OK); const TileKey a={1U,0U,0U}, b={1U,1U,0U}, c={1U,0U,1U}; CHECK(put(s,a,png())==TileStoreResult::OK); CHECK(put(s,b,png())==TileStoreResult::OK); fs.fail_rename_call=fs.rename_calls+1; CHECK(put(s,c,png())==TileStoreResult::IO_ERROR); CHECK(s.entryCount()==2U); CHECK(s.totalBytes()==80U); drain(s,a,40U); drain(s,b,40U); } void testEvictionPreflightFailurePreservesAllVictims() { beginTest(); FakeStorage fs; MapTileStore s(fs,config(3U,80U,80U)); CHECK(s.initialize()==TileStoreResult::OK); const TileKey a={1U,0U,0U}, b={1U,1U,0U}, c={1U,0U,1U}; CHECK(put(s,a,png())==TileStoreResult::OK); CHECK(put(s,b,png())==TileStoreResult::OK); fs.fail_remove_path="/pyxis-map/tiles/.evict.txn.tmp"; CHECK(put(s,c,png(80U))==TileStoreResult::IO_ERROR); fs.fail_remove_path.clear(); CHECK(s.entryCount()==2U); CHECK(s.totalBytes()==80U); drain(s,a,40U); drain(s,b,40U); } void testEvictionStageFailureRollsBackAllVictims() { beginTest(); FakeStorage fs; MapTileStore s(fs,config(3U,80U,80U)); CHECK(s.initialize()==TileStoreResult::OK); const TileKey a={1U,0U,0U}, b={1U,1U,0U}, c={1U,0U,1U}; CHECK(put(s,a,png())==TileStoreResult::OK); CHECK(put(s,b,png())==TileStoreResult::OK); fs.fail_rename_call=fs.rename_calls+4; CHECK(put(s,c,png(80U))==TileStoreResult::IO_ERROR); CHECK(s.entryCount()==2U); CHECK(s.totalBytes()==80U); drain(s,a,40U); drain(s,b,40U); } void testEvictionPowerCutsRestoreWholeOldGeneration() { beginTest(); for (int cut=2;cut<=4;++cut) { FakeStorage fs; MapTileStore s(fs,config(3U,80U,80U)); CHECK(s.initialize()==TileStoreResult::OK); const TileKey a={1U,0U,0U}, b={1U,1U,0U}, c={1U,0U,1U}; CHECK(put(s,a,png())==TileStoreResult::OK); CHECK(put(s,b,png())==TileStoreResult::OK); fs.power_cut_after_rename_call=fs.rename_calls+cut; CHECK(put(s,c,png(80U))!=TileStoreResult::OK); fs.available=true; fs.power_cut_after_rename_call=0; MapTileStore recovered(fs,config(3U,80U,80U)); CHECK(recovered.initialize()==TileStoreResult::OK); CHECK(recovered.entryCount()==2U); CHECK(recovered.totalBytes()==80U); drain(recovered,a,40U); drain(recovered,b,40U); } } void testDuplicateEvictionPowerCutsRestoreOldCandidateAndVictim() { beginTest(); for (int cut=1;cut<=4;++cut) { FakeStorage fs; MapTileStore s(fs,config(2U,80U,80U)); CHECK(s.initialize()==TileStoreResult::OK); const TileKey a={1U,0U,0U}, b={1U,1U,0U}; CHECK(put(s,a,png())==TileStoreResult::OK); CHECK(put(s,b,png())==TileStoreResult::OK); fs.power_cut_after_rename_call=fs.rename_calls+cut; CHECK(put(s,a,png(80U))!=TileStoreResult::OK); fs.available=true; fs.power_cut_after_rename_call=0; MapTileStore recovered(fs,config(2U,80U,80U)); CHECK(recovered.initialize()==TileStoreResult::OK); CHECK(recovered.entryCount()==2U); CHECK(recovered.totalBytes()==80U); drain(recovered,a,40U); drain(recovered,b,40U); } } void testStaleDuplicateBackupMustClearBeforeManifest() { beginTest(); FakeStorage fs; MapTileStore s(fs,config(2U,80U,80U)); CHECK(s.initialize()==TileStoreResult::OK); const TileKey a={1U,0U,0U}, b={1U,1U,0U}; CHECK(put(s,a,png())==TileStoreResult::OK); CHECK(put(s,b,png())==TileStoreResult::OK); fs.add("/pyxis-map/tiles/1/0/0.png.bak",png(60U)); fs.fail_remove_path="/pyxis-map/tiles/1/0/0.png.bak"; CHECK(put(s,a,png(80U))==TileStoreResult::IO_ERROR); fs.fail_remove_path.clear(); CHECK(s.entryCount()==2U); CHECK(s.totalBytes()==80U); drain(s,a,40U); drain(s,b,40U); } void testSemanticManifestValidationPrecedesMutation() { beginTest(); FakeStorage fs; const TileKey candidate={1U,0U,1U}, victim={1U,0U,0U}, invalid={23U,0U,0U}; fs.add("/pyxis-map/tiles/1/0/1.png",png(80U)); fs.add("/pyxis-map/tiles/1/0/0.png",png()); fs.add("/pyxis-map/tiles/.evict.txn",evictionManifest(candidate,std::vector{victim,invalid})); MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::INDEX_MISMATCH); CHECK(fs.find("/pyxis-map/tiles/1/0/1.png")>=0); } void testMalformedEvictionManifestFailsClosed() { beginTest(); FakeStorage fs; fs.add("/pyxis-map/tiles/.evict.txn",std::vector(21U,0U)); MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::INDEX_MISMATCH); } void testPostCommitCleanupResidueKeepsWholeNewGeneration() { beginTest(); FakeStorage fs; const TileKey a={1U,0U,0U}, b={1U,1U,0U}, c={1U,0U,1U}; fs.add("/pyxis-map/tiles/1/0/0.png.evict",png()); fs.add("/pyxis-map/tiles/1/1/0.png",png()); fs.add("/pyxis-map/tiles/1/0/1.png",png()); MapTileStore s(fs,config(2U,80U,80U)); CHECK(s.initialize()==TileStoreResult::OK); CHECK(s.entryCount()==2U); CHECK(s.totalBytes()==80U); drain(s,b,40U); drain(s,c,40U); std::uint32_t size=0U; CHECK(s.beginGet(a,size)==TileStoreResult::MISS); CHECK(fs.find("/pyxis-map/tiles/1/0/0.png.evict")<0); } void testHardMaxLivePlusCrashTempRecovers() { beginTest(); FakeStorage fs; for(std::uint32_t i=0U;i<128U;++i) { const std::string path="/pyxis-map/tiles/8/"+std::to_string(i)+"/0.png"; fs.add(path.c_str(),png()); } fs.add("/pyxis-map/tiles/8/128/0.png.tmp",png()); MapTileStore s(fs,config(128U,5120U,80U)); CHECK(s.initialize()==TileStoreResult::OK); CHECK(s.entryCount()==128U); CHECK(fs.find("/pyxis-map/tiles/8/128/0.png.tmp")<0); } void testUnboundedCommittedEvictionResidueCleansInBatches() { beginTest(); FakeStorage fs; for(std::uint32_t i=0U;i<129U;++i) { const std::string path="/pyxis-map/tiles/8/"+std::to_string(i)+"/0.png.evict"; fs.add(path.c_str(),png()); } MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::OK); CHECK(s.entryCount()==0U); for(std::uint32_t i=0U;i<129U;++i) { const std::string path="/pyxis-map/tiles/8/"+std::to_string(i)+"/0.png.evict"; CHECK(fs.find(path.c_str())<0); } } void testDeterministicStress() { beginTest(); FakeStorage fs; MapTileStore s(fs,config(3U,120U,80U)); CHECK(s.initialize()==TileStoreResult::OK); CHECK(put(s,TileKey{2U,0U,0U},png())==TileStoreResult::OK); std::uint32_t size=0U; for(std::uint32_t i=0U;i<100000U;++i) { const TileKey k={2U,i&3U,(i>>2)&3U}; TileStoreResult r=s.beginGet(k,size); CHECK(r==TileStoreResult::OK||r==TileStoreResult::MISS); if(r==TileStoreResult::OK)s.endGet(); } } } int main() { testKeyAndCanonicalPath(); testMissHitAndRemoval(); testMalformedPngs(); testShortWriteAbortsTemp(); testCommitFailureDoesNotAcknowledgeOrReplaceLive(); testExactQuotaAndLruEviction(); testDuplicateAtomicReplacement(); testInterruptedFilesRecover(); testLiveWinsRecovery(); testCorruptLiveRecoversValidBackup(); testCorruptLiveWithoutBackupIsRemoved(); testStaleTempRemovalFailureAbortsPut(); testRecoveryRejectsMalformedAndExhaustion(); testRecoveryQuotaFailsClosed(); testRenameFailureRestoresDuplicate(); testDuplicateRollbackFailureInvalidatesStore(); testPromotionFailureDoesNotEvictVictims(); testEvictionPreflightFailurePreservesAllVictims(); testEvictionStageFailureRollsBackAllVictims(); testEvictionPowerCutsRestoreWholeOldGeneration(); testDuplicateEvictionPowerCutsRestoreOldCandidateAndVictim(); testStaleDuplicateBackupMustClearBeforeManifest(); testSemanticManifestValidationPrecedesMutation(); testMalformedEvictionManifestFailsClosed(); testPostCommitCleanupResidueKeepsWholeNewGeneration(); testHardMaxLivePlusCrashTempRecovers(); testUnboundedCommittedEvictionResidueCleansInBatches(); testDeterministicStress(); std::cout<<"map tile store: "<