use alloc::collections::btree_map::BTreeMap;
use alloc::vec::Vec;
use azul_core::dom::DomNodeId;
use azul_core::events::{
EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent,
};
use azul_core::task::Instant;
pub use azul_core::geolocation::{GeolocationProbeConfig, LocationFix};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C, u8)]
pub enum GeolocationDiffEvent {
Subscribe { config: GeolocationProbeConfig },
Release,
Reconfigure { config: GeolocationProbeConfig },
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct GeolocationManager {
pub latest_fix: Option<LocationFix>,
pub active_config: Option<GeolocationProbeConfig>,
pending_events: Vec<GeolocationDiffEvent>,
refcount: u32,
pending_event: bool,
pub last_error: Option<LocationError>,
pending_error_event: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct LocationError {
pub code: u32,
pub message: String,
}
impl GeolocationManager {
#[must_use] pub fn new() -> Self {
Self::default()
}
#[must_use] pub const fn latest_fix(&self) -> Option<LocationFix> {
self.latest_fix
}
#[must_use] pub const fn refcount(&self) -> u32 {
self.refcount
}
pub fn set_latest_fix(&mut self, fix: LocationFix) -> bool {
let changed = self
.latest_fix
.is_none_or(|prev| !Self::location_fix_bitwise_eq(&prev, &fix));
self.latest_fix = Some(fix);
if changed {
self.pending_event = true;
self.last_error = None;
}
changed
}
pub fn set_last_error(&mut self, error: LocationError) {
self.last_error = Some(error);
self.pending_error_event = true;
}
pub const fn clear_pending_event(&mut self) {
self.pending_event = false;
self.pending_error_event = false;
}
#[must_use] pub const fn has_active_subscription(&self) -> bool {
self.refcount > 0
}
const fn location_fix_bitwise_eq(a: &LocationFix, b: &LocationFix) -> bool {
a.latitude_deg.to_bits() == b.latitude_deg.to_bits()
&& a.longitude_deg.to_bits() == b.longitude_deg.to_bits()
&& a.accuracy_m.to_bits() == b.accuracy_m.to_bits()
&& a.altitude_m.to_bits() == b.altitude_m.to_bits()
&& a.altitude_accuracy_m.to_bits() == b.altitude_accuracy_m.to_bits()
&& a.heading_deg.to_bits() == b.heading_deg.to_bits()
&& a.speed_mps.to_bits() == b.speed_mps.to_bits()
&& a.timestamp_ms == b.timestamp_ms
}
pub fn take_pending_events(&mut self) -> Vec<GeolocationDiffEvent> {
core::mem::take(&mut self.pending_events)
}
pub fn diff_layout<F>(&mut self, mut for_each_probe: F)
where
F: FnMut(&mut dyn FnMut(GeolocationProbeConfig)),
{
let mut new_count: u32 = 0;
let mut next_config: Option<GeolocationProbeConfig> = None;
for_each_probe(&mut |cfg| {
new_count += 1;
if next_config.is_none() {
next_config = Some(cfg);
}
});
let old_count = self.refcount;
self.refcount = new_count;
match (old_count, new_count) {
(0, n) if n > 0 => {
let config = next_config.unwrap_or_default();
self.active_config = Some(config);
self.pending_events
.push(GeolocationDiffEvent::Subscribe { config });
}
(m, 0) if m > 0 => {
self.active_config = None;
self.latest_fix = None;
self.pending_events.push(GeolocationDiffEvent::Release);
}
(m, n) if m > 0 && n > 0 => {
let new_config = next_config.unwrap_or_default();
if Some(new_config) != self.active_config {
self.active_config = Some(new_config);
self.pending_events
.push(GeolocationDiffEvent::Reconfigure { config: new_config });
}
}
_ => {
}
}
}
}
impl EventProvider for GeolocationManager {
fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
let mut events = Vec::new();
if self.pending_event {
events.push(SyntheticEvent::new(
EventType::GeolocationFix,
CoreEventSource::User,
DomNodeId::ROOT,
timestamp.clone(),
EventData::None,
));
}
if self.pending_error_event {
events.push(SyntheticEvent::new(
EventType::GeolocationError,
CoreEventSource::User,
DomNodeId::ROOT,
timestamp,
EventData::None,
));
}
events
}
}
static PENDING_FIXES: std::sync::Mutex<Vec<LocationFix>> =
std::sync::Mutex::new(Vec::new());
pub fn push_location_fix(fix: LocationFix) {
let mut q = PENDING_FIXES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
q.push(fix);
}
pub fn drain_location_fixes() -> Vec<LocationFix> {
let mut q = PENDING_FIXES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
core::mem::take(&mut *q)
}
static PENDING_ERRORS: std::sync::Mutex<Vec<LocationError>> =
std::sync::Mutex::new(Vec::new());
pub fn push_location_error(error: LocationError) {
let mut q = PENDING_ERRORS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
q.push(error);
}
pub fn drain_location_errors() -> Vec<LocationError> {
let mut q = PENDING_ERRORS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
core::mem::take(&mut *q)
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg() -> GeolocationProbeConfig {
GeolocationProbeConfig::default()
}
fn high_accuracy_cfg() -> GeolocationProbeConfig {
GeolocationProbeConfig {
high_accuracy: true,
..GeolocationProbeConfig::default()
}
}
fn fix(lat: f64, lon: f64) -> LocationFix {
LocationFix {
latitude_deg: lat,
longitude_deg: lon,
accuracy_m: 10.0,
altitude_m: f32::NAN,
altitude_accuracy_m: f32::NAN,
heading_deg: f32::NAN,
speed_mps: f32::NAN,
timestamp_ms: 0,
}
}
#[test]
fn first_probe_emits_subscribe_with_config() {
let mut mgr = GeolocationManager::new();
mgr.diff_layout(|emit| emit(cfg()));
assert_eq!(mgr.refcount(), 1);
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
assert!(matches!(events[0], GeolocationDiffEvent::Subscribe { .. }));
}
#[test]
fn last_probe_drop_emits_release_and_clears_fix() {
let mut mgr = GeolocationManager::new();
mgr.diff_layout(|emit| emit(cfg()));
mgr.set_latest_fix(fix(37.0, -122.0));
drop(mgr.take_pending_events());
mgr.diff_layout(|_emit| {});
assert_eq!(mgr.refcount(), 0);
assert_eq!(mgr.latest_fix(), None);
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
assert!(matches!(events[0], GeolocationDiffEvent::Release));
}
#[test]
fn config_drift_emits_reconfigure() {
let mut mgr = GeolocationManager::new();
mgr.diff_layout(|emit| emit(cfg()));
drop(mgr.take_pending_events());
mgr.diff_layout(|emit| emit(high_accuracy_cfg()));
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
let ev = &events[0];
match ev {
GeolocationDiffEvent::Reconfigure { config } => {
assert!(config.high_accuracy);
}
_ => panic!("expected Reconfigure, got {ev:?}"),
}
}
#[test]
fn stable_config_does_not_re_emit() {
let mut mgr = GeolocationManager::new();
mgr.diff_layout(|emit| emit(cfg()));
drop(mgr.take_pending_events());
mgr.diff_layout(|emit| emit(cfg()));
assert!(mgr.take_pending_events().is_empty());
}
#[test]
fn set_latest_fix_returns_change_flag() {
let mut mgr = GeolocationManager::new();
assert!(mgr.set_latest_fix(fix(37.0, -122.0)));
assert!(!mgr.set_latest_fix(fix(37.0, -122.0)));
assert!(mgr.set_latest_fix(fix(37.7749, -122.4194)));
}
#[test]
fn missing_fields_decode_to_none() {
let f = fix(0.0, 0.0);
assert_eq!(f.altitude(), None);
assert_eq!(f.heading(), None);
assert_eq!(f.speed(), None);
}
#[test]
fn provider_yields_fix_event_then_clears() {
use azul_core::task::{Instant, SystemTick};
let ts = Instant::Tick(SystemTick::new(0));
let mut mgr = GeolocationManager::new();
assert!(mgr.get_pending_events(ts.clone()).is_empty(), "no fix yet");
mgr.set_latest_fix(fix(37.0, -122.0));
let events = mgr.get_pending_events(ts.clone());
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_type, EventType::GeolocationFix);
mgr.clear_pending_event();
assert!(mgr.get_pending_events(ts.clone()).is_empty(), "cleared after dispatch");
mgr.set_latest_fix(fix(37.0, -122.0));
assert!(mgr.get_pending_events(ts).is_empty());
}
#[test]
fn error_channel_and_provider_event() {
use azul_core::task::{Instant, SystemTick};
drop(drain_location_errors());
push_location_error(LocationError { code: 1, message: "denied".into() });
let errs = drain_location_errors();
assert_eq!(errs.len(), 1);
assert!(drain_location_errors().is_empty());
let ts = Instant::Tick(SystemTick::new(0));
let mut mgr = GeolocationManager::new();
mgr.set_last_error(errs[0].clone());
let events = mgr.get_pending_events(ts.clone());
assert_eq!(events.len(), 1);
assert_eq!(
events[0].event_type,
EventType::GeolocationError
);
mgr.clear_pending_event();
assert!(mgr.get_pending_events(ts).is_empty());
mgr.set_latest_fix(fix(1.0, 2.0));
assert!(mgr.last_error.is_none());
}
#[test]
fn subscription_flag_follows_probe_refcount() {
let mut mgr = GeolocationManager::new();
assert!(!mgr.has_active_subscription());
mgr.diff_layout(|emit| emit(cfg()));
assert!(mgr.has_active_subscription());
mgr.diff_layout(|_emit| {});
assert!(!mgr.has_active_subscription());
}
#[test]
#[allow(clippy::float_cmp)] fn async_fixes_round_trip_through_manager() {
drop(drain_location_fixes());
push_location_fix(fix(37.0, -122.0));
push_location_fix(fix(48.8566, 2.3522)); let drained = drain_location_fixes();
assert_eq!(drained.len(), 2, "both parked fixes drain in order");
assert_eq!(drained[0].latitude_deg, 37.0);
assert_eq!(drained[1].latitude_deg, 48.8566);
let mut mgr = GeolocationManager::new();
for f in &drained {
mgr.set_latest_fix(*f);
}
let got = mgr.latest_fix().expect("a fix was applied");
assert_eq!(got.latitude_deg, 48.8566, "the last applied fix wins");
assert!(drain_location_fixes().is_empty());
}
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::eq_op, clippy::redundant_clone)]
mod autotest_generated {
use azul_core::task::{Instant, SystemTick};
use super::*;
const fn full_fix() -> LocationFix {
LocationFix {
latitude_deg: 48.208_8,
longitude_deg: 16.372_1,
accuracy_m: 5.0,
altitude_m: 171.0,
altitude_accuracy_m: 3.0,
heading_deg: 90.0,
speed_mps: 1.5,
timestamp_ms: 1_234,
}
}
const fn nan_fix() -> LocationFix {
LocationFix {
latitude_deg: 37.0,
longitude_deg: -122.0,
accuracy_m: 10.0,
altitude_m: f32::NAN,
altitude_accuracy_m: f32::NAN,
heading_deg: f32::NAN,
speed_mps: f32::NAN,
timestamp_ms: 0,
}
}
const fn probe_cfg(high_accuracy: bool, max_accuracy_m: f32) -> GeolocationProbeConfig {
GeolocationProbeConfig {
high_accuracy,
background: false,
max_accuracy_m,
min_interval_ms: 0,
}
}
fn tick() -> Instant {
Instant::Tick(SystemTick::new(0))
}
fn single_field_mutations() -> Vec<(&'static str, LocationFix)> {
let base = full_fix();
let mut out = Vec::new();
let mut f = base;
f.latitude_deg = -48.208_8;
out.push(("latitude_deg", f));
let mut f = base;
f.longitude_deg = 16.372_2;
out.push(("longitude_deg", f));
let mut f = base;
f.accuracy_m = 5.000_001;
out.push(("accuracy_m", f));
let mut f = base;
f.altitude_m = f32::NAN;
out.push(("altitude_m", f));
let mut f = base;
f.altitude_accuracy_m = f32::NAN;
out.push(("altitude_accuracy_m", f));
let mut f = base;
f.heading_deg = 270.0;
out.push(("heading_deg", f));
let mut f = base;
f.speed_mps = -1.5;
out.push(("speed_mps", f));
let mut f = base;
f.timestamp_ms = u64::MAX;
out.push(("timestamp_ms", f));
out
}
#[test]
fn new_equals_default_and_holds_construction_invariants() {
let mgr = GeolocationManager::new();
assert_eq!(mgr, GeolocationManager::default());
assert_eq!(mgr, GeolocationManager::new(), "construction is deterministic");
assert_eq!(mgr.latest_fix(), None);
assert_eq!(mgr.refcount(), 0);
assert_eq!(mgr.active_config, None);
assert_eq!(mgr.last_error, None);
assert!(!mgr.has_active_subscription());
assert!(mgr.get_pending_events(tick()).is_empty());
}
#[test]
fn getters_on_a_fresh_manager_do_not_panic() {
let mut mgr = GeolocationManager::new();
for _ in 0..3 {
assert_eq!(mgr.latest_fix(), None);
assert_eq!(mgr.refcount(), 0);
assert!(!mgr.has_active_subscription());
assert!(mgr.take_pending_events().is_empty());
mgr.clear_pending_event();
}
}
#[test]
fn latest_fix_round_trips_extreme_bit_patterns() {
let extreme = LocationFix {
latitude_deg: f64::INFINITY,
longitude_deg: f64::NEG_INFINITY,
accuracy_m: f32::MAX,
altitude_m: f32::MIN_POSITIVE,
altitude_accuracy_m: f32::from_bits(1), heading_deg: -0.0,
speed_mps: f32::NEG_INFINITY,
timestamp_ms: u64::MAX,
};
let mut mgr = GeolocationManager::new();
assert!(mgr.set_latest_fix(extreme), "first fix is always a change");
let got = mgr.latest_fix().expect("a fix was stored");
assert_eq!(got.latitude_deg.to_bits(), extreme.latitude_deg.to_bits());
assert_eq!(got.longitude_deg.to_bits(), extreme.longitude_deg.to_bits());
assert_eq!(got.accuracy_m.to_bits(), extreme.accuracy_m.to_bits());
assert_eq!(got.altitude_m.to_bits(), extreme.altitude_m.to_bits());
assert_eq!(
got.altitude_accuracy_m.to_bits(),
extreme.altitude_accuracy_m.to_bits()
);
assert_eq!(got.heading_deg.to_bits(), extreme.heading_deg.to_bits());
assert_eq!(got.speed_mps.to_bits(), extreme.speed_mps.to_bits());
assert_eq!(got.timestamp_ms, u64::MAX);
assert_eq!(got.speed(), Some(f32::NEG_INFINITY));
assert!(mgr.latest_fix().is_some());
assert!(!mgr.set_latest_fix(extreme));
}
#[test]
fn nan_fix_round_trips_and_getters_still_report_missing() {
let mut mgr = GeolocationManager::new();
assert!(mgr.set_latest_fix(nan_fix()));
let got = mgr.latest_fix().expect("a fix was stored");
assert_eq!(got.altitude(), None);
assert_eq!(got.altitude_accuracy(), None);
assert_eq!(got.heading(), None);
assert_eq!(got.speed(), None);
assert_eq!(got.latitude_deg.to_bits(), 37.0_f64.to_bits());
}
#[test]
fn bitwise_eq_is_reflexive_on_nan_unlike_partial_eq() {
let f = nan_fix();
assert!(f != f, "PartialEq on a NaN-carrying fix is not reflexive");
assert!(GeolocationManager::location_fix_bitwise_eq(&f, &f));
assert!(GeolocationManager::location_fix_bitwise_eq(&nan_fix(), &nan_fix()));
}
#[test]
fn bitwise_eq_is_reflexive_and_symmetric_over_extremes() {
let extremes = [
full_fix(),
nan_fix(),
LocationFix {
latitude_deg: f64::INFINITY,
longitude_deg: f64::NEG_INFINITY,
accuracy_m: f32::INFINITY,
altitude_m: f32::MAX,
altitude_accuracy_m: f32::MIN,
heading_deg: f32::from_bits(1),
speed_mps: -0.0,
timestamp_ms: u64::MAX,
},
LocationFix {
latitude_deg: 0.0,
longitude_deg: 0.0,
accuracy_m: 0.0,
altitude_m: 0.0,
altitude_accuracy_m: 0.0,
heading_deg: 0.0,
speed_mps: 0.0,
timestamp_ms: 0,
},
];
for (i, a) in extremes.iter().enumerate() {
assert!(
GeolocationManager::location_fix_bitwise_eq(a, a),
"not reflexive for extreme #{i}"
);
for (j, b) in extremes.iter().enumerate() {
assert_eq!(
GeolocationManager::location_fix_bitwise_eq(a, b),
GeolocationManager::location_fix_bitwise_eq(b, a),
"not symmetric for ({i}, {j})"
);
if i != j {
assert!(
!GeolocationManager::location_fix_bitwise_eq(a, b),
"distinct extremes #{i}/#{j} compared equal"
);
}
}
}
}
#[test]
fn bitwise_eq_looks_at_every_field() {
let base = full_fix();
for (field, mutated) in single_field_mutations() {
assert!(
!GeolocationManager::location_fix_bitwise_eq(&base, &mutated),
"`{field}` is ignored by location_fix_bitwise_eq"
);
assert!(
!GeolocationManager::location_fix_bitwise_eq(&mutated, &base),
"`{field}` comparison is asymmetric"
);
assert!(GeolocationManager::location_fix_bitwise_eq(&mutated, &mutated));
}
}
#[test]
fn bitwise_eq_separates_positive_and_negative_zero() {
let mut a = full_fix();
a.heading_deg = 0.0;
let mut b = a;
b.heading_deg = -0.0;
assert!(a.heading_deg == b.heading_deg);
assert!(!GeolocationManager::location_fix_bitwise_eq(&a, &b));
}
#[test]
fn repeated_identical_nan_fix_reports_no_change() {
let mut mgr = GeolocationManager::new();
assert!(mgr.set_latest_fix(nan_fix()), "first fix advances");
for _ in 0..100 {
assert!(
!mgr.set_latest_fix(nan_fix()),
"an unchanged NaN-carrying fix must not look like movement"
);
}
assert_eq!(mgr.get_pending_events(tick()).len(), 1);
}
#[test]
fn set_latest_fix_detects_a_change_in_every_field() {
for (field, mutated) in single_field_mutations() {
let mut mgr = GeolocationManager::new();
assert!(mgr.set_latest_fix(full_fix()));
mgr.clear_pending_event();
assert!(
mgr.set_latest_fix(mutated),
"a change in `{field}` was not reported"
);
assert_eq!(
mgr.get_pending_events(tick()).len(),
1,
"a change in `{field}` raised no GeolocationFix event"
);
}
}
#[test]
fn set_latest_fix_survives_extreme_and_pathological_values() {
let mut mgr = GeolocationManager::new();
let pathological = [
(f64::MAX, f64::MIN),
(f64::INFINITY, f64::NEG_INFINITY),
(f64::NAN, f64::NAN),
(1e308, -1e308),
(f64::MIN_POSITIVE, -f64::MIN_POSITIVE),
(91.0, 181.0), (-91.0, -181.0), ];
for (lat, lon) in pathological {
let f = LocationFix {
latitude_deg: lat,
longitude_deg: lon,
accuracy_m: f32::INFINITY,
altitude_m: f32::NAN,
altitude_accuracy_m: f32::NAN,
heading_deg: f32::NAN,
speed_mps: f32::NAN,
timestamp_ms: u64::MAX,
};
mgr.set_latest_fix(f);
let got = mgr.latest_fix().expect("stored");
assert_eq!(got.latitude_deg.to_bits(), lat.to_bits());
assert_eq!(got.longitude_deg.to_bits(), lon.to_bits());
assert!(!mgr.set_latest_fix(f), "re-applying the same fix is not a change");
}
}
#[test]
fn timestamp_only_movement_counts_as_a_change() {
let mut mgr = GeolocationManager::new();
let mut f = nan_fix();
assert!(mgr.set_latest_fix(f));
f.timestamp_ms = u64::MAX;
assert!(
mgr.set_latest_fix(f),
"a fresh sample at the same coordinates is still a new fix"
);
assert_eq!(mgr.latest_fix().expect("stored").timestamp_ms, u64::MAX);
}
#[test]
fn a_changed_fix_supersedes_the_stored_error_but_an_unchanged_one_does_not() {
let mut mgr = GeolocationManager::new();
assert!(mgr.set_latest_fix(full_fix()));
mgr.set_last_error(LocationError {
code: 2,
message: String::from("timed out"),
});
assert!(mgr.last_error.is_some());
assert!(!mgr.set_latest_fix(full_fix()));
assert!(
mgr.last_error.is_some(),
"an unchanged fix does not clear the error (documented `changed`-gated behaviour)"
);
assert!(mgr.set_latest_fix(nan_fix()));
assert_eq!(mgr.last_error, None);
}
#[test]
fn set_last_error_round_trips_extreme_payloads() {
let huge = "x".repeat(1 << 16);
let unicode = String::from("\u{0}dénié 🛰️\u{0301}\u{fffd}中文\u{1f680}");
let payloads = [
LocationError { code: 0, message: String::new() },
LocationError { code: u32::MAX, message: huge.clone() },
LocationError { code: 1, message: unicode.clone() },
];
for err in payloads {
let mut mgr = GeolocationManager::new();
mgr.set_last_error(err.clone());
let got = mgr.last_error.clone().expect("error was stored");
assert_eq!(got, err, "error payload did not round-trip");
assert_eq!(got.message.len(), err.message.len());
let events = mgr.get_pending_events(tick());
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_type, EventType::GeolocationError);
}
assert_eq!(huge.len(), 1 << 16);
assert!(unicode.contains('\u{0}'));
}
#[test]
fn every_repeated_error_raises_its_own_event() {
let mut mgr = GeolocationManager::new();
let err = LocationError {
code: 7,
message: String::from("revoked"),
};
for _ in 0..5 {
mgr.set_last_error(err.clone());
let events = mgr.get_pending_events(tick());
assert_eq!(
events.len(),
1,
"a repeated error must still answer the live subscription"
);
assert_eq!(events[0].event_type, EventType::GeolocationError);
mgr.clear_pending_event();
assert!(mgr.get_pending_events(tick()).is_empty());
}
assert_eq!(mgr.last_error, Some(err));
}
#[test]
fn clear_pending_event_is_idempotent_and_spares_the_diff_queue() {
let mut mgr = GeolocationManager::new();
mgr.diff_layout(|emit| emit(probe_cfg(false, 0.0)));
mgr.set_latest_fix(full_fix());
mgr.set_last_error(LocationError {
code: 1,
message: String::from("e"),
});
for _ in 0..3 {
mgr.clear_pending_event();
assert!(
mgr.get_pending_events(tick()).is_empty(),
"clearing twice must stay cleared"
);
}
let diffs = mgr.take_pending_events();
assert_eq!(diffs.len(), 1);
assert!(matches!(diffs[0], GeolocationDiffEvent::Subscribe { .. }));
assert!(mgr.latest_fix().is_some());
assert!(mgr.last_error.is_some());
}
#[test]
fn take_pending_events_drains_in_arrival_order_and_leaves_the_queue_empty() {
let mut mgr = GeolocationManager::new();
mgr.diff_layout(|emit| emit(probe_cfg(false, 0.0))); mgr.diff_layout(|emit| emit(probe_cfg(true, 0.0))); mgr.diff_layout(|_emit| {});
let events = mgr.take_pending_events();
assert_eq!(events.len(), 3);
assert!(matches!(events[0], GeolocationDiffEvent::Subscribe { .. }));
assert!(matches!(events[1], GeolocationDiffEvent::Reconfigure { .. }));
assert_eq!(events[2], GeolocationDiffEvent::Release);
assert!(mgr.take_pending_events().is_empty(), "taken, not copied");
assert!(mgr.take_pending_events().is_empty(), "still empty");
}
#[test]
fn get_pending_events_does_not_consume_the_flags() {
let mut mgr = GeolocationManager::new();
mgr.set_latest_fix(full_fix());
mgr.set_last_error(LocationError {
code: 3,
message: String::from("boom"),
});
for _ in 0..3 {
let events = mgr.get_pending_events(tick());
assert_eq!(events.len(), 2, "the getter is a read, not a drain");
assert_eq!(events[0].event_type, EventType::GeolocationFix);
assert_eq!(events[1].event_type, EventType::GeolocationError);
assert_eq!(events[0].target, DomNodeId::ROOT);
assert_eq!(events[1].target, DomNodeId::ROOT);
}
mgr.clear_pending_event();
assert!(mgr.get_pending_events(tick()).is_empty());
}
#[test]
fn no_probes_on_a_fresh_manager_emits_nothing() {
let mut mgr = GeolocationManager::new();
for _ in 0..4 {
mgr.diff_layout(|_emit| {});
assert_eq!(mgr.refcount(), 0);
assert!(!mgr.has_active_subscription());
assert_eq!(mgr.active_config, None);
assert!(
mgr.take_pending_events().is_empty(),
"0 → 0 must not emit a spurious Release"
);
}
}
#[test]
fn refcount_tracks_the_probe_count_and_gates_the_subscription_flag() {
let mut mgr = GeolocationManager::new();
mgr.diff_layout(|emit| {
for _ in 0..10_000 {
emit(probe_cfg(false, 0.0));
}
});
assert_eq!(mgr.refcount(), 10_000);
assert!(mgr.has_active_subscription());
assert_eq!(mgr.take_pending_events().len(), 1, "one Subscribe, not 10k");
mgr.diff_layout(|emit| {
for _ in 0..3 {
emit(probe_cfg(false, 0.0));
}
});
assert_eq!(mgr.refcount(), 3);
assert!(mgr.has_active_subscription());
assert!(mgr.take_pending_events().is_empty());
mgr.diff_layout(|emit| emit(probe_cfg(false, 0.0)));
assert_eq!(mgr.refcount(), 1);
assert!(mgr.has_active_subscription());
mgr.diff_layout(|_emit| {});
assert_eq!(mgr.refcount(), 0);
assert!(!mgr.has_active_subscription(), "0 probes ⇒ no subscription");
assert_eq!(mgr.take_pending_events(), vec![GeolocationDiffEvent::Release]);
}
#[test]
fn the_first_probes_config_wins_the_subscribe() {
let mut mgr = GeolocationManager::new();
mgr.diff_layout(|emit| {
emit(probe_cfg(true, 25.0));
emit(probe_cfg(false, 999.0)); emit(probe_cfg(false, 0.0));
});
assert_eq!(mgr.refcount(), 3);
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
match events[0] {
GeolocationDiffEvent::Subscribe { config } => {
assert!(config.high_accuracy);
assert_eq!(config.max_accuracy_m, 25.0);
}
ref other => panic!("expected Subscribe, got {other:?}"),
}
assert_eq!(mgr.active_config, Some(probe_cfg(true, 25.0)));
mgr.diff_layout(|emit| {
emit(probe_cfg(true, 25.0));
emit(probe_cfg(false, 0.0));
});
assert!(mgr.take_pending_events().is_empty());
}
#[test]
fn release_clears_the_fix_and_a_resubscribe_starts_from_scratch() {
let mut mgr = GeolocationManager::new();
mgr.diff_layout(|emit| emit(probe_cfg(false, 0.0)));
assert!(mgr.set_latest_fix(full_fix()));
assert!(mgr.take_pending_events().len() == 1);
mgr.diff_layout(|_emit| {}); assert_eq!(mgr.latest_fix(), None, "Release drops the stale fix");
assert_eq!(mgr.active_config, None);
assert_eq!(mgr.take_pending_events(), vec![GeolocationDiffEvent::Release]);
mgr.diff_layout(|emit| emit(probe_cfg(true, 5.0)));
assert_eq!(mgr.refcount(), 1);
assert_eq!(mgr.latest_fix(), None);
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
assert_eq!(
events[0],
GeolocationDiffEvent::Subscribe {
config: probe_cfg(true, 5.0)
}
);
}
#[test]
fn release_leaves_the_fix_event_flag_raised_with_no_fix_behind_it() {
let mut mgr = GeolocationManager::new();
mgr.diff_layout(|emit| emit(probe_cfg(false, 0.0)));
assert!(mgr.set_latest_fix(full_fix()));
mgr.diff_layout(|_emit| {}); assert_eq!(mgr.latest_fix(), None);
let events = mgr.get_pending_events(tick());
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_type, EventType::GeolocationFix);
mgr.clear_pending_event();
assert!(mgr.get_pending_events(tick()).is_empty());
}
#[test]
fn a_nan_probe_config_settles_like_any_other() {
let nan_cfg = probe_cfg(false, f32::NAN);
let mut mgr = GeolocationManager::new();
mgr.diff_layout(|emit| emit(nan_cfg));
assert_eq!(mgr.take_pending_events().len(), 1, "the initial Subscribe");
for _ in 0..3 {
mgr.diff_layout(|emit| emit(nan_cfg));
assert!(
mgr.take_pending_events().is_empty(),
"an unchanged NaN config must NOT re-queue a Reconfigure"
);
}
mgr.diff_layout(|emit| emit(probe_cfg(false, 0.0)));
assert_eq!(mgr.take_pending_events().len(), 1, "one Reconfigure to sane");
mgr.diff_layout(|emit| emit(probe_cfg(false, 0.0)));
assert!(mgr.take_pending_events().is_empty(), "then quiet");
}
#[test]
fn extreme_probe_configs_survive_the_diff() {
let extremes = [
probe_cfg(true, f32::MAX),
probe_cfg(false, f32::INFINITY),
probe_cfg(true, -0.0),
GeolocationProbeConfig {
high_accuracy: true,
background: true,
max_accuracy_m: f32::MIN_POSITIVE,
min_interval_ms: u32::MAX,
},
];
let mut mgr = GeolocationManager::new();
for cfg in extremes {
mgr.diff_layout(|emit| emit(cfg));
assert_eq!(mgr.refcount(), 1);
assert_eq!(mgr.active_config, Some(cfg));
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1, "each distinct config emits exactly one event");
match events[0] {
GeolocationDiffEvent::Subscribe { config }
| GeolocationDiffEvent::Reconfigure { config } => {
assert_eq!(config.max_accuracy_m.to_bits(), cfg.max_accuracy_m.to_bits());
assert_eq!(config.min_interval_ms, cfg.min_interval_ms);
}
GeolocationDiffEvent::Release => panic!("probes are mounted, not released"),
}
}
}
#[test]
fn manager_equality_is_bit_blind_for_nan_fixes() {
let mut with_nan = GeolocationManager::new();
with_nan.set_latest_fix(nan_fix());
assert_ne!(with_nan, with_nan.clone());
let mut without_nan = GeolocationManager::new();
without_nan.set_latest_fix(full_fix());
assert_eq!(without_nan, without_nan.clone());
}
}