1use alloc::collections::btree_map::BTreeMap;
28use alloc::vec::Vec;
29
30use azul_core::dom::DomNodeId;
31use azul_core::events::{
32 EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent,
33};
34use azul_core::task::Instant;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
44#[repr(C)]
45pub enum Capability {
46 Camera,
48 Microphone,
50 ScreenCapture,
52 Geolocation,
54 GeolocationBackground,
56 Biometric,
58 Motion,
60 PhotoLibrary,
62 PhotoLibraryWrite,
64 Contacts,
66 Calendars,
68 Reminders,
70 Notifications,
72 Bluetooth,
74 BluetoothBackground,
76 NearbyWifi,
78 LocalNetwork,
80 AppTrackingTransparency,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
86#[repr(C)]
87pub enum PermissionQuality {
88 Full,
90 Reduced,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
102#[repr(C, u8)]
103pub enum PermissionState {
104 NotDetermined,
106 Requested,
108 Granted(PermissionQuality),
110 Denied,
112 Restricted,
114 EphemeralGranted(bool),
116}
117
118impl PermissionState {
119 #[must_use] pub const fn is_granted(self) -> bool {
121 matches!(
122 self,
123 Self::Granted(..) | Self::EphemeralGranted(..)
124 )
125 }
126
127 #[must_use] pub const fn could_re_prompt(self) -> bool {
129 matches!(self, Self::NotDetermined)
130 }
131}
132
133#[derive(Copy, Debug, Clone, PartialEq, Eq)]
143#[repr(C, u8)]
144pub enum PermissionDiffEvent {
145 Subscribe {
147 capability: Capability,
148 node_id: DomNodeId,
149 },
150 Release {
152 capability: Capability,
153 },
154 Reconfigure {
157 capability: Capability,
158 },
159}
160
161#[derive(Copy, Debug, Clone, PartialEq, Eq)]
169pub struct CapabilityEntry {
170 pub state: PermissionState,
171 pub refcount: u32,
172 pub last_subscriber: Option<DomNodeId>,
173}
174
175impl CapabilityEntry {
176 const fn new() -> Self {
177 Self {
178 state: PermissionState::NotDetermined,
179 refcount: 0,
180 last_subscriber: None,
181 }
182 }
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Default)]
192pub struct PermissionManager {
193 pub statuses: BTreeMap<Capability, CapabilityEntry>,
195 pending_events: Vec<PermissionDiffEvent>,
201 pending_changed: Vec<(Capability, Option<DomNodeId>)>,
206}
207
208impl EventProvider for PermissionManager {
209 fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
214 self.pending_changed
215 .iter()
216 .map(|(_capability, node)| {
217 SyntheticEvent::new(
218 EventType::PermissionChanged,
219 CoreEventSource::User,
220 node.unwrap_or(DomNodeId::ROOT),
221 timestamp.clone(),
222 EventData::None,
223 )
224 })
225 .collect()
226 }
227}
228
229impl PermissionManager {
230 #[must_use] pub fn new() -> Self {
231 Self::default()
232 }
233
234 #[must_use] pub fn get_status(&self, capability: Capability) -> PermissionState {
236 self.statuses
237 .get(&capability)
238 .map_or(PermissionState::NotDetermined, |e| e.state)
239 }
240
241 pub fn subscribe(&mut self, capability: Capability, node_id: DomNodeId) {
245 let entry = self
246 .statuses
247 .entry(capability)
248 .or_insert_with(CapabilityEntry::new);
249 entry.last_subscriber = Some(node_id);
250 entry.refcount = entry.refcount.saturating_add(1);
251 if entry.refcount == 1 {
252 self.pending_events.push(PermissionDiffEvent::Subscribe {
253 capability,
254 node_id,
255 });
256 }
257 }
258
259 pub fn release(&mut self, capability: Capability) {
262 let Some(entry) = self.statuses.get_mut(&capability) else {
263 return;
264 };
265 if entry.refcount == 0 {
266 return;
267 }
268 entry.refcount -= 1;
269 if entry.refcount == 0 {
270 entry.last_subscriber = None;
271 self.pending_events
272 .push(PermissionDiffEvent::Release { capability });
273 }
274 }
275
276 pub fn force_release(&mut self, capability: Capability) {
280 let Some(entry) = self.statuses.get_mut(&capability) else {
281 return;
282 };
283 if entry.refcount == 0 {
284 return;
285 }
286 entry.refcount = 0;
287 entry.last_subscriber = None;
288 self.pending_events
289 .push(PermissionDiffEvent::Release { capability });
290 }
291
292 pub fn set_status(&mut self, capability: Capability, state: PermissionState) -> bool {
298 let entry = self
299 .statuses
300 .entry(capability)
301 .or_insert_with(CapabilityEntry::new);
302 if entry.state == state {
303 return false;
304 }
305 entry.state = state;
306 self.pending_changed.push((capability, entry.last_subscriber));
309 true
310 }
311
312 pub fn clear_pending_changed(&mut self) {
315 self.pending_changed.clear();
316 }
317
318 #[must_use] pub fn has_pending_async(&self) -> bool {
324 self.statuses
325 .values()
326 .any(|e| e.state == PermissionState::Requested)
327 }
328
329 pub fn take_pending_events(&mut self) -> Vec<PermissionDiffEvent> {
331 core::mem::take(&mut self.pending_events)
332 }
333
334 #[must_use] pub fn refcount(&self, capability: Capability) -> u32 {
336 self.statuses
337 .get(&capability)
338 .map_or(0, |e| e.refcount)
339 }
340
341 pub fn diff_layout<F>(&mut self, mut for_each_bearing_node: F)
350 where
351 F: FnMut(&mut dyn FnMut(Capability, DomNodeId)),
352 {
353 let mut next: BTreeMap<Capability, (u32, Option<DomNodeId>)> = BTreeMap::new();
355 for_each_bearing_node(&mut |cap, node| {
356 let slot = next.entry(cap).or_insert((0, None));
357 slot.0 = slot.0.saturating_add(1);
358 if slot.1.is_none() {
359 slot.1 = Some(node);
360 }
361 });
362
363 let mut all_caps: Vec<Capability> = self.statuses.keys().copied().collect();
366 for cap in next.keys() {
367 if !all_caps.contains(cap) {
368 all_caps.push(*cap);
369 }
370 }
371
372 for cap in all_caps {
373 let (new_count, first_node) = next.get(&cap).copied().unwrap_or((0, None));
374 let entry = self
375 .statuses
376 .entry(cap)
377 .or_insert_with(CapabilityEntry::new);
378 let old_count = entry.refcount;
379 entry.refcount = new_count;
380 if new_count == 0 && old_count > 0 {
381 entry.last_subscriber = None;
382 self.pending_events
383 .push(PermissionDiffEvent::Release { capability: cap });
384 } else if new_count > 0 && old_count == 0 {
385 let node = first_node.unwrap_or(DomNodeId::ROOT);
386 entry.last_subscriber = first_node;
387 self.pending_events.push(PermissionDiffEvent::Subscribe {
388 capability: cap,
389 node_id: node,
390 });
391 }
392 }
393 }
394}
395
396static ASYNC_RESULTS: std::sync::Mutex<Vec<(Capability, PermissionState)>> =
408 std::sync::Mutex::new(Vec::new());
409
410pub fn push_async_result(capability: Capability, state: PermissionState) {
414 let mut q = ASYNC_RESULTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
415 q.push((capability, state));
416}
417
418pub fn drain_async_results() -> Vec<(Capability, PermissionState)> {
422 let mut q = ASYNC_RESULTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
423 core::mem::take(&mut *q)
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429 use azul_core::dom::{DomId, NodeId};
430
431 fn node(idx: usize) -> DomNodeId {
432 DomNodeId {
433 dom: DomId::ROOT_ID,
434 node: NodeId::from_usize(idx).into(),
435 }
436 }
437
438 #[test]
439 fn subscribe_release_round_trip_emits_paired_events() {
440 let mut mgr = PermissionManager::new();
441 assert_eq!(mgr.get_status(Capability::Geolocation), PermissionState::NotDetermined);
442 assert_eq!(mgr.refcount(Capability::Geolocation), 0);
443
444 mgr.subscribe(Capability::Geolocation, node(1));
445 assert_eq!(mgr.refcount(Capability::Geolocation), 1);
446 let events = mgr.take_pending_events();
447 assert_eq!(events.len(), 1);
448 assert!(matches!(
449 events[0],
450 PermissionDiffEvent::Subscribe { capability: Capability::Geolocation, .. }
451 ));
452
453 mgr.release(Capability::Geolocation);
454 assert_eq!(mgr.refcount(Capability::Geolocation), 0);
455 let events = mgr.take_pending_events();
456 assert_eq!(events.len(), 1);
457 assert!(matches!(
458 events[0],
459 PermissionDiffEvent::Release { capability: Capability::Geolocation }
460 ));
461 }
462
463 #[test]
464 fn second_subscriber_does_not_re_emit_subscribe() {
465 let mut mgr = PermissionManager::new();
466 mgr.subscribe(Capability::Camera, node(1));
467 mgr.subscribe(Capability::Camera, node(2));
468 assert_eq!(mgr.refcount(Capability::Camera), 2);
469 let events = mgr.take_pending_events();
470 assert_eq!(events.len(), 1);
472 }
473
474 #[test]
475 fn release_only_after_last_subscriber_drops() {
476 let mut mgr = PermissionManager::new();
477 mgr.subscribe(Capability::Microphone, node(1));
478 mgr.subscribe(Capability::Microphone, node(2));
479 drop(mgr.take_pending_events());
481
482 mgr.release(Capability::Microphone);
483 assert_eq!(mgr.refcount(Capability::Microphone), 1);
484 assert!(mgr.take_pending_events().is_empty());
485
486 mgr.release(Capability::Microphone);
487 assert_eq!(mgr.refcount(Capability::Microphone), 0);
488 let events = mgr.take_pending_events();
489 assert_eq!(events.len(), 1);
490 assert!(matches!(
491 events[0],
492 PermissionDiffEvent::Release { capability: Capability::Microphone }
493 ));
494 }
495
496 #[test]
497 fn force_release_drops_refcount_and_emits_event() {
498 let mut mgr = PermissionManager::new();
499 mgr.subscribe(Capability::Camera, node(1));
500 mgr.subscribe(Capability::Camera, node(2));
501 drop(mgr.take_pending_events());
502
503 mgr.force_release(Capability::Camera);
504 assert_eq!(mgr.refcount(Capability::Camera), 0);
505 let events = mgr.take_pending_events();
506 assert_eq!(events.len(), 1);
507 assert!(matches!(
508 events[0],
509 PermissionDiffEvent::Release { capability: Capability::Camera }
510 ));
511 }
512
513 #[test]
514 fn set_status_returns_change_flag() {
515 let mut mgr = PermissionManager::new();
516 assert!(mgr.set_status(Capability::Camera, PermissionState::Requested));
517 assert!(!mgr.set_status(Capability::Camera, PermissionState::Requested));
518 assert!(mgr.set_status(
519 Capability::Camera,
520 PermissionState::Granted(PermissionQuality::Full)
521 ));
522 assert!(mgr.get_status(Capability::Camera).is_granted());
523 }
524
525 #[test]
526 fn diff_layout_picks_up_appearing_node_and_releases_it_next_frame() {
527 let mut mgr = PermissionManager::new();
528
529 mgr.diff_layout(|emit| {
531 emit(Capability::Geolocation, node(7));
532 });
533 assert_eq!(mgr.refcount(Capability::Geolocation), 1);
534 let events = mgr.take_pending_events();
535 assert_eq!(events.len(), 1);
536 assert!(matches!(
537 events[0],
538 PermissionDiffEvent::Subscribe { capability: Capability::Geolocation, .. }
539 ));
540
541 mgr.diff_layout(|_emit| { });
543 assert_eq!(mgr.refcount(Capability::Geolocation), 0);
544 let events = mgr.take_pending_events();
545 assert_eq!(events.len(), 1);
546 assert!(matches!(
547 events[0],
548 PermissionDiffEvent::Release { capability: Capability::Geolocation }
549 ));
550 }
551
552 #[test]
553 fn diff_layout_re_emits_subscribe_after_release_cycle() {
554 let mut mgr = PermissionManager::new();
555
556 mgr.diff_layout(|emit| emit(Capability::Camera, node(1)));
557 drop(mgr.take_pending_events());
558
559 mgr.diff_layout(|_emit| {});
560 drop(mgr.take_pending_events());
561
562 mgr.diff_layout(|emit| emit(Capability::Camera, node(2)));
565 let events = mgr.take_pending_events();
566 assert_eq!(events.len(), 1);
567 assert!(matches!(
568 events[0],
569 PermissionDiffEvent::Subscribe { capability: Capability::Camera, .. }
570 ));
571 }
572
573 #[test]
574 fn async_results_round_trip_through_manager() {
575 let _serialize = super::autotest_generated::lock_async_channel();
578 drop(drain_async_results());
581
582 push_async_result(
583 Capability::Camera,
584 PermissionState::Granted(PermissionQuality::Full),
585 );
586 push_async_result(Capability::Geolocation, PermissionState::Denied);
587
588 let drained = drain_async_results();
589 assert_eq!(drained.len(), 2, "both parked results drain in order");
590 assert_eq!(drained[0].0, Capability::Camera);
592 assert_eq!(drained[1].0, Capability::Geolocation);
593
594 let mut mgr = PermissionManager::new();
597 for (cap, state) in drained {
598 mgr.set_status(cap, state);
599 }
600 assert!(mgr.get_status(Capability::Camera).is_granted());
601 assert_eq!(mgr.get_status(Capability::Geolocation), PermissionState::Denied);
602
603 assert!(drain_async_results().is_empty());
605 }
606}
607
608#[cfg(test)]
609mod pump_provider_tests {
610 use super::*;
611 use azul_core::task::{Instant, SystemTick};
612
613 fn ts() -> Instant {
614 Instant::Tick(SystemTick::new(0))
615 }
616
617 #[test]
618 fn status_flip_yields_targeted_permission_changed_event() {
619 let node = DomNodeId::ROOT;
620 let mut mgr = PermissionManager::new();
621 mgr.subscribe(Capability::Geolocation, node);
622 drop(mgr.take_pending_events());
623 assert!(mgr.get_pending_events(ts()).is_empty(), "no flip yet");
624
625 assert!(mgr.set_status(Capability::Geolocation, PermissionState::Requested));
626 assert!(mgr.has_pending_async(), "Requested = OS prompt in flight");
627 let events = mgr.get_pending_events(ts());
628 assert_eq!(events.len(), 1);
629 assert_eq!(
630 events[0].event_type,
631 EventType::PermissionChanged
632 );
633 assert_eq!(events[0].target, node, "targeted at the subscriber node");
634
635 mgr.clear_pending_changed();
636 assert!(mgr.get_pending_events(ts()).is_empty(), "cleared after dispatch");
637
638 assert!(mgr.set_status(
639 Capability::Geolocation,
640 PermissionState::Granted(PermissionQuality::Full),
641 ));
642 assert!(!mgr.has_pending_async(), "prompt resolved");
643 assert_eq!(mgr.get_pending_events(ts()).len(), 1);
644 }
645
646 #[test]
647 fn unchanged_status_emits_no_event() {
648 let mut mgr = PermissionManager::new();
649 assert!(mgr.set_status(Capability::Geolocation, PermissionState::Denied));
650 mgr.clear_pending_changed();
651 assert!(!mgr.set_status(Capability::Geolocation, PermissionState::Denied));
652 assert!(mgr.get_pending_events(ts()).is_empty());
653 }
654}
655
656#[cfg(test)]
657mod autotest_generated {
658 use alloc::collections::BTreeSet;
659
660 use azul_core::dom::{DomId, NodeId};
661 use azul_core::task::{Instant, SystemTick};
662
663 use super::*;
664 use crate::managers::{NodeIdMap, NodeIdRemap};
665
666 const ALL_CAPS: [Capability; 18] = [
672 Capability::Camera,
673 Capability::Microphone,
674 Capability::ScreenCapture,
675 Capability::Geolocation,
676 Capability::GeolocationBackground,
677 Capability::Biometric,
678 Capability::Motion,
679 Capability::PhotoLibrary,
680 Capability::PhotoLibraryWrite,
681 Capability::Contacts,
682 Capability::Calendars,
683 Capability::Reminders,
684 Capability::Notifications,
685 Capability::Bluetooth,
686 Capability::BluetoothBackground,
687 Capability::NearbyWifi,
688 Capability::LocalNetwork,
689 Capability::AppTrackingTransparency,
690 ];
691
692 const ALL_STATES: [PermissionState; 8] = [
695 PermissionState::NotDetermined,
696 PermissionState::Requested,
697 PermissionState::Granted(PermissionQuality::Full),
698 PermissionState::Granted(PermissionQuality::Reduced),
699 PermissionState::Denied,
700 PermissionState::Restricted,
701 PermissionState::EphemeralGranted(true),
702 PermissionState::EphemeralGranted(false),
703 ];
704
705 fn node(idx: usize) -> DomNodeId {
708 DomNodeId {
709 dom: DomId::ROOT_ID,
710 node: NodeId::from_usize(idx).into(),
711 }
712 }
713
714 fn node_in_dom(dom: usize, idx: usize) -> DomNodeId {
715 DomNodeId {
716 dom: DomId { inner: dom },
717 node: NodeId::from_usize(idx).into(),
718 }
719 }
720
721 fn ts(tick: u64) -> Instant {
722 Instant::Tick(SystemTick::new(tick))
723 }
724
725 pub(super) fn lock_async_channel() -> std::sync::MutexGuard<'static, ()> {
731 static ASYNC_CHANNEL_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
732 ASYNC_CHANNEL_TEST_LOCK
733 .lock()
734 .unwrap_or_else(std::sync::PoisonError::into_inner)
735 }
736
737 #[test]
740 fn all_capabilities_are_distinct_and_totally_ordered() {
741 for cap in ALL_CAPS {
746 match cap {
747 Capability::Camera
748 | Capability::Microphone
749 | Capability::ScreenCapture
750 | Capability::Geolocation
751 | Capability::GeolocationBackground
752 | Capability::Biometric
753 | Capability::Motion
754 | Capability::PhotoLibrary
755 | Capability::PhotoLibraryWrite
756 | Capability::Contacts
757 | Capability::Calendars
758 | Capability::Reminders
759 | Capability::Notifications
760 | Capability::Bluetooth
761 | Capability::BluetoothBackground
762 | Capability::NearbyWifi
763 | Capability::LocalNetwork
764 | Capability::AppTrackingTransparency => {}
765 }
766 }
767
768 let set: BTreeSet<Capability> = ALL_CAPS.iter().copied().collect();
769 assert_eq!(set.len(), ALL_CAPS.len(), "ALL_CAPS contains a duplicate");
770
771 let mut sorted = ALL_CAPS.to_vec();
774 sorted.sort_unstable();
775 assert_eq!(sorted, ALL_CAPS.to_vec());
776 }
777
778 #[test]
779 fn all_states_are_distinct() {
780 for state in ALL_STATES {
782 match state {
783 PermissionState::NotDetermined
784 | PermissionState::Requested
785 | PermissionState::Granted(_)
786 | PermissionState::Denied
787 | PermissionState::Restricted
788 | PermissionState::EphemeralGranted(_) => {}
789 }
790 }
791 let set: BTreeSet<PermissionState> = ALL_STATES.iter().copied().collect();
792 assert_eq!(set.len(), ALL_STATES.len(), "ALL_STATES contains a duplicate");
793 }
794
795 #[test]
798 fn is_granted_matches_the_documented_state_matrix() {
799 assert!(!PermissionState::NotDetermined.is_granted());
800 assert!(!PermissionState::Requested.is_granted());
801 assert!(PermissionState::Granted(PermissionQuality::Full).is_granted());
802 assert!(
803 PermissionState::Granted(PermissionQuality::Reduced).is_granted(),
804 "reduced quality is still usable — `is_granted` ignores quality"
805 );
806 assert!(!PermissionState::Denied.is_granted());
807 assert!(!PermissionState::Restricted.is_granted());
808 assert!(PermissionState::EphemeralGranted(true).is_granted());
811 assert!(PermissionState::EphemeralGranted(false).is_granted());
812 }
813
814 #[test]
815 fn could_re_prompt_is_true_only_for_not_determined() {
816 for state in ALL_STATES {
817 let expected = state == PermissionState::NotDetermined;
818 assert_eq!(
819 state.could_re_prompt(),
820 expected,
821 "could_re_prompt({state:?}) should be {expected}"
822 );
823 }
824 assert!(!PermissionState::Denied.could_re_prompt());
827 assert!(!PermissionState::Restricted.could_re_prompt());
828 }
829
830 #[test]
831 fn granted_and_re_promptable_are_mutually_exclusive() {
832 for state in ALL_STATES {
833 assert!(
834 !(state.is_granted() && state.could_re_prompt()),
835 "{state:?} claims to be both granted and re-promptable"
836 );
837 }
838 }
839
840 #[test]
841 fn predicates_are_usable_in_const_context() {
842 const GRANTED: bool = PermissionState::Granted(PermissionQuality::Reduced).is_granted();
845 const RE_PROMPT: bool = PermissionState::NotDetermined.could_re_prompt();
846 const DENIED_GRANTED: bool = PermissionState::Denied.is_granted();
847 const _: () = assert!(GRANTED && RE_PROMPT && !DENIED_GRANTED);
848 }
849
850 #[test]
853 fn capability_entry_new_is_the_zero_state() {
854 let e = CapabilityEntry::new();
855 assert_eq!(e.state, PermissionState::NotDetermined);
856 assert_eq!(e.refcount, 0);
857 assert_eq!(e.last_subscriber, None);
858 assert_eq!(e, CapabilityEntry::new());
860 assert!(!e.state.is_granted());
863 assert!(e.state.could_re_prompt());
864 }
865
866 #[test]
867 fn new_manager_is_empty_for_every_capability() {
868 let mgr = PermissionManager::new();
869 assert_eq!(mgr, PermissionManager::default());
870 assert!(mgr.statuses.is_empty());
871 assert!(!mgr.has_pending_async());
872 assert!(mgr.get_pending_events(ts(0)).is_empty());
873
874 for cap in ALL_CAPS {
875 assert_eq!(mgr.get_status(cap), PermissionState::NotDetermined);
876 assert_eq!(mgr.refcount(cap), 0);
877 }
878 assert!(mgr.statuses.is_empty(), "get_status/refcount created entries");
881 }
882
883 #[test]
884 fn take_pending_events_on_a_fresh_manager_is_empty_and_idempotent() {
885 let mut mgr = PermissionManager::new();
886 assert!(mgr.take_pending_events().is_empty());
887 assert!(mgr.take_pending_events().is_empty());
888 mgr.clear_pending_changed();
889 mgr.clear_pending_changed();
890 assert_eq!(mgr, PermissionManager::new());
891 }
892
893 #[test]
896 fn release_on_unknown_capability_is_a_noop() {
897 let mut mgr = PermissionManager::new();
898 for cap in ALL_CAPS {
899 mgr.release(cap);
900 mgr.force_release(cap);
901 }
902 assert!(mgr.statuses.is_empty(), "release must not create entries");
903 assert!(mgr.take_pending_events().is_empty());
904 assert!(mgr.get_pending_events(ts(0)).is_empty());
905 }
906
907 #[test]
908 fn release_at_zero_refcount_does_not_underflow() {
909 let mut mgr = PermissionManager::new();
910 mgr.set_status(Capability::Camera, PermissionState::Denied);
913 assert_eq!(mgr.refcount(Capability::Camera), 0);
914
915 for _ in 0..8 {
916 mgr.release(Capability::Camera);
917 }
918 assert_eq!(mgr.refcount(Capability::Camera), 0, "refcount wrapped around");
919 assert!(
920 mgr.take_pending_events().is_empty(),
921 "a release that never had a subscriber must not emit Release"
922 );
923 }
924
925 #[test]
926 fn double_release_emits_exactly_one_release_event() {
927 let mut mgr = PermissionManager::new();
928 mgr.subscribe(Capability::Motion, node(1));
929 drop(mgr.take_pending_events());
930
931 mgr.release(Capability::Motion);
932 mgr.release(Capability::Motion);
933 mgr.release(Capability::Motion);
934
935 assert_eq!(mgr.refcount(Capability::Motion), 0);
936 let events = mgr.take_pending_events();
937 assert_eq!(
938 events.len(),
939 1,
940 "over-releasing must not double-tear-down the native session: {events:?}"
941 );
942 }
943
944 #[test]
945 fn subscribe_saturates_at_u32_max_instead_of_overflowing() {
946 let mut mgr = PermissionManager::new();
947 mgr.subscribe(Capability::Bluetooth, node(1));
948 drop(mgr.take_pending_events());
949 mgr.statuses.get_mut(&Capability::Bluetooth).unwrap().refcount = u32::MAX;
952
953 mgr.subscribe(Capability::Bluetooth, node(2));
954
955 assert_eq!(mgr.refcount(Capability::Bluetooth), u32::MAX, "refcount wrapped to 0");
956 assert!(
957 mgr.take_pending_events().is_empty(),
958 "a saturating subscribe must not look like a 0 -> 1 transition"
959 );
960 assert_eq!(
962 mgr.statuses[&Capability::Bluetooth].last_subscriber,
963 Some(node(2))
964 );
965 }
966
967 #[test]
968 fn release_from_u32_max_does_not_wrap_or_emit() {
969 let mut mgr = PermissionManager::new();
970 mgr.subscribe(Capability::Contacts, node(1));
971 drop(mgr.take_pending_events());
972 mgr.statuses.get_mut(&Capability::Contacts).unwrap().refcount = u32::MAX;
973
974 mgr.release(Capability::Contacts);
975
976 assert_eq!(mgr.refcount(Capability::Contacts), u32::MAX - 1);
977 assert!(mgr.take_pending_events().is_empty());
978 }
979
980 #[test]
981 fn force_release_from_saturated_refcount_emits_one_release() {
982 let mut mgr = PermissionManager::new();
983 mgr.subscribe(Capability::ScreenCapture, node(1));
984 drop(mgr.take_pending_events());
985 mgr.statuses.get_mut(&Capability::ScreenCapture).unwrap().refcount = u32::MAX;
986
987 mgr.force_release(Capability::ScreenCapture);
988 assert_eq!(mgr.refcount(Capability::ScreenCapture), 0);
989 assert_eq!(
990 mgr.statuses[&Capability::ScreenCapture].last_subscriber,
991 None
992 );
993 assert_eq!(mgr.take_pending_events().len(), 1);
994
995 mgr.force_release(Capability::ScreenCapture);
997 assert!(
998 mgr.take_pending_events().is_empty(),
999 "force_release on a zero refcount must be a no-op"
1000 );
1001 }
1002
1003 #[test]
1004 fn subscribe_after_a_full_release_re_emits_subscribe() {
1005 let mut mgr = PermissionManager::new();
1006 mgr.subscribe(Capability::Camera, node(1));
1007 mgr.release(Capability::Camera);
1008 drop(mgr.take_pending_events());
1009
1010 mgr.subscribe(Capability::Camera, node(2));
1013 let events = mgr.take_pending_events();
1014 assert_eq!(
1015 events,
1016 [PermissionDiffEvent::Subscribe {
1017 capability: Capability::Camera,
1018 node_id: node(2),
1019 }]
1020 .to_vec()
1021 );
1022 }
1023
1024 #[test]
1025 fn release_cycle_preserves_the_os_observed_state() {
1026 let mut mgr = PermissionManager::new();
1027 mgr.subscribe(Capability::Geolocation, node(1));
1028 mgr.set_status(
1029 Capability::Geolocation,
1030 PermissionState::Granted(PermissionQuality::Reduced),
1031 );
1032 mgr.release(Capability::Geolocation);
1033
1034 assert_eq!(mgr.refcount(Capability::Geolocation), 0);
1037 assert_eq!(
1038 mgr.get_status(Capability::Geolocation),
1039 PermissionState::Granted(PermissionQuality::Reduced)
1040 );
1041 assert_eq!(mgr.statuses[&Capability::Geolocation].last_subscriber, None);
1042 }
1043
1044 #[test]
1045 fn subscribe_accepts_boundary_node_ids() {
1046 let mut mgr = PermissionManager::new();
1047 mgr.subscribe(Capability::Notifications, DomNodeId::ROOT);
1050 mgr.subscribe(Capability::Biometric, node(usize::MAX));
1051
1052 let events = mgr.take_pending_events();
1053 assert_eq!(events.len(), 2);
1054 assert!(events.contains(&PermissionDiffEvent::Subscribe {
1055 capability: Capability::Notifications,
1056 node_id: DomNodeId::ROOT,
1057 }));
1058 assert!(events.contains(&PermissionDiffEvent::Subscribe {
1059 capability: Capability::Biometric,
1060 node_id: node(usize::MAX),
1061 }));
1062 }
1063
1064 #[test]
1067 fn set_status_get_status_round_trips_over_the_whole_matrix() {
1068 for cap in ALL_CAPS {
1069 for state in ALL_STATES {
1070 let mut mgr = PermissionManager::new();
1071 mgr.set_status(cap, state);
1072 assert_eq!(mgr.get_status(cap), state, "{cap:?} / {state:?} did not round-trip");
1073 for other in ALL_CAPS.iter().copied().filter(|c| *c != cap) {
1075 assert_eq!(mgr.get_status(other), PermissionState::NotDetermined);
1076 }
1077 }
1078 }
1079 }
1080
1081 #[test]
1082 fn set_status_change_flag_is_exact_over_every_transition() {
1083 for a in ALL_STATES {
1084 for b in ALL_STATES {
1085 let mut mgr = PermissionManager::new();
1086 let cap = Capability::Calendars;
1087
1088 let changed_a = mgr.set_status(cap, a);
1091 assert_eq!(changed_a, a != PermissionState::NotDetermined, "{a:?}");
1092
1093 let changed_b = mgr.set_status(cap, b);
1094 assert_eq!(changed_b, b != a, "{a:?} -> {b:?} reported the wrong flag");
1095
1096 assert_eq!(mgr.get_status(cap), b);
1097 assert_eq!(mgr.has_pending_async(), b == PermissionState::Requested);
1098
1099 let expected = usize::from(changed_a) + usize::from(changed_b);
1101 assert_eq!(mgr.get_pending_events(ts(0)).len(), expected, "{a:?} -> {b:?}");
1102 }
1103 }
1104 }
1105
1106 #[test]
1107 fn set_status_creates_an_entry_with_a_zero_refcount() {
1108 let mut mgr = PermissionManager::new();
1109 assert!(mgr.set_status(Capability::Reminders, PermissionState::Restricted));
1110 let entry = mgr.statuses[&Capability::Reminders];
1111 assert_eq!(entry.refcount, 0, "a status write is not a subscription");
1112 assert_eq!(entry.last_subscriber, None);
1113 assert!(mgr.take_pending_events().is_empty(), "set_status is not a diff event");
1114 }
1115
1116 #[test]
1117 fn quality_and_payload_changes_count_as_state_changes() {
1118 let mut mgr = PermissionManager::new();
1119 let cap = Capability::PhotoLibrary;
1120 assert!(mgr.set_status(cap, PermissionState::Granted(PermissionQuality::Full)));
1121 assert!(mgr.set_status(cap, PermissionState::Granted(PermissionQuality::Reduced)));
1123 assert!(!mgr.set_status(cap, PermissionState::Granted(PermissionQuality::Reduced)));
1124 assert!(mgr.set_status(cap, PermissionState::EphemeralGranted(true)));
1126 assert!(mgr.set_status(cap, PermissionState::EphemeralGranted(false)));
1127 assert!(!mgr.set_status(cap, PermissionState::EphemeralGranted(false)));
1128 assert!(mgr.get_status(cap).is_granted());
1129 }
1130
1131 #[test]
1132 fn has_pending_async_tracks_only_the_requested_state() {
1133 for state in ALL_STATES {
1134 let mut mgr = PermissionManager::new();
1135 mgr.set_status(Capability::Camera, state);
1136 assert_eq!(
1137 mgr.has_pending_async(),
1138 state == PermissionState::Requested,
1139 "has_pending_async({state:?})"
1140 );
1141 }
1142
1143 let mut mgr = PermissionManager::new();
1146 for cap in ALL_CAPS {
1147 mgr.set_status(cap, PermissionState::Granted(PermissionQuality::Full));
1148 }
1149 assert!(!mgr.has_pending_async());
1150 mgr.set_status(Capability::AppTrackingTransparency, PermissionState::Requested);
1151 assert!(mgr.has_pending_async());
1152 mgr.set_status(Capability::AppTrackingTransparency, PermissionState::Denied);
1153 assert!(!mgr.has_pending_async());
1154 }
1155
1156 #[test]
1157 fn pending_changed_targets_the_root_when_there_is_no_subscriber() {
1158 let mut mgr = PermissionManager::new();
1159 mgr.set_status(Capability::Microphone, PermissionState::Denied);
1162 let events = mgr.get_pending_events(ts(42));
1163 assert_eq!(events.len(), 1);
1164 assert_eq!(events[0].event_type, EventType::PermissionChanged);
1165 assert_eq!(events[0].source, CoreEventSource::User);
1166 assert_eq!(events[0].target, DomNodeId::ROOT);
1167 assert_eq!(events[0].timestamp, ts(42), "the caller's timestamp is preserved");
1168 }
1169
1170 #[test]
1171 fn pending_changed_falls_back_to_root_after_the_subscriber_leaves() {
1172 let mut mgr = PermissionManager::new();
1173 mgr.subscribe(Capability::Microphone, node(4));
1174 mgr.set_status(Capability::Microphone, PermissionState::Requested);
1175 assert_eq!(mgr.get_pending_events(ts(0))[0].target, node(4));
1176 mgr.clear_pending_changed();
1177
1178 mgr.release(Capability::Microphone);
1181 mgr.set_status(Capability::Microphone, PermissionState::Denied);
1182 let events = mgr.get_pending_events(ts(0));
1183 assert_eq!(events.len(), 1);
1184 assert_eq!(events[0].target, DomNodeId::ROOT);
1185 }
1186
1187 #[test]
1188 fn repeated_flips_accumulate_one_event_each_until_cleared() {
1189 let mut mgr = PermissionManager::new();
1190 let cap = Capability::LocalNetwork;
1191 mgr.set_status(cap, PermissionState::Requested);
1192 mgr.set_status(cap, PermissionState::Granted(PermissionQuality::Full));
1193 mgr.set_status(cap, PermissionState::Denied);
1194 assert_eq!(mgr.get_pending_events(ts(0)).len(), 3);
1195 assert_eq!(mgr.get_pending_events(ts(0)).len(), 3);
1197
1198 mgr.clear_pending_changed();
1199 assert!(mgr.get_pending_events(ts(0)).is_empty());
1200 mgr.clear_pending_changed();
1201 assert!(mgr.get_pending_events(ts(0)).is_empty(), "clear is idempotent");
1202 }
1203
1204 #[test]
1205 fn the_two_queues_drain_independently() {
1206 let mut mgr = PermissionManager::new();
1207 mgr.subscribe(Capability::NearbyWifi, node(1)); mgr.set_status(Capability::NearbyWifi, PermissionState::Requested); mgr.clear_pending_changed();
1213 assert!(mgr.get_pending_events(ts(0)).is_empty());
1214 assert_eq!(mgr.take_pending_events().len(), 1);
1215
1216 mgr.set_status(Capability::NearbyWifi, PermissionState::Denied);
1218 mgr.subscribe(Capability::Contacts, node(2));
1219 assert_eq!(mgr.take_pending_events().len(), 1);
1220 assert!(mgr.take_pending_events().is_empty(), "take drains");
1221 assert_eq!(
1222 mgr.get_pending_events(ts(0)).len(),
1223 1,
1224 "take_pending_events must not clear pending_changed"
1225 );
1226 }
1227
1228 #[test]
1231 fn diff_layout_on_an_empty_manager_with_no_nodes_is_a_noop() {
1232 let mut mgr = PermissionManager::new();
1233 mgr.diff_layout(|_emit| {});
1234 assert!(mgr.statuses.is_empty());
1235 assert!(mgr.take_pending_events().is_empty());
1236 assert!(mgr.get_pending_events(ts(0)).is_empty());
1237 }
1238
1239 #[test]
1240 fn diff_layout_counts_duplicates_and_anchors_the_first_node() {
1241 let mut mgr = PermissionManager::new();
1242 mgr.diff_layout(|emit| {
1243 emit(Capability::Camera, node(3));
1244 emit(Capability::Camera, node(4));
1245 emit(Capability::Camera, node(5));
1246 });
1247
1248 assert_eq!(mgr.refcount(Capability::Camera), 3);
1249 assert_eq!(
1250 mgr.statuses[&Capability::Camera].last_subscriber,
1251 Some(node(3)),
1252 "the FIRST emitted node anchors the capability"
1253 );
1254 assert_eq!(
1255 mgr.take_pending_events(),
1256 [PermissionDiffEvent::Subscribe {
1257 capability: Capability::Camera,
1258 node_id: node(3),
1259 }]
1260 .to_vec(),
1261 "three bearing nodes still mean exactly one native subscribe"
1262 );
1263 }
1264
1265 #[test]
1266 fn diff_layout_is_idempotent_for_a_stable_layout() {
1267 let mut mgr = PermissionManager::new();
1268 for _ in 0..5 {
1269 mgr.diff_layout(|emit| {
1270 emit(Capability::Camera, node(1));
1271 emit(Capability::Microphone, node(2));
1272 });
1273 }
1274 assert_eq!(mgr.refcount(Capability::Camera), 1);
1275 assert_eq!(mgr.refcount(Capability::Microphone), 1);
1276 assert_eq!(
1277 mgr.take_pending_events().len(),
1278 2,
1279 "an unchanged layout must not re-emit subscribes every frame"
1280 );
1281 }
1282
1283 #[test]
1284 fn diff_layout_emits_release_and_subscribe_in_the_same_frame() {
1285 let mut mgr = PermissionManager::new();
1286 mgr.diff_layout(|emit| emit(Capability::Camera, node(1)));
1287 drop(mgr.take_pending_events());
1288
1289 mgr.diff_layout(|emit| emit(Capability::Geolocation, node(2)));
1291
1292 let events = mgr.take_pending_events();
1293 assert_eq!(events.len(), 2, "{events:?}");
1294 assert!(events.contains(&PermissionDiffEvent::Release {
1295 capability: Capability::Camera
1296 }));
1297 assert!(events.contains(&PermissionDiffEvent::Subscribe {
1298 capability: Capability::Geolocation,
1299 node_id: node(2),
1300 }));
1301 assert_eq!(mgr.refcount(Capability::Camera), 0);
1302 assert_eq!(mgr.refcount(Capability::Geolocation), 1);
1303 }
1304
1305 #[test]
1306 fn diff_layout_reconciles_a_manually_subscribed_capability() {
1307 let mut mgr = PermissionManager::new();
1308 mgr.subscribe(Capability::Camera, node(1));
1309 mgr.subscribe(Capability::Camera, node(2));
1310 drop(mgr.take_pending_events());
1311
1312 mgr.diff_layout(|_emit| {});
1315 assert_eq!(mgr.refcount(Capability::Camera), 0);
1316 assert_eq!(mgr.statuses[&Capability::Camera].last_subscriber, None);
1317 assert_eq!(
1318 mgr.take_pending_events(),
1319 [PermissionDiffEvent::Release {
1320 capability: Capability::Camera
1321 }]
1322 .to_vec()
1323 );
1324 }
1325
1326 #[test]
1327 fn diff_layout_ignores_capabilities_that_only_have_a_status() {
1328 let mut mgr = PermissionManager::new();
1329 for cap in ALL_CAPS {
1332 mgr.set_status(cap, PermissionState::Denied);
1333 }
1334 mgr.diff_layout(|_emit| {});
1335 assert!(
1336 mgr.take_pending_events().is_empty(),
1337 "0 -> 0 is not a transition"
1338 );
1339 for cap in ALL_CAPS {
1340 assert_eq!(mgr.refcount(cap), 0);
1341 assert_eq!(mgr.get_status(cap), PermissionState::Denied);
1342 }
1343 }
1344
1345 #[test]
1346 fn diff_layout_from_a_saturated_refcount_does_not_emit() {
1347 let mut mgr = PermissionManager::new();
1348 mgr.subscribe(Capability::Motion, node(1));
1349 drop(mgr.take_pending_events());
1350 mgr.statuses.get_mut(&Capability::Motion).unwrap().refcount = u32::MAX;
1351
1352 mgr.diff_layout(|emit| emit(Capability::Motion, node(1)));
1355 assert_eq!(mgr.refcount(Capability::Motion), 1);
1356 assert!(mgr.take_pending_events().is_empty());
1357 }
1358
1359 #[test]
1360 fn diff_layout_handles_every_capability_at_once() {
1361 let mut mgr = PermissionManager::new();
1362 mgr.diff_layout(|emit| {
1363 for (i, cap) in ALL_CAPS.iter().enumerate() {
1364 emit(*cap, node(i + 1));
1365 }
1366 });
1367
1368 let events = mgr.take_pending_events();
1369 assert_eq!(events.len(), ALL_CAPS.len(), "one Subscribe per capability");
1370 for (i, cap) in ALL_CAPS.iter().enumerate() {
1371 assert_eq!(mgr.refcount(*cap), 1);
1372 assert!(
1373 events.contains(&PermissionDiffEvent::Subscribe {
1374 capability: *cap,
1375 node_id: node(i + 1),
1376 }),
1377 "missing Subscribe for {cap:?}"
1378 );
1379 }
1380
1381 mgr.diff_layout(|_emit| {});
1383 assert_eq!(mgr.take_pending_events().len(), ALL_CAPS.len());
1384 }
1385
1386 #[test]
1387 fn diff_layout_event_order_is_deterministic() {
1388 let run = || {
1389 let mut mgr = PermissionManager::new();
1390 mgr.diff_layout(|emit| {
1391 emit(Capability::Notifications, node(1));
1392 emit(Capability::Camera, node(2));
1393 emit(Capability::Geolocation, node(3));
1394 });
1395 let first = mgr.take_pending_events();
1396 mgr.diff_layout(|emit| emit(Capability::Camera, node(2)));
1397 let second = mgr.take_pending_events();
1398 (first, second)
1399 };
1400 assert_eq!(run(), run(), "the diff-event order must not depend on run order");
1401 }
1402
1403 #[test]
1404 fn diff_layout_accepts_the_root_sentinel_as_a_bearing_node() {
1405 let mut mgr = PermissionManager::new();
1406 mgr.diff_layout(|emit| emit(Capability::Biometric, DomNodeId::ROOT));
1410 assert_eq!(mgr.refcount(Capability::Biometric), 1);
1411 assert_eq!(
1412 mgr.take_pending_events(),
1413 [PermissionDiffEvent::Subscribe {
1414 capability: Capability::Biometric,
1415 node_id: DomNodeId::ROOT,
1416 }]
1417 .to_vec()
1418 );
1419 }
1420
1421 #[test]
1422 fn diff_layout_handles_a_large_bearing_node_set() {
1423 let mut mgr = PermissionManager::new();
1424 const N: usize = 10_000;
1425 mgr.diff_layout(|emit| {
1426 for i in 1..=N {
1427 emit(Capability::PhotoLibraryWrite, node(i));
1428 }
1429 });
1430 assert_eq!(mgr.refcount(Capability::PhotoLibraryWrite), N as u32);
1431 assert_eq!(mgr.take_pending_events().len(), 1);
1432
1433 mgr.diff_layout(|_emit| {});
1434 assert_eq!(mgr.refcount(Capability::PhotoLibraryWrite), 0);
1435 assert_eq!(mgr.take_pending_events().len(), 1);
1436 }
1437
1438 #[test]
1439 fn diff_layout_does_not_disturb_the_state_machine() {
1440 let mut mgr = PermissionManager::new();
1441 mgr.set_status(
1442 Capability::Geolocation,
1443 PermissionState::EphemeralGranted(true),
1444 );
1445 mgr.clear_pending_changed();
1446
1447 mgr.diff_layout(|emit| emit(Capability::Geolocation, node(1)));
1448 mgr.diff_layout(|_emit| {});
1449
1450 assert_eq!(
1453 mgr.get_status(Capability::Geolocation),
1454 PermissionState::EphemeralGranted(true)
1455 );
1456 assert!(mgr.get_pending_events(ts(0)).is_empty());
1457 }
1458
1459 #[test]
1462 fn cloning_a_manager_deep_copies_its_queues() {
1463 let mut mgr = PermissionManager::new();
1464 mgr.subscribe(Capability::Camera, node(1));
1465 mgr.set_status(Capability::Camera, PermissionState::Requested);
1466
1467 let mut clone = mgr.clone();
1468 assert_eq!(clone, mgr);
1469
1470 clone.subscribe(Capability::Camera, node(2));
1471 clone.force_release(Capability::Microphone);
1472 drop(clone.take_pending_events());
1473 clone.clear_pending_changed();
1474
1475 assert_ne!(clone, mgr, "the clone shares state with the original");
1476 assert_eq!(mgr.refcount(Capability::Camera), 1);
1477 assert_eq!(mgr.take_pending_events().len(), 1, "original's queue was drained");
1478 assert_eq!(mgr.get_pending_events(ts(0)).len(), 1);
1479 }
1480
1481 #[test]
1484 fn remap_rewrites_the_subscriber_and_the_queued_target() {
1485 let mut mgr = PermissionManager::new();
1486 mgr.subscribe(Capability::Camera, node(3)); mgr.set_status(Capability::Camera, PermissionState::Requested);
1488
1489 let map = NodeIdMap::from_pairs([(NodeId::new(2), NodeId::new(9))]);
1491 mgr.remap_node_ids(DomId::ROOT_ID, &map);
1492
1493 assert_eq!(
1494 mgr.statuses[&Capability::Camera].last_subscriber,
1495 Some(node(10)), );
1497 assert_eq!(mgr.get_pending_events(ts(0))[0].target, node(10));
1498 }
1499
1500 #[test]
1501 fn remap_drops_an_unmounted_subscriber_instead_of_recycling_the_index() {
1502 let mut mgr = PermissionManager::new();
1503 mgr.subscribe(Capability::Camera, node(3));
1504 mgr.set_status(Capability::Camera, PermissionState::Requested);
1505
1506 mgr.remap_node_ids(DomId::ROOT_ID, &NodeIdMap::default());
1508
1509 assert_eq!(
1510 mgr.statuses[&Capability::Camera].last_subscriber,
1511 None,
1512 "an unmounted subscriber must fall back to None, never to a live-but-wrong node"
1513 );
1514 assert_eq!(
1515 mgr.get_pending_events(ts(0))[0].target,
1516 DomNodeId::ROOT,
1517 "the queued event retargets to the window root"
1518 );
1519 assert_eq!(mgr.get_status(Capability::Camera), PermissionState::Requested);
1521 }
1522
1523 #[test]
1524 fn remap_leaves_nodes_from_other_doms_untouched() {
1525 let mut mgr = PermissionManager::new();
1526 let foreign = node_in_dom(7, 3);
1527 mgr.subscribe(Capability::Camera, foreign);
1528 mgr.set_status(Capability::Camera, PermissionState::Requested);
1529
1530 mgr.remap_node_ids(DomId::ROOT_ID, &NodeIdMap::default());
1534
1535 assert_eq!(
1536 mgr.statuses[&Capability::Camera].last_subscriber,
1537 Some(foreign)
1538 );
1539 assert_eq!(mgr.get_pending_events(ts(0))[0].target, foreign);
1540 }
1541
1542 #[test]
1545 fn async_channel_preserves_arrival_order_across_all_states() {
1546 let _serialize = lock_async_channel();
1547 drop(drain_async_results());
1548
1549 for state in ALL_STATES {
1550 push_async_result(Capability::Camera, state);
1551 }
1552 let drained = drain_async_results();
1553 assert_eq!(drained.len(), ALL_STATES.len());
1554 for (i, state) in ALL_STATES.iter().enumerate() {
1555 assert_eq!(drained[i], (Capability::Camera, *state));
1556 }
1557 assert!(drain_async_results().is_empty(), "the queue is taken, not copied");
1558 }
1559
1560 #[test]
1561 fn async_channel_recovers_from_a_poisoned_lock() {
1562 let _serialize = lock_async_channel();
1563 drop(drain_async_results());
1564
1565 let unwound = std::panic::catch_unwind(|| {
1569 let _guard = ASYNC_RESULTS.lock().unwrap();
1570 panic!("intentional: poisoning ASYNC_RESULTS");
1571 });
1572 assert!(unwound.is_err(), "the panic must have unwound through the guard");
1573 assert!(ASYNC_RESULTS.is_poisoned(), "the lock should now be poisoned");
1574
1575 push_async_result(Capability::Geolocation, PermissionState::Denied);
1577 let drained = drain_async_results();
1578 assert_eq!(
1579 drained,
1580 [(Capability::Geolocation, PermissionState::Denied)].to_vec(),
1581 "a poisoned lock must not wedge permission delivery forever"
1582 );
1583 assert!(drain_async_results().is_empty());
1584 }
1585
1586 #[test]
1587 fn async_channel_survives_concurrent_pushers() {
1588 let _serialize = lock_async_channel();
1589 drop(drain_async_results());
1590
1591 const THREADS: usize = 8;
1592 const PER_THREAD: usize = 50;
1593
1594 let handles: Vec<_> = (0..THREADS)
1595 .map(|t| {
1596 let cap = ALL_CAPS[t];
1597 std::thread::spawn(move || {
1598 for _ in 0..PER_THREAD {
1599 push_async_result(cap, PermissionState::Granted(PermissionQuality::Full));
1600 }
1601 })
1602 })
1603 .collect();
1604 for h in handles {
1605 h.join().expect("a pusher thread panicked");
1606 }
1607
1608 let drained = drain_async_results();
1609 assert_eq!(drained.len(), THREADS * PER_THREAD, "results were lost");
1610 for cap in ALL_CAPS.iter().take(THREADS) {
1611 let count = drained.iter().filter(|(c, _)| c == cap).count();
1612 assert_eq!(count, PER_THREAD, "{cap:?} lost results");
1613 }
1614 assert!(drain_async_results().is_empty());
1615 }
1616
1617 #[test]
1618 fn draining_an_empty_async_channel_is_safe_and_repeatable() {
1619 let _serialize = lock_async_channel();
1620 for _ in 0..3 {
1621 assert!(drain_async_results().is_empty());
1622 }
1623 }
1624}
1625
1626impl crate::managers::NodeIdRemap for PermissionManager {
1627 fn remap_node_ids(&mut self, dom: azul_core::dom::DomId, map: &crate::managers::NodeIdMap) {
1632 for entry in self.statuses.values_mut() {
1633 if let Some(node) = entry.last_subscriber {
1634 entry.last_subscriber = map.resolve_dom_node_id(dom, node);
1635 }
1636 }
1637 for (_capability, node) in &mut self.pending_changed {
1638 if let Some(n) = *node {
1639 *node = map.resolve_dom_node_id(dom, n);
1640 }
1641 }
1642 }
1643}