mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-08-28 23:00:28 +00:00
feat: add bounded peer location state
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
#include "LocationShareState.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
namespace Telemetry {
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t NO_SLOT = MAX_PEER_LOCATIONS;
|
||||
|
||||
bool effectiveTimestampMillis(
|
||||
const LocationTelemetry& location,
|
||||
const CustomLocationMeta& meta,
|
||||
uint64_t& timestamp_millis) {
|
||||
if (meta.has_timestamp) {
|
||||
timestamp_millis = meta.timestamp_millis;
|
||||
return true;
|
||||
}
|
||||
if (location.timestamp_seconds >
|
||||
std::numeric_limits<uint64_t>::max() / 1000ULL) {
|
||||
return false;
|
||||
}
|
||||
timestamp_millis = location.timestamp_seconds * 1000ULL;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool locationInRange(const LocationTelemetry& location) {
|
||||
return location.latitude_e6 >= -90000000 &&
|
||||
location.latitude_e6 <= 90000000 &&
|
||||
location.longitude_e6 >= -180000000 &&
|
||||
location.longitude_e6 <= 180000000;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PeerLocationStore::peerEquals(const PeerId& left, const PeerId& right) {
|
||||
return std::memcmp(left.bytes, right.bytes, PEER_ID_SIZE) == 0;
|
||||
}
|
||||
|
||||
std::size_t PeerLocationStore::find(const PeerId& peer) const {
|
||||
for (std::size_t index = 0; index < MAX_PEER_LOCATIONS; ++index) {
|
||||
if (slots_[index].occupied && peerEquals(slots_[index].record.peer, peer)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return NO_SLOT;
|
||||
}
|
||||
|
||||
std::size_t PeerLocationStore::firstVacant() const {
|
||||
for (std::size_t index = 0; index < MAX_PEER_LOCATIONS; ++index) {
|
||||
if (!slots_[index].occupied) return index;
|
||||
}
|
||||
return NO_SLOT;
|
||||
}
|
||||
|
||||
std::size_t PeerLocationStore::evictionCandidate() const {
|
||||
std::size_t candidate = NO_SLOT;
|
||||
for (std::size_t index = 0; index < MAX_PEER_LOCATIONS; ++index) {
|
||||
if (!slots_[index].occupied) continue;
|
||||
if (candidate == NO_SLOT ||
|
||||
slots_[index].record.received_at_millis <
|
||||
slots_[candidate].record.received_at_millis) {
|
||||
candidate = index;
|
||||
}
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
bool PeerLocationStore::visible(
|
||||
const PeerLocationRecord& record,
|
||||
uint64_t now_millis,
|
||||
uint64_t maximum_age_millis) {
|
||||
if (record.expires_at_millis != 0 &&
|
||||
now_millis >= record.expires_at_millis) {
|
||||
return false;
|
||||
}
|
||||
if (now_millis >= record.received_at_millis &&
|
||||
now_millis - record.received_at_millis > maximum_age_millis) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void PeerLocationStore::clear(std::size_t index) {
|
||||
if (index >= MAX_PEER_LOCATIONS || !slots_[index].occupied) return;
|
||||
slots_[index] = Slot{};
|
||||
--size_;
|
||||
}
|
||||
|
||||
PeerLocationResult PeerLocationStore::apply(
|
||||
const PeerId& peer,
|
||||
const LocationTelemetry& location,
|
||||
const CustomLocationMeta& meta,
|
||||
uint64_t received_at_millis) {
|
||||
uint64_t source_timestamp_millis = 0;
|
||||
if (!effectiveTimestampMillis(location, meta, source_timestamp_millis)) {
|
||||
return PeerLocationResult::INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const std::size_t existing = find(peer);
|
||||
if (existing != NO_SLOT &&
|
||||
source_timestamp_millis <
|
||||
slots_[existing].record.source_timestamp_millis) {
|
||||
return PeerLocationResult::STALE;
|
||||
}
|
||||
|
||||
if (meta.has_cease && meta.cease) {
|
||||
if (existing == NO_SLOT) return PeerLocationResult::NOT_FOUND;
|
||||
clear(existing);
|
||||
return PeerLocationResult::CEASED;
|
||||
}
|
||||
|
||||
const uint64_t expires_at_millis =
|
||||
meta.has_expires ? meta.expires_millis : 0;
|
||||
if (expires_at_millis != 0 &&
|
||||
received_at_millis >= expires_at_millis) {
|
||||
if (existing != NO_SLOT) clear(existing);
|
||||
return PeerLocationResult::EXPIRED;
|
||||
}
|
||||
if (!locationInRange(location)) {
|
||||
return PeerLocationResult::INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
PeerLocationRecord record{};
|
||||
record.peer = peer;
|
||||
record.location = location;
|
||||
record.source_timestamp_millis = source_timestamp_millis;
|
||||
record.received_at_millis = received_at_millis;
|
||||
record.expires_at_millis = expires_at_millis;
|
||||
record.approx_radius_meters =
|
||||
meta.has_approx_radius ? meta.approx_radius_meters : 0;
|
||||
|
||||
if (existing != NO_SLOT) {
|
||||
slots_[existing].record = record;
|
||||
return PeerLocationResult::UPDATED;
|
||||
}
|
||||
|
||||
std::size_t target = firstVacant();
|
||||
if (target == NO_SLOT) target = evictionCandidate();
|
||||
if (target == NO_SLOT) return PeerLocationResult::INVALID_ARGUMENT;
|
||||
if (!slots_[target].occupied) ++size_;
|
||||
slots_[target].record = record;
|
||||
slots_[target].occupied = true;
|
||||
return PeerLocationResult::INSERTED;
|
||||
}
|
||||
|
||||
bool PeerLocationStore::get(
|
||||
const PeerId& peer,
|
||||
PeerLocationRecord& output) const {
|
||||
const std::size_t index = find(peer);
|
||||
if (index == NO_SLOT) return false;
|
||||
output = slots_[index].record;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t PeerLocationStore::snapshot(
|
||||
uint64_t now_millis,
|
||||
uint64_t maximum_age_millis,
|
||||
PeerLocationRecord* output,
|
||||
std::size_t capacity) const {
|
||||
if (output == nullptr || capacity == 0) return 0;
|
||||
std::size_t copied = 0;
|
||||
for (std::size_t index = 0;
|
||||
index < MAX_PEER_LOCATIONS && copied < capacity;
|
||||
++index) {
|
||||
if (!slots_[index].occupied ||
|
||||
!visible(slots_[index].record, now_millis, maximum_age_millis)) {
|
||||
continue;
|
||||
}
|
||||
output[copied++] = slots_[index].record;
|
||||
}
|
||||
return copied;
|
||||
}
|
||||
|
||||
std::size_t PeerLocationStore::prune(
|
||||
uint64_t now_millis,
|
||||
uint64_t maximum_age_millis) {
|
||||
std::size_t removed = 0;
|
||||
for (std::size_t index = 0; index < MAX_PEER_LOCATIONS; ++index) {
|
||||
if (slots_[index].occupied &&
|
||||
!visible(slots_[index].record, now_millis, maximum_age_millis)) {
|
||||
clear(index);
|
||||
++removed;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
} // namespace Telemetry
|
||||
@@ -0,0 +1,80 @@
|
||||
#ifndef PYXIS_TELEMETRY_LOCATION_SHARE_STATE_H
|
||||
#define PYXIS_TELEMETRY_LOCATION_SHARE_STATE_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "LocationTelemetryCodec.h"
|
||||
|
||||
namespace Telemetry {
|
||||
|
||||
constexpr std::size_t PEER_ID_SIZE = 16;
|
||||
constexpr std::size_t MAX_PEER_LOCATIONS = 32;
|
||||
|
||||
struct PeerId {
|
||||
PeerId() : bytes{} {}
|
||||
uint8_t bytes[PEER_ID_SIZE];
|
||||
};
|
||||
|
||||
struct PeerLocationRecord {
|
||||
PeerId peer{};
|
||||
LocationTelemetry location{};
|
||||
uint64_t source_timestamp_millis = 0;
|
||||
uint64_t received_at_millis = 0;
|
||||
uint64_t expires_at_millis = 0;
|
||||
uint32_t approx_radius_meters = 0;
|
||||
};
|
||||
|
||||
enum class PeerLocationResult : uint8_t {
|
||||
INSERTED,
|
||||
UPDATED,
|
||||
CEASED,
|
||||
NOT_FOUND,
|
||||
STALE,
|
||||
EXPIRED,
|
||||
INVALID_ARGUMENT,
|
||||
};
|
||||
|
||||
class PeerLocationStore {
|
||||
public:
|
||||
PeerLocationStore() = default;
|
||||
|
||||
PeerLocationResult apply(
|
||||
const PeerId& peer,
|
||||
const LocationTelemetry& location,
|
||||
const CustomLocationMeta& meta,
|
||||
uint64_t received_at_millis);
|
||||
|
||||
bool get(const PeerId& peer, PeerLocationRecord& output) const;
|
||||
|
||||
std::size_t snapshot(
|
||||
uint64_t now_millis,
|
||||
uint64_t maximum_age_millis,
|
||||
PeerLocationRecord* output,
|
||||
std::size_t capacity) const;
|
||||
|
||||
std::size_t prune(uint64_t now_millis, uint64_t maximum_age_millis);
|
||||
std::size_t size() const { return size_; }
|
||||
|
||||
private:
|
||||
struct Slot {
|
||||
bool occupied = false;
|
||||
PeerLocationRecord record{};
|
||||
};
|
||||
|
||||
std::size_t find(const PeerId& peer) const;
|
||||
std::size_t firstVacant() const;
|
||||
std::size_t evictionCandidate() const;
|
||||
static bool peerEquals(const PeerId& left, const PeerId& right);
|
||||
static bool visible(const PeerLocationRecord& record,
|
||||
uint64_t now_millis,
|
||||
uint64_t maximum_age_millis);
|
||||
void clear(std::size_t index);
|
||||
|
||||
Slot slots_[MAX_PEER_LOCATIONS]{};
|
||||
std::size_t size_ = 0;
|
||||
};
|
||||
|
||||
} // namespace Telemetry
|
||||
|
||||
#endif // PYXIS_TELEMETRY_LOCATION_SHARE_STATE_H
|
||||
@@ -0,0 +1,254 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
|
||||
#include "Telemetry/LocationShareState.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)
|
||||
|
||||
Telemetry::PeerId peer(uint8_t seed) {
|
||||
Telemetry::PeerId id{};
|
||||
for (std::size_t index = 0; index < Telemetry::PEER_ID_SIZE; ++index) {
|
||||
id.bytes[index] = static_cast<uint8_t>(seed + index);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
Telemetry::LocationTelemetry location(uint64_t seconds, int32_t latitude = 1000000) {
|
||||
Telemetry::LocationTelemetry value{};
|
||||
value.latitude_e6 = latitude;
|
||||
value.longitude_e6 = -1000000;
|
||||
value.accuracy_cm = 100;
|
||||
value.timestamp_seconds = seconds;
|
||||
value.sensor_timestamp_seconds = seconds;
|
||||
return value;
|
||||
}
|
||||
|
||||
Telemetry::CustomLocationMeta metaTimestamp(uint64_t millis) {
|
||||
Telemetry::CustomLocationMeta meta{};
|
||||
meta.has_timestamp = true;
|
||||
meta.timestamp_millis = millis;
|
||||
return meta;
|
||||
}
|
||||
|
||||
bool hasPeer(const Telemetry::PeerLocationStore& store,
|
||||
const Telemetry::PeerId& id,
|
||||
Telemetry::PeerLocationRecord* record = nullptr) {
|
||||
Telemetry::PeerLocationRecord candidate{};
|
||||
const bool found = store.get(id, candidate);
|
||||
if (found && record != nullptr) *record = candidate;
|
||||
return found;
|
||||
}
|
||||
|
||||
void insertsUpdatesAndRejectsStaleData() {
|
||||
Telemetry::PeerLocationStore store;
|
||||
const auto id = peer(1);
|
||||
Telemetry::CustomLocationMeta no_meta{};
|
||||
|
||||
CHECK(store.apply(id, location(10, 100), no_meta, 1000) ==
|
||||
Telemetry::PeerLocationResult::INSERTED);
|
||||
CHECK(store.size() == 1);
|
||||
CHECK(store.apply(id, location(10, 200), no_meta, 1001) ==
|
||||
Telemetry::PeerLocationResult::UPDATED);
|
||||
|
||||
Telemetry::PeerLocationRecord record{};
|
||||
CHECK(hasPeer(store, id, &record));
|
||||
CHECK(record.location.latitude_e6 == 200);
|
||||
CHECK(record.source_timestamp_millis == 10000);
|
||||
CHECK(record.received_at_millis == 1001);
|
||||
|
||||
CHECK(store.apply(id, location(9, 300), no_meta, 1002) ==
|
||||
Telemetry::PeerLocationResult::STALE);
|
||||
CHECK(hasPeer(store, id, &record));
|
||||
CHECK(record.location.latitude_e6 == 200);
|
||||
CHECK(record.received_at_millis == 1001);
|
||||
|
||||
const auto precise = metaTimestamp(10001);
|
||||
CHECK(store.apply(id, location(1, 400), precise, 1003) ==
|
||||
Telemetry::PeerLocationResult::UPDATED);
|
||||
CHECK(hasPeer(store, id, &record));
|
||||
CHECK(record.source_timestamp_millis == 10001);
|
||||
CHECK(record.location.latitude_e6 == 400);
|
||||
}
|
||||
|
||||
void appliesOrderedCeaseWithoutTouchingOtherPeers() {
|
||||
Telemetry::PeerLocationStore store;
|
||||
Telemetry::CustomLocationMeta no_meta{};
|
||||
const auto first = peer(10);
|
||||
const auto second = peer(20);
|
||||
CHECK(store.apply(first, location(10), no_meta, 1000) ==
|
||||
Telemetry::PeerLocationResult::INSERTED);
|
||||
CHECK(store.apply(second, location(10), no_meta, 1001) ==
|
||||
Telemetry::PeerLocationResult::INSERTED);
|
||||
|
||||
auto cease = metaTimestamp(9999);
|
||||
cease.has_cease = true;
|
||||
cease.cease = true;
|
||||
CHECK(store.apply(first, location(0), cease, 1002) ==
|
||||
Telemetry::PeerLocationResult::STALE);
|
||||
CHECK(hasPeer(store, first));
|
||||
CHECK(hasPeer(store, second));
|
||||
|
||||
cease.timestamp_millis = 10000;
|
||||
CHECK(store.apply(first, location(0), cease, 1003) ==
|
||||
Telemetry::PeerLocationResult::CEASED);
|
||||
CHECK(!hasPeer(store, first));
|
||||
CHECK(hasPeer(store, second));
|
||||
CHECK(store.size() == 1);
|
||||
CHECK(store.apply(first, location(0), cease, 1004) ==
|
||||
Telemetry::PeerLocationResult::NOT_FOUND);
|
||||
}
|
||||
|
||||
void reusesVacanciesBeforeDeterministicEviction() {
|
||||
Telemetry::PeerLocationStore store;
|
||||
Telemetry::CustomLocationMeta no_meta{};
|
||||
for (std::size_t index = 0; index < Telemetry::MAX_PEER_LOCATIONS; ++index) {
|
||||
CHECK(store.apply(peer(static_cast<uint8_t>(index)), location(10), no_meta, 5) ==
|
||||
Telemetry::PeerLocationResult::INSERTED);
|
||||
}
|
||||
CHECK(store.size() == Telemetry::MAX_PEER_LOCATIONS);
|
||||
|
||||
auto cease = metaTimestamp(10000);
|
||||
cease.has_cease = true;
|
||||
cease.cease = true;
|
||||
CHECK(store.apply(peer(10), location(0), cease, 6) ==
|
||||
Telemetry::PeerLocationResult::CEASED);
|
||||
CHECK(store.apply(peer(100), location(11), no_meta, 6) ==
|
||||
Telemetry::PeerLocationResult::INSERTED);
|
||||
CHECK(hasPeer(store, peer(0)));
|
||||
CHECK(hasPeer(store, peer(100)));
|
||||
|
||||
// Every live record had received_at=5 except the replacement at 6. With
|
||||
// no vacancy, the stable tie-breaker evicts the lowest slot, peer 0.
|
||||
CHECK(store.apply(peer(101), location(12), no_meta, 7) ==
|
||||
Telemetry::PeerLocationResult::INSERTED);
|
||||
CHECK(!hasPeer(store, peer(0)));
|
||||
CHECK(hasPeer(store, peer(1)));
|
||||
CHECK(hasPeer(store, peer(101)));
|
||||
CHECK(store.size() == Telemetry::MAX_PEER_LOCATIONS);
|
||||
}
|
||||
|
||||
void enforcesExpiryAndStaleDisplayBoundaries() {
|
||||
Telemetry::PeerLocationStore store;
|
||||
const auto id = peer(30);
|
||||
auto meta = metaTimestamp(10000);
|
||||
meta.has_expires = true;
|
||||
meta.expires_millis = 5000;
|
||||
CHECK(store.apply(id, location(10), meta, 1000) ==
|
||||
Telemetry::PeerLocationResult::INSERTED);
|
||||
|
||||
Telemetry::PeerLocationRecord snapshot[2]{};
|
||||
CHECK(store.snapshot(4999, 10000, snapshot, 2) == 1);
|
||||
CHECK(store.snapshot(5000, 10000, snapshot, 2) == 0);
|
||||
CHECK(store.prune(4999, 10000) == 0);
|
||||
CHECK(store.prune(5000, 10000) == 1);
|
||||
CHECK(store.size() == 0);
|
||||
|
||||
Telemetry::CustomLocationMeta no_meta{};
|
||||
CHECK(store.apply(id, location(20), no_meta, 1000) ==
|
||||
Telemetry::PeerLocationResult::INSERTED);
|
||||
CHECK(store.snapshot(1100, 100, snapshot, 2) == 1);
|
||||
CHECK(store.snapshot(1101, 100, snapshot, 2) == 0);
|
||||
CHECK(store.snapshot(999, 100, snapshot, 2) == 1); // clock moved backward
|
||||
CHECK(store.prune(1100, 100) == 0);
|
||||
CHECK(store.prune(1101, 100) == 1);
|
||||
}
|
||||
|
||||
void expiredNewerUpdateClearsExistingState() {
|
||||
Telemetry::PeerLocationStore store;
|
||||
const auto id = peer(40);
|
||||
Telemetry::CustomLocationMeta no_meta{};
|
||||
CHECK(store.apply(id, location(10), no_meta, 1000) ==
|
||||
Telemetry::PeerLocationResult::INSERTED);
|
||||
|
||||
auto expired = metaTimestamp(11000);
|
||||
expired.has_expires = true;
|
||||
expired.expires_millis = 1999;
|
||||
CHECK(store.apply(id, location(11), expired, 2000) ==
|
||||
Telemetry::PeerLocationResult::EXPIRED);
|
||||
CHECK(!hasPeer(store, id));
|
||||
CHECK(store.size() == 0);
|
||||
}
|
||||
|
||||
void snapshotsAreCallerOwnedAndCapacityBounded() {
|
||||
Telemetry::PeerLocationStore store;
|
||||
Telemetry::CustomLocationMeta no_meta{};
|
||||
CHECK(store.apply(peer(1), location(1, 10), no_meta, 1) ==
|
||||
Telemetry::PeerLocationResult::INSERTED);
|
||||
CHECK(store.apply(peer(2), location(2, 20), no_meta, 2) ==
|
||||
Telemetry::PeerLocationResult::INSERTED);
|
||||
|
||||
Telemetry::PeerLocationRecord snapshot[1]{};
|
||||
CHECK(store.snapshot(2, std::numeric_limits<uint64_t>::max(), snapshot, 1) == 1);
|
||||
CHECK(snapshot[0].location.latitude_e6 == 10);
|
||||
CHECK(store.apply(peer(1), location(3, 99), no_meta, 3) ==
|
||||
Telemetry::PeerLocationResult::UPDATED);
|
||||
CHECK(snapshot[0].location.latitude_e6 == 10);
|
||||
}
|
||||
|
||||
void rejectsTimestampOverflowWithoutMutation() {
|
||||
Telemetry::PeerLocationStore store;
|
||||
Telemetry::CustomLocationMeta no_meta{};
|
||||
auto invalid = location(std::numeric_limits<uint64_t>::max());
|
||||
CHECK(store.apply(peer(50), invalid, no_meta, 1) ==
|
||||
Telemetry::PeerLocationResult::INVALID_ARGUMENT);
|
||||
CHECK(store.size() == 0);
|
||||
}
|
||||
|
||||
void survivesDeterministicHundredThousandOperationStress() {
|
||||
Telemetry::PeerLocationStore store;
|
||||
CHECK(sizeof(store) <= 4096);
|
||||
uint32_t state = 0x12345678U;
|
||||
for (std::size_t operation = 0; operation < 100000; ++operation) {
|
||||
state = state * 1664525U + 1013904223U;
|
||||
const auto id = peer(static_cast<uint8_t>(state >> 24U));
|
||||
Telemetry::CustomLocationMeta meta{};
|
||||
meta.has_timestamp = true;
|
||||
meta.timestamp_millis = operation;
|
||||
if ((state & 0x3fU) == 0) {
|
||||
meta.has_cease = true;
|
||||
meta.cease = true;
|
||||
}
|
||||
if ((state & 0x1fU) == 1) {
|
||||
meta.has_expires = true;
|
||||
meta.expires_millis = operation + 10;
|
||||
}
|
||||
store.apply(id, location(operation / 1000U), meta, operation);
|
||||
if ((operation % 257U) == 0) {
|
||||
store.prune(operation, 1000);
|
||||
}
|
||||
CHECK(store.size() <= Telemetry::MAX_PEER_LOCATIONS);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
insertsUpdatesAndRejectsStaleData();
|
||||
appliesOrderedCeaseWithoutTouchingOtherPeers();
|
||||
reusesVacanciesBeforeDeterministicEviction();
|
||||
enforcesExpiryAndStaleDisplayBoundaries();
|
||||
expiredNewerUpdateClearsExistingState();
|
||||
snapshotsAreCallerOwnedAndCapacityBounded();
|
||||
rejectsTimestampOverflowWithoutMutation();
|
||||
survivesDeterministicHundredThousandOperationStress();
|
||||
std::cout << "location share state: " << passed << " passed, "
|
||||
<< failures << " failed\n";
|
||||
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Compile and execute bounded peer location state tests."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from native_test import compile_and_run
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
PYXIS_ROOT = HERE.parent.parent
|
||||
|
||||
|
||||
def test_location_share_state(tmp_path):
|
||||
ran = compile_and_run(
|
||||
tmp_path,
|
||||
name="test_location_share_state",
|
||||
sources=[
|
||||
HERE / "test_location_share_state.cpp",
|
||||
PYXIS_ROOT / "lib" / "tdeck_ui" / "Telemetry" / "LocationShareState.cpp",
|
||||
],
|
||||
include_dirs=[PYXIS_ROOT / "lib" / "tdeck_ui"],
|
||||
sanitize=True,
|
||||
timeout=60,
|
||||
)
|
||||
assert "location share state:" in ran.stdout
|
||||
assert "0 failed" in ran.stdout
|
||||
Reference in New Issue
Block a user