diff --git a/lib/tdeck_ui/Telemetry/LocationPersistence.cpp b/lib/tdeck_ui/Telemetry/LocationPersistence.cpp new file mode 100644 index 00000000..458193b1 --- /dev/null +++ b/lib/tdeck_ui/Telemetry/LocationPersistence.cpp @@ -0,0 +1,164 @@ +#include "LocationPersistence.h" + +#include +#include + +namespace Telemetry { + +TransactionalLocationPersistence::CandidateResult +TransactionalLocationPersistence::readCandidate( + LocationPersistenceSlot slot, + std::size_t& size) { + size = 0; + bool exists = false; + if (!storage_.stat(slot, exists)) return CandidateResult::IO_ERROR; + if (!exists) return CandidateResult::ABSENT; + if (!storage_.read(slot, buffer_, sizeof(buffer_), size)) { + return CandidateResult::IO_ERROR; + } + if (validateLocationStateRecord(buffer_, size) != + LocationStateRecordResult::OK) { + return CandidateResult::INVALID; + } + return CandidateResult::VALID; +} + +bool TransactionalLocationPersistence::removeIfPresent( + LocationPersistenceSlot slot) { + bool exists = false; + if (!storage_.stat(slot, exists)) return false; + return !exists || storage_.remove(slot); +} + +LocationPersistenceResult TransactionalLocationPersistence::save( + const LocationStateSnapshot& state) { + if (!storage_.available()) return LocationPersistenceResult::UNAVAILABLE; + + std::size_t encoded_size = 0; + if (encodeLocationStateRecord( + state, buffer_, sizeof(buffer_), encoded_size) != + LocationStateRecordResult::OK) { + return LocationPersistenceResult::ENCODE_ERROR; + } + if (!removeIfPresent(LocationPersistenceSlot::TEMP)) { + return LocationPersistenceResult::IO_ERROR; + } + if (!storage_.write(LocationPersistenceSlot::TEMP, buffer_, encoded_size)) { + return LocationPersistenceResult::IO_ERROR; + } + + std::size_t candidate_size = 0; + const CandidateResult temp = + readCandidate(LocationPersistenceSlot::TEMP, candidate_size); + if (temp == CandidateResult::IO_ERROR) { + return LocationPersistenceResult::IO_ERROR; + } + if (temp != CandidateResult::VALID) { + return LocationPersistenceResult::INVALID_STATE; + } + + const CandidateResult live = + readCandidate(LocationPersistenceSlot::LIVE, candidate_size); + if (live == CandidateResult::IO_ERROR) { + return LocationPersistenceResult::IO_ERROR; + } + if (live == CandidateResult::VALID) { + if (!removeIfPresent(LocationPersistenceSlot::BACKUP)) { + return LocationPersistenceResult::IO_ERROR; + } + if (!storage_.rename(LocationPersistenceSlot::LIVE, + LocationPersistenceSlot::BACKUP)) { + return LocationPersistenceResult::IO_ERROR; + } + } else if (live == CandidateResult::INVALID) { + if (!storage_.remove(LocationPersistenceSlot::LIVE)) { + return LocationPersistenceResult::IO_ERROR; + } + } + + if (!storage_.rename(LocationPersistenceSlot::TEMP, + LocationPersistenceSlot::LIVE)) { + return LocationPersistenceResult::IO_ERROR; + } + const CandidateResult promoted = + readCandidate(LocationPersistenceSlot::LIVE, candidate_size); + if (promoted == CandidateResult::IO_ERROR) { + return LocationPersistenceResult::IO_ERROR; + } + if (promoted != CandidateResult::VALID) { + return LocationPersistenceResult::INVALID_STATE; + } + return LocationPersistenceResult::SAVED; +} + +void TransactionalLocationPersistence::repairFromTemp() { + bool live_exists = false; + if (!storage_.stat(LocationPersistenceSlot::LIVE, live_exists)) return; + if (live_exists && !storage_.remove(LocationPersistenceSlot::LIVE)) return; + storage_.rename(LocationPersistenceSlot::TEMP, + LocationPersistenceSlot::LIVE); +} + +void TransactionalLocationPersistence::repairFromBackup(std::size_t size) { + if (!removeIfPresent(LocationPersistenceSlot::TEMP)) return; + if (!storage_.write(LocationPersistenceSlot::TEMP, buffer_, size)) return; + + std::size_t verified_size = 0; + if (readCandidate(LocationPersistenceSlot::TEMP, verified_size) != + CandidateResult::VALID) { + return; + } + bool live_exists = false; + if (!storage_.stat(LocationPersistenceSlot::LIVE, live_exists)) return; + if (live_exists && !storage_.remove(LocationPersistenceSlot::LIVE)) return; + storage_.rename(LocationPersistenceSlot::TEMP, + LocationPersistenceSlot::LIVE); +} + +LocationPersistenceResult TransactionalLocationPersistence::load( + LocationStateSnapshot& output) { + if (!storage_.available()) return LocationPersistenceResult::UNAVAILABLE; + + bool saw_io_error = false; + bool saw_invalid = false; + std::size_t size = 0; + CandidateResult result = readCandidate(LocationPersistenceSlot::LIVE, size); + if (result == CandidateResult::VALID) { + return decodeLocationStateRecord(buffer_, size, output) == + LocationStateRecordResult::OK + ? LocationPersistenceResult::LOADED_LIVE + : LocationPersistenceResult::INVALID_STATE; + } + saw_io_error = result == CandidateResult::IO_ERROR; + saw_invalid = result == CandidateResult::INVALID; + + result = readCandidate(LocationPersistenceSlot::TEMP, size); + if (result == CandidateResult::VALID) { + if (decodeLocationStateRecord(buffer_, size, output) != + LocationStateRecordResult::OK) { + return LocationPersistenceResult::INVALID_STATE; + } + repairFromTemp(); + return LocationPersistenceResult::RECOVERED_TEMP; + } + saw_io_error = saw_io_error || result == CandidateResult::IO_ERROR; + saw_invalid = saw_invalid || result == CandidateResult::INVALID; + + result = readCandidate(LocationPersistenceSlot::BACKUP, size); + if (result == CandidateResult::VALID) { + if (decodeLocationStateRecord(buffer_, size, output) != + LocationStateRecordResult::OK) { + return LocationPersistenceResult::INVALID_STATE; + } + repairFromBackup(size); + return LocationPersistenceResult::RECOVERED_BACKUP; + } + saw_io_error = saw_io_error || result == CandidateResult::IO_ERROR; + saw_invalid = saw_invalid || result == CandidateResult::INVALID; + + if (saw_io_error) return LocationPersistenceResult::IO_ERROR; + if (saw_invalid) return LocationPersistenceResult::INVALID_STATE; + return LocationPersistenceResult::NOT_FOUND; +} + +} // namespace Telemetry diff --git a/lib/tdeck_ui/Telemetry/LocationPersistence.h b/lib/tdeck_ui/Telemetry/LocationPersistence.h new file mode 100644 index 00000000..b9be59c4 --- /dev/null +++ b/lib/tdeck_ui/Telemetry/LocationPersistence.h @@ -0,0 +1,73 @@ +#ifndef PYXIS_TELEMETRY_LOCATION_PERSISTENCE_H +#define PYXIS_TELEMETRY_LOCATION_PERSISTENCE_H + +#include +#include + +#include "LocationStateRecord.h" + +namespace Telemetry { + +enum class LocationPersistenceSlot : uint8_t { + LIVE = 0, + TEMP = 1, + BACKUP = 2, +}; + +class LocationPersistenceStorage { +public: + virtual ~LocationPersistenceStorage() {} + virtual bool available() const = 0; + virtual bool stat(LocationPersistenceSlot slot, bool& exists) = 0; + virtual bool read(LocationPersistenceSlot slot, uint8_t* output, + std::size_t capacity, std::size_t& size) = 0; + virtual bool write(LocationPersistenceSlot slot, const uint8_t* data, + std::size_t size) = 0; + virtual bool remove(LocationPersistenceSlot slot) = 0; + virtual bool rename(LocationPersistenceSlot from, + LocationPersistenceSlot to) = 0; +}; + +enum class LocationPersistenceResult : uint8_t { + SAVED, + LOADED_LIVE, + RECOVERED_TEMP, + RECOVERED_BACKUP, + NOT_FOUND, + UNAVAILABLE, + IO_ERROR, + INVALID_STATE, + ENCODE_ERROR, +}; + +// This object owns a 3,860-byte I/O buffer. Instantiate it in static/durable +// storage on embedded targets; do not place it on a constrained task stack. +class TransactionalLocationPersistence { +public: + explicit TransactionalLocationPersistence(LocationPersistenceStorage& storage) + : storage_(storage) {} + + LocationPersistenceResult save(const LocationStateSnapshot& state); + LocationPersistenceResult load(LocationStateSnapshot& output); + +private: + enum class CandidateResult : uint8_t { + VALID, + ABSENT, + IO_ERROR, + INVALID, + }; + + CandidateResult readCandidate(LocationPersistenceSlot slot, + std::size_t& size); + bool removeIfPresent(LocationPersistenceSlot slot); + void repairFromTemp(); + void repairFromBackup(std::size_t size); + + LocationPersistenceStorage& storage_; + uint8_t buffer_[MAX_LOCATION_STATE_RECORD_BYTES]{}; +}; + +} // namespace Telemetry + +#endif // PYXIS_TELEMETRY_LOCATION_PERSISTENCE_H diff --git a/lib/tdeck_ui/Telemetry/LocationPersistenceLittleFS.cpp b/lib/tdeck_ui/Telemetry/LocationPersistenceLittleFS.cpp new file mode 100644 index 00000000..db8068c1 --- /dev/null +++ b/lib/tdeck_ui/Telemetry/LocationPersistenceLittleFS.cpp @@ -0,0 +1,85 @@ +#include "LocationPersistenceLittleFS.h" + +#include +#include + +#include +#include + +namespace Telemetry { + +const char* LocationPersistenceLittleFS::path(LocationPersistenceSlot slot) { + switch (slot) { + case LocationPersistenceSlot::LIVE: + return "/location_state.bin"; + case LocationPersistenceSlot::TEMP: + return "/location_state.tmp"; + case LocationPersistenceSlot::BACKUP: + return "/location_state.bak"; + } + return ""; +} + +bool LocationPersistenceLittleFS::stat( + LocationPersistenceSlot slot, + bool& exists) { + if (!available_) return false; + exists = LittleFS.exists(path(slot)); + return true; +} + +bool LocationPersistenceLittleFS::read( + LocationPersistenceSlot slot, + uint8_t* output, + std::size_t capacity, + std::size_t& size) { + size = 0; + if (!available_ || output == nullptr) return false; + File file = LittleFS.open(path(slot), FILE_READ); + if (!file || file.isDirectory()) { + if (file) file.close(); + return false; + } + const std::size_t file_size = file.size(); + if (file_size > capacity) { + file.close(); + return false; + } + const std::size_t read_size = file.read(output, file_size); + file.close(); + if (read_size != file_size) return false; + size = read_size; + return true; +} + +bool LocationPersistenceLittleFS::write( + LocationPersistenceSlot slot, + const uint8_t* data, + std::size_t size) { + if (!available_ || data == nullptr) return false; + File file = LittleFS.open(path(slot), FILE_WRITE); + if (!file || file.isDirectory()) { + if (file) file.close(); + return false; + } + const std::size_t written = file.write(data, size); + file.flush(); + const bool successful = + written == size && file.getWriteError() == 0; + file.close(); + return successful; +} + +bool LocationPersistenceLittleFS::remove(LocationPersistenceSlot slot) { + if (!available_) return false; + return LittleFS.remove(path(slot)); +} + +bool LocationPersistenceLittleFS::rename( + LocationPersistenceSlot from, + LocationPersistenceSlot to) { + if (!available_) return false; + return LittleFS.rename(path(from), path(to)); +} + +} // namespace Telemetry diff --git a/lib/tdeck_ui/Telemetry/LocationPersistenceLittleFS.h b/lib/tdeck_ui/Telemetry/LocationPersistenceLittleFS.h new file mode 100644 index 00000000..d8cf6084 --- /dev/null +++ b/lib/tdeck_ui/Telemetry/LocationPersistenceLittleFS.h @@ -0,0 +1,34 @@ +#ifndef PYXIS_TELEMETRY_LOCATION_PERSISTENCE_LITTLEFS_H +#define PYXIS_TELEMETRY_LOCATION_PERSISTENCE_LITTLEFS_H + +#include "LocationPersistence.h" + +namespace Telemetry { + +// Adapter for an already-mounted LittleFS instance. The mount result must be +// supplied by the owner; this adapter never mounts, formats, or erases a +// filesystem in response to an availability failure. +class LocationPersistenceLittleFS : public LocationPersistenceStorage { +public: + explicit LocationPersistenceLittleFS(bool filesystem_available) + : available_(filesystem_available) {} + + void setAvailable(bool available) { available_ = available; } + bool available() const override { return available_; } + bool stat(LocationPersistenceSlot slot, bool& exists) override; + bool read(LocationPersistenceSlot slot, uint8_t* output, + std::size_t capacity, std::size_t& size) override; + bool write(LocationPersistenceSlot slot, const uint8_t* data, + std::size_t size) override; + bool remove(LocationPersistenceSlot slot) override; + bool rename(LocationPersistenceSlot from, + LocationPersistenceSlot to) override; + +private: + static const char* path(LocationPersistenceSlot slot); + bool available_ = false; +}; + +} // namespace Telemetry + +#endif // PYXIS_TELEMETRY_LOCATION_PERSISTENCE_LITTLEFS_H diff --git a/lib/tdeck_ui/Telemetry/LocationStateRecord.cpp b/lib/tdeck_ui/Telemetry/LocationStateRecord.cpp index cad74944..10a38a49 100644 --- a/lib/tdeck_ui/Telemetry/LocationStateRecord.cpp +++ b/lib/tdeck_ui/Telemetry/LocationStateRecord.cpp @@ -283,10 +283,9 @@ LocationStateRecordResult encodeLocationStateRecord( return LocationStateRecordResult::OK; } -LocationStateRecordResult decodeLocationStateRecord( +LocationStateRecordResult validateLocationStateRecord( const uint8_t* data, - std::size_t size, - LocationStateSnapshot& output) { + std::size_t size) { if (data == nullptr) return LocationStateRecordResult::INVALID_ARGUMENT; if (size < LOCATION_STATE_HEADER_BYTES + LOCATION_STATE_CRC_BYTES) { return LocationStateRecordResult::MALFORMED; @@ -321,10 +320,21 @@ LocationStateRecordResult decodeLocationStateRecord( } const uint8_t* payload = data + LOCATION_STATE_HEADER_BYTES; + return validateEncodedRecords(payload, session_count, location_count); +} + +LocationStateRecordResult decodeLocationStateRecord( + const uint8_t* data, + std::size_t size, + LocationStateSnapshot& output) { const LocationStateRecordResult validation = - validateEncodedRecords(payload, session_count, location_count); + validateLocationStateRecord(data, size); if (validation != LocationStateRecordResult::OK) return validation; + const std::size_t session_count = readU16(data + 12); + const std::size_t location_count = readU16(data + 14); + const uint8_t* payload = data + LOCATION_STATE_HEADER_BYTES; + output.session_count = 0; output.location_count = 0; for (std::size_t index = 0; index < MAX_SHARE_SESSIONS; ++index) { diff --git a/lib/tdeck_ui/Telemetry/LocationStateRecord.h b/lib/tdeck_ui/Telemetry/LocationStateRecord.h index 5bfe2a6f..49f4ed65 100644 --- a/lib/tdeck_ui/Telemetry/LocationStateRecord.h +++ b/lib/tdeck_ui/Telemetry/LocationStateRecord.h @@ -52,6 +52,10 @@ LocationStateRecordResult decodeLocationStateRecord( std::size_t size, LocationStateSnapshot& output); +LocationStateRecordResult validateLocationStateRecord( + const uint8_t* data, + std::size_t size); + } // namespace Telemetry #endif // PYXIS_TELEMETRY_LOCATION_STATE_RECORD_H diff --git a/tests/build_scripts/test_location_persistence_contract.py b/tests/build_scripts/test_location_persistence_contract.py new file mode 100644 index 00000000..2a847ff8 --- /dev/null +++ b/tests/build_scripts/test_location_persistence_contract.py @@ -0,0 +1,20 @@ +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +ADAPTER = ROOT / "lib" / "tdeck_ui" / "Telemetry" / "LocationPersistenceLittleFS.cpp" +HEADER = ROOT / "lib" / "tdeck_ui" / "Telemetry" / "LocationPersistenceLittleFS.h" + + +def test_location_persistence_never_mounts_or_formats_littlefs(): + source = ADAPTER.read_text() + HEADER.read_text() + assert ".format(" not in source + assert "LittleFS.format" not in source + assert "LittleFS.begin" not in source + assert "already-mounted LittleFS" in source + + +def test_location_persistence_uses_isolated_temp_live_backup_paths(): + source = ADAPTER.read_text() + assert '"/location_state.bin"' in source + assert '"/location_state.tmp"' in source + assert '"/location_state.bak"' in source diff --git a/tests/native/test_location_persistence.cpp b/tests/native/test_location_persistence.cpp new file mode 100644 index 00000000..c8f68739 --- /dev/null +++ b/tests/native/test_location_persistence.cpp @@ -0,0 +1,229 @@ +#include +#include +#include +#include +#include + +#include "Telemetry/LocationPersistence.h" + +namespace { + +int passed = 0; +int failures = 0; +#define CHECK(expr) do { if (expr) { ++passed; } else { ++failures; \ + std::cerr << "FAIL line " << __LINE__ << ": " #expr << '\n'; } } while (false) + +struct SlotData { + bool present = false; + std::size_t size = 0; + uint8_t bytes[Telemetry::MAX_LOCATION_STATE_RECORD_BYTES]{}; +}; + +class FakeStorage : public Telemetry::LocationPersistenceStorage { +public: + bool mounted = true; + int operations = 0; + int fail_at = 0; + bool corrupt_write = false; + SlotData slots[3]{}; + + bool available() const override { return mounted; } + + bool stat(Telemetry::LocationPersistenceSlot slot, bool& exists) override { + if (fails()) return false; + exists = at(slot).present; + return true; + } + + bool read(Telemetry::LocationPersistenceSlot slot, uint8_t* output, + std::size_t capacity, std::size_t& size) override { + if (fails()) return false; + const SlotData& source = at(slot); + if (!source.present || source.size > capacity) return false; + std::memcpy(output, source.bytes, source.size); + size = source.size; + return true; + } + + bool write(Telemetry::LocationPersistenceSlot slot, const uint8_t* data, + std::size_t size) override { + if (fails()) { + SlotData& target = at(slot); + target.present = true; + target.size = size / 2; + if (target.size > 0) std::memcpy(target.bytes, data, target.size); + return false; + } + SlotData& target = at(slot); + target.present = true; + target.size = size; + std::memcpy(target.bytes, data, size); + if (corrupt_write && size > 20) target.bytes[20] ^= 1U; + return true; + } + + bool remove(Telemetry::LocationPersistenceSlot slot) override { + if (fails()) return false; + at(slot) = SlotData{}; + return true; + } + + bool rename(Telemetry::LocationPersistenceSlot from, + Telemetry::LocationPersistenceSlot to) override { + if (fails()) return false; + if (!at(from).present || at(to).present) return false; + at(to) = at(from); + at(from) = SlotData{}; + return true; + } + +private: + bool fails() { + ++operations; + return fail_at != 0 && operations == fail_at; + } + SlotData& at(Telemetry::LocationPersistenceSlot slot) { + return slots[static_cast(slot)]; + } + const SlotData& at(Telemetry::LocationPersistenceSlot slot) const { + return slots[static_cast(slot)]; + } +}; + +Telemetry::LocationStateSnapshot state(uint32_t marker) { + Telemetry::LocationStateSnapshot value{}; + value.session_count = 1; + for (std::size_t i = 0; i < Telemetry::PEER_ID_SIZE; ++i) { + value.sessions[0].peer.bytes[i] = static_cast(marker + i); + } + value.sessions[0].record.cadence_millis = + Telemetry::MIN_SHARE_CADENCE_MILLIS + marker; + value.sessions[0].record.approx_radius_meters = static_cast(marker); + value.location_count = 1; + value.locations[0].peer = value.sessions[0].peer; + value.locations[0].location.latitude_e6 = 1000000 + static_cast(marker); + value.locations[0].location.longitude_e6 = -2000000; + value.locations[0].source_timestamp_millis = marker; + value.locations[0].received_at_millis = marker + 1; + return value; +} + +uint32_t marker(const Telemetry::LocationStateSnapshot& value) { + return value.sessions[0].record.cadence_millis - + Telemetry::MIN_SHARE_CADENCE_MILLIS; +} + +void put(FakeStorage& storage, Telemetry::LocationPersistenceSlot slot, + const Telemetry::LocationStateSnapshot& value) { + SlotData& target = storage.slots[static_cast(slot)]; + target.present = true; + std::size_t written = 0; + CHECK(Telemetry::encodeLocationStateRecord( + value, target.bytes, sizeof(target.bytes), written) == + Telemetry::LocationStateRecordResult::OK); + target.size = written; +} + +void recoversByLiveTempBackupPriority() { + FakeStorage storage; + put(storage, Telemetry::LocationPersistenceSlot::LIVE, state(1)); + put(storage, Telemetry::LocationPersistenceSlot::TEMP, state(2)); + put(storage, Telemetry::LocationPersistenceSlot::BACKUP, state(3)); + Telemetry::TransactionalLocationPersistence persistence(storage); + Telemetry::LocationStateSnapshot output{}; + CHECK(persistence.load(output) == Telemetry::LocationPersistenceResult::LOADED_LIVE); + CHECK(marker(output) == 1); + + storage.slots[0].bytes[20] ^= 1U; + CHECK(persistence.load(output) == Telemetry::LocationPersistenceResult::RECOVERED_TEMP); + CHECK(marker(output) == 2); + CHECK(storage.slots[0].present); + + FakeStorage backup_only; + put(backup_only, Telemetry::LocationPersistenceSlot::LIVE, state(4)); + backup_only.slots[0].bytes[20] ^= 1U; + put(backup_only, Telemetry::LocationPersistenceSlot::TEMP, state(5)); + backup_only.slots[1].bytes[20] ^= 1U; + put(backup_only, Telemetry::LocationPersistenceSlot::BACKUP, state(6)); + Telemetry::TransactionalLocationPersistence backup_persistence(backup_only); + CHECK(backup_persistence.load(output) == + Telemetry::LocationPersistenceResult::RECOVERED_BACKUP); + CHECK(marker(output) == 6); + CHECK(backup_only.slots[2].present); +} + +void failsClosedWhenUnavailableMissingOrCorrupt() { + Telemetry::LocationStateSnapshot output = state(9); + FakeStorage unavailable; + unavailable.mounted = false; + Telemetry::TransactionalLocationPersistence unavailable_persistence(unavailable); + CHECK(unavailable_persistence.load(output) == + Telemetry::LocationPersistenceResult::UNAVAILABLE); + CHECK(marker(output) == 9); + + FakeStorage empty; + Telemetry::TransactionalLocationPersistence empty_persistence(empty); + CHECK(empty_persistence.load(output) == + Telemetry::LocationPersistenceResult::NOT_FOUND); + CHECK(marker(output) == 9); + + FakeStorage corrupt; + put(corrupt, Telemetry::LocationPersistenceSlot::LIVE, state(1)); + put(corrupt, Telemetry::LocationPersistenceSlot::TEMP, state(2)); + put(corrupt, Telemetry::LocationPersistenceSlot::BACKUP, state(3)); + for (auto& slot : corrupt.slots) slot.bytes[20] ^= 1U; + Telemetry::TransactionalLocationPersistence corrupt_persistence(corrupt); + CHECK(corrupt_persistence.load(output) == + Telemetry::LocationPersistenceResult::INVALID_STATE); + CHECK(marker(output) == 9); +} + +void everyInterruptedSaveRetainsAValidGeneration() { + FakeStorage baseline; + put(baseline, Telemetry::LocationPersistenceSlot::LIVE, state(10)); + put(baseline, Telemetry::LocationPersistenceSlot::BACKUP, state(9)); + int observed_failures = 0; + for (int fail_at = 1; fail_at <= 16; ++fail_at) { + FakeStorage interrupted = baseline; + interrupted.fail_at = fail_at; + Telemetry::TransactionalLocationPersistence persistence(interrupted); + const auto result = persistence.save(state(20)); + if (result != Telemetry::LocationPersistenceResult::SAVED) { + ++observed_failures; + } + interrupted.fail_at = 0; + interrupted.operations = 0; + Telemetry::LocationStateSnapshot output{}; + const auto loaded = persistence.load(output); + CHECK(loaded == Telemetry::LocationPersistenceResult::LOADED_LIVE || + loaded == Telemetry::LocationPersistenceResult::RECOVERED_TEMP || + loaded == Telemetry::LocationPersistenceResult::RECOVERED_BACKUP); + CHECK(marker(output) == 10 || marker(output) == 20); + } + CHECK(observed_failures >= 8); +} + +void validatesTempBeforeReplacingLive() { + FakeStorage storage; + put(storage, Telemetry::LocationPersistenceSlot::LIVE, state(30)); + storage.corrupt_write = true; + Telemetry::TransactionalLocationPersistence persistence(storage); + CHECK(persistence.save(state(40)) == + Telemetry::LocationPersistenceResult::INVALID_STATE); + storage.corrupt_write = false; + Telemetry::LocationStateSnapshot output{}; + CHECK(persistence.load(output) == Telemetry::LocationPersistenceResult::LOADED_LIVE); + CHECK(marker(output) == 30); +} + +} // namespace + +int main() { + recoversByLiveTempBackupPriority(); + failsClosedWhenUnavailableMissingOrCorrupt(); + everyInterruptedSaveRetainsAValidGeneration(); + validatesTempBeforeReplacingLive(); + std::cout << "location persistence: " << passed << " passed, " + << failures << " failed\n"; + return failures == 0 ? 0 : 1; +} diff --git a/tests/native/test_location_persistence.py b/tests/native/test_location_persistence.py new file mode 100644 index 00000000..68f49427 --- /dev/null +++ b/tests/native/test_location_persistence.py @@ -0,0 +1,24 @@ +"""Compile and execute transactional location-persistence tests.""" + +from pathlib import Path + +from native_test import compile_and_run + +HERE = Path(__file__).resolve().parent +PYXIS_ROOT = HERE.parents[1] + + +def test_location_persistence(tmp_path): + ran = compile_and_run( + tmp_path, + name="test_location_persistence", + sources=[ + HERE / "test_location_persistence.cpp", + PYXIS_ROOT / "lib" / "tdeck_ui" / "Telemetry" / "LocationPersistence.cpp", + PYXIS_ROOT / "lib" / "tdeck_ui" / "Telemetry" / "LocationStateRecord.cpp", + ], + include_dirs=[PYXIS_ROOT / "lib" / "tdeck_ui"], + sanitize=True, + timeout=60, + ) + assert "0 failed" in ran.stdout diff --git a/tests/native/test_location_state_record.cpp b/tests/native/test_location_state_record.cpp index 16dffb1d..5083254e 100644 --- a/tests/native/test_location_state_record.cpp +++ b/tests/native/test_location_state_record.cpp @@ -224,8 +224,12 @@ void rejectsCountOverflowDuplicatesAndInvalidCoordinates() { Telemetry::LocationStateRecordResult::OK); encoded[Telemetry::LOCATION_STATE_HEADER_BYTES + 16] |= 0x80U; repairCrc(encoded, written); + output = sample(); + output.sessions[0].record.cadence_millis = 12345; + const auto malformed_before = output; CHECK(Telemetry::decodeLocationStateRecord(encoded, written, output) == Telemetry::LocationStateRecordResult::MALFORMED); + CHECK(sameState(output, malformed_before)); auto duplicate = sample(); duplicate.session_count = 2;