diff --git a/lib/lxst_audio/es7210.cpp b/lib/lxst_audio/es7210.cpp index b3bcd21e..3ca33c77 100644 --- a/lib/lxst_audio/es7210.cpp +++ b/lib/lxst_audio/es7210.cpp @@ -258,8 +258,10 @@ esp_err_t es7210_adc_init(TwoWire *tw, audio_hal_codec_config_t *codec_cfg) // Runtime register poke for the T:REG diagnostic harness — write/read any ES7210 register // over I2C while capturing, to probe the mic analog config (MICBIAS REG41/42, VMID REG40, // ADC DC-block HPF REG22/23, etc.) without reflashing for every guess. +#ifdef PYXIS_TEST_HOOKS extern "C" void pyxis_es7210_write_reg(int addr, int val) { es7210_write_reg((uint8_t)addr, (uint8_t)val); } extern "C" int pyxis_es7210_read_reg(int addr) { return es7210_read_reg((uint8_t)addr); } +#endif esp_err_t es7210_adc_deinit() { diff --git a/lib/tdeck_ui/Hardware/TDeck/MapTileStore.cpp b/lib/tdeck_ui/Hardware/TDeck/MapTileStore.cpp index 7ae52273..bc48991c 100644 --- a/lib/tdeck_ui/Hardware/TDeck/MapTileStore.cpp +++ b/lib/tdeck_ui/Hardware/TDeck/MapTileStore.cpp @@ -273,6 +273,21 @@ TileStoreResult MapTileStore::readGetChunk(std::uint8_t* output, std::size_t cap void MapTileStore::endGet() { if (read_open_) storage_.endRead(); read_open_ = false; } +TileStoreResult MapTileStore::removeTile(const TileKey& key) { + if (!initialized_) return TileStoreResult::NOT_INITIALIZED; + if (read_open_ || write_open_) return TileStoreResult::BUSY; + if (!storage_.isAvailable()) return TileStoreResult::STORAGE_UNAVAILABLE; + const int index = findEntry(key); + if (index < 0) return TileStoreResult::MISS; + char path[PATH_CAPACITY] = {}; + TileStoreResult result = canonicalPath(key, path, sizeof(path)); + if (result != TileStoreResult::OK) return result; + result = storage_.remove(path); + if (result != TileStoreResult::OK && result != TileStoreResult::MISS) return result; + removeEntry(static_cast(index)); + return TileStoreResult::OK; +} + TileStoreResult MapTileStore::beginPut(const TileKey& key) { if (!initialized_) return TileStoreResult::NOT_INITIALIZED; if (read_open_ || write_open_) return TileStoreResult::BUSY; @@ -355,8 +370,6 @@ TileStoreResult MapTileStore::finishPut() { TileStoreResult result = storage_.commitWrite(); if (result != TileStoreResult::OK) { failPut(); return result; } write_open_ = false; - result = evictFor(put_key_, put_size_); - if (result != TileStoreResult::OK) { storage_.remove(put_temp_); return result; } int index = findEntry(put_key_); const bool duplicate = index >= 0; if (duplicate) { @@ -370,6 +383,16 @@ TileStoreResult MapTileStore::finishPut() { storage_.remove(put_temp_); return result; } + // Only commit quota eviction after the candidate is safely promoted. If + // eviction cannot complete, remove the candidate and restore a replaced + // live generation rather than losing the new tile and victims up front. + result = evictFor(put_key_, put_size_); + if (result != TileStoreResult::OK) { + storage_.remove(put_live_); + if (duplicate) storage_.rename(put_backup_, put_live_); + return result; + } + index = findEntry(put_key_); if (duplicate) { storage_.remove(put_backup_); Entry& entry = entries_[static_cast(index)]; diff --git a/lib/tdeck_ui/Hardware/TDeck/MapTileStore.h b/lib/tdeck_ui/Hardware/TDeck/MapTileStore.h index 964a57a5..59c492c9 100644 --- a/lib/tdeck_ui/Hardware/TDeck/MapTileStore.h +++ b/lib/tdeck_ui/Hardware/TDeck/MapTileStore.h @@ -82,6 +82,7 @@ public: TileStoreResult beginGet(const TileKey& key, std::uint32_t& size); TileStoreResult readGetChunk(std::uint8_t* output, std::size_t capacity, std::size_t& count); void endGet(); + TileStoreResult removeTile(const TileKey& key); TileStoreResult beginPut(const TileKey& key); TileStoreResult writePutChunk(const std::uint8_t* data, std::size_t size); diff --git a/lib/tdeck_ui/UI/LXMF/MapScreen.cpp b/lib/tdeck_ui/UI/LXMF/MapScreen.cpp index ed63eaea..1fcefd60 100644 --- a/lib/tdeck_ui/UI/LXMF/MapScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/MapScreen.cpp @@ -11,6 +11,7 @@ #include "../LVGL/LVGLLock.h" #include +#include #define LODEPNG_NO_COMPILE_CPP extern "C" { #include @@ -268,7 +269,16 @@ void MapScreen::stopWorker() { if (!worker_started_) return; stop_requested_.store(true, std::memory_order_release); if (worker_task_) xTaskNotifyGive(worker_task_); + const TickType_t started = xTaskGetTickCount(); + const TickType_t shutdown_limit = pdMS_TO_TICKS(20000); while (!worker_exited_.load(std::memory_order_acquire)) { + if ((xTaskGetTickCount() - started) >= shutdown_limit) { + // Do not destroy storage/decoder/transport state while the worker + // may still own it. A controlled restart is safer than UAF or an + // unbounded UI teardown after a wedged hardware operation. + esp_restart(); + for (;;) vTaskDelay(pdMS_TO_TICKS(1000)); + } vTaskDelay(pdMS_TO_TICKS(1)); } worker_task_ = nullptr; @@ -421,7 +431,11 @@ Pyxis::MapTileLoadResult MapScreen::readTile( &width, &height, &decode_state, compressed_staging_, total); if (decode_error != 0U || width != 256U || height != 256U) { lodepng_state_cleanup(&decode_state); - return Pyxis::MapTileLoadResult::INVALID_PNG; + const Hardware::TDeck::TileStoreResult removed = store_.removeTile(request.key); + return (removed == Hardware::TDeck::TileStoreResult::OK || + removed == Hardware::TDeck::TileStoreResult::MISS) + ? Pyxis::MapTileLoadResult::MISS + : Pyxis::MapTileLoadResult::INVALID_PNG; } decode_state.info_raw.colortype = LCT_RGB; decode_state.info_raw.bitdepth = 8U; @@ -430,7 +444,11 @@ Pyxis::MapTileLoadResult MapScreen::readTile( lodepng_state_cleanup(&decode_state); if (decode_error != 0U || rgb == nullptr || width != 256U || height != 256U) { if (rgb) lv_mem_free(rgb); - return Pyxis::MapTileLoadResult::INVALID_PNG; + const Hardware::TDeck::TileStoreResult removed = store_.removeTile(request.key); + return (removed == Hardware::TDeck::TileStoreResult::OK || + removed == Hardware::TDeck::TileStoreResult::MISS) + ? Pyxis::MapTileLoadResult::MISS + : Pyxis::MapTileLoadResult::INVALID_PNG; } if (request.slot_index >= TILE_COUNT || !tile_pixels_[request.slot_index]) { lv_mem_free(rgb); diff --git a/lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp b/lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp index b0ed058f..7d3c0393 100644 --- a/lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp @@ -74,7 +74,7 @@ SettingsScreen::SettingsScreen(lv_obj_t* parent) _transport_modal_group(nullptr), _transport_enable_confirmed(false), _switch_map_download(nullptr), _btn_propagation_nodes(nullptr), _switch_prop_fallback(nullptr), _switch_prop_only(nullptr), - _gps(nullptr) { + _save_state(0U), _gps(nullptr) { LVGL_LOCK(); // Create screen object @@ -1088,52 +1088,57 @@ void SettingsScreen::load_settings() { void SettingsScreen::save_settings() { update_settings_from_ui(); + std::uint8_t expected = 0U; + if (!_save_state.compare_exchange_strong( + expected, 2U, std::memory_order_acq_rel)) { + WARNING("Settings save already pending"); + return; + } + _pending_save_settings = _settings; + _save_state.store(1U, std::memory_order_release); +} + +void SettingsScreen::service_pending_save() { + std::uint8_t expected = 1U; + if (!_save_state.compare_exchange_strong( + expected, 2U, std::memory_order_acq_rel)) return; + const AppSettings settings = _pending_save_settings; + _save_state.store(0U, std::memory_order_release); Preferences prefs; - prefs.begin(NVS_NAMESPACE, false); // read-write - - prefs.putString(KEY_WIFI_SSID, _settings.wifi_ssid); - prefs.putString(KEY_WIFI_PASS, _settings.wifi_password); - prefs.putString(KEY_TCP_HOST, _settings.tcp_host); - prefs.putUShort(KEY_TCP_PORT, _settings.tcp_port); - prefs.putString(KEY_DISPLAY_NAME, _settings.display_name); - prefs.putUChar(KEY_BRIGHTNESS, _settings.brightness); - prefs.putBool(KEY_KB_LIGHT, _settings.keyboard_light); - prefs.putUShort(KEY_TIMEOUT, _settings.screen_timeout); - prefs.putUInt(KEY_ANNOUNCE_INT, _settings.announce_interval); - prefs.putUInt(KEY_SYNC_INT, _settings.sync_interval); - prefs.putBool(KEY_GPS_SYNC, _settings.gps_time_sync); - prefs.putBool(KEY_TRANSPORT_ENABLED, _settings.transport_enabled); - prefs.putBool(KEY_MAP_DOWNLOAD, _settings.map_download_enabled); - - // Notification settings - prefs.putBool(KEY_NOTIF_SND, _settings.notification_sound); - prefs.putUChar(KEY_NOTIF_VOL, _settings.notification_volume); - - // Interface settings - prefs.putBool(KEY_TCP_ENABLED, _settings.tcp_enabled); - prefs.putBool(KEY_LORA_ENABLED, _settings.lora_enabled); - prefs.putFloat(KEY_LORA_FREQ, _settings.lora_frequency); - prefs.putFloat(KEY_LORA_BW, _settings.lora_bandwidth); - prefs.putUChar(KEY_LORA_SF, _settings.lora_sf); - prefs.putUChar(KEY_LORA_CR, _settings.lora_cr); - prefs.putChar(KEY_LORA_POWER, _settings.lora_power); - prefs.putBool(KEY_AUTO_ENABLED, _settings.auto_enabled); - prefs.putBool(KEY_BLE_ENABLED, _settings.ble_enabled); - - // Propagation settings - prefs.putBool(KEY_PROP_AUTO, _settings.prop_auto_select); - prefs.putString(KEY_PROP_NODE, _settings.prop_selected_node); - prefs.putBool(KEY_PROP_FALLBACK, _settings.prop_fallback_enabled); - prefs.putBool(KEY_PROP_ONLY, _settings.prop_only); - + prefs.begin(NVS_NAMESPACE, false); + prefs.putString(KEY_WIFI_SSID, settings.wifi_ssid); + prefs.putString(KEY_WIFI_PASS, settings.wifi_password); + prefs.putString(KEY_TCP_HOST, settings.tcp_host); + prefs.putUShort(KEY_TCP_PORT, settings.tcp_port); + prefs.putString(KEY_DISPLAY_NAME, settings.display_name); + prefs.putUChar(KEY_BRIGHTNESS, settings.brightness); + prefs.putBool(KEY_KB_LIGHT, settings.keyboard_light); + prefs.putUShort(KEY_TIMEOUT, settings.screen_timeout); + prefs.putUInt(KEY_ANNOUNCE_INT, settings.announce_interval); + prefs.putUInt(KEY_SYNC_INT, settings.sync_interval); + prefs.putBool(KEY_GPS_SYNC, settings.gps_time_sync); + prefs.putBool(KEY_TRANSPORT_ENABLED, settings.transport_enabled); + prefs.putBool(KEY_MAP_DOWNLOAD, settings.map_download_enabled); + prefs.putBool(KEY_NOTIF_SND, settings.notification_sound); + prefs.putUChar(KEY_NOTIF_VOL, settings.notification_volume); + prefs.putBool(KEY_TCP_ENABLED, settings.tcp_enabled); + prefs.putBool(KEY_LORA_ENABLED, settings.lora_enabled); + prefs.putFloat(KEY_LORA_FREQ, settings.lora_frequency); + prefs.putFloat(KEY_LORA_BW, settings.lora_bandwidth); + prefs.putUChar(KEY_LORA_SF, settings.lora_sf); + prefs.putUChar(KEY_LORA_CR, settings.lora_cr); + prefs.putChar(KEY_LORA_POWER, settings.lora_power); + prefs.putBool(KEY_AUTO_ENABLED, settings.auto_enabled); + prefs.putBool(KEY_BLE_ENABLED, settings.ble_enabled); + prefs.putBool(KEY_PROP_AUTO, settings.prop_auto_select); + prefs.putString(KEY_PROP_NODE, settings.prop_selected_node); + prefs.putBool(KEY_PROP_FALLBACK, settings.prop_fallback_enabled); + prefs.putBool(KEY_PROP_ONLY, settings.prop_only); prefs.end(); INFO("Settings saved to NVS"); - - if (_save_callback) { - _save_callback(_settings); - } + if (_save_callback) _save_callback(settings); } void SettingsScreen::update_ui_from_settings() { diff --git a/lib/tdeck_ui/UI/LXMF/SettingsScreen.h b/lib/tdeck_ui/UI/LXMF/SettingsScreen.h index 5e7f3fa0..50d1d6b3 100644 --- a/lib/tdeck_ui/UI/LXMF/SettingsScreen.h +++ b/lib/tdeck_ui/UI/LXMF/SettingsScreen.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -160,11 +161,12 @@ public: */ void load_settings(); - /** - * Save settings to NVS - */ + /** Capture a save request from LVGL without persistence or network I/O. */ void save_settings(); + /** Persist and apply one pending snapshot from the main owner loop. */ + void service_pending_save(); + /** * Get current settings */ @@ -310,6 +312,8 @@ private: // Data AppSettings _settings; + AppSettings _pending_save_settings; + std::atomic _save_state; // 0 idle, 1 pending, 2 processing RNS::Bytes _identity_hash; RNS::Bytes _lxmf_address; TinyGPSPlus* _gps; diff --git a/lib/tdeck_ui/UI/LXMF/UIManager.cpp b/lib/tdeck_ui/UI/LXMF/UIManager.cpp index cec5c957..4acb0c31 100644 --- a/lib/tdeck_ui/UI/LXMF/UIManager.cpp +++ b/lib/tdeck_ui/UI/LXMF/UIManager.cpp @@ -587,6 +587,9 @@ bool UIManager::init() { } void UIManager::update() { + // Settings Save only publishes a snapshot from the LVGL event. Persistence + // and interface changes execute here on the main owner loop, before LVGL. + if (_settings_screen) _settings_screen->service_pending_save(); // Flush display-name write-throughs the last conversation-list refresh // deferred. Done here, BEFORE LVGL_LOCK, so the microStore/LittleFS I/O // never runs under the render lock (same reason as on_message_received). diff --git a/src/main.cpp b/src/main.cpp index 8aa111b7..c89de4dc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -244,6 +244,7 @@ extern "C" void pyxis_log(const char* msg) { } } +#ifdef PYXIS_TEST_HOOKS // --- Audio loopback PCM dump (test harness) --------------------------------- // In LOOPBACK test mode the decoded PCM is streamed over a SECOND multicast // destination (239.0.99.99:9998) so the Mac harness can score voice quality. @@ -351,6 +352,17 @@ extern "C" void pyxis_audio_dump(const void* pcm, size_t bytes) { g_audio_dump_offset += (uint32_t)chunk; } } +#else +// Production keeps only ABI-compatible constant-time stubs used by the audio +// pipeline. Diagnostic state, buffers, multicast output, register access and +// recorder synchronization are absent from the image. +extern "C" bool pyxis_rawmic_mode() { return false; } +extern "C" int pyxis_rawmic_stage() { return 0; } +extern "C" bool pyxis_record_active() { return false; } +extern "C" void pyxis_record_write_ch0(const int16_t*, int) {} +extern "C" void pyxis_audio_dump_arm(bool) {} +extern "C" void pyxis_audio_dump(const void*, size_t) {} +#endif // Forward declarations void start_tcp_interface(); @@ -1447,37 +1459,19 @@ void setup_ui_manager() { WARNING("Transport mode setting changed; reboot required before it takes effect"); } - // Handle WiFi credential changes - auto reconnect + // Reconnect is serviced by the main loop's existing bounded, + // watchdog-fed reconnect path after this settings application + // releases the router lock. if (wifi_settings_changed && new_settings.wifi_ssid.length() > 0) { - INFO(("WiFi credentials changed, reconnecting to: " + new_settings.wifi_ssid).c_str()); - udp_log_ready = false; // Suspend UDP logging during WiFi transition - WiFi.disconnect(); - delay(100); - WiFi.begin(new_settings.wifi_ssid.c_str(), new_settings.wifi_password.c_str()); - - // Wait for connection (with timeout) - uint32_t start = millis(); - while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) { - delay(100); - } - - if (WiFi.status() == WL_CONNECTED) { - udp_log_init(); // Rebind to new WiFi interface IP - udp_log_ready = true; // Resume UDP logging - INFO(("WiFi connected! IP: " + WiFi.localIP().toString()).c_str()); - } else { - WARNING("WiFi connection failed"); - } + pending_wifi_ssid = new_settings.wifi_ssid; + pending_wifi_password = new_settings.wifi_password; + wifi_reconnect_pending = true; + INFO(("WiFi reconnect queued for: " + new_settings.wifi_ssid).c_str()); } - // Update router display name + // Update router display name while the callback's outer RouterLock is held. if (router && !new_settings.display_name.isEmpty()) { - UI::LXMF::RouterLock router_lock(0); - if (router_lock.acquired()) { - router->set_display_name(new_settings.display_name.c_str()); - } else { - WARNING("Router busy; display-name update not applied"); - } + router->set_display_name(new_settings.display_name.c_str()); } // Handle TCP interface changes at runtime @@ -1717,11 +1711,13 @@ void setup() { // GATT operations. Task subscriptions are added at their creation sites. ESP_ERROR_CHECK(esp_task_wdt_init(60, true)); +#ifdef PYXIS_TEST_HOOKS // Create diagnostic recorder synchronization before any audio task starts. g_rec_mutex = xSemaphoreCreateMutex(); if (!g_rec_mutex) { ERROR("Failed to create recorder mutex; T:RECORD will be unavailable"); } +#endif INFO("\n"); INFO("╔══════════════════════════════════════╗"); diff --git a/tests/build_scripts/test_map_screen_contract.py b/tests/build_scripts/test_map_screen_contract.py index e7197273..555a166b 100644 --- a/tests/build_scripts/test_map_screen_contract.py +++ b/tests/build_scripts/test_map_screen_contract.py @@ -52,6 +52,7 @@ def test_worker_predecodes_and_render_path_has_no_io(): assert "lodepng_decode(" in source assert "lodepng_inspect" in source assert "max_output_size" in source + assert "store_.removeTile(request.key)" in source assert "beginGet" in source and "readGetChunk" in source assert 'lv_img_set_src(tile_images_[index], &tile_descriptors_[index])' in source assert 'lv_img_set_src(tile_images_[index], "' not in source diff --git a/tests/build_scripts/test_map_tile_downloader_contract.py b/tests/build_scripts/test_map_tile_downloader_contract.py index aea09eac..98011ea9 100644 --- a/tests/build_scripts/test_map_tile_downloader_contract.py +++ b/tests/build_scripts/test_map_tile_downloader_contract.py @@ -7,6 +7,7 @@ ADAPTER_H = ROOT / "lib/tdeck_ui/Hardware/TDeck/MapTileHttpArduino.h" ADAPTER_CPP = ROOT / "lib/tdeck_ui/Hardware/TDeck/MapTileHttpArduino.cpp" MAP_SCREEN = ROOT / "lib/tdeck_ui/UI/LXMF/MapScreen.cpp" SETTINGS = ROOT / "lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp" +UI_MANAGER = ROOT / "lib/tdeck_ui/UI/LXMF/UIManager.cpp" def test_portable_core_is_bounded_and_allocation_free(): @@ -42,3 +43,20 @@ def test_downloader_is_explicitly_opt_in_and_wired_only_for_visible_misses(): assert 'KEY_MAP_DOWNLOAD = "map_dl"' in settings assert "prefs.getBool(KEY_MAP_DOWNLOAD, false)" in settings assert "Download map tiles:" in settings + + +def test_settings_save_defers_persistence_and_application_outside_lvgl(): + settings = SETTINGS.read_text() + capture = settings[settings.index("void SettingsScreen::save_settings()"): + settings.index("void SettingsScreen::service_pending_save()")] + service = settings[settings.index("void SettingsScreen::service_pending_save()"): + settings.index("void SettingsScreen::update_ui_from_settings()")] + assert "Preferences" not in capture + assert "_save_callback" not in capture + assert "Preferences prefs" in service + assert "_save_callback(settings)" in service + + update = UI_MANAGER.read_text() + body = update[update.index("void UIManager::update()"): + update.index("void UIManager::refresh_current_screen()")] + assert body.index("service_pending_save()") < body.index("LVGL_LOCK") diff --git a/tests/build_scripts/test_tdeck_environment_isolation.py b/tests/build_scripts/test_tdeck_environment_isolation.py index ea11ab2d..6c3e10d0 100644 --- a/tests/build_scripts/test_tdeck_environment_isolation.py +++ b/tests/build_scripts/test_tdeck_environment_isolation.py @@ -26,6 +26,13 @@ def test_test_hooks_are_isolated_from_production_tdeck(): assert '\'-DPYXIS_TEST_TCP_PORT="${sysenv.PYXIS_TEST_TCP_PORT}"\'' in instrumented +def test_audio_diagnostic_state_and_recorder_initialization_are_test_only(): + main = MAIN.read_text() + assert "#ifdef PYXIS_TEST_HOOKS\n// --- Audio loopback PCM dump" in main + mutex = main.index("g_rec_mutex = xSemaphoreCreateMutex()") + assert main.rfind("#ifdef PYXIS_TEST_HOOKS", 0, mutex) > main.rfind("#endif", 0, mutex) + + def test_test_hook_defaults_and_harness_target_are_safe(): main = MAIN.read_text() hook_defaults = main[main.index("#ifdef PYXIS_TEST_HOOKS") : main.index("#include ")] diff --git a/tests/native/test_map_tile_store.cpp b/tests/native/test_map_tile_store.cpp index 6f336de3..4e45b031 100644 --- a/tests/native/test_map_tile_store.cpp +++ b/tests/native/test_map_tile_store.cpp @@ -133,7 +133,9 @@ void testKeyAndCanonicalPath() { beginTest(); FakeStorage fs; MapTileStore s(fs, CHECK(MapTileStore::canonicalPath(TileKey{1U,2U,0U},p,sizeof(p))==TileStoreResult::INVALID_KEY); } void testMissHitAndRemoval() { beginTest(); FakeStorage fs; MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::OK); - std::uint32_t z=99U; CHECK(s.beginGet(TileKey{0U,0U,0U},z)==TileStoreResult::MISS); CHECK(put(s,TileKey{0U,0U,0U},png())==TileStoreResult::OK); drain(s,TileKey{0U,0U,0U},40U); + const TileKey key={0U,0U,0U}; std::uint32_t z=99U; CHECK(s.beginGet(key,z)==TileStoreResult::MISS); CHECK(put(s,key,png())==TileStoreResult::OK); drain(s,key,40U); + CHECK(s.removeTile(key)==TileStoreResult::OK); CHECK(s.entryCount()==0U); CHECK(s.totalBytes()==0U); CHECK(s.beginGet(key,z)==TileStoreResult::MISS); + CHECK(put(s,key,png())==TileStoreResult::OK); fs.available=false; CHECK(s.beginGet(TileKey{0U,0U,0U},z)==TileStoreResult::STORAGE_UNAVAILABLE); } void testMalformedPngs() { beginTest(); FakeStorage fs; MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::OK); @@ -178,8 +180,14 @@ void testRecoveryQuotaFailsClosed() { beginTest(); FakeStorage fs; fs.add("/pyxi 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 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 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(); testExactQuotaAndLruEviction(); testDuplicateAtomicReplacement(); testInterruptedFilesRecover(); testLiveWinsRecovery(); testCorruptLiveRecoversValidBackup(); testCorruptLiveWithoutBackupIsRemoved(); testStaleTempRemovalFailureAbortsPut(); testRecoveryRejectsMalformedAndExhaustion(); testRecoveryQuotaFailsClosed(); testRenameFailureRestoresDuplicate(); testDeterministicStress(); std::cout<<"map tile store: "< None: env["UBSAN_OPTIONS"] = "halt_on_error=1:print_stacktrace=1" ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=60, env=env) assert ran.returncode == 0, ran.stdout + ran.stderr - assert ran.stdout == "map tile store: 15 tests passed\n" + assert ran.stdout == "map tile store: 16 tests passed\n"