[verified] feat: prefer selected offline map packs

This commit is contained in:
torlando-agent[bot]
2026-08-07 03:27:18 +00:00
parent e79c36e65b
commit bf322baf21
13 changed files with 722 additions and 65 deletions
@@ -129,6 +129,9 @@ public:
bool takeResult(MapTileDownloadResult& result);
bool isBusy() const { return active_ || queue_count_ != 0U; }
bool willStartTransportOnNextPump() const {
return active_ && stage_ == Stage::SELECTED;
}
std::size_t queuedCount() const { return queue_count_; }
std::size_t resultCount() const { return result_count_; }
std::uint32_t droppedResultCount() const { return dropped_results_; }
+203 -52
View File
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: MIT
#include "MapScreen.h"
#include "MapTileLookupPolicy.h"
#include "MapTileStreamReader.h"
#include "Hardware/TDeck/MapTileCa.h"
#ifdef ARDUINO
@@ -39,6 +41,77 @@ Hardware::TDeck::MapTileDownloadConfig makeDownloadConfig() {
return config;
}
class PackReadStream final : public Pyxis::MapTileReadStream {
public:
explicit PackReadStream(Hardware::TDeck::MapTilePack& pack) : pack_(pack) {}
Pyxis::MapTileStreamResult begin(const Hardware::TDeck::TileKey& key,
std::uint32_t& size) override {
const Hardware::TDeck::MapTilePackResult result = pack_.beginGet(key, size);
if (result == Hardware::TDeck::MapTilePackResult::OK) return Pyxis::MapTileStreamResult::OK;
if (result == Hardware::TDeck::MapTilePackResult::UNCOVERED ||
result == Hardware::TDeck::MapTilePackResult::TILE_MISSING ||
result == Hardware::TDeck::MapTilePackResult::NOT_INITIALIZED) {
return Pyxis::MapTileStreamResult::MISS;
}
if (result == Hardware::TDeck::MapTilePackResult::STORAGE_UNAVAILABLE) {
return Pyxis::MapTileStreamResult::STORAGE_UNAVAILABLE;
}
return Pyxis::MapTileStreamResult::IO_ERROR;
}
Pyxis::MapTileStreamResult read(std::uint8_t* output, std::size_t capacity,
std::size_t& count) override {
const Hardware::TDeck::MapTilePackResult result =
pack_.readGetChunk(output, capacity, count);
if (result == Hardware::TDeck::MapTilePackResult::OK) return Pyxis::MapTileStreamResult::OK;
if (result == Hardware::TDeck::MapTilePackResult::STORAGE_UNAVAILABLE) {
return Pyxis::MapTileStreamResult::STORAGE_UNAVAILABLE;
}
return Pyxis::MapTileStreamResult::IO_ERROR;
}
void end() override { pack_.endGet(); }
private:
Hardware::TDeck::MapTilePack& pack_;
};
class LiveReadStream final : public Pyxis::MapTileReadStream {
public:
explicit LiveReadStream(Hardware::TDeck::MapTileStore& store) : store_(store) {}
Pyxis::MapTileStreamResult begin(const Hardware::TDeck::TileKey& key,
std::uint32_t& size) override {
const Hardware::TDeck::TileStoreResult result = store_.beginGet(key, size);
if (result == Hardware::TDeck::TileStoreResult::OK) return Pyxis::MapTileStreamResult::OK;
if (result == Hardware::TDeck::TileStoreResult::MISS) return Pyxis::MapTileStreamResult::MISS;
if (result == Hardware::TDeck::TileStoreResult::STORAGE_UNAVAILABLE ||
result == Hardware::TDeck::TileStoreResult::NOT_INITIALIZED) {
return Pyxis::MapTileStreamResult::STORAGE_UNAVAILABLE;
}
return Pyxis::MapTileStreamResult::IO_ERROR;
}
Pyxis::MapTileStreamResult read(std::uint8_t* output, std::size_t capacity,
std::size_t& count) override {
const Hardware::TDeck::TileStoreResult result =
store_.readGetChunk(output, capacity, count);
if (result == Hardware::TDeck::TileStoreResult::OK) return Pyxis::MapTileStreamResult::OK;
if (result == Hardware::TDeck::TileStoreResult::STORAGE_UNAVAILABLE) {
return Pyxis::MapTileStreamResult::STORAGE_UNAVAILABLE;
}
return Pyxis::MapTileStreamResult::IO_ERROR;
}
void end() override { store_.endGet(); }
private:
Hardware::TDeck::MapTileStore& store_;
};
class AtomicStopSource final : public Pyxis::MapTileStopSource {
public:
explicit AtomicStopSource(const std::atomic<bool>& stop) : stop_(stop) {}
bool stopRequested() const override {
return stop_.load(std::memory_order_acquire);
}
private:
const std::atomic<bool>& stop_;
};
lv_obj_t* createToolbarButton(lv_obj_t* parent, const char* text,
lv_event_cb_t callback, void* context,
lv_coord_t width) {
@@ -73,22 +146,24 @@ MapScreen::MapScreen(lv_obj_t* parent)
presenter_(), storage_(),
store_config_{STORE_ENTRY_CAPACITY, STORE_BYTE_QUOTA,
MAX_COMPRESSED_TILE_BYTES},
store_(storage_, store_config_), download_store_(store_),
store_(storage_, store_config_), pack_(storage_), download_store_(store_),
download_transport_(), download_clock_(), download_policy_(),
download_config_(makeDownloadConfig()),
downloader_(download_store_, download_transport_, download_clock_,
download_policy_, download_config_),
downloads_enabled_(false), screen_visible_(false), transport_close_epoch_(0U),
downloads_enabled_(false), screen_visible_(false), pack_refresh_epoch_(0U),
transport_close_epoch_(0U),
download_failed_frame_epoch_(0U),
decode_failed_keys_{}, decode_failed_generations_{},
compressed_staging_(nullptr),
state_mutex_(nullptr), worker_task_(nullptr), stop_requested_(false),
state_mutex_(nullptr), transport_start_mutex_(nullptr), worker_task_(nullptr),
worker_exited_(true), worker_started_(false), store_initialized_(false),
requests_released_(false),
has_location_fix_(false), center_initialized_(false), current_location_{}, dragging_(false),
last_drag_point_{0, 0}, back_callback_() {
LVGL_LOCK();
state_mutex_ = xSemaphoreCreateMutex();
transport_start_mutex_ = xSemaphoreCreateMutex();
// Reserve the mandatory SD/PNG staging buffer before optional decoded
// cache entries. A partial cache must never prevent the worker starting.
compressed_staging_ = static_cast<std::uint8_t*>(heap_caps_malloc(
@@ -257,6 +332,8 @@ MapScreen::~MapScreen() {
compressed_staging_ = nullptr;
if (state_mutex_) vSemaphoreDelete(state_mutex_);
state_mutex_ = nullptr;
if (transport_start_mutex_) vSemaphoreDelete(transport_start_mutex_);
transport_start_mutex_ = nullptr;
}
bool MapScreen::lockState(TickType_t ticks) {
@@ -267,9 +344,26 @@ void MapScreen::unlockState() {
xSemaphoreGive(state_mutex_);
}
void MapScreen::synchronizeTransportStart() {
if (transport_start_mutex_ &&
xSemaphoreTake(transport_start_mutex_, portMAX_DELAY) == pdTRUE) {
xSemaphoreGive(transport_start_mutex_);
}
}
void MapScreen::setDownloadEnabled(bool enabled) {
const bool was_enabled =
downloads_enabled_.exchange(enabled, std::memory_order_acq_rel);
if (was_enabled && !enabled) {
transport_close_epoch_.fetch_add(1U, std::memory_order_acq_rel);
}
if (worker_task_) xTaskNotifyGive(worker_task_);
if (!enabled) synchronizeTransportStart();
}
bool MapScreen::startWorker() {
if (worker_started_) return true;
if (!state_mutex_ || !compressed_staging_) return false;
if (!state_mutex_ || !transport_start_mutex_ || !compressed_staging_) return false;
stop_requested_.store(false, std::memory_order_release);
worker_exited_.store(false, std::memory_order_release);
const BaseType_t created = xTaskCreatePinnedToCore(
@@ -310,14 +404,15 @@ void MapScreen::workerEntry(void* context) {
void MapScreen::workerLoop() {
Hardware::TDeck::TileStoreResult initialized = store_.initialize();
store_initialized_ = initialized == Hardware::TDeck::TileStoreResult::OK;
std::uint32_t handled_pack_refresh_epoch =
pack_refresh_epoch_.load(std::memory_order_acquire);
(void)pack_.initialize();
std::uint32_t handled_close_epoch =
transport_close_epoch_.load(std::memory_order_acquire);
while (!stop_requested_.load(std::memory_order_acquire)) {
// The worker exclusively owns HTTP/TLS teardown. Keep a successful
// session across frame/zoom boundaries only while the map is visible
// and online acquisition remains explicitly enabled.
const bool downloads_enabled =
downloads_enabled_.load(std::memory_order_acquire);
const std::uint32_t close_epoch =
transport_close_epoch_.load(std::memory_order_acquire);
if (close_epoch != handled_close_epoch) {
@@ -325,13 +420,31 @@ void MapScreen::workerLoop() {
download_transport_.disconnectIdle();
handled_close_epoch = close_epoch;
}
const bool should_retain_download_transport = downloads_enabled &&
const bool screen_visible =
screen_visible_.load(std::memory_order_acquire);
const std::uint32_t pack_refresh_epoch =
pack_refresh_epoch_.load(std::memory_order_acquire);
if (pack_refresh_epoch != handled_pack_refresh_epoch) {
const bool had_selection = pack_.hasSelection();
char previous_pack_id[Pyxis::MapPackManifest::PACK_ID_CAPACITY] = {};
if (had_selection) {
std::memcpy(previous_pack_id, pack_.metadata().pack_id,
sizeof(previous_pack_id));
}
(void)pack_.initialize();
const bool has_selection = pack_.hasSelection();
if (had_selection != has_selection ||
(had_selection && has_selection &&
std::strcmp(previous_pack_id, pack_.metadata().pack_id) != 0)) {
decoded_tile_cache_.clear();
}
handled_pack_refresh_epoch = pack_refresh_epoch;
}
Pyxis::MapTileRequest request{};
bool have_request = false;
if (lockState(pdMS_TO_TICKS(20))) {
if (requests_released_ && should_retain_download_transport) {
if (requests_released_ && screen_visible) {
have_request = presenter_.takeRequest(request);
}
unlockState();
@@ -360,9 +473,14 @@ void MapScreen::workerLoop() {
Pyxis::MapTileLoadResult MapScreen::loadTile(
const Pyxis::MapTileRequest& request, std::uint32_t transport_epoch) {
const Pyxis::MapTileLoadResult cached = readTile(request);
if (cached != Pyxis::MapTileLoadResult::MISS &&
cached != Pyxis::MapTileLoadResult::INVALID_PNG) return cached;
if (decodeFailedFor(request)) return Pyxis::MapTileLoadResult::DOWNLOAD_FAILED;
const bool decode_failed = decodeFailedFor(request);
if (!Pyxis::MapTileLookupPolicy::shouldStartOnline(
cached, downloads_enabled_.load(std::memory_order_acquire),
screen_visible_.load(std::memory_order_acquire), decode_failed,
transport_epoch,
transport_close_epoch_.load(std::memory_order_acquire))) {
return decode_failed ? Pyxis::MapTileLoadResult::DOWNLOAD_FAILED : cached;
}
const Pyxis::MapTileLoadResult downloaded =
downloadTile(request, transport_epoch);
if (downloaded != Pyxis::MapTileLoadResult::READY) return downloaded;
@@ -433,7 +551,25 @@ Pyxis::MapTileLoadResult MapScreen::downloadTile(
(void)downloader_.cancelGeneration(request.frame_epoch);
download_transport_.disconnectIdle();
}
(void)downloader_.pump();
if (downloader_.willStartTransportOnNextPump()) {
if (xSemaphoreTake(transport_start_mutex_, portMAX_DELAY) == pdTRUE) {
const bool start_allowed =
!stop_requested_.load(std::memory_order_acquire) &&
downloads_enabled_.load(std::memory_order_acquire) &&
screen_visible_.load(std::memory_order_acquire) &&
transport_close_epoch_.load(std::memory_order_acquire) ==
transport_epoch;
if (start_allowed) {
(void)downloader_.pump();
} else {
downloader_.setEnabled(false);
download_transport_.disconnectIdle();
}
xSemaphoreGive(transport_start_mutex_);
}
} else {
(void)downloader_.pump();
}
if (downloader_.isBusy()) vTaskDelay(pdMS_TO_TICKS(1));
}
@@ -464,44 +600,49 @@ Pyxis::MapTileLoadResult MapScreen::readTile(
TILE_PIXEL_COUNT)) {
return Pyxis::MapTileLoadResult::READY;
}
if (!store_initialized_) {
struct LocalReadContext {
MapScreen* screen;
const Pyxis::MapTileRequest* request;
} context = {this, &request};
return Pyxis::MapTileLookupPolicy::readLocal(
&context,
[](void* opaque, Pyxis::MapTileLookupPolicy::LocalSource source) ->
Pyxis::MapTileLoadResult {
LocalReadContext* local = static_cast<LocalReadContext*>(opaque);
const CompressedTileSource mapped =
source == Pyxis::MapTileLookupPolicy::LocalSource::PACK
? CompressedTileSource::PACK
: CompressedTileSource::LIVE_STORE;
return local->screen->readCompressedTile(*local->request, mapped);
});
}
Pyxis::MapTileLoadResult MapScreen::readCompressedTile(
const Pyxis::MapTileRequest& request, CompressedTileSource source) {
if (source == CompressedTileSource::LIVE_STORE && !store_initialized_) {
return Pyxis::MapTileLoadResult::STORAGE_UNAVAILABLE;
}
std::uint32_t size = 0U;
Hardware::TDeck::TileStoreResult result =
store_.beginGet(request.key, size);
if (result == Hardware::TDeck::TileStoreResult::MISS) {
PackReadStream pack_stream(pack_);
LiveReadStream live_stream(store_);
Pyxis::MapTileReadStream& stream = source == CompressedTileSource::PACK
? static_cast<Pyxis::MapTileReadStream&>(pack_stream)
: static_cast<Pyxis::MapTileReadStream&>(live_stream);
AtomicStopSource stop(stop_requested_);
std::size_t total = 0U;
const Pyxis::MapTileStreamResult read = Pyxis::MapTileStreamReader::readExact(
stream, stop, request.key, compressed_staging_, MAX_COMPRESSED_TILE_BYTES,
READ_CHUNK_BYTES, total);
if (read == Pyxis::MapTileStreamResult::MISS) {
return Pyxis::MapTileLoadResult::MISS;
}
if (result == Hardware::TDeck::TileStoreResult::STORAGE_UNAVAILABLE ||
result == Hardware::TDeck::TileStoreResult::NOT_INITIALIZED) {
if (read == Pyxis::MapTileStreamResult::STORAGE_UNAVAILABLE) {
return Pyxis::MapTileLoadResult::STORAGE_UNAVAILABLE;
}
if (result != Hardware::TDeck::TileStoreResult::OK) {
return Pyxis::MapTileLoadResult::IO_ERROR;
}
if (size > MAX_COMPRESSED_TILE_BYTES) {
store_.endGet();
if (read == Pyxis::MapTileStreamResult::TOO_LARGE) {
return Pyxis::MapTileLoadResult::TOO_LARGE;
}
std::size_t total = 0U;
while (total < size &&
!stop_requested_.load(std::memory_order_acquire)) {
const std::size_t capacity =
(size - total) < READ_CHUNK_BYTES ? (size - total) : READ_CHUNK_BYTES;
std::size_t count = 0U;
result = store_.readGetChunk(compressed_staging_ + total,
capacity, count);
if (result != Hardware::TDeck::TileStoreResult::OK || count == 0U) {
store_.endGet();
return result == Hardware::TDeck::TileStoreResult::STORAGE_UNAVAILABLE
? Pyxis::MapTileLoadResult::STORAGE_UNAVAILABLE
: Pyxis::MapTileLoadResult::IO_ERROR;
}
total += count;
}
store_.endGet();
if (total != size || stop_requested_.load(std::memory_order_acquire)) {
if (read != Pyxis::MapTileStreamResult::OK) {
return Pyxis::MapTileLoadResult::IO_ERROR;
}
@@ -517,11 +658,15 @@ Pyxis::MapTileLoadResult MapScreen::readTile(
&width, &height, &decode_state, compressed_staging_, total);
if (decode_error != 0U || width != 256U || height != 256U) {
lodepng_state_cleanup(&decode_state);
const Hardware::TDeck::TileStoreResult removed = store_.removeTile(request.key);
return (removed == Hardware::TDeck::TileStoreResult::OK ||
removed == Hardware::TDeck::TileStoreResult::MISS)
? Pyxis::MapTileLoadResult::INVALID_PNG
: Pyxis::MapTileLoadResult::IO_ERROR;
if (source == CompressedTileSource::LIVE_STORE) {
const Hardware::TDeck::TileStoreResult removed =
store_.removeTile(request.key);
return (removed == Hardware::TDeck::TileStoreResult::OK ||
removed == Hardware::TDeck::TileStoreResult::MISS)
? Pyxis::MapTileLoadResult::INVALID_PNG
: Pyxis::MapTileLoadResult::IO_ERROR;
}
return Pyxis::MapTileLoadResult::INVALID_PNG;
}
decode_state.info_raw.colortype = LCT_RGB;
decode_state.info_raw.bitdepth = 8U;
@@ -530,11 +675,15 @@ 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);
const Hardware::TDeck::TileStoreResult removed = store_.removeTile(request.key);
return (removed == Hardware::TDeck::TileStoreResult::OK ||
removed == Hardware::TDeck::TileStoreResult::MISS)
? Pyxis::MapTileLoadResult::INVALID_PNG
: Pyxis::MapTileLoadResult::IO_ERROR;
if (source == CompressedTileSource::LIVE_STORE) {
const Hardware::TDeck::TileStoreResult removed =
store_.removeTile(request.key);
return (removed == Hardware::TDeck::TileStoreResult::OK ||
removed == Hardware::TDeck::TileStoreResult::MISS)
? Pyxis::MapTileLoadResult::INVALID_PNG
: Pyxis::MapTileLoadResult::IO_ERROR;
}
return Pyxis::MapTileLoadResult::INVALID_PNG;
}
if (request.slot_index >= TILE_COUNT || !tile_pixels_[request.slot_index]) {
lv_mem_free(rgb);
@@ -688,6 +837,7 @@ bool MapScreen::applyOneCompletion() {
}
void MapScreen::show() {
pack_refresh_epoch_.fetch_add(1U, std::memory_order_acq_rel);
screen_visible_.store(true, std::memory_order_release);
if (worker_task_) xTaskNotifyGive(worker_task_);
if (lockState(pdMS_TO_TICKS(100))) {
@@ -712,6 +862,7 @@ void MapScreen::hide() {
transport_close_epoch_.fetch_add(1U, std::memory_order_acq_rel);
}
if (worker_task_) xTaskNotifyGive(worker_task_);
synchronizeTransportStart();
if (lockState(pdMS_TO_TICKS(100))) {
presenter_.hide();
unlockState();
+12 -8
View File
@@ -19,6 +19,7 @@
#include "Hardware/TDeck/MapTileStore.h"
#include "Hardware/TDeck/MapTileStoreSD.h"
#include "Hardware/TDeck/MapTilePack.h"
#include "Hardware/TDeck/MapTileDownloader.h"
#include "Hardware/TDeck/MapTileHttpArduino.h"
@@ -49,14 +50,7 @@ public:
// These methods never call LVGL and are invoked before LVGL_LOCK.
void serviceIo();
void updateModel(const Pyxis::MapView::Request& request);
void setDownloadEnabled(bool enabled) {
const bool was_enabled =
downloads_enabled_.exchange(enabled, std::memory_order_acq_rel);
if (was_enabled && !enabled) {
transport_close_epoch_.fetch_add(1U, std::memory_order_acq_rel);
}
if (worker_task_) xTaskNotifyGive(worker_task_);
}
void setDownloadEnabled(bool enabled);
// These methods only mutate the pre-created object pool and are invoked
// while UIManager owns LVGL_LOCK.
@@ -88,6 +82,7 @@ private:
Hardware::TDeck::MapTileStoreSD storage_;
Hardware::TDeck::TileStoreConfig store_config_;
Hardware::TDeck::MapTileStore store_;
Hardware::TDeck::MapTilePack pack_;
Hardware::TDeck::MapTileStoreDownloadAdapter download_store_;
Hardware::TDeck::MapTileHttpArduino download_transport_;
Hardware::TDeck::MapTileMillisClock download_clock_;
@@ -96,12 +91,14 @@ private:
Hardware::TDeck::MapTileDownloader downloader_;
std::atomic<bool> downloads_enabled_;
std::atomic<bool> screen_visible_;
std::atomic<std::uint32_t> pack_refresh_epoch_;
std::atomic<std::uint32_t> transport_close_epoch_;
std::uint32_t download_failed_frame_epoch_;
Hardware::TDeck::TileKey decode_failed_keys_[TILE_COUNT];
std::uint32_t decode_failed_generations_[TILE_COUNT];
std::uint8_t* compressed_staging_;
SemaphoreHandle_t state_mutex_;
SemaphoreHandle_t transport_start_mutex_;
TaskHandle_t worker_task_;
std::atomic<bool> stop_requested_;
std::atomic<bool> worker_exited_;
@@ -116,10 +113,16 @@ private:
BackCallback back_callback_;
static void workerEntry(void* context);
enum class CompressedTileSource : std::uint8_t {
PACK = 0,
LIVE_STORE
};
void workerLoop();
Pyxis::MapTileLoadResult loadTile(const Pyxis::MapTileRequest& request,
std::uint32_t transport_epoch);
Pyxis::MapTileLoadResult readTile(const Pyxis::MapTileRequest& request);
Pyxis::MapTileLoadResult readCompressedTile(
const Pyxis::MapTileRequest& request, CompressedTileSource source);
Pyxis::MapTileLoadResult downloadTile(const Pyxis::MapTileRequest& request,
std::uint32_t transport_epoch);
bool decodeFailedFor(const Pyxis::MapTileRequest& request) const;
@@ -128,6 +131,7 @@ private:
void stopWorker();
bool lockState(TickType_t ticks = portMAX_DELAY);
void unlockState();
void synchronizeTransportStart();
void setPlaceholder(std::size_t index);
void setStatusFor(Pyxis::MapTileLoadResult result);
void pan(double dx, double dy);
@@ -0,0 +1,52 @@
// Copyright (c) 2026 Pyxis contributors
// SPDX-License-Identifier: MIT
#ifndef UI_LXMF_MAP_TILE_LOOKUP_POLICY_H
#define UI_LXMF_MAP_TILE_LOOKUP_POLICY_H
#include <cstddef>
#include <cstdint>
#include "MapScreenPresenter.h"
namespace Pyxis {
/** Portable policy for pack -> live cache -> optional network orchestration. */
class MapTileLookupPolicy {
public:
enum class LocalSource : std::uint8_t { PACK = 0, LIVE_STORE };
typedef MapTileLoadResult (*ReadLocalSource)(void*, LocalSource);
static MapTileLoadResult readLocal(void* context, ReadLocalSource read) {
if (read == NULL) return MapTileLoadResult::IO_ERROR;
const MapTileLoadResult pack = read(context, LocalSource::PACK);
if (pack == MapTileLoadResult::READY) return pack;
const MapTileLoadResult live = read(context, LocalSource::LIVE_STORE);
return resolveLocal(pack, live);
}
static MapTileLoadResult resolveLocal(MapTileLoadResult pack,
MapTileLoadResult live) {
if (pack == MapTileLoadResult::READY) return pack;
if (live != MapTileLoadResult::MISS) return live;
if (pack == MapTileLoadResult::IO_ERROR ||
pack == MapTileLoadResult::STORAGE_UNAVAILABLE) return pack;
return MapTileLoadResult::MISS;
}
static bool shouldStartOnline(MapTileLoadResult local,
bool enabled,
bool visible,
bool decode_failed,
std::uint32_t request_epoch,
std::uint32_t current_epoch) {
return enabled && visible && !decode_failed &&
request_epoch == current_epoch &&
(local == MapTileLoadResult::MISS ||
local == MapTileLoadResult::INVALID_PNG);
}
};
} // namespace Pyxis
#endif
@@ -0,0 +1,84 @@
// Copyright (c) 2026 Pyxis contributors
// SPDX-License-Identifier: MIT
#ifndef UI_LXMF_MAP_TILE_STREAM_READER_H
#define UI_LXMF_MAP_TILE_STREAM_READER_H
#include <cstddef>
#include <cstdint>
#include "Hardware/TDeck/MapTileStore.h"
namespace Pyxis {
enum class MapTileStreamResult : std::uint8_t {
OK = 0,
MISS,
STORAGE_UNAVAILABLE,
TOO_LARGE,
IO_ERROR
};
class MapTileReadStream {
public:
virtual ~MapTileReadStream() {}
virtual MapTileStreamResult begin(const Hardware::TDeck::TileKey& key,
std::uint32_t& size) = 0;
virtual MapTileStreamResult read(std::uint8_t* output,
std::size_t capacity,
std::size_t& count) = 0;
virtual void end() = 0;
};
class MapTileStopSource {
public:
virtual ~MapTileStopSource() {}
virtual bool stopRequested() const = 0;
};
class MapTileStreamReader {
public:
static MapTileStreamResult readExact(
MapTileReadStream& stream,
MapTileStopSource& stop,
const Hardware::TDeck::TileKey& key,
std::uint8_t* output,
std::size_t maximum_size,
std::size_t chunk_size,
std::size_t& total) {
total = 0U;
if (output == NULL || maximum_size == 0U || chunk_size == 0U) {
return MapTileStreamResult::IO_ERROR;
}
std::uint32_t declared = 0U;
const MapTileStreamResult begun = stream.begin(key, declared);
if (begun != MapTileStreamResult::OK) return begun;
if (declared > maximum_size) {
stream.end();
return MapTileStreamResult::TOO_LARGE;
}
while (total < declared) {
if (stop.stopRequested()) {
stream.end();
return MapTileStreamResult::IO_ERROR;
}
const std::size_t remaining = static_cast<std::size_t>(declared) - total;
const std::size_t capacity = remaining < chunk_size ? remaining : chunk_size;
std::size_t count = 0U;
const MapTileStreamResult read = stream.read(output + total, capacity, count);
if (read != MapTileStreamResult::OK || count == 0U || count > capacity) {
stream.end();
return read == MapTileStreamResult::STORAGE_UNAVAILABLE
? read : MapTileStreamResult::IO_ERROR;
}
total += count;
}
stream.end();
if (stop.stopRequested()) return MapTileStreamResult::IO_ERROR;
return MapTileStreamResult::OK;
}
};
} // namespace Pyxis
#endif
@@ -69,6 +69,94 @@ def test_worker_predecodes_and_render_path_has_no_io():
assert "MAX_COMPLETIONS_PER_TICK = 1" in text(UI / "MapScreen.h")
def test_selected_pack_is_worker_owned_read_only_and_precedes_live_cache():
source = text(UI / "MapScreen.cpp")
header = text(UI / "MapScreen.h")
assert '#include "Hardware/TDeck/MapTilePack.h"' in header
assert "Hardware::TDeck::MapTilePack pack_;" in header
constructor = source[source.index("MapScreen::MapScreen"):
source.index("MapScreen::~MapScreen")]
assert "pack_(storage_)" in constructor
worker = function_body(source, "void MapScreen::workerLoop()")
assert worker.index("store_.initialize()") < worker.index("pack_.initialize()")
read_tile = function_body(source, "Pyxis::MapTileLoadResult MapScreen::readTile(")
assert read_tile.index("decoded_tile_cache_.get") < read_tile.index("PACK")
assert read_tile.index("PACK") < read_tile.index("LIVE_STORE")
pack_read = function_body(
source, "Pyxis::MapTileLoadResult MapScreen::readCompressedTile(")
assert ("source == CompressedTileSource::LIVE_STORE && !store_initialized_"
in pack_read)
assert "MapTileStreamReader::readExact" in pack_read
assert "PackReadStream pack_stream(pack_)" in pack_read
assert "LiveReadStream live_stream(store_)" in pack_read
pack_adapter = source[source.index("class PackReadStream"):
source.index("class LiveReadStream")]
live_adapter = source[source.index("class LiveReadStream"):
source.index("class AtomicStopSource")]
assert "pack_.beginGet" in pack_adapter
assert "pack_.readGetChunk" in pack_adapter
assert "pack_.endGet" in pack_adapter
assert "remove" not in pack_adapter
assert "store_.beginGet" in live_adapter
assert "store_.readGetChunk" in live_adapter
assert "store_.endGet" in live_adapter
remove_token = "store_.removeTile(request.key)"
remove_offsets = []
cursor = 0
while True:
offset = pack_read.find(remove_token, cursor)
if offset < 0:
break
remove_offsets.append(offset)
cursor = offset + len(remove_token)
assert len(remove_offsets) == 2
for offset in remove_offsets:
remove_block = pack_read[max(0, offset - 220):offset + 80]
assert "source == CompressedTileSource::LIVE_STORE" in remove_block
# Covered-missing, uncovered, and corrupt immutable-pack tiles all continue
# to the mutable live cache before optional online acquisition.
assert "MapTilePackResult::UNCOVERED" in pack_adapter
assert "MapTilePackResult::TILE_MISSING" in pack_adapter
assert "MapTileLookupPolicy::readLocal" in read_tile
assert "static MapTileLoadResult resolveLocal" in text(UI / "MapTileLookupPolicy.h")
assert "MapTileLookupPolicy::shouldStartOnline" in source
# Every activation publishes a durable pack refresh edge. The worker owns
# reinitialization and clears decoded tiles only when selection identity
# actually changes; failed replacement remains transactional in MapTilePack.
show = function_body(source, "void MapScreen::show()")
assert "pack_refresh_epoch_.fetch_add" in show
assert "std::atomic<std::uint32_t> pack_refresh_epoch_;" in header
assert "pack_refresh_epoch != handled_pack_refresh_epoch" in worker
initial_epoch = worker.index("handled_pack_refresh_epoch")
initial_pack_init = worker.index("pack_.initialize()")
assert initial_epoch < initial_pack_init
assert worker.index("pack_.initialize()", worker.index("pack_refresh_epoch !=")) < worker.index("presenter_.takeRequest")
assert "decoded_tile_cache_.clear()" in worker
assert "std::strcmp(previous_pack_id, pack_.metadata().pack_id)" in worker
download = function_body(source, "Pyxis::MapTileLoadResult MapScreen::downloadTile(")
assert "willStartTransportOnNextPump()" in download
assert "xSemaphoreTake(transport_start_mutex_, portMAX_DELAY)" in download
guarded_start = download[download.index("willStartTransportOnNextPump()"):
download.index("xSemaphoreGive(transport_start_mutex_)")]
assert "downloads_enabled_.load" in guarded_start
assert "screen_visible_.load" in guarded_start
assert "transport_close_epoch_.load" in guarded_start
assert "downloader_.pump()" in guarded_start
disable = function_body(source, "void MapScreen::setDownloadEnabled(")
hide = function_body(source, "void MapScreen::hide()")
assert "synchronizeTransportStart()" in disable
assert "synchronizeTransportStart()" in hide
library = text(ROOT / "lib/tdeck_ui/library.json")
assert '"+<Hardware/TDeck/*.cpp>"' in library
def test_sd_adapter_never_mounts_or_formats():
source = text(HW / "MapTileStoreSD.cpp")
for forbidden in ("SD.begin", "SD.format", "LittleFS"):
@@ -86,10 +86,13 @@ def test_downloader_is_explicitly_opt_in_and_wired_only_for_visible_misses():
worker = screen[screen.index("void MapScreen::workerLoop()"):
screen.index("Pyxis::MapTileLoadResult MapScreen::loadTile")]
assert "frame_drained" not in worker
assert "retain_download_transport" in worker
assert "screen_visible_.load(std::memory_order_acquire)" in worker
assert "transport_close_epoch_.load(std::memory_order_acquire)" in worker
assert "requests_released_ && should_retain_download_transport" in worker
assert "requests_released_ && screen_visible" in worker
take_request = worker[worker.index("presenter_.takeRequest") - 160:
worker.index("presenter_.takeRequest") + 80]
assert "downloads_enabled" not in take_request
assert "should_retain_download_transport" not in take_request
assert "download_transport_.disconnectIdle()" in worker
assert "screen_visible_.store(true, std::memory_order_release)" in screen
assert "screen_visible_.exchange(false, std::memory_order_acq_rel)" in screen
@@ -113,7 +116,9 @@ def test_recent_decoded_tiles_use_a_fixed_psram_lru_before_sd_decode():
cache = (ROOT / "lib/tdeck_ui/UI/LXMF/DecodedTileCache.h").read_text()
assert "CAPACITY = 12U" in cache
assert "decoded_tile_cache_.get" in screen
assert screen.index("decoded_tile_cache_.get") < screen.index("store_.beginGet")
read_tile = screen[screen.index("Pyxis::MapTileLoadResult MapScreen::readTile("):
screen.index("Pyxis::MapTileLoadResult MapScreen::readCompressedTile(")]
assert read_tile.index("decoded_tile_cache_.get") < read_tile.index("MapTileLookupPolicy::readLocal")
assert "decoded_tile_cache_.put" in screen
assert "MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT" in screen
assert "decoded_cache_pixels_[Pyxis::DecodedTileCache::CAPACITY]" in header
+5 -1
View File
@@ -136,6 +136,10 @@ void testSuccessExactChunksAndPublicContract() { beginTest(); FakeStore s; s.max
CHECK(r.code==MapTileResultCode::SUCCESS && r.bytes==8192U && same(r.key,key()) && r.generation==11U);
CHECK(s.chunks.size()==2U && s.chunks[0]==4096U && s.chunks[1]==4096U); CHECK(t.url=="https://tile.openstreetmap.org/3/1/2.png");
CHECK(t.agent=="Pyxis/1.2.3 (+https://github.com/torlando-tech/pyxis)"); CHECK(t.ca=="TEST CA"); CHECK(t.connect_timeout==321U && t.read_timeout==654U); CHECK(t.starts==1 && t.closes==1); }
void testTransportStartGateStage() { beginTest(); FakeStore s; FakeTransport t; t.body=bytes(1U); FakeClock c; MapTileDownloader d(s,t,c,enabled(),config());
CHECK(d.enqueue(key(),1U)==MapTileEnqueueResult::ACCEPTED); CHECK(!d.willStartTransportOnNextPump());
CHECK(d.pump()==MapTilePumpResult::PROGRESSED); CHECK(d.willStartTransportOnNextPump()); CHECK(t.starts==0);
CHECK(d.pump()==MapTilePumpResult::PROGRESSED); CHECK(!d.willStartTransportOnNextPump()); CHECK(t.starts==1); runUntilIdle(d,c); CHECK(take(d).code==MapTileResultCode::SUCCESS); }
void testStatusAndContentTypeFailures() { beginTest(); const char* bad[]={"text/plain","image/pngx","image/ png",NULL};
for(int i=0;i<4;++i){ FakeStore s; FakeTransport t; t.type=bad[i]; FakeClock c; MapTileDownloader d(s,t,c,enabled(),config()); CHECK(d.enqueue(key(),1U)==MapTileEnqueueResult::ACCEPTED); runUntilIdle(d,c); CHECK(take(d).code==MapTileResultCode::CONTENT_TYPE_ERROR); CHECK(s.begins==0); }
FakeStore s; FakeTransport t; t.status=204; FakeClock c; MapTileDownloader d(s,t,c,enabled(),config()); CHECK(d.enqueue(key(),1U)==MapTileEnqueueResult::ACCEPTED); runUntilIdle(d,c); CHECK(take(d).code==MapTileResultCode::HTTP_STATUS_ERROR); CHECK(s.begins==0);
@@ -228,4 +232,4 @@ void testSdDisappearanceAndMailboxBound() { beginTest(); FakeStore s; FakeTransp
void testStress() { beginTest(); FakeStore s; FakeTransport t; FakeClock c; MapTileDownloader d(s,t,c,enabled(),config());
for(std::uint32_t i=0;i<100000U;++i){ TileKey k=key(i&3U); const std::uint32_t g=i&7U; MapTileEnqueueResult r=d.enqueue(k,g); CHECK(r==MapTileEnqueueResult::ACCEPTED||r==MapTileEnqueueResult::DUPLICATE||r==MapTileEnqueueResult::QUEUE_FULL); if((i&3U)==0U)d.cancelGeneration(g); MapTileDownloadResult ignored; while(d.takeResult(ignored)){} } CHECK(d.queuedCount()<=MapTileDownloader::QUEUE_CAPACITY); }
}
int main(){ testDisabledByDefault(); testCanonicalUrlAndBounds(); testDedupeAndQueueFullNoEviction(); testSuccessExactChunksAndPublicContract(); testStatusAndContentTypeFailures(); testLengthOverUnderAndChunkOverCap(); testTransportAndStoreFailuresAbort(); testTransportStartFailureHardClosesAndRetriesOnce(); testTransportStartRetryIsBounded(); testTransportStartTimeoutIsNotRetried(); testTransportRetryHonorsOverallDeadline(); testSecondTransportFailureHonorsOverallDeadline(); testTransportRetryCanBeCanceledBetweenAttempts(); testTransportRetryBudgetResetsForNextRequest(); testCancellationAtStagesAndGenerationIsolation(); testTimeoutRollbackAndSaturation(); testDestructorAbortsOwnedResources(); testRuntimeDisableCancelsAllWork(); testSdDisappearanceAndMailboxBound(); testStress(); std::cout<<"map tile downloader: "<<tests_run<<" tests passed\n"; }
int main(){ testDisabledByDefault(); testCanonicalUrlAndBounds(); testDedupeAndQueueFullNoEviction(); testSuccessExactChunksAndPublicContract(); testTransportStartGateStage(); testStatusAndContentTypeFailures(); testLengthOverUnderAndChunkOverCap(); testTransportAndStoreFailuresAbort(); testTransportStartFailureHardClosesAndRetriesOnce(); testTransportStartRetryIsBounded(); testTransportStartTimeoutIsNotRetried(); testTransportRetryHonorsOverallDeadline(); testSecondTransportFailureHonorsOverallDeadline(); testTransportRetryCanBeCanceledBetweenAttempts(); testTransportRetryBudgetResetsForNextRequest(); testCancellationAtStagesAndGenerationIsolation(); testTimeoutRollbackAndSaturation(); testDestructorAbortsOwnedResources(); testRuntimeDisableCancelsAllWork(); testSdDisappearanceAndMailboxBound(); testStress(); std::cout<<"map tile downloader: "<<tests_run<<" tests passed\n"; }
+1 -1
View File
@@ -29,4 +29,4 @@ def test_bounded_map_tile_downloader(tmp_path: Path, sanitize: bool) -> 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 downloader: 20 tests passed\n"
assert ran.stdout == "map tile downloader: 21 tests passed\n"
@@ -0,0 +1,106 @@
#include "UI/LXMF/MapTileLookupPolicy.h"
#include <cstdlib>
#include <iostream>
using Pyxis::MapTileLoadResult;
using Pyxis::MapTileLookupPolicy;
namespace {
std::size_t tests_run = 0U;
void fail(const char* expression, int line) {
std::cerr << "line " << line << ": " << expression << '\n';
std::exit(1);
}
#define CHECK(expression) do { if (!(expression)) fail(#expression, __LINE__); } while (false)
void beginTest() { ++tests_run; }
void testLocalPrecedenceTable() {
beginTest();
CHECK(MapTileLookupPolicy::resolveLocal(MapTileLoadResult::READY,
MapTileLoadResult::IO_ERROR) ==
MapTileLoadResult::READY);
const MapTileLoadResult fallthrough[] = {
MapTileLoadResult::MISS, MapTileLoadResult::INVALID_PNG,
MapTileLoadResult::TOO_LARGE
};
for (std::size_t index = 0U; index < sizeof(fallthrough) / sizeof(fallthrough[0]); ++index) {
CHECK(MapTileLookupPolicy::resolveLocal(fallthrough[index],
MapTileLoadResult::READY) ==
MapTileLoadResult::READY);
CHECK(MapTileLookupPolicy::resolveLocal(fallthrough[index],
MapTileLoadResult::MISS) ==
MapTileLoadResult::MISS);
}
CHECK(MapTileLookupPolicy::resolveLocal(MapTileLoadResult::IO_ERROR,
MapTileLoadResult::MISS) ==
MapTileLoadResult::IO_ERROR);
CHECK(MapTileLookupPolicy::resolveLocal(MapTileLoadResult::STORAGE_UNAVAILABLE,
MapTileLoadResult::MISS) ==
MapTileLoadResult::STORAGE_UNAVAILABLE);
CHECK(MapTileLookupPolicy::resolveLocal(MapTileLoadResult::IO_ERROR,
MapTileLoadResult::READY) ==
MapTileLoadResult::READY);
CHECK(MapTileLookupPolicy::resolveLocal(MapTileLoadResult::MISS,
MapTileLoadResult::INVALID_PNG) ==
MapTileLoadResult::INVALID_PNG);
}
void testOnlineGate() {
beginTest();
CHECK(MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::MISS,
true, true, false, 7U, 7U));
CHECK(MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::INVALID_PNG,
true, true, false, 7U, 7U));
CHECK(!MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::READY,
true, true, false, 7U, 7U));
CHECK(!MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::MISS,
false, true, false, 7U, 7U));
CHECK(!MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::MISS,
true, false, false, 7U, 7U));
CHECK(!MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::MISS,
true, true, true, 7U, 7U));
CHECK(!MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::MISS,
true, true, false, 7U, 8U));
CHECK(!MapTileLookupPolicy::shouldStartOnline(MapTileLoadResult::IO_ERROR,
true, true, false, 7U, 7U));
}
struct FakeLocalSources {
MapTileLoadResult pack;
MapTileLoadResult live;
int calls[2];
std::size_t count;
};
MapTileLoadResult readFake(void* opaque, MapTileLookupPolicy::LocalSource source) {
FakeLocalSources* fake = static_cast<FakeLocalSources*>(opaque);
fake->calls[fake->count++] = source == MapTileLookupPolicy::LocalSource::PACK ? 0 : 1;
return source == MapTileLookupPolicy::LocalSource::PACK ? fake->pack : fake->live;
}
void testExecutableSourceOrderAndFallback() {
beginTest();
FakeLocalSources ready = {MapTileLoadResult::READY, MapTileLoadResult::IO_ERROR, {0, 0}, 0U};
CHECK(MapTileLookupPolicy::readLocal(&ready, readFake) == MapTileLoadResult::READY);
CHECK(ready.count == 1U && ready.calls[0] == 0);
const MapTileLoadResult fallthrough[] = {
MapTileLoadResult::MISS, MapTileLoadResult::INVALID_PNG,
MapTileLoadResult::TOO_LARGE, MapTileLoadResult::IO_ERROR
};
for (std::size_t index = 0U; index < sizeof(fallthrough) / sizeof(fallthrough[0]); ++index) {
FakeLocalSources fake = {fallthrough[index], MapTileLoadResult::READY, {0, 0}, 0U};
CHECK(MapTileLookupPolicy::readLocal(&fake, readFake) == MapTileLoadResult::READY);
CHECK(fake.count == 2U && fake.calls[0] == 0 && fake.calls[1] == 1);
}
CHECK(MapTileLookupPolicy::readLocal(NULL, NULL) == MapTileLoadResult::IO_ERROR);
}
} // namespace
int main() {
testLocalPrecedenceTable();
testOnlineGate();
testExecutableSourceOrderAndFallback();
std::cout << "map tile lookup policy: " << tests_run << " tests passed\n";
return 0;
}
@@ -0,0 +1,33 @@
from __future__ import annotations
from pathlib import Path
import os
import subprocess
import pytest
from native_test import find_cxx
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "tests/native/test_map_tile_lookup_policy.cpp"
@pytest.mark.parametrize("sanitize", [False, True], ids=["strict-cxx11", "asan-ubsan"])
def test_map_tile_lookup_policy(tmp_path: Path, sanitize: bool) -> None:
binary = tmp_path / "test_map_tile_lookup_policy"
command = [
find_cxx(), "-std=c++11", "-Wall", "-Wextra", "-Werror", "-pedantic",
"-Wconversion", "-Wsign-conversion", f"-I{ROOT / 'lib/tdeck_ui'}",
str(SOURCE), "-o", str(binary),
]
if sanitize:
command[1:1] = ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
compiled = subprocess.run(command, capture_output=True, text=True, timeout=60)
assert compiled.returncode == 0, compiled.stdout + compiled.stderr
env = os.environ.copy()
if sanitize:
env["ASAN_OPTIONS"] = "detect_leaks=1:halt_on_error=1"
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 lookup policy: 3 tests passed\n"
@@ -0,0 +1,96 @@
#include "UI/LXMF/MapTileStreamReader.h"
#include <cstdlib>
#include <cstring>
#include <iostream>
using Hardware::TDeck::TileKey;
using Pyxis::MapTileReadStream;
using Pyxis::MapTileStopSource;
using Pyxis::MapTileStreamReader;
using Pyxis::MapTileStreamResult;
namespace {
std::size_t tests_run = 0U;
void fail(const char* expression, int line) { std::cerr << "line " << line << ": " << expression << '\n'; std::exit(1); }
#define CHECK(expression) do { if (!(expression)) fail(#expression, __LINE__); } while (false)
void beginTest() { ++tests_run; }
class Stop final : public MapTileStopSource {
public:
Stop() : requested(false) {}
bool stopRequested() const override { return requested; }
bool requested;
};
class Stream final : public MapTileReadStream {
public:
Stream() : begin_result(MapTileStreamResult::OK), read_result(MapTileStreamResult::OK),
declared(3U), position(0U), begin_calls(0U), read_calls(0U), end_calls(0U),
zero_progress(false), oversized_count(false), stop(NULL) {
bytes[0] = 1U; bytes[1] = 2U; bytes[2] = 3U;
}
MapTileStreamResult begin(const TileKey&, std::uint32_t& size) override {
++begin_calls; size = declared; return begin_result;
}
MapTileStreamResult read(std::uint8_t* output, std::size_t capacity,
std::size_t& count) override {
++read_calls;
if (read_result != MapTileStreamResult::OK) { count = 0U; return read_result; }
if (zero_progress) { count = 0U; return MapTileStreamResult::OK; }
if (oversized_count) { count = capacity + 1U; return MapTileStreamResult::OK; }
const std::size_t remaining = declared - position;
count = remaining < capacity ? remaining : capacity;
if (count != 0U) std::memcpy(output, bytes + position, count);
position += count;
if (stop != NULL) stop->requested = true;
return MapTileStreamResult::OK;
}
void end() override { ++end_calls; }
MapTileStreamResult begin_result;
MapTileStreamResult read_result;
std::uint32_t declared;
std::size_t position;
std::size_t begin_calls;
std::size_t read_calls;
std::size_t end_calls;
bool zero_progress;
bool oversized_count;
Stop* stop;
std::uint8_t bytes[8];
};
void testSuccessAndMiss() {
beginTest(); Stop stop; Stream stream; std::uint8_t output[8] = {}; std::size_t total = 99U;
CHECK(MapTileStreamReader::readExact(stream, stop, TileKey{1U,0U,0U}, output, sizeof(output), 2U, total) == MapTileStreamResult::OK);
CHECK(total == 3U && output[0] == 1U && output[2] == 3U); CHECK(stream.end_calls == 1U);
Stream miss; miss.begin_result = MapTileStreamResult::MISS; total = 99U;
CHECK(MapTileStreamReader::readExact(miss, stop, TileKey{1U,0U,0U}, output, sizeof(output), 2U, total) == MapTileStreamResult::MISS);
CHECK(total == 0U && miss.end_calls == 0U);
}
void testEveryOpenedFailureCloses() {
beginTest(); std::uint8_t output[4] = {}; std::size_t total = 0U; Stop stop;
Stream large; large.declared = 5U;
CHECK(MapTileStreamReader::readExact(large, stop, TileKey{1U,0U,0U}, output, sizeof(output), 2U, total) == MapTileStreamResult::TOO_LARGE); CHECK(large.end_calls == 1U);
Stream error; error.read_result = MapTileStreamResult::IO_ERROR;
CHECK(MapTileStreamReader::readExact(error, stop, TileKey{1U,0U,0U}, output, sizeof(output), 2U, total) == MapTileStreamResult::IO_ERROR); CHECK(error.end_calls == 1U);
Stream unavailable; unavailable.read_result = MapTileStreamResult::STORAGE_UNAVAILABLE;
CHECK(MapTileStreamReader::readExact(unavailable, stop, TileKey{1U,0U,0U}, output, sizeof(output), 2U, total) == MapTileStreamResult::STORAGE_UNAVAILABLE); CHECK(unavailable.end_calls == 1U);
Stream zero; zero.zero_progress = true;
CHECK(MapTileStreamReader::readExact(zero, stop, TileKey{1U,0U,0U}, output, sizeof(output), 2U, total) == MapTileStreamResult::IO_ERROR); CHECK(zero.end_calls == 1U);
Stream excessive; excessive.oversized_count = true;
CHECK(MapTileStreamReader::readExact(excessive, stop, TileKey{1U,0U,0U}, output, sizeof(output), 2U, total) == MapTileStreamResult::IO_ERROR); CHECK(excessive.end_calls == 1U);
}
void testStopMidReadCloses() {
beginTest(); Stop stop; Stream stream; stream.stop = &stop; std::uint8_t output[8] = {}; std::size_t total = 0U;
CHECK(MapTileStreamReader::readExact(stream, stop, TileKey{1U,0U,0U}, output, sizeof(output), 1U, total) == MapTileStreamResult::IO_ERROR);
CHECK(stream.read_calls == 1U && stream.end_calls == 1U);
}
}
int main() {
testSuccessAndMiss(); testEveryOpenedFailureCloses(); testStopMidReadCloses();
std::cout << "map tile stream reader: " << tests_run << " tests passed\n";
}
@@ -0,0 +1,31 @@
from __future__ import annotations
from pathlib import Path
import os
import subprocess
import pytest
from native_test import find_cxx
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "tests/native/test_map_tile_stream_reader.cpp"
@pytest.mark.parametrize("sanitize", [False, True], ids=["strict-cxx11", "asan-ubsan"])
def test_map_tile_stream_reader(tmp_path: Path, sanitize: bool) -> None:
binary = tmp_path / "test_map_tile_stream_reader"
command = [find_cxx(), "-std=c++11", "-Wall", "-Wextra", "-Werror", "-pedantic",
"-Wconversion", "-Wsign-conversion", f"-I{ROOT / 'lib/tdeck_ui'}",
str(SOURCE), "-o", str(binary)]
if sanitize:
command[1:1] = ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
compiled = subprocess.run(command, capture_output=True, text=True, timeout=60)
assert compiled.returncode == 0, compiled.stdout + compiled.stderr
env = os.environ.copy()
if sanitize:
env["ASAN_OPTIONS"] = "detect_leaks=1:halt_on_error=1"
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 stream reader: 3 tests passed\n"