use std::any::TypeId;
use std::collections::HashMap;
use std::collections::HashSet;
use std::time::Duration;
use bevy::ecs::entity::Entity;
use bevy::ecs::reflect::ReflectComponent;
use bevy::ecs::reflect::ReflectResource;
use bevy::prelude::Component;
use bevy::prelude::Reflect;
use bevy::prelude::Resource;
use thiserror::Error;
use crate::ApplyPermit;
use crate::AttachmentPath;
use crate::Attempt;
use crate::AttemptId;
use crate::Claim;
use crate::ConfiguredDeviceConnection;
use crate::ConfiguredDeviceMode;
use crate::DeviceId;
use crate::DeviceKey;
use crate::IdentityDecisionOwed;
use crate::IdentityVerdict;
use crate::Presence;
use crate::ReportedParent;
use crate::ReporterId;
use crate::RoleKey;
use crate::SchemeName;
use crate::attempt::AttemptDeadlineStatus;
#[cfg(test)]
use crate::binding::Bindings;
use crate::reconcile::FrameClockReading;
const FIRST_ISSUED_ATTEMPT: u64 = 1;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Component, Reflect)]
#[reflect(Component, PartialEq)]
pub struct Device;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Component, Reflect)]
#[reflect(Component, PartialEq)]
pub(crate) struct PresentWithUsableClaim;
#[derive(Debug, Default, Resource, Reflect)]
#[reflect(Resource)]
pub struct Devices {
ids: HashMap<DeviceKey, DeviceId>,
state: HashMap<DeviceId, ReconciledDeviceState>,
revision: HashMap<DeviceId, DeviceRevision>,
entity: HashMap<DeviceId, Entity>,
next: u64,
duplicate_keys: HashSet<DeviceKey>,
unregistered_schemes: HashSet<SchemeName>,
}
impl Devices {
#[must_use]
pub fn resolve(&self, key: &DeviceKey) -> DeviceResolution {
self.ids
.get(key)
.map_or(DeviceResolution::NotResolved, |device_id| {
DeviceResolution::Resolved(*device_id)
})
}
#[must_use]
pub fn state(&self, device_id: DeviceId) -> DeviceStateLookup<'_> {
self.state
.get(&device_id)
.map_or(DeviceStateLookup::Retired, DeviceStateLookup::Retained)
}
#[must_use]
pub fn revision(&self, device_id: DeviceId) -> DeviceRevisionLookup {
self.revision
.get(&device_id)
.map_or(DeviceRevisionLookup::Retired, |device_revision| {
DeviceRevisionLookup::Retained(*device_revision)
})
}
#[must_use]
pub fn count(&self) -> usize { self.state.len() }
#[must_use]
pub const fn duplicate_keys(&self) -> &HashSet<DeviceKey> { &self.duplicate_keys }
#[must_use]
pub const fn unregistered_schemes(&self) -> &HashSet<SchemeName> { &self.unregistered_schemes }
pub(crate) fn replace_reconciled(
&mut self,
reconciled: Vec<ReconciledDeviceState>,
duplicate_keys: HashSet<DeviceKey>,
unregistered_schemes: HashSet<SchemeName>,
) -> ReconciledDeviceReplacement {
let mut ids = HashMap::with_capacity(reconciled.len());
let mut state = HashMap::with_capacity(reconciled.len());
let mut revision = HashMap::with_capacity(reconciled.len());
let mut changes = ReconciledDeviceChanges::default();
for reconciled_device_state in reconciled {
let device_id = self
.ids
.get(&reconciled_device_state.key)
.copied()
.unwrap_or_else(|| self.issue());
revision.insert(
device_id,
self.advanced_revision(device_id, &reconciled_device_state),
);
let dispute_changed = self
.state
.get(&device_id)
.map_or(!reconciled_device_state.disputed.is_empty(), |held| {
held.disputed != reconciled_device_state.disputed
});
if dispute_changed {
changes.disputes_changed.push(device_id);
}
if self.state.get(&device_id).is_some_and(|held| {
held.presence == Presence::Present
&& !held
.presence
.is_same_variant(reconciled_device_state.presence)
}) {
changes.departed.push(DepartedDevice {
key: reconciled_device_state.key.clone(),
departure: DeviceDeparture::RetainedButNotPresent,
});
}
ids.insert(reconciled_device_state.key.clone(), device_id);
state.insert(device_id, reconciled_device_state);
}
for (key, device_id) in &self.ids {
if !state.contains_key(device_id) {
changes.departed.push(DepartedDevice {
key: key.clone(),
departure: DeviceDeparture::KeyLeftTheSet,
});
}
}
let device_register_change_detection = if self.ids == ids
&& self.state.len() == state.len()
&& state.iter().all(|(device_id, reported)| {
self.state
.get(device_id)
.is_some_and(|retained| retained.holds_same_facts(reported))
})
&& self.revision == revision
&& self.duplicate_keys == duplicate_keys
&& self.unregistered_schemes == unregistered_schemes
&& self
.entity
.keys()
.all(|device_id| state.contains_key(device_id))
{
DeviceRegisterChangeDetection::Preserve
} else {
DeviceRegisterChangeDetection::MarkChanged
};
self.entity.retain(|device_id, entity| {
let retained = state.contains_key(device_id);
if !retained {
changes.orphaned_entities.push(*entity);
}
retained
});
self.ids = ids;
self.state = state;
self.revision = revision;
self.duplicate_keys = duplicate_keys;
self.unregistered_schemes = unregistered_schemes;
ReconciledDeviceReplacement {
changes,
device_register_change_detection,
}
}
pub(crate) fn project_entity(&mut self, device_id: DeviceId, entity: Entity) {
self.entity.insert(device_id, entity);
}
pub fn states(&self) -> impl Iterator<Item = &ReconciledDeviceState> { self.state.values() }
pub(crate) fn discharge_identity_decision(&mut self, key: &DeviceKey) {
let Some(device_id) = self.ids.get(key).copied() else {
return;
};
let verdict = IdentityVerdict::concluded_from_scan(key, &self.duplicate_keys);
if let Some(reconciled_device_state) = self.state.get_mut(&device_id) {
reconciled_device_state.decision_owed = IdentityDecisionOwed::Nothing;
reconciled_device_state.verdict = verdict;
}
}
#[must_use]
pub(crate) fn entity(&self, device_id: DeviceId) -> DeviceEntityLookup {
self.entity
.get(&device_id)
.map_or(DeviceEntityLookup::NotProjected, |entity| {
DeviceEntityLookup::Projected(*entity)
})
}
pub fn authorize_service(
&self,
device_id: DeviceId,
) -> Result<ApplyPermit, ApplyAuthorizationError> {
self.in_service_state(device_id)?;
Ok(ApplyPermit::in_service())
}
pub fn authorize_restore(
&self,
device_id: DeviceId,
) -> Result<ApplyPermit, ApplyAuthorizationError> {
let reconciled_device_state = self.authorized_state(device_id)?;
if !reconciled_device_state.verdict.identified() {
return Err(ApplyAuthorizationError::IdentityNotProven {
key: reconciled_device_state.key.clone(),
});
}
Ok(ApplyPermit::restore_only())
}
fn in_service_state(
&self,
device_id: DeviceId,
) -> Result<&ReconciledDeviceState, ApplyAuthorizationError> {
let reconciled_device_state = self.authorized_state(device_id)?;
match reconciled_device_state.verdict {
IdentityVerdict::Proven | IdentityVerdict::Authored => Ok(reconciled_device_state),
_ => Err(ApplyAuthorizationError::IdentityNotProven {
key: reconciled_device_state.key.clone(),
}),
}
}
fn authorized_state(
&self,
device_id: DeviceId,
) -> Result<&ReconciledDeviceState, ApplyAuthorizationError> {
let DeviceStateLookup::Retained(reconciled_device_state) = self.state(device_id) else {
return Err(ApplyAuthorizationError::DeviceRetired { device_id });
};
if reconciled_device_state.mode == ConfiguredDeviceMode::Offline {
return Err(ApplyAuthorizationError::Offline {
key: reconciled_device_state.key.clone(),
});
}
if reconciled_device_state.presence != Presence::Present {
return Err(ApplyAuthorizationError::NotPresent {
key: reconciled_device_state.key.clone(),
});
}
match reconciled_device_state.claim {
Claim::Held | Claim::Free | Claim::NotApplicable => {},
Claim::Contended { .. } | Claim::Blocked { .. } => {
return Err(ApplyAuthorizationError::ClaimUnavailable {
key: reconciled_device_state.key.clone(),
});
},
}
Ok(reconciled_device_state)
}
fn advanced_revision(
&self,
device_id: DeviceId,
reported: &ReconciledDeviceState,
) -> DeviceRevision {
let Some(retained) = self.state.get(&device_id) else {
return DeviceRevision::default();
};
let device_revision = self.revision.get(&device_id).copied().unwrap_or_default();
if retained.holds_same_facts(reported) {
device_revision
} else {
device_revision.advanced()
}
}
const fn issue(&mut self) -> DeviceId {
let device_id = DeviceId::new(self.next);
self.next += 1;
device_id
}
}
#[derive(Clone, Debug, Reflect)]
pub struct ReconciledDeviceState {
pub key: DeviceKey,
pub verdict: IdentityVerdict,
pub decision_owed: IdentityDecisionOwed,
pub mode: ConfiguredDeviceMode,
pub attachment: AttachmentPath,
pub parent: ReportedParent,
pub presence: Presence,
pub claim: Claim,
pub contributors: Vec<ReporterId>,
pub declared: HashSet<TypeId>,
pub disputed: HashSet<TypeId>,
}
impl ReconciledDeviceState {
fn holds_same_facts(&self, reported: &Self) -> bool {
self.key == reported.key
&& self.verdict == reported.verdict
&& self.decision_owed == reported.decision_owed
&& self.mode == reported.mode
&& self.attachment == reported.attachment
&& self.parent == reported.parent
&& self.presence.is_same_variant(reported.presence)
&& self.claim == reported.claim
&& self.contributors == reported.contributors
&& self.declared == reported.declared
&& self.disputed == reported.disputed
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum DeviceRegisterChangeDetection {
Preserve,
MarkChanged,
}
#[derive(Debug)]
pub(crate) struct ReconciledDeviceReplacement {
pub(crate) changes: ReconciledDeviceChanges,
pub(crate) device_register_change_detection: DeviceRegisterChangeDetection,
}
#[derive(Debug, Default, Resource)]
pub(crate) struct ReconciledDeviceChanges {
pub(crate) departed: Vec<DepartedDevice>,
pub(crate) orphaned_entities: Vec<Entity>,
pub(crate) disputes_changed: Vec<DeviceId>,
pub(crate) connections: Vec<ConfiguredDeviceConnectionChange>,
}
#[derive(Debug)]
pub(crate) struct DepartedDevice {
pub(crate) key: DeviceKey,
pub(crate) departure: DeviceDeparture,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Reflect)]
pub enum DeviceDeparture {
KeyLeftTheSet,
RetainedButNotPresent,
}
#[derive(Debug, Default, Resource)]
pub(crate) struct DepartureAnnouncements {
pub(crate) departed: Vec<DepartedDevice>,
pub(crate) connections: Vec<ConfiguredDeviceConnectionChange>,
}
#[derive(Debug)]
pub(crate) struct ConfiguredDeviceConnectionChange {
pub(crate) key: DeviceKey,
pub(crate) connection: ConfiguredDeviceConnection,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Reflect)]
pub enum DeviceResolution {
NotResolved,
Resolved(DeviceId),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum DeviceEntityLookup {
NotProjected,
Projected(Entity),
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum ApplyAuthorizationError {
#[error("device handle `{device_id:?}` addresses no retained device")]
DeviceRetired {
device_id: DeviceId,
},
#[error("device `{key:?}` is configured offline")]
Offline {
key: DeviceKey,
},
#[error("device `{key:?}` is not present")]
NotPresent {
key: DeviceKey,
},
#[error("device `{key:?}` claim does not permit use by this process")]
ClaimUnavailable {
key: DeviceKey,
},
#[error("device `{key:?}` identity does not authorize this operation")]
IdentityNotProven {
key: DeviceKey,
},
}
#[derive(Clone, Copy, Debug)]
pub enum DeviceStateLookup<'a> {
Retired,
Retained(&'a ReconciledDeviceState),
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default, Resource, Reflect)]
#[reflect(opaque)]
#[reflect(Resource)]
pub struct RiggingRevision(u64);
impl RiggingRevision {
#[must_use]
pub const fn get(self) -> u64 { self.0 }
pub(crate) const fn advance(&mut self) { self.0 += 1; }
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default, Reflect)]
#[reflect(opaque)]
pub struct DeviceRevision(u64);
impl DeviceRevision {
#[must_use]
pub const fn get(self) -> u64 { self.0 }
pub(crate) const fn advanced(self) -> Self { Self(self.0 + 1) }
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DeviceRevisionLookup {
Retired,
Retained(DeviceRevision),
}
#[derive(Debug, Resource, Reflect)]
#[reflect(Resource)]
pub struct Attempts {
in_flight: HashMap<AttemptId, Attempt>,
attempt_by_role: HashMap<RoleKey, AttemptId>,
next: u64,
}
impl Default for Attempts {
fn default() -> Self {
Self {
in_flight: HashMap::new(),
attempt_by_role: HashMap::new(),
next: FIRST_ISSUED_ATTEMPT,
}
}
}
impl Attempts {
#[must_use]
pub fn in_flight(&self, attempt: AttemptId) -> AttemptLookup<'_> {
self.in_flight
.get(&attempt)
.map_or(AttemptLookup::Finished, AttemptLookup::InFlight)
}
pub(crate) fn issue(&mut self) -> Result<AttemptId, AttemptIssueError> {
let next = self
.next
.checked_add(1)
.ok_or(AttemptIssueError::SequenceExhausted)?;
let attempt = AttemptId::new(self.next);
self.next = next;
Ok(attempt)
}
fn release(&mut self, attempt: AttemptId) {
if self.in_flight.contains_key(&attempt) {
return;
}
if self.next == attempt.value().saturating_add(1) {
self.next = attempt.value();
}
}
pub(crate) fn rollback_dispatch(&mut self, attempt: AttemptId) {
let Some(next) = attempt.value().checked_add(1) else {
return;
};
if self.next != next {
return;
}
self.end(attempt);
self.release(attempt);
}
#[must_use]
pub fn len(&self) -> usize { self.in_flight.len() }
#[must_use]
pub fn is_empty(&self) -> bool { self.in_flight.is_empty() }
pub(crate) fn begin(&mut self, attempt: Attempt) {
self.attempt_by_role
.insert(attempt.role.clone(), attempt.id);
self.in_flight.insert(attempt.id, attempt);
}
pub(crate) fn end(&mut self, attempt: AttemptId) {
if let Some(ended) = self.in_flight.remove(&attempt)
&& self.attempt_by_role.get(&ended.role) == Some(&attempt)
{
self.attempt_by_role.remove(&ended.role);
}
}
pub(crate) fn in_flight_attempts(&self) -> impl Iterator<Item = &Attempt> {
self.in_flight.values()
}
#[must_use]
#[cfg(test)]
pub(crate) fn in_flight_for(&self, role: &RoleKey, bindings: &Bindings) -> RoleAttemptLookup {
self.attempt_by_role.get(role).copied().map_or_else(
|| {
if bindings.binding(role).is_ok() {
RoleAttemptLookup::Idle
} else {
RoleAttemptLookup::NoSuchRole
}
},
RoleAttemptLookup::InFlight,
)
}
#[must_use]
pub(crate) fn deadline_status(
&self,
attempt: AttemptId,
now: FrameClockReading,
apply_overrun: Duration,
) -> AttemptDeadlineStatus {
let Some(retained) = self.in_flight.get(&attempt) else {
return AttemptDeadlineStatus::NoSuchAttempt;
};
let FrameClockReading::Measurable(now) = now else {
return AttemptDeadlineStatus::WithinDeadline;
};
if now <= retained.deadline {
return AttemptDeadlineStatus::WithinDeadline;
}
let past_deadline = now.duration_since(retained.deadline);
if past_deadline > apply_overrun {
AttemptDeadlineStatus::OverrunExhausted { past_deadline }
} else {
AttemptDeadlineStatus::OverdueWithinOverrun { past_deadline }
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg(test)]
pub(crate) enum RoleAttemptLookup {
NoSuchRole,
Idle,
InFlight(AttemptId),
}
#[derive(Clone, Copy, Debug)]
pub enum AttemptLookup<'a> {
Finished,
InFlight(&'a Attempt),
}
#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)]
pub(crate) enum AttemptIssueError {
#[error("attempt identifier sequence is exhausted")]
SequenceExhausted,
}
#[cfg(test)]
#[allow(
clippy::expect_used,
clippy::panic,
reason = "tests should panic on unexpected values"
)]
mod tests {
use std::any::TypeId;
use std::collections::HashSet;
use std::error::Error;
use std::time::Duration;
use bevy::app::App;
use bevy::ecs::reflect::AppTypeRegistry;
use bevy::ecs::reflect::ReflectComponent;
use bevy::ecs::reflect::ReflectResource;
use bevy::platform::time::Instant;
use bevy::prelude::Component;
use bevy::prelude::Reflect;
use bevy::reflect::FromReflect;
use bevy::reflect::tuple_struct::DynamicTupleStruct;
use super::ApplyAuthorizationError;
use super::AttemptLookup;
use super::Attempts;
use super::Device;
use super::DeviceDeparture;
use super::DeviceRegisterChangeDetection;
use super::DeviceResolution;
use super::DeviceRevision;
use super::DeviceRevisionLookup;
use super::DeviceStateLookup;
use super::Devices;
use super::PresentWithUsableClaim;
use super::ReconciledDeviceState;
use super::RiggingRevision;
use super::RoleAttemptLookup;
use crate::ApplyDeadline;
use crate::AttachmentPath;
use crate::Attempt;
use crate::AttemptId;
use crate::Claim;
use crate::ClaimHolder;
use crate::ConfiguredDeviceMode;
use crate::DeviceId;
use crate::DeviceIdSource;
use crate::DeviceKey;
use crate::DeviceKind;
use crate::EndpointId;
use crate::IdentityDecisionOwed;
use crate::IdentityVerdict;
use crate::PermissionGate;
use crate::Presence;
use crate::RecoveryPolicy;
use crate::ReportedId;
use crate::ReportedParent;
use crate::RetryOn;
use crate::SchemeName;
use crate::UnverifiedReason;
use crate::attempt::AttemptDeadlineStatus;
use crate::reconcile::FrameClockReading;
#[derive(Component, Reflect)]
#[reflect(Component)]
struct DisputedCapability;
fn reported_key(value: &str) -> Result<DeviceKey, Box<dyn Error>> {
Ok(DeviceKey {
kind: DeviceKind::Display,
id: DeviceIdSource::Reported {
scheme: SchemeName::new("edid-serial")?,
value: ReportedId::new(value)?,
},
})
}
fn reconciled(key: DeviceKey) -> ReconciledDeviceState {
ReconciledDeviceState {
key,
verdict: IdentityVerdict::Proven,
decision_owed: IdentityDecisionOwed::Nothing,
mode: ConfiguredDeviceMode::Managed,
attachment: AttachmentPath::PlatformHasNoConcept,
parent: ReportedParent::Root,
presence: Presence::Present,
claim: Claim::NotApplicable,
contributors: Vec::new(),
declared: HashSet::new(),
disputed: HashSet::new(),
}
}
#[test]
fn resolution_distinguishes_an_unknown_key_from_a_retained_handle() -> Result<(), Box<dyn Error>>
{
let key = reported_key("DELL-U2723QE-9J4K2H3")?;
let absent_key = reported_key("DELL-U2723QE-OTHER")?;
let mut devices = Devices::default();
assert_eq!(devices.resolve(&key), DeviceResolution::NotResolved);
devices.replace_reconciled(
vec![reconciled(key.clone())],
HashSet::new(),
HashSet::new(),
);
let DeviceResolution::Resolved(device_id) = devices.resolve(&key) else {
panic!("an ingested key must resolve to the handle the registry issued");
};
assert!(matches!(
devices.state(device_id),
DeviceStateLookup::Retained(state) if state.key == key
));
assert_eq!(devices.resolve(&absent_key), DeviceResolution::NotResolved);
Ok(())
}
#[test]
fn a_rescan_reporting_the_same_state_leaves_the_device_revision_alone()
-> Result<(), Box<dyn Error>> {
let key = reported_key("DELL-U2723QE-9J4K2H3")?;
let mut devices = Devices::default();
let first_replacement = devices.replace_reconciled(
vec![reconciled(key.clone())],
HashSet::new(),
HashSet::new(),
);
assert_eq!(
first_replacement.device_register_change_detection,
DeviceRegisterChangeDetection::MarkChanged
);
let DeviceResolution::Resolved(device_id) = devices.resolve(&key) else {
panic!("an ingested key must resolve to the handle the registry issued");
};
let issued = devices.revision(device_id);
assert_eq!(
issued,
DeviceRevisionLookup::Retained(DeviceRevision::default())
);
for _ in 0..3 {
let replacement = devices.replace_reconciled(
vec![reconciled(key.clone())],
HashSet::new(),
HashSet::new(),
);
assert_eq!(
replacement.device_register_change_detection,
DeviceRegisterChangeDetection::Preserve
);
}
assert_eq!(devices.revision(device_id), issued);
let mut unreachable = reconciled(key.clone());
unreachable.presence = Presence::Unreachable {
since: Duration::from_secs(9),
};
let unreachable_replacement =
devices.replace_reconciled(vec![unreachable], HashSet::new(), HashSet::new());
assert_eq!(
unreachable_replacement.device_register_change_detection,
DeviceRegisterChangeDetection::MarkChanged
);
assert_eq!(
devices.revision(device_id),
DeviceRevisionLookup::Retained(DeviceRevision::default().advanced())
);
let mut later = reconciled(key);
later.presence = Presence::Unreachable {
since: Duration::from_secs(30),
};
let later_replacement =
devices.replace_reconciled(vec![later], HashSet::new(), HashSet::new());
assert_eq!(
later_replacement.device_register_change_detection,
DeviceRegisterChangeDetection::Preserve
);
assert_eq!(
devices.revision(device_id),
DeviceRevisionLookup::Retained(DeviceRevision::default().advanced())
);
Ok(())
}
#[test]
fn a_retired_handle_reports_no_revision_rather_than_a_first_one() -> Result<(), Box<dyn Error>>
{
let key = reported_key("DELL-U2723QE-9J4K2H3")?;
let mut devices = Devices::default();
devices.replace_reconciled(
vec![reconciled(key.clone())],
HashSet::new(),
HashSet::new(),
);
let DeviceResolution::Resolved(device_id) = devices.resolve(&key) else {
panic!("an ingested key must resolve to the handle the registry issued");
};
devices.replace_reconciled(Vec::new(), HashSet::new(), HashSet::new());
assert_eq!(devices.revision(device_id), DeviceRevisionLookup::Retired);
Ok(())
}
#[test]
fn a_returning_key_never_reuses_the_retired_handle() -> Result<(), Box<dyn Error>> {
let key = reported_key("DELL-U2723QE-9J4K2H3")?;
let mut devices = Devices::default();
devices.replace_reconciled(
vec![reconciled(key.clone())],
HashSet::new(),
HashSet::new(),
);
let DeviceResolution::Resolved(first) = devices.resolve(&key) else {
panic!("an ingested key must resolve");
};
devices.replace_reconciled(Vec::new(), HashSet::new(), HashSet::new());
assert!(matches!(devices.state(first), DeviceStateLookup::Retired));
devices.replace_reconciled(
vec![reconciled(key.clone())],
HashSet::new(),
HashSet::new(),
);
let DeviceResolution::Resolved(second) = devices.resolve(&key) else {
panic!("a returning key must resolve again");
};
assert_ne!(first, second);
Ok(())
}
#[test]
fn an_unchanged_key_keeps_its_handle_across_passes() -> Result<(), Box<dyn Error>> {
let key = reported_key("DELL-U2723QE-9J4K2H3")?;
let mut devices = Devices::default();
devices.replace_reconciled(
vec![reconciled(key.clone())],
HashSet::new(),
HashSet::new(),
);
let first = devices.resolve(&key);
devices.replace_reconciled(
vec![reconciled(key.clone())],
HashSet::new(),
HashSet::new(),
);
assert_eq!(devices.resolve(&key), first);
Ok(())
}
#[test]
fn reflection_cannot_construct_a_rigging_revision() {
let mut dynamic_rigging_revision = DynamicTupleStruct::default();
dynamic_rigging_revision.insert(0_u64);
assert!(RiggingRevision::from_reflect(&dynamic_rigging_revision).is_none());
}
#[test]
fn device_registry_types_register_reflection_metadata() {
let app = App::new();
let type_registry = app.world().resource::<AppTypeRegistry>().read();
for type_id in [
TypeId::of::<Device>(),
TypeId::of::<PresentWithUsableClaim>(),
] {
assert!(
type_registry
.get_type_data::<ReflectComponent>(type_id)
.is_some()
);
}
for type_id in [TypeId::of::<Devices>(), TypeId::of::<RiggingRevision>()] {
assert!(
type_registry
.get_type_data::<ReflectResource>(type_id)
.is_some()
);
}
drop(type_registry);
}
#[test]
fn both_ways_a_device_leaves_are_reported_and_stay_distinguishable()
-> Result<(), Box<dyn Error>> {
let unplugged = reported_key("UNPLUGGED")?;
let removed = reported_key("REMOVED")?;
let mut devices = Devices::default();
devices.replace_reconciled(
vec![reconciled(unplugged.clone()), reconciled(removed.clone())],
HashSet::new(),
HashSet::new(),
);
let unplugged_handle = handle(&devices, &unplugged);
let mut absent = reconciled(unplugged.clone());
absent.presence = Presence::Absent;
let replacement = devices.replace_reconciled(vec![absent], HashSet::new(), HashSet::new());
let departures: Vec<(DeviceKey, DeviceDeparture)> = replacement
.changes
.departed
.iter()
.map(|departed_device| (departed_device.key.clone(), departed_device.departure))
.collect();
assert!(departures.contains(&(unplugged.clone(), DeviceDeparture::RetainedButNotPresent)));
assert!(departures.contains(&(removed, DeviceDeparture::KeyLeftTheSet)));
assert_eq!(
devices.resolve(&unplugged),
DeviceResolution::Resolved(unplugged_handle)
);
assert!(replacement.changes.orphaned_entities.is_empty());
Ok(())
}
fn handle(devices: &Devices, key: &DeviceKey) -> DeviceId {
match devices.resolve(key) {
DeviceResolution::Resolved(device_id) => device_id,
DeviceResolution::NotResolved => panic!("key `{key:?}` must resolve after ingest"),
}
}
#[test]
fn a_proven_present_unclaimed_device_authorizes_both_service_and_restore()
-> Result<(), Box<dyn Error>> {
let key = reported_key("DELL-U2723QE-9J4K2H3")?;
let mut devices = Devices::default();
devices.replace_reconciled(
vec![reconciled(key.clone())],
HashSet::new(),
HashSet::new(),
);
let device_id = handle(&devices, &key);
assert!(
devices
.authorize_service(device_id)?
.allows_in_service_use()
);
assert!(
!devices
.authorize_restore(device_id)?
.allows_in_service_use()
);
Ok(())
}
#[test]
fn every_refusal_names_the_check_that_actually_failed() -> Result<(), Box<dyn Error>> {
let offline = reported_key("OFFLINE")?;
let absent = reported_key("ABSENT")?;
let contended = reported_key("CONTENDED")?;
let blocked = reported_key("BLOCKED")?;
let unverified = reported_key("UNVERIFIED")?;
let mut offline_state = reconciled(offline.clone());
offline_state.mode = ConfiguredDeviceMode::Offline;
let mut absent_state = reconciled(absent.clone());
absent_state.presence = Presence::Absent;
let mut contended_state = reconciled(contended.clone());
contended_state.claim = Claim::Contended {
holder: ClaimHolder::Unidentified,
};
let mut blocked_state = reconciled(blocked.clone());
blocked_state.claim = Claim::Blocked {
gate: PermissionGate::CameraAccess,
};
let mut unverified_state = reconciled(unverified.clone());
unverified_state.verdict = IdentityVerdict::Unverified(UnverifiedReason::NotUniqueInScan);
let mut devices = Devices::default();
devices.replace_reconciled(
vec![
offline_state,
absent_state,
contended_state,
blocked_state,
unverified_state,
],
HashSet::new(),
HashSet::new(),
);
let retired = DeviceId::new(u64::MAX);
assert_eq!(
devices.authorize_service(retired).err(),
Some(ApplyAuthorizationError::DeviceRetired { device_id: retired })
);
for (key, expected) in [
(
&offline,
ApplyAuthorizationError::Offline {
key: offline.clone(),
},
),
(
&absent,
ApplyAuthorizationError::NotPresent {
key: absent.clone(),
},
),
(
&contended,
ApplyAuthorizationError::ClaimUnavailable {
key: contended.clone(),
},
),
(
&blocked,
ApplyAuthorizationError::ClaimUnavailable {
key: blocked.clone(),
},
),
(
&unverified,
ApplyAuthorizationError::IdentityNotProven {
key: unverified.clone(),
},
),
] {
let device_id = handle(&devices, key);
assert_eq!(
devices.authorize_service(device_id).err(),
Some(expected.clone())
);
assert_eq!(devices.authorize_restore(device_id).err(), Some(expected));
}
Ok(())
}
#[test]
fn a_restore_only_verdict_returns_a_saved_configuration_and_drives_nothing()
-> Result<(), Box<dyn Error>> {
let key = reported_key("SERIAL-LESS-PANEL")?;
let mut restore_only = reconciled(key.clone());
restore_only.verdict = IdentityVerdict::RestoreOnly;
let mut devices = Devices::default();
devices.replace_reconciled(vec![restore_only], HashSet::new(), HashSet::new());
let device_id = handle(&devices, &key);
assert_eq!(
devices.authorize_service(device_id).err(),
Some(ApplyAuthorizationError::IdentityNotProven { key })
);
assert!(
!devices
.authorize_restore(device_id)?
.allows_in_service_use()
);
Ok(())
}
#[test]
fn an_offline_authored_entry_refuses_a_restore_as_well_as_service() -> Result<(), Box<dyn Error>>
{
let key = reported_key("WITHDRAWN")?;
let mut offline = reconciled(key.clone());
offline.mode = ConfiguredDeviceMode::Offline;
let mut devices = Devices::default();
devices.replace_reconciled(vec![offline], HashSet::new(), HashSet::new());
let device_id = handle(&devices, &key);
assert_eq!(
devices.authorize_restore(device_id).err(),
Some(ApplyAuthorizationError::Offline { key })
);
Ok(())
}
#[test]
fn a_disputed_capability_does_not_block_device_wide_service() -> Result<(), Box<dyn Error>> {
let key = reported_key("STREAMDECK-XL-A00")?;
let mut disputed_state = reconciled(key.clone());
disputed_state.disputed = HashSet::from([TypeId::of::<DisputedCapability>()]);
let mut devices = Devices::default();
devices.replace_reconciled(vec![disputed_state], HashSet::new(), HashSet::new());
let device_id = handle(&devices, &key);
assert!(devices.authorize_service(device_id).is_ok());
assert!(devices.authorize_restore(device_id).is_ok());
Ok(())
}
#[test]
fn successive_issued_attempt_identifiers_differ_and_none_equals_the_default() {
let mut attempts = Attempts::default();
let first = attempts
.issue()
.expect("a fresh registry can issue an identifier");
let second = attempts
.issue()
.expect("a fresh registry can issue a second identifier");
assert_ne!(first, second);
assert_ne!(first, AttemptId::default());
assert_ne!(second, AttemptId::default());
}
#[test]
fn an_identifier_whose_dispatch_never_committed_goes_back_on_offer() {
let mut attempts = Attempts::default();
let first = attempts
.issue()
.expect("a fresh registry can issue an identifier");
attempts.release(first);
assert_eq!(
attempts
.issue()
.expect("the released identifier is on offer again"),
first
);
let retained = attempts
.issue()
.expect("a fresh registry can issue a second identifier");
attempts.begin(test_attempt(retained, bevy::platform::time::Instant::now()));
attempts.release(retained);
attempts.release(first);
assert_ne!(
attempts
.issue()
.expect("the registry can still issue after two refused releases"),
retained
);
}
#[test]
fn a_provisional_dispatch_rollback_removes_the_attempt_and_its_role_index() {
let mut attempts = Attempts::default();
let attempt = attempts
.issue()
.expect("a fresh registry can issue an identifier");
attempts.begin(test_attempt(attempt, bevy::platform::time::Instant::now()));
attempts.rollback_dispatch(attempt);
assert!(matches!(
attempts.in_flight(attempt),
AttemptLookup::Finished
));
assert!(attempts.attempt_by_role.is_empty());
assert_eq!(
attempts
.issue()
.expect("the provisional identifier is on offer again"),
attempt
);
}
#[test]
fn a_dispatch_rollback_can_reclaim_only_the_most_recently_issued_identifier() {
let mut attempts = Attempts::default();
let retained = attempts
.issue()
.expect("a fresh registry can issue an identifier");
attempts.begin(test_attempt(retained, bevy::platform::time::Instant::now()));
let provisional = attempts
.issue()
.expect("the registry can issue beside retained work");
attempts.rollback_dispatch(retained);
assert!(matches!(
attempts.in_flight(retained),
AttemptLookup::InFlight(_)
));
attempts.rollback_dispatch(provisional);
assert_eq!(
attempts
.issue()
.expect("only the provisional identifier was reclaimed"),
provisional
);
}
#[test]
fn a_retained_attempt_is_in_flight_while_an_unissued_identifier_is_finished() {
let mut attempts = Attempts::default();
let attempt = attempts
.issue()
.expect("a fresh registry can issue an identifier");
attempts.begin(Attempt {
id: attempt,
role: crate::RoleKey::new("window/main")
.expect("`window/main` is a valid role handle"),
endpoint: crate::DeviceEndpoint {
device: DeviceKey {
kind: DeviceKind::Display,
id: DeviceIdSource::Authored {
value: crate::AuthoredId::new("panel")
.expect("`panel` is a valid authored identifier"),
},
},
id: EndpointId::Whole,
},
permit: crate::ApplyPermit::in_service(),
binding_generation: crate::BindingGeneration::default(),
expected_device_id: DeviceId::new(0),
device_revision: DeviceRevision::default(),
deadline: bevy::platform::time::Instant::now(),
});
assert!(matches!(
attempts.in_flight(attempt),
AttemptLookup::InFlight(_)
));
assert!(matches!(
attempts.in_flight(AttemptId::default()),
AttemptLookup::Finished
));
}
#[test]
fn the_attempt_registry_registers_reflection_metadata() {
let app = App::new();
let type_registry = app.world().resource::<AppTypeRegistry>().read();
let type_id = TypeId::of::<Attempts>();
assert!(type_registry.contains(type_id));
assert!(
type_registry
.get_type_data::<ReflectResource>(type_id)
.is_some()
);
drop(type_registry);
}
fn test_attempt(id: AttemptId, deadline: Instant) -> Attempt {
Attempt {
id,
role: crate::RoleKey::new("window/main").expect("`window/main` is a valid role handle"),
endpoint: test_endpoint(),
permit: crate::ApplyPermit::in_service(),
binding_generation: crate::BindingGeneration::default(),
expected_device_id: DeviceId::new(0),
device_revision: DeviceRevision::default(),
deadline,
}
}
fn test_endpoint() -> crate::DeviceEndpoint {
crate::DeviceEndpoint {
device: DeviceKey {
kind: DeviceKind::Display,
id: DeviceIdSource::Authored {
value: crate::AuthoredId::new("panel")
.expect("`panel` is a valid authored identifier"),
},
},
id: EndpointId::Whole,
}
}
#[test]
fn the_deadline_query_separates_healthy_overdue_exhausted_and_unknown_attempts() {
let mut attempts = Attempts::default();
let attempt = attempts
.issue()
.expect("a fresh registry can issue an identifier");
let now = bevy::platform::time::Instant::now();
let overrun = Duration::from_secs(5);
attempts.begin(test_attempt(attempt, now + Duration::from_secs(10)));
let reading = |elapsed| FrameClockReading::Measurable(now + Duration::from_secs(elapsed));
assert_eq!(
attempts.deadline_status(attempt, reading(1), overrun),
AttemptDeadlineStatus::WithinDeadline
);
assert_eq!(
attempts.deadline_status(attempt, reading(12), overrun),
AttemptDeadlineStatus::OverdueWithinOverrun {
past_deadline: Duration::from_secs(2),
}
);
assert_eq!(
attempts.deadline_status(attempt, reading(20), overrun),
AttemptDeadlineStatus::OverrunExhausted {
past_deadline: Duration::from_secs(10),
}
);
assert_eq!(
attempts.deadline_status(AttemptId::default(), reading(20), overrun),
AttemptDeadlineStatus::NoSuchAttempt
);
}
#[test]
fn a_clock_that_has_not_advanced_never_reports_an_attempt_overdue() {
let mut attempts = Attempts::default();
let attempt = attempts
.issue()
.expect("a fresh registry can issue an identifier");
let long_past = bevy::platform::time::Instant::now()
.checked_sub(Duration::from_mins(1))
.expect("the process started after the clock's origin");
attempts.begin(test_attempt(attempt, long_past));
assert_eq!(
attempts.deadline_status(
attempt,
crate::reconcile::FrameClockReading::NotYetAdvanced,
Duration::ZERO,
),
AttemptDeadlineStatus::WithinDeadline
);
}
#[test]
fn the_role_keyed_lookup_separates_an_unbound_role_from_an_idle_and_a_working_one()
-> Result<(), Box<dyn std::error::Error>> {
let role = crate::RoleKey::new("window/main")?;
let mut attempts = Attempts::default();
let mut bindings = crate::binding::Bindings::default();
assert_eq!(
attempts.in_flight_for(&role, &bindings),
RoleAttemptLookup::NoSuchRole
);
bindings.register(crate::Binding {
role: role.clone(),
endpoint: test_endpoint(),
driver: crate::DriverId(0),
recovery: RecoveryPolicy::Forget,
retry: RetryOn::NewRevision,
on_abort: crate::OnAbort::default(),
on_loss: crate::OnSessionLoss::default(),
state: crate::RoleState::default(),
requested: crate::RequestedConfiguration::new(RoleAttemptTestConfiguration(3)),
last_known_good: crate::LastKnownGoodConfiguration::default(),
apply_deadline: ApplyDeadline::ProcessDefault,
})?;
assert_eq!(
attempts.in_flight_for(&role, &bindings),
RoleAttemptLookup::Idle
);
let attempt = attempts
.issue()
.expect("a fresh registry can issue an identifier");
attempts.begin(test_attempt(attempt, bevy::platform::time::Instant::now()));
assert_eq!(
attempts.in_flight_for(&role, &bindings),
RoleAttemptLookup::InFlight(attempt)
);
bindings.retire(&role)?;
assert_eq!(
attempts.in_flight_for(&role, &bindings),
RoleAttemptLookup::InFlight(attempt)
);
Ok(())
}
#[derive(Component, Reflect)]
#[reflect(Component)]
struct RoleAttemptTestConfiguration(u32);
}