use std::collections::HashMap;
use std::collections::VecDeque;
use std::num::NonZeroUsize;
use std::time::Duration;
use bevy::ecs::entity::Entity;
use bevy::ecs::reflect::ReflectComponent;
use bevy::ecs::reflect::ReflectResource;
use bevy::prelude::Commands;
use bevy::prelude::Component;
use bevy::prelude::Query;
use bevy::prelude::Reflect;
use bevy::prelude::Res;
use bevy::prelude::ResMut;
use bevy::prelude::Resource;
use bevy::prelude::With;
use thiserror::Error;
use crate::ApplyPermit;
use crate::AttemptId;
use crate::AttemptOutcome;
use crate::BindingRetired;
use crate::CaptureOutcome;
use crate::DeviceEndpoint;
use crate::DeviceKey;
use crate::DeviceRevisionLookup;
use crate::DriverId;
use crate::LastKnownGoodConfiguration;
use crate::OnAbort;
use crate::OnSessionLoss;
use crate::RecoveryPolicy;
use crate::RetryOn;
use crate::RiggingLimits;
use crate::RoleKey;
use crate::RoleState;
use crate::attempt::RetryGate;
use crate::reconcile::FrameClockReading;
const CONSECUTIVE_FAILURE_LIMIT: u32 = 3;
const DEFAULT_PENDING_TRANSITION_CAPACITY: usize = 4_096;
#[derive(Reflect)]
pub struct Binding {
pub role: RoleKey,
pub endpoint: DeviceEndpoint,
pub driver: DriverId,
pub recovery: RecoveryPolicy,
pub retry: RetryOn,
pub on_abort: OnAbort,
pub on_loss: OnSessionLoss,
pub state: RoleState,
pub requested: RequestedConfiguration,
pub last_known_good: LastKnownGoodConfiguration,
pub apply_deadline: ApplyDeadline,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
pub enum ApplyDeadline {
#[default]
ProcessDefault,
Authored(Duration),
}
impl ApplyDeadline {
#[must_use]
pub(crate) const fn resolve(self, rigging_limits: &RiggingLimits) -> ApplyDeadlineLookup {
match self {
Self::ProcessDefault => {
ApplyDeadlineLookup::ProcessDefault(rigging_limits.apply_deadline)
},
Self::Authored(apply_deadline) => ApplyDeadlineLookup::Authored(apply_deadline),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Reflect)]
pub(crate) enum ApplyDeadlineLookup {
ProcessDefault(Duration),
Authored(Duration),
}
impl ApplyDeadlineLookup {
#[must_use]
pub(crate) const fn duration(self) -> Duration {
match self {
Self::ProcessDefault(apply_deadline) | Self::Authored(apply_deadline) => apply_deadline,
}
}
}
#[derive(Reflect)]
pub struct RequestedConfiguration(
#[reflect(ignore, default = "default_erased_configuration")] Box<dyn Reflect>,
);
impl RequestedConfiguration {
#[must_use]
pub fn new(configuration: impl Reflect) -> Self { Self(Box::new(configuration)) }
fn as_reflect(&self) -> &dyn Reflect { self.0.as_ref() }
}
fn default_erased_configuration() -> Box<dyn Reflect> { Box::new(()) }
pub enum AvailableConfiguration<'a> {
LastKnownGood(&'a dyn Reflect),
Requested(&'a dyn Reflect),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
#[reflect(opaque)]
pub struct BindingGeneration(u64);
#[derive(Default, Resource, Reflect)]
#[reflect(Resource)]
pub struct Bindings {
#[reflect(ignore, default = "default_bindings_by_role")]
by_role: HashMap<RoleKey, Binding>,
#[reflect(ignore, default = "default_owner_by_endpoint")]
owner_by_endpoint: HashMap<DeviceEndpoint, RoleKey>,
#[reflect(ignore, default = "default_roles_by_device")]
roles_by_device: HashMap<DeviceKey, Vec<RoleKey>>,
#[reflect(ignore, default = "default_configuration_readability")]
configuration_readability: HashMap<RoleKey, ConfigurationReadability>,
#[reflect(ignore, default = "default_waiting_work")]
waiting_work: HashMap<RoleKey, WaitingWork>,
#[reflect(ignore, default = "default_applying_source")]
applying_source: HashMap<RoleKey, ApplyConfigurationSource>,
#[reflect(ignore, default = "default_establishing_attempts")]
establishing_attempts: HashMap<RoleKey, AttemptId>,
#[reflect(ignore, default = "default_failure_counts")]
attempt_failures: HashMap<RoleKey, u32>,
#[reflect(ignore, default = "default_failure_counts")]
capture_failures: HashMap<RoleKey, u32>,
#[reflect(ignore, default = "default_retry_gates")]
retry_gates: HashMap<RoleKey, RetryGate>,
#[reflect(ignore, default = "default_stopped_role_endpoints")]
stopped_role_endpoints: HashMap<RoleKey, EndpointAvailability>,
#[reflect(ignore, default = "default_generation_by_role")]
generation_by_role: HashMap<RoleKey, BindingGeneration>,
#[reflect(ignore, default = "default_generation_counter")]
next_generation: u64,
#[reflect(ignore, default = "PendingBindingTransitions::default")]
pending_transitions: PendingBindingTransitions,
#[reflect(ignore, default = "default_transition_sequence")]
next_transition_sequence: u64,
}
fn default_bindings_by_role() -> HashMap<RoleKey, Binding> { HashMap::new() }
fn default_owner_by_endpoint() -> HashMap<DeviceEndpoint, RoleKey> { HashMap::new() }
fn default_roles_by_device() -> HashMap<DeviceKey, Vec<RoleKey>> { HashMap::new() }
fn default_establishing_attempts() -> HashMap<RoleKey, AttemptId> { HashMap::new() }
fn default_configuration_readability() -> HashMap<RoleKey, ConfigurationReadability> {
HashMap::new()
}
fn default_waiting_work() -> HashMap<RoleKey, WaitingWork> { HashMap::new() }
fn default_applying_source() -> HashMap<RoleKey, ApplyConfigurationSource> { HashMap::new() }
fn default_failure_counts() -> HashMap<RoleKey, u32> { HashMap::new() }
fn default_retry_gates() -> HashMap<RoleKey, RetryGate> { HashMap::new() }
fn default_generation_by_role() -> HashMap<RoleKey, BindingGeneration> { HashMap::new() }
const fn default_generation_counter() -> u64 { 0 }
fn default_stopped_role_endpoints() -> HashMap<RoleKey, EndpointAvailability> { HashMap::new() }
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
pub enum WaitingWork {
#[default]
Nothing,
RestorationOwed,
ApplicationRequestOwed,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
pub(crate) enum ConfigurationReadability {
#[default]
Readable,
PermanentlyUnreadable,
}
const fn default_transition_sequence() -> u64 { 0 }
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Reflect)]
#[reflect(opaque)]
pub(crate) struct BindingTransitionSequence(u64);
#[derive(Debug, PartialEq, Eq, Reflect)]
pub(crate) enum BindingTransition {
Registered {
sequence: BindingTransitionSequence,
role: RoleKey,
},
Replaced {
sequence: BindingTransitionSequence,
role: RoleKey,
},
Retired {
sequence: BindingTransitionSequence,
role: RoleKey,
endpoint: DeviceEndpoint,
},
}
struct PendingBindingTransitions {
capacity: NonZeroUsize,
queue: VecDeque<BindingTransition>,
}
impl Default for PendingBindingTransitions {
fn default() -> Self {
Self {
capacity: NonZeroUsize::new(DEFAULT_PENDING_TRANSITION_CAPACITY)
.unwrap_or(NonZeroUsize::MIN),
queue: VecDeque::new(),
}
}
}
impl PendingBindingTransitions {
fn has_capacity(&self) -> bool { self.queue.len() < self.capacity.get() }
fn push(&mut self, binding_transition: BindingTransition) {
self.queue.push_back(binding_transition);
}
}
impl Bindings {
pub fn register(&mut self, mut binding: Binding) -> Result<(), BindingError> {
if self.by_role.contains_key(&binding.role) {
return Err(BindingError::RoleAlreadyBound { role: binding.role });
}
if let Some(owner) = self.owner_by_endpoint.get(&binding.endpoint) {
return Err(BindingError::EndpointAlreadyOwned {
endpoint: binding.endpoint,
owner: owner.clone(),
});
}
let reserved_transition = self.reserve_transition()?;
binding.state = RoleState::Waiting;
let role = binding.role.clone();
let endpoint = binding.endpoint.clone();
let device_key = endpoint.device.clone();
self.owner_by_endpoint.insert(endpoint, role.clone());
self.roles_by_device
.entry(device_key)
.or_default()
.push(role.clone());
self.by_role.insert(role.clone(), binding);
let generation = self.mint_generation();
self.generation_by_role.insert(role.clone(), generation);
self.enqueue(BindingTransitionKind::Registered, role, reserved_transition);
Ok(())
}
pub fn replace(&mut self, mut binding: Binding) -> Result<Binding, BindingError> {
let old_binding =
self.by_role
.get(&binding.role)
.ok_or_else(|| BindingError::RoleNotBound {
role: binding.role.clone(),
})?;
if let Some(owner) = self.owner_by_endpoint.get(&binding.endpoint)
&& owner != &binding.role
{
return Err(BindingError::EndpointAlreadyOwned {
endpoint: binding.endpoint,
owner: owner.clone(),
});
}
let reserved_transition = self.reserve_transition()?;
binding.state = RoleState::Waiting;
let role = binding.role.clone();
let old_endpoint = old_binding.endpoint.clone();
let new_endpoint = binding.endpoint.clone();
let displaced = self
.by_role
.insert(role.clone(), binding)
.ok_or_else(|| BindingError::RoleNotBound { role: role.clone() })?;
let generation = self.mint_generation();
self.generation_by_role.insert(role.clone(), generation);
if old_endpoint != new_endpoint {
let new_device_key = new_endpoint.device.clone();
self.owner_by_endpoint.remove(&old_endpoint);
self.remove_role_from_device(&old_endpoint.device, &role);
self.owner_by_endpoint.insert(new_endpoint, role.clone());
self.roles_by_device
.entry(new_device_key)
.or_default()
.push(role.clone());
}
self.configuration_readability.remove(&role);
self.waiting_work.remove(&role);
self.applying_source.remove(&role);
self.establishing_attempts.remove(&role);
self.attempt_failures.remove(&role);
self.capture_failures.remove(&role);
self.retry_gates.remove(&role);
self.stopped_role_endpoints.remove(&role);
self.enqueue(BindingTransitionKind::Replaced, role, reserved_transition);
Ok(displaced)
}
pub(crate) fn readdress(
&mut self,
role: &RoleKey,
device: DeviceKey,
) -> Result<(), BindingError> {
let binding = self
.by_role
.get(role)
.ok_or_else(|| BindingError::RoleNotBound { role: role.clone() })?;
let old_endpoint = binding.endpoint.clone();
let new_endpoint = DeviceEndpoint {
device,
id: old_endpoint.id.clone(),
};
if let Some(owner) = self.owner_by_endpoint.get(&new_endpoint)
&& owner != role
{
return Err(BindingError::EndpointAlreadyOwned {
endpoint: new_endpoint,
owner: owner.clone(),
});
}
if old_endpoint == new_endpoint {
return Ok(());
}
let reserved_transition = self.reserve_transition()?;
let binding = self
.by_role
.get_mut(role)
.ok_or_else(|| BindingError::RoleNotBound { role: role.clone() })?;
binding.endpoint = new_endpoint.clone();
binding.state = RoleState::Waiting;
let new_device_key = new_endpoint.device.clone();
self.owner_by_endpoint.remove(&old_endpoint);
self.remove_role_from_device(&old_endpoint.device, role);
self.owner_by_endpoint.insert(new_endpoint, role.clone());
self.roles_by_device
.entry(new_device_key)
.or_default()
.push(role.clone());
self.configuration_readability.remove(role);
self.waiting_work.remove(role);
self.applying_source.remove(role);
self.establishing_attempts.remove(role);
self.attempt_failures.remove(role);
self.capture_failures.remove(role);
self.retry_gates.remove(role);
self.stopped_role_endpoints.remove(role);
let generation = self.mint_generation();
self.generation_by_role.insert(role.clone(), generation);
self.enqueue(
BindingTransitionKind::Replaced,
role.clone(),
reserved_transition,
);
Ok(())
}
pub fn validate_device_readdress(
&self,
saved: &DeviceKey,
adopted: &DeviceKey,
) -> Result<(), BindingError> {
self.prepare_device_readdress(saved, adopted).map(|_| ())
}
pub fn readdress_device(
&mut self,
saved: &DeviceKey,
adopted: DeviceKey,
) -> Result<(), BindingError> {
let prepared = self.prepare_device_readdress(saved, &adopted)?;
if prepared.is_empty() {
return Ok(());
}
let roles = prepared
.iter()
.map(|readdress| readdress.role.clone())
.collect::<Vec<_>>();
for readdress in prepared {
let Some(binding) = self.by_role.get_mut(&readdress.role) else {
continue;
};
binding.endpoint = readdress.adopted_endpoint.clone();
binding.state = RoleState::Waiting;
self.owner_by_endpoint.remove(&readdress.saved_endpoint);
self.owner_by_endpoint
.insert(readdress.adopted_endpoint, readdress.role.clone());
self.configuration_readability.remove(&readdress.role);
self.waiting_work.remove(&readdress.role);
self.applying_source.remove(&readdress.role);
self.establishing_attempts.remove(&readdress.role);
self.attempt_failures.remove(&readdress.role);
self.capture_failures.remove(&readdress.role);
self.retry_gates.remove(&readdress.role);
self.stopped_role_endpoints.remove(&readdress.role);
let generation = self.mint_generation();
self.generation_by_role
.insert(readdress.role.clone(), generation);
self.enqueue(
BindingTransitionKind::Replaced,
readdress.role,
readdress.reserved_transition,
);
}
self.roles_by_device.remove(saved);
self.roles_by_device
.entry(adopted)
.or_default()
.extend(roles);
Ok(())
}
fn prepare_device_readdress(
&self,
saved: &DeviceKey,
adopted: &DeviceKey,
) -> Result<Vec<PreparedDeviceReaddress>, BindingError> {
if saved == adopted {
return Ok(Vec::new());
}
let roles = self.roles_by_device.get(saved).cloned().unwrap_or_default();
for role in &roles {
let binding = self
.by_role
.get(role)
.ok_or_else(|| BindingError::RoleNotBound { role: role.clone() })?;
let adopted_endpoint = DeviceEndpoint {
device: adopted.clone(),
id: binding.endpoint.id.clone(),
};
if let Some(owner) = self.owner_by_endpoint.get(&adopted_endpoint)
&& owner != role
{
return Err(BindingError::EndpointAlreadyOwned {
endpoint: adopted_endpoint,
owner: owner.clone(),
});
}
}
let reservations = self.reserve_transitions(roles.len())?;
roles
.into_iter()
.zip(reservations)
.map(|(role, reserved_transition)| {
let binding = self
.by_role
.get(&role)
.ok_or_else(|| BindingError::RoleNotBound { role: role.clone() })?;
Ok(PreparedDeviceReaddress {
adopted_endpoint: DeviceEndpoint {
device: adopted.clone(),
id: binding.endpoint.id.clone(),
},
saved_endpoint: binding.endpoint.clone(),
role,
reserved_transition,
})
})
.collect()
}
pub(crate) fn candidate_endpoint_owner(
&self,
role: &RoleKey,
candidate: &DeviceKey,
) -> EndpointOwner {
let Some(binding) = self.by_role.get(role) else {
return EndpointOwner::Unowned;
};
let candidate_endpoint = DeviceEndpoint {
device: candidate.clone(),
id: binding.endpoint.id.clone(),
};
self.owner_by_endpoint
.get(&candidate_endpoint)
.filter(|owner| *owner != role)
.map_or(EndpointOwner::Unowned, |owner| {
EndpointOwner::OwnedBy(owner.clone())
})
}
pub fn retire(&mut self, role: &RoleKey) -> Result<RetirementOutcome, BindingError> {
if !self.by_role.contains_key(role) {
return Ok(RetirementOutcome::AlreadyUnbound);
}
let reserved_transition = self.reserve_transition()?;
let mut binding = self
.by_role
.remove(role)
.ok_or_else(|| BindingError::RoleNotBound { role: role.clone() })?;
self.owner_by_endpoint.remove(&binding.endpoint);
self.remove_role_from_device(&binding.endpoint.device, role);
self.configuration_readability.remove(role);
self.waiting_work.remove(role);
self.applying_source.remove(role);
self.establishing_attempts.remove(role);
self.attempt_failures.remove(role);
self.capture_failures.remove(role);
self.retry_gates.remove(role);
self.stopped_role_endpoints.remove(role);
self.generation_by_role.remove(role);
binding.state = RoleState::Retired;
self.enqueue(
BindingTransitionKind::Retired(binding.endpoint.clone()),
role.clone(),
reserved_transition,
);
Ok(RetirementOutcome::Retired(binding))
}
pub(crate) fn generation(&self, role: &RoleKey) -> Option<BindingGeneration> {
self.generation_by_role.get(role).copied()
}
const fn mint_generation(&mut self) -> BindingGeneration {
self.next_generation += 1;
BindingGeneration(self.next_generation)
}
pub fn binding(&self, role: &RoleKey) -> Result<&Binding, BindingError> {
self.by_role
.get(role)
.ok_or_else(|| BindingError::RoleNotBound { role: role.clone() })
}
pub fn roles_for(&self, device_key: &DeviceKey) -> impl Iterator<Item = &RoleKey> {
self.roles_by_device.get(device_key).into_iter().flatten()
}
pub(crate) fn role_view(&mut self, role: &RoleKey) -> Result<RoleView<'_>, BindingError> {
let configuration_readability = &mut self.configuration_readability;
let applying_source = &mut self.applying_source;
let establishing_attempts = &mut self.establishing_attempts;
let waiting_work = self.waiting_work.get(role).copied().unwrap_or_default();
let binding = self
.by_role
.get_mut(role)
.ok_or_else(|| BindingError::RoleNotBound { role: role.clone() })?;
Ok(match binding.state {
RoleState::Waiting => RoleView::Waiting(match waiting_work {
WaitingWork::Nothing => WaitingRole::Hardware(RequestingRole {
binding,
applying_source,
}),
WaitingWork::RestorationOwed => WaitingRole::Restoration(RestoringRole {
binding,
applying_source,
}),
WaitingWork::ApplicationRequestOwed => WaitingRole::ApplicationRequest,
}),
RoleState::Ready => RoleView::Ready(ReadyRole {
binding,
configuration_readability,
capture_failures: &mut self.capture_failures,
}),
RoleState::Applying(_) => RoleView::Applying(ApplyingRole {
binding,
waiting_work: &mut self.waiting_work,
applying_source,
establishing_attempts,
}),
RoleState::StoppedAfterRepeatedFailures => RoleView::StoppedAfterRepeatedFailures,
RoleState::Retired => RoleView::Retired,
})
}
#[must_use]
pub fn waiting_work(&self, role: &RoleKey) -> WaitingWork {
self.waiting_work.get(role).copied().unwrap_or_default()
}
pub(crate) fn establishing_attempt(&self, role: &RoleKey) -> EstablishingAttemptLookup {
self.establishing_attempts.get(role).copied().map_or(
EstablishingAttemptLookup::NotEstablished,
EstablishingAttemptLookup::EstablishedBy,
)
}
pub(crate) fn await_departed_device(&mut self, role: &RoleKey) {
if let Some(binding) = self.by_role.get_mut(role)
&& binding.state == RoleState::Ready
{
binding.state = RoleState::Waiting;
self.establishing_attempts.remove(role);
}
}
pub(crate) fn set_waiting_work(&mut self, role: &RoleKey, waiting_work: WaitingWork) {
match waiting_work {
WaitingWork::Nothing => {
self.waiting_work.remove(role);
},
WaitingWork::RestorationOwed | WaitingWork::ApplicationRequestOwed => {
self.waiting_work.insert(role.clone(), waiting_work);
},
}
}
pub(crate) fn request_reapply(&mut self, role: &RoleKey) {
let established = self.by_role.get(role).is_some_and(|binding| {
matches!(
binding.last_known_good,
LastKnownGoodConfiguration::Known(_)
)
});
let waiting_work = if established {
WaitingWork::RestorationOwed
} else {
WaitingWork::Nothing
};
self.set_waiting_work(role, waiting_work);
}
pub fn forget_last_known_good(&mut self, role: &RoleKey) {
if let Some(binding) = self.by_role.get_mut(role) {
binding.last_known_good = LastKnownGoodConfiguration::NotEstablished;
}
}
pub(crate) fn record_attempt_ending(
&mut self,
role: &RoleKey,
ended_generation: BindingGeneration,
outcome: AttemptOutcome,
device_revision: DeviceRevisionLookup,
now: FrameClockReading,
) {
if self.generation(role) != Some(ended_generation) {
return;
}
match outcome {
AttemptOutcome::Succeeded | AttemptOutcome::Substituted => {
self.attempt_failures.remove(role);
self.retry_gates.remove(role);
self.stopped_role_endpoints.remove(role);
},
AttemptOutcome::Aborted => {
if let Some(binding) = self.by_role.get(role) {
let retry_gate = RetryGate::from_policy(binding.retry, device_revision, now);
self.retry_gates.insert(role.clone(), retry_gate);
}
},
AttemptOutcome::Failed(_) => {
let consecutive = self
.attempt_failures
.get(role)
.copied()
.unwrap_or_default()
.saturating_add(1);
self.attempt_failures.insert(role.clone(), consecutive);
if consecutive >= CONSECUTIVE_FAILURE_LIMIT {
self.retry_gates.remove(role);
if let Some(binding) = self.by_role.get_mut(role) {
binding.state = RoleState::StoppedAfterRepeatedFailures;
}
} else if let Some(binding) = self.by_role.get(role) {
let retry_gate = RetryGate::from_policy(binding.retry, device_revision, now);
self.retry_gates.insert(role.clone(), retry_gate);
}
},
}
}
pub(crate) fn apply_session_loss(
&mut self,
role: &RoleKey,
device_revision: crate::DeviceRevision,
now: FrameClockReading,
) -> SessionLossApplication {
let Some(binding) = self.by_role.get(role) else {
return SessionLossApplication::BindingAbsent;
};
let on_loss = binding.on_loss;
let retry = binding.retry;
let has_last_known_good = matches!(
binding.last_known_good,
LastKnownGoodConfiguration::Known(_)
);
if let Some(binding) = self.by_role.get_mut(role) {
binding.state = RoleState::Waiting;
}
self.applying_source.remove(role);
self.establishing_attempts.remove(role);
match on_loss {
OnSessionLoss::Recreate => {
self.set_waiting_work(
role,
if has_last_known_good {
WaitingWork::RestorationOwed
} else {
WaitingWork::Nothing
},
);
self.retry_gates.insert(
role.clone(),
RetryGate::from_policy(
retry,
DeviceRevisionLookup::Retained(device_revision),
now,
),
);
},
OnSessionLoss::ReportOnly => {
self.set_waiting_work(role, WaitingWork::ApplicationRequestOwed);
self.retry_gates.remove(role);
},
}
SessionLossApplication::Applied(on_loss)
}
pub(crate) fn record_dispatch_refused(
&mut self,
role: &RoleKey,
device_revision: DeviceRevisionLookup,
now: FrameClockReading,
) {
if let Some(binding) = self.by_role.get(role) {
let retry_gate = RetryGate::from_policy(binding.retry, device_revision, now);
self.retry_gates.insert(role.clone(), retry_gate);
}
}
pub(crate) fn retry_pacing(&self, role: &RoleKey) -> RetryPacing {
self.retry_gates
.get(role)
.copied()
.map_or(RetryPacing::Ready, RetryPacing::AwaitingGate)
}
pub fn restart_after_repeated_failures(&mut self, role: &RoleKey) -> Result<(), BindingError> {
let binding = self
.by_role
.get_mut(role)
.ok_or_else(|| BindingError::RoleNotBound { role: role.clone() })?;
if binding.state != RoleState::StoppedAfterRepeatedFailures {
return Err(BindingError::RoleNotStopped { role: role.clone() });
}
binding.state = RoleState::Waiting;
self.attempt_failures.remove(role);
self.retry_gates.remove(role);
self.stopped_role_endpoints.remove(role);
Ok(())
}
pub(crate) fn observe_stopped_role_endpoint(
&mut self,
role: &RoleKey,
endpoint_availability: EndpointAvailability,
) {
if self
.by_role
.get(role)
.is_none_or(|binding| binding.state != RoleState::StoppedAfterRepeatedFailures)
{
return;
}
let previous = self
.stopped_role_endpoints
.insert(role.clone(), endpoint_availability);
if previous != Some(EndpointAvailability::Gone)
|| endpoint_availability != EndpointAvailability::Available
{
return;
}
self.stopped_role_endpoints.remove(role);
self.retry_gates.remove(role);
if let Some(binding) = self.by_role.get_mut(role) {
binding.state = RoleState::Waiting;
}
}
#[must_use]
pub(crate) fn capture_dispatch(&self, role: &RoleKey) -> CaptureDispatch {
if self.capture_failures.get(role).copied().unwrap_or_default() >= CONSECUTIVE_FAILURE_LIMIT
{
CaptureDispatch::SuspendedAfterRepeatedFailures
} else {
CaptureDispatch::Eligible
}
}
#[must_use]
pub(crate) fn configuration_readability(&self, role: &RoleKey) -> ConfigurationReadability {
self.configuration_readability
.get(role)
.copied()
.unwrap_or_default()
}
pub(crate) fn registered_roles(&self) -> impl Iterator<Item = &RoleKey> { self.by_role.keys() }
pub fn configuration_for(
&self,
role: &RoleKey,
) -> Result<AvailableConfiguration<'_>, BindingError> {
let binding = self.binding(role)?;
Ok(match &binding.last_known_good {
LastKnownGoodConfiguration::Known(configuration) => {
AvailableConfiguration::LastKnownGood(configuration.as_ref())
},
LastKnownGoodConfiguration::NotEstablished => {
AvailableConfiguration::Requested(binding.requested.as_reflect())
},
})
}
pub fn set_pending_transition_capacity(
&mut self,
capacity: NonZeroUsize,
) -> Result<(), BindingCapacityError> {
let pending = self.pending_transitions.queue.len();
if capacity.get() < pending {
return Err(BindingCapacityError::BelowPendingCount { capacity, pending });
}
self.pending_transitions.capacity = capacity;
Ok(())
}
fn has_pending_transitions(&self) -> bool { !self.pending_transitions.queue.is_empty() }
pub(crate) fn pending_transitions(
&self,
) -> impl DoubleEndedIterator<Item = &BindingTransition> + ExactSizeIterator {
self.pending_transitions.queue.iter()
}
fn take_pending_transitions(&mut self) -> VecDeque<BindingTransition> {
std::mem::take(&mut self.pending_transitions.queue)
}
fn reserve_transition(&self) -> Result<ReservedBindingTransition, BindingError> {
if !self.pending_transitions.has_capacity() {
return Err(BindingError::PendingTransitionCapacityReached);
}
let next_transition_sequence = self
.next_transition_sequence
.checked_add(1)
.ok_or(BindingError::TransitionSequenceExhausted)?;
Ok(ReservedBindingTransition {
sequence: BindingTransitionSequence(self.next_transition_sequence),
next_transition_sequence,
})
}
fn reserve_transitions(
&self,
count: usize,
) -> Result<Vec<ReservedBindingTransition>, BindingError> {
let Some(pending) = self.pending_transitions.queue.len().checked_add(count) else {
return Err(BindingError::PendingTransitionCapacityReached);
};
if pending > self.pending_transitions.capacity.get() {
return Err(BindingError::PendingTransitionCapacityReached);
}
let mut reservations = Vec::with_capacity(count);
let mut sequence = self.next_transition_sequence;
for _ in 0..count {
let Some(next_transition_sequence) = sequence.checked_add(1) else {
return Err(BindingError::TransitionSequenceExhausted);
};
reservations.push(ReservedBindingTransition {
sequence: BindingTransitionSequence(sequence),
next_transition_sequence,
});
sequence = next_transition_sequence;
}
Ok(reservations)
}
fn enqueue(
&mut self,
binding_transition_kind: BindingTransitionKind,
role: RoleKey,
reserved_transition: ReservedBindingTransition,
) {
let ReservedBindingTransition {
sequence,
next_transition_sequence,
} = reserved_transition;
self.next_transition_sequence = next_transition_sequence;
let binding_transition = match binding_transition_kind {
BindingTransitionKind::Registered => BindingTransition::Registered { sequence, role },
BindingTransitionKind::Replaced => BindingTransition::Replaced { sequence, role },
BindingTransitionKind::Retired(endpoint) => BindingTransition::Retired {
sequence,
role,
endpoint,
},
};
self.pending_transitions.push(binding_transition);
}
fn remove_role_from_device(&mut self, device_key: &DeviceKey, role: &RoleKey) {
let remove_device_entry = if let Some(roles) = self.roles_by_device.get_mut(device_key) {
roles.retain(|stored_role| stored_role != role);
roles.is_empty()
} else {
false
};
if remove_device_entry {
self.roles_by_device.remove(device_key);
}
}
}
enum BindingTransitionKind {
Registered,
Replaced,
Retired(DeviceEndpoint),
}
struct ReservedBindingTransition {
sequence: BindingTransitionSequence,
next_transition_sequence: u64,
}
struct PreparedDeviceReaddress {
role: RoleKey,
saved_endpoint: DeviceEndpoint,
adopted_endpoint: DeviceEndpoint,
reserved_transition: ReservedBindingTransition,
}
pub enum RetirementOutcome {
Retired(Binding),
AlreadyUnbound,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum BindingError {
#[error("role `{role}` is already bound")]
RoleAlreadyBound {
role: RoleKey,
},
#[error("role `{role}` is not bound")]
RoleNotBound {
role: RoleKey,
},
#[error("endpoint `{endpoint:?}` is already owned by role `{owner}`")]
EndpointAlreadyOwned {
endpoint: DeviceEndpoint,
owner: RoleKey,
},
#[error("pending binding transition capacity has been reached")]
PendingTransitionCapacityReached,
#[error("binding transition sequence is exhausted")]
TransitionSequenceExhausted,
#[error("requested configuration requires an in-service apply permit")]
RequestedConfigurationRequiresInServicePermit,
#[error("last-known-good configuration requires a restore-only apply permit")]
LastKnownGoodConfigurationRequiresRestoreOnlyPermit,
#[error("role `{role}` has no last-known-good configuration")]
LastKnownGoodNotEstablished {
role: RoleKey,
},
#[error("configured device `{device_key:?}` is offline")]
ConfiguredDeviceOffline {
device_key: DeviceKey,
},
#[error("role `{role}` was not stopped after repeated failures")]
RoleNotStopped {
role: RoleKey,
},
#[error("role `{role}` has no readable endpoint configuration")]
ConfigurationNotReadable {
role: RoleKey,
},
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum BindingCapacityError {
#[error("pending transition count {pending} exceeds requested capacity {capacity}")]
BelowPendingCount {
capacity: NonZeroUsize,
pending: usize,
},
}
pub(crate) enum RoleView<'a> {
Waiting(WaitingRole<'a>),
Ready(ReadyRole<'a>),
Applying(ApplyingRole<'a>),
StoppedAfterRepeatedFailures,
Retired,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum EstablishingAttemptLookup {
EstablishedBy(AttemptId),
NotEstablished,
}
pub(crate) enum SessionLossApplication {
Applied(OnSessionLoss),
BindingAbsent,
}
pub(crate) enum WaitingRole<'a> {
Hardware(RequestingRole<'a>),
Restoration(RestoringRole<'a>),
ApplicationRequest,
}
pub(crate) struct RequestingRole<'a> {
binding: &'a mut Binding,
applying_source: &'a mut HashMap<RoleKey, ApplyConfigurationSource>,
}
impl<'a> RequestingRole<'a> {
pub(crate) fn start_requested_apply(
self,
attempt: AttemptId,
permit: ApplyPermit,
hardware_inventory: &HardwareInventory,
) -> Result<StartApplyRequest<'a>, BindingError> {
if !permit.allows_in_service_use()
&& !matches!(
self.binding.last_known_good,
LastKnownGoodConfiguration::NotEstablished
)
{
return Err(BindingError::RequestedConfigurationRequiresInServicePermit);
}
hardware_inventory.ensure_operational(&self.binding.endpoint.device)?;
self.applying_source.insert(
self.binding.role.clone(),
ApplyConfigurationSource::Requested,
);
Ok(StartApplyRequest {
binding: self.binding,
configuration_source: ApplyConfigurationSource::Requested,
attempt,
permit,
})
}
}
pub(crate) struct RestoringRole<'a> {
binding: &'a mut Binding,
applying_source: &'a mut HashMap<RoleKey, ApplyConfigurationSource>,
}
impl<'a> RestoringRole<'a> {
pub(crate) fn start_last_known_good_restore(
self,
attempt: AttemptId,
permit: ApplyPermit,
hardware_inventory: &HardwareInventory,
) -> Result<StartApplyRequest<'a>, BindingError> {
if permit.allows_in_service_use() {
return Err(BindingError::LastKnownGoodConfigurationRequiresRestoreOnlyPermit);
}
hardware_inventory.ensure_operational(&self.binding.endpoint.device)?;
self.binding.last_known_good.as_reflect().map_err(|_| {
BindingError::LastKnownGoodNotEstablished {
role: self.binding.role.clone(),
}
})?;
self.applying_source.insert(
self.binding.role.clone(),
ApplyConfigurationSource::LastKnownGood,
);
Ok(StartApplyRequest {
binding: self.binding,
configuration_source: ApplyConfigurationSource::LastKnownGood,
attempt,
permit,
})
}
}
pub struct ReadyRole<'a> {
binding: &'a mut Binding,
configuration_readability: &'a mut HashMap<RoleKey, ConfigurationReadability>,
capture_failures: &'a mut HashMap<RoleKey, u32>,
}
impl<'a> ReadyRole<'a> {
pub(crate) fn capture_request(
self,
hardware_inventory: &HardwareInventory,
) -> Result<CaptureRequest<'a>, BindingError> {
hardware_inventory.ensure_operational(&self.binding.endpoint.device)?;
if self
.configuration_readability
.get(&self.binding.role)
.copied()
.unwrap_or_default()
== ConfigurationReadability::PermanentlyUnreadable
{
return Err(BindingError::ConfigurationNotReadable {
role: self.binding.role.clone(),
});
}
Ok(CaptureRequest {
role: &self.binding.role,
driver: self.binding.driver,
endpoint: &self.binding.endpoint,
})
}
pub(crate) fn record_capture(
&mut self,
capture_outcome: CaptureOutcome<LastKnownGoodConfiguration>,
) {
match capture_outcome {
CaptureOutcome::Read(last_known_good) => {
if !self
.binding
.last_known_good
.holds_same_value(&last_known_good)
{
self.binding.last_known_good = last_known_good;
}
self.capture_failures.remove(&self.binding.role);
},
CaptureOutcome::NotReadable => {
if self
.configuration_readability
.get(&self.binding.role)
.copied()
.unwrap_or_default()
!= ConfigurationReadability::PermanentlyUnreadable
{
self.configuration_readability.insert(
self.binding.role.clone(),
ConfigurationReadability::PermanentlyUnreadable,
);
}
},
CaptureOutcome::ReadFailed(_) => {
let consecutive = self
.capture_failures
.get(&self.binding.role)
.copied()
.unwrap_or_default()
.saturating_add(1);
self.capture_failures
.insert(self.binding.role.clone(), consecutive);
},
}
}
}
pub(crate) struct ApplyingRole<'a> {
binding: &'a mut Binding,
waiting_work: &'a mut HashMap<RoleKey, WaitingWork>,
applying_source: &'a mut HashMap<RoleKey, ApplyConfigurationSource>,
establishing_attempts: &'a mut HashMap<RoleKey, AttemptId>,
}
impl<'a> ApplyingRole<'a> {
pub(crate) fn poll_request(
self,
hardware_inventory: &HardwareInventory,
) -> Result<PollRequest<'a>, BindingError> {
hardware_inventory.ensure_operational(&self.binding.endpoint.device)?;
let RoleState::Applying(attempt) = self.binding.state else {
return Err(BindingError::RoleNotBound {
role: self.binding.role.clone(),
});
};
Ok(PollRequest {
role: &self.binding.role,
driver: self.binding.driver,
endpoint: &self.binding.endpoint,
attempt,
})
}
pub(crate) fn abort(&mut self) {
self.binding.state = RoleState::Waiting;
self.establishing_attempts.remove(&self.binding.role);
self.take_applying_source();
}
fn take_applying_source(&mut self) -> ApplySourceLookup {
self.applying_source.remove(&self.binding.role).map_or(
ApplySourceLookup::NotDispatched,
ApplySourceLookup::Dispatched,
)
}
pub(crate) fn finish(&mut self, attempt_outcome: AttemptOutcome) {
let establishing_attempt = match self.binding.state {
RoleState::Applying(attempt) => EstablishingAttemptLookup::EstablishedBy(attempt),
RoleState::Waiting
| RoleState::Ready
| RoleState::StoppedAfterRepeatedFailures
| RoleState::Retired => EstablishingAttemptLookup::NotEstablished,
};
self.binding.state = match &attempt_outcome {
AttemptOutcome::Succeeded | AttemptOutcome::Substituted => RoleState::Ready,
AttemptOutcome::Failed(_) | AttemptOutcome::Aborted => RoleState::Waiting,
};
match (attempt_outcome, establishing_attempt) {
(AttemptOutcome::Succeeded, EstablishingAttemptLookup::EstablishedBy(attempt)) => {
self.establishing_attempts
.insert(self.binding.role.clone(), attempt);
},
(AttemptOutcome::Succeeded, EstablishingAttemptLookup::NotEstablished)
| (
AttemptOutcome::Failed(_) | AttemptOutcome::Aborted | AttemptOutcome::Substituted,
_,
) => {
self.establishing_attempts.remove(&self.binding.role);
},
}
let restoration_completed = self.take_applying_source().restored_last_known_good();
if self.binding.state == RoleState::Ready
&& restoration_completed
&& self
.waiting_work
.get(&self.binding.role)
.copied()
.unwrap_or_default()
!= WaitingWork::Nothing
{
self.waiting_work
.insert(self.binding.role.clone(), WaitingWork::Nothing);
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
pub(crate) enum CaptureDispatch {
#[default]
Eligible,
SuspendedAfterRepeatedFailures,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum EndpointAvailability {
Available,
Gone,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ApplySourceLookup {
NotDispatched,
Dispatched(ApplyConfigurationSource),
}
impl ApplySourceLookup {
const fn restored_last_known_good(self) -> bool {
matches!(
self,
Self::Dispatched(ApplyConfigurationSource::LastKnownGood)
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RetryPacing {
Ready,
AwaitingGate(RetryGate),
}
impl RetryPacing {
pub(crate) fn permits_dispatch(
self,
device_revision: DeviceRevisionLookup,
now: FrameClockReading,
) -> bool {
match self {
Self::Ready => true,
Self::AwaitingGate(retry_gate) => retry_gate.opened(device_revision, now),
}
}
}
pub(crate) struct CaptureRequest<'a> {
pub(crate) role: &'a RoleKey,
pub(crate) driver: DriverId,
pub(crate) endpoint: &'a DeviceEndpoint,
}
pub struct StartApplyRequest<'a> {
pub(crate) binding: &'a mut Binding,
pub(crate) configuration_source: ApplyConfigurationSource,
pub(crate) attempt: AttemptId,
pub(crate) permit: ApplyPermit,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum ApplyConfigurationSource {
Requested,
LastKnownGood,
}
impl ApplyConfigurationSource {
pub(crate) fn configuration(self, binding: &Binding) -> Result<&dyn Reflect, BindingError> {
match self {
Self::Requested => Ok(binding.requested.as_reflect()),
Self::LastKnownGood => binding.last_known_good.as_reflect().map_err(|_| {
BindingError::LastKnownGoodNotEstablished {
role: binding.role.clone(),
}
}),
}
}
}
pub(crate) struct PollRequest<'a> {
pub(crate) role: &'a RoleKey,
pub(crate) driver: DriverId,
pub(crate) endpoint: &'a DeviceEndpoint,
pub(crate) attempt: AttemptId,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Reflect)]
pub enum ConfiguredDeviceMode {
Managed,
Offline,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Reflect)]
pub(crate) enum EndpointOwner {
#[default]
Unowned,
OwnedBy(RoleKey),
}
#[derive(Clone, Debug, PartialEq, Eq, Reflect)]
pub struct ConfiguredDevice {
pub key: DeviceKey,
pub mode: ConfiguredDeviceMode,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Reflect)]
pub enum ConfiguredDeviceConnection {
NotObserved,
Present,
Absent,
Unreachable,
}
#[derive(Default, Resource, Reflect)]
#[reflect(Resource)]
pub struct HardwareInventory {
#[reflect(ignore, default = "default_configured_devices")]
configured: HashMap<DeviceKey, ConfiguredDevice>,
#[reflect(ignore, default = "default_configured_device_connections")]
connections: HashMap<DeviceKey, ConfiguredDeviceConnection>,
}
fn default_configured_devices() -> HashMap<DeviceKey, ConfiguredDevice> { HashMap::new() }
fn default_configured_device_connections() -> HashMap<DeviceKey, ConfiguredDeviceConnection> {
HashMap::new()
}
impl HardwareInventory {
pub fn configure(&mut self, configured_device: ConfiguredDevice) {
let device_key = configured_device.key.clone();
self.configured
.insert(device_key.clone(), configured_device);
self.connections
.entry(device_key)
.or_insert(ConfiguredDeviceConnection::NotObserved);
}
pub(crate) fn readdress(&mut self, saved: &DeviceKey, candidate: DeviceKey) {
let Some(mut configured_device) = self.configured.remove(saved) else {
return;
};
let connection = self
.connections
.remove(saved)
.unwrap_or(ConfiguredDeviceConnection::NotObserved);
configured_device.key = candidate.clone();
self.configured.insert(candidate.clone(), configured_device);
self.connections.insert(candidate, connection);
}
pub fn configured_device(
&self,
device_key: &DeviceKey,
) -> Result<&ConfiguredDevice, HardwareInventoryError> {
self.configured
.get(device_key)
.ok_or_else(|| HardwareInventoryError::DeviceNotConfigured {
device_key: device_key.clone(),
})
}
pub fn connection(
&self,
device_key: &DeviceKey,
) -> Result<ConfiguredDeviceConnection, HardwareInventoryError> {
self.configured_device(device_key)?;
self.connections.get(device_key).copied().ok_or_else(|| {
HardwareInventoryError::DeviceNotConfigured {
device_key: device_key.clone(),
}
})
}
pub(crate) fn configured_keys(&self) -> impl Iterator<Item = &DeviceKey> {
self.configured.keys()
}
pub(crate) fn set_connection(
&mut self,
device_key: &DeviceKey,
connection: ConfiguredDeviceConnection,
) -> Result<(), HardwareInventoryError> {
self.configured_device(device_key)?;
self.connections.insert(device_key.clone(), connection);
Ok(())
}
pub(crate) fn ensure_operational(&self, device_key: &DeviceKey) -> Result<(), BindingError> {
match self.configured.get(device_key) {
Some(ConfiguredDevice {
mode: ConfiguredDeviceMode::Offline,
..
}) => Err(BindingError::ConfiguredDeviceOffline {
device_key: device_key.clone(),
}),
Some(ConfiguredDevice {
mode: ConfiguredDeviceMode::Managed,
..
})
| None => Ok(()),
}
}
}
#[derive(Debug, Default, Resource, Reflect)]
#[reflect(Resource)]
pub struct BindingEntities {
by_role: HashMap<RoleKey, Entity>,
}
impl BindingEntities {
#[must_use]
pub fn entity(&self, role: &RoleKey) -> BindingEntityLookup {
self.by_role
.get(role)
.map_or(BindingEntityLookup::Unregistered, |entity| {
BindingEntityLookup::Registered(*entity)
})
}
#[must_use]
pub fn count(&self) -> usize { self.by_role.len() }
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum BindingEntityLookup {
Unregistered,
Registered(Entity),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Component, Reflect)]
#[relationship(relationship_target = ResolvedBindings)]
#[reflect(Component, PartialEq)]
pub struct ResolvedToDevice(Entity);
impl ResolvedToDevice {
pub(crate) const fn new(device: Entity) -> Self { Self(device) }
#[must_use]
pub const fn device(self) -> Entity { self.0 }
}
#[derive(Debug, Component, Reflect)]
#[relationship_target(relationship = ResolvedToDevice)]
#[reflect(Component)]
pub struct ResolvedBindings(Vec<Entity>);
#[derive(Debug, Default, Resource)]
pub(crate) struct BindingTransitionBatch {
transitions: Vec<BindingTransition>,
}
impl BindingTransitionBatch {
pub(crate) fn transitions(&self) -> &[BindingTransition] { &self.transitions }
pub(crate) fn clear(&mut self) { self.transitions.clear(); }
}
pub(crate) fn drain_binding_transitions(
mut bindings: ResMut<Bindings>,
mut binding_transition_batch: ResMut<BindingTransitionBatch>,
) {
if bindings.has_pending_transitions() {
binding_transition_batch.transitions = bindings.take_pending_transitions().into();
} else if !binding_transition_batch.transitions.is_empty() {
binding_transition_batch.clear();
}
}
pub(crate) fn project_binding_entities(
mut commands: Commands,
binding_transition_batch: Res<BindingTransitionBatch>,
bindings: Res<Bindings>,
mut binding_entities: ResMut<BindingEntities>,
mut mirrors: Query<(&mut RecoveryPolicy, &mut RoleState), With<RoleKey>>,
live_entities: Query<()>,
) {
binding_entities.by_role.retain(|role, entity| {
let Ok(binding) = bindings.binding(role) else {
return true;
};
let Ok((mut recovery_policy, mut role_state)) = mirrors.get_mut(*entity) else {
if live_entities.get(*entity).is_ok() {
commands
.entity(*entity)
.insert((role.clone(), binding.recovery, binding.state));
return true;
}
return false;
};
if *recovery_policy != binding.recovery {
*recovery_policy = binding.recovery;
}
if *role_state != binding.state {
*role_state = binding.state;
}
true
});
for binding_transition in binding_transition_batch.transitions() {
match binding_transition {
BindingTransition::Registered { role, .. } => {
let Ok(binding) = bindings.binding(role) else {
continue;
};
let entity = commands
.spawn((role.clone(), binding.recovery, binding.state))
.id();
binding_entities.by_role.insert(role.clone(), entity);
},
BindingTransition::Replaced { .. } => {},
BindingTransition::Retired { role, endpoint, .. } => {
if let Some(entity) = binding_entities.by_role.remove(role) {
commands.entity(entity).despawn();
}
commands.trigger(BindingRetired {
role: role.clone(),
endpoint: endpoint.clone(),
});
},
}
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum HardwareInventoryError {
#[error("device `{device_key:?}` is not configured")]
DeviceNotConfigured {
device_key: DeviceKey,
},
}
#[cfg(test)]
#[allow(
clippy::expect_used,
clippy::panic,
reason = "tests should panic on unexpected values"
)]
mod tests {
use std::any::TypeId;
use std::error::Error;
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::sync::Mutex;
use bevy::app::App;
use bevy::app::Update;
use bevy::ecs::change_detection::DetectChanges;
use bevy::ecs::entity::Entity;
use bevy::ecs::observer::On;
use bevy::ecs::reflect::AppTypeRegistry;
use bevy::ecs::reflect::ReflectComponent;
use bevy::ecs::relationship::Relationship;
use bevy::ecs::relationship::RelationshipTarget;
use bevy::ecs::schedule::IntoScheduleConfigs;
use bevy::prelude::Component;
use bevy::prelude::Reflect;
use bevy::prelude::Res;
use bevy::prelude::ResMut;
use bevy::prelude::Resource;
use bevy::prelude::World;
use super::ApplyDeadline;
use super::AvailableConfiguration;
use super::Binding;
use super::BindingCapacityError;
use super::BindingEntities;
use super::BindingEntityLookup;
use super::BindingError;
use super::BindingTransition;
use super::BindingTransitionBatch;
use super::BindingTransitionSequence;
use super::Bindings;
use super::ConfiguredDevice;
use super::ConfiguredDeviceConnection;
use super::ConfiguredDeviceMode;
use super::HardwareInventory;
use super::RequestedConfiguration;
use super::ResolvedBindings;
use super::ResolvedToDevice;
use super::RetirementOutcome;
use super::RoleView;
use super::WaitingRole;
use super::WaitingWork;
use super::drain_binding_transitions;
use super::project_binding_entities;
use crate::ApplyPermit;
use crate::AttemptId;
use crate::AttemptOutcome;
use crate::AttemptProgress;
use crate::BindingRetired;
use crate::CaptureOutcome;
use crate::DeviceAccessError;
use crate::DeviceEndpoint;
use crate::DeviceIdSource;
use crate::DeviceKey;
use crate::DeviceKind;
use crate::DeviceRevisionLookup;
use crate::DriverContractError;
use crate::EndpointDriver;
use crate::EndpointId;
use crate::LastKnownGoodConfiguration;
use crate::OnAbort;
use crate::OnSessionLoss;
use crate::PartName;
use crate::RecoveryPolicy;
use crate::RetryOn;
use crate::RiggingPlugin;
use crate::RoleKey;
use crate::RoleState;
use crate::reconcile::FrameClockReading;
use crate::registration::DriverId;
use crate::registration::Drivers;
use crate::scheme::AuthoredId;
#[derive(Component, Reflect)]
struct TestConfiguration(u8);
struct RecordingDriver {
applied_configurations: Arc<Mutex<Vec<u8>>>,
}
impl EndpointDriver for RecordingDriver {
type Configuration = TestConfiguration;
fn capture(
&mut self,
_: &mut World,
_: &DeviceEndpoint,
) -> CaptureOutcome<Self::Configuration> {
CaptureOutcome::Read(TestConfiguration(7))
}
fn start_apply(
&mut self,
_: &mut World,
_: &DeviceEndpoint,
configuration: &Self::Configuration,
_: AttemptId,
_: ApplyPermit,
) {
self.applied_configurations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(configuration.0);
}
fn poll(&mut self, _: &mut World, _: AttemptId) -> AttemptProgress {
AttemptProgress::Pending
}
}
#[derive(Debug, Default, PartialEq, Eq)]
struct DriverCallLog {
captures: usize,
applied_configurations: Vec<u8>,
polls: usize,
}
struct CallCountingDriver {
driver_call_log: Arc<Mutex<DriverCallLog>>,
}
impl EndpointDriver for CallCountingDriver {
type Configuration = TestConfiguration;
fn capture(
&mut self,
_: &mut World,
_: &DeviceEndpoint,
) -> CaptureOutcome<Self::Configuration> {
self.driver_call_log
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.captures += 1;
CaptureOutcome::Read(TestConfiguration(7))
}
fn start_apply(
&mut self,
_: &mut World,
_: &DeviceEndpoint,
configuration: &Self::Configuration,
_: AttemptId,
_: ApplyPermit,
) {
self.driver_call_log
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.applied_configurations
.push(configuration.0);
}
fn poll(&mut self, _: &mut World, _: AttemptId) -> AttemptProgress {
self.driver_call_log
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.polls += 1;
AttemptProgress::Pending
}
}
#[derive(Component, Reflect)]
struct MismatchedConfiguration;
struct MismatchedDriver;
impl EndpointDriver for MismatchedDriver {
type Configuration = MismatchedConfiguration;
fn capture(
&mut self,
_: &mut World,
_: &DeviceEndpoint,
) -> CaptureOutcome<Self::Configuration> {
CaptureOutcome::NotReadable
}
fn start_apply(
&mut self,
_: &mut World,
_: &DeviceEndpoint,
_: &Self::Configuration,
_: AttemptId,
_: ApplyPermit,
) {
}
fn poll(&mut self, _: &mut World, _: AttemptId) -> AttemptProgress {
AttemptProgress::Pending
}
}
#[test]
fn duplicate_role_and_endpoint_registration_preserve_the_first_binding()
-> Result<(), Box<dyn Error>> {
let endpoint = display_endpoint("studio-display")?;
let first_role = RoleKey::new("primary-window")?;
let second_role = RoleKey::new("secondary-window")?;
let mut bindings = Bindings::default();
bindings.register(binding(first_role.clone(), endpoint.clone()))?;
assert!(matches!(
bindings.register(binding(first_role.clone(), display_endpoint("other-display")?)),
Err(BindingError::RoleAlreadyBound { role }) if role == first_role
));
assert!(matches!(
bindings.register(binding(second_role, endpoint)),
Err(BindingError::EndpointAlreadyOwned { owner, .. }) if owner == first_role
));
assert_eq!(
bindings.roles_for(&device_key("studio-display")?).count(),
1
);
assert!(bindings.binding(&first_role).is_ok());
Ok(())
}
#[test]
fn failed_replace_keeps_each_existing_reverse_index() -> Result<(), Box<dyn Error>> {
let first_role = RoleKey::new("primary-window")?;
let second_role = RoleKey::new("secondary-window")?;
let first_endpoint = display_endpoint("studio-display")?;
let second_endpoint = display_endpoint("edit-display")?;
let mut bindings = Bindings::default();
bindings.register(binding(first_role.clone(), first_endpoint.clone()))?;
bindings.register(binding(second_role.clone(), second_endpoint.clone()))?;
assert!(matches!(
bindings.replace(binding(first_role.clone(), second_endpoint.clone())),
Err(BindingError::EndpointAlreadyOwned { owner, .. }) if owner == second_role
));
assert_eq!(bindings.binding(&first_role)?.endpoint, first_endpoint);
assert_eq!(bindings.binding(&second_role)?.endpoint, second_endpoint);
assert_eq!(
bindings.roles_for(&device_key("studio-display")?).count(),
1
);
assert_eq!(bindings.roles_for(&device_key("edit-display")?).count(), 1);
Ok(())
}
#[test]
fn successful_replace_releases_only_its_old_endpoint() -> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let old_endpoint = display_endpoint("studio-display")?;
let new_endpoint = display_endpoint("edit-display")?;
let mut bindings = Bindings::default();
bindings.register(binding(role.clone(), old_endpoint.clone()))?;
let displaced = bindings.replace(binding(role.clone(), new_endpoint.clone()))?;
assert_eq!(displaced.endpoint, old_endpoint);
assert_eq!(bindings.binding(&role)?.endpoint, new_endpoint);
assert_eq!(
bindings.roles_for(&device_key("studio-display")?).count(),
0
);
assert_eq!(bindings.roles_for(&device_key("edit-display")?).count(), 1);
Ok(())
}
#[test]
fn retirement_is_idempotent_and_removes_every_index() -> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let endpoint = display_endpoint("studio-display")?;
let device_key = endpoint.device.clone();
let mut bindings = Bindings::default();
bindings.register(binding(role.clone(), endpoint))?;
let retirement = bindings.retire(&role)?;
assert!(matches!(
retirement,
RetirementOutcome::Retired(Binding {
state: RoleState::Retired,
..
})
));
assert!(matches!(
bindings.retire(&role)?,
RetirementOutcome::AlreadyUnbound
));
assert!(matches!(
bindings.binding(&role),
Err(BindingError::RoleNotBound { .. })
));
assert_eq!(bindings.roles_for(&device_key).count(), 0);
Ok(())
}
#[test]
fn one_device_can_serve_several_roles_at_distinct_endpoints() -> Result<(), Box<dyn Error>> {
let device_key = device_key("control-panel")?;
let first_role = RoleKey::new("cut")?;
let second_role = RoleKey::new("fade")?;
let mut bindings = Bindings::default();
bindings.register(binding(
first_role,
DeviceEndpoint {
device: device_key.clone(),
id: EndpointId::Part(crate::PartName::new("key/1")?),
},
))?;
bindings.register(binding(
second_role,
DeviceEndpoint {
device: device_key.clone(),
id: EndpointId::Part(crate::PartName::new("key/2")?),
},
))?;
assert_eq!(bindings.roles_for(&device_key).count(), 2);
Ok(())
}
#[test]
fn transitions_are_monotonic_and_hold_only_lifecycle_metadata() -> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let mut bindings = Bindings::default();
bindings.register(binding(role.clone(), display_endpoint("studio-display")?))?;
bindings.replace(binding(role.clone(), display_endpoint("edit-display")?))?;
let _ = bindings.retire(&role)?;
let _ = bindings.retire(&role)?;
let transitions = bindings.take_pending_transitions();
let sequences = transitions
.iter()
.map(|binding_transition| match binding_transition {
BindingTransition::Registered {
sequence,
role: transition_role,
}
| BindingTransition::Replaced {
sequence,
role: transition_role,
}
| BindingTransition::Retired {
sequence,
role: transition_role,
..
} => {
assert_eq!(transition_role, &role);
sequence.0
},
})
.collect::<Vec<_>>();
assert_eq!(sequences, vec![0, 1, 2]);
Ok(())
}
#[test]
fn configured_transition_capacity_keeps_register_replace_and_retire_atomic()
-> Result<(), Box<dyn Error>> {
let first_role = RoleKey::new("primary-window")?;
let second_role = RoleKey::new("secondary-window")?;
let third_role = RoleKey::new("tertiary-window")?;
let first_endpoint = display_endpoint("studio-display")?;
let second_endpoint = display_endpoint("edit-display")?;
let third_endpoint = display_endpoint("presentation-display")?;
let replacement_endpoint = display_endpoint("replacement-display")?;
let mut bindings = Bindings::default();
let capacity = NonZeroUsize::new(2).ok_or("nonzero capacity")?;
bindings.set_pending_transition_capacity(capacity)?;
bindings.register(binding(first_role.clone(), first_endpoint.clone()))?;
bindings.register(binding(second_role.clone(), second_endpoint.clone()))?;
assert_eq!(
bindings.register(binding(third_role.clone(), third_endpoint.clone())),
Err(BindingError::PendingTransitionCapacityReached)
);
assert!(matches!(
bindings.replace(binding(first_role.clone(), replacement_endpoint.clone())),
Err(BindingError::PendingTransitionCapacityReached)
));
assert!(matches!(
bindings.retire(&first_role),
Err(BindingError::PendingTransitionCapacityReached)
));
assert_eq!(bindings.binding(&first_role)?.endpoint, first_endpoint);
assert_eq!(bindings.binding(&second_role)?.endpoint, second_endpoint);
assert!(matches!(
bindings.binding(&third_role),
Err(BindingError::RoleNotBound { .. })
));
assert_eq!(
bindings.owner_by_endpoint.get(&first_endpoint),
Some(&first_role)
);
assert_eq!(
bindings.owner_by_endpoint.get(&second_endpoint),
Some(&second_role)
);
assert!(!bindings.owner_by_endpoint.contains_key(&third_endpoint));
assert!(
!bindings
.owner_by_endpoint
.contains_key(&replacement_endpoint)
);
assert_eq!(
bindings.roles_by_device.get(&first_endpoint.device),
Some(&vec![first_role.clone()])
);
assert_eq!(
bindings.roles_by_device.get(&second_endpoint.device),
Some(&vec![second_role])
);
assert!(
!bindings
.roles_by_device
.contains_key(&third_endpoint.device)
);
assert!(
!bindings
.roles_by_device
.contains_key(&replacement_endpoint.device)
);
assert_eq!(
bindings.set_pending_transition_capacity(NonZeroUsize::MIN),
Err(BindingCapacityError::BelowPendingCount {
capacity: NonZeroUsize::MIN,
pending: 2,
})
);
Ok(())
}
#[test]
fn device_readdress_moves_all_endpoint_parts_together() -> Result<(), Box<dyn Error>> {
let saved = device_key("saved-camera")?;
let adopted = device_key("adopted-camera")?;
let first_role = RoleKey::new("camera/first-clone")?;
let second_role = RoleKey::new("camera/second-clone")?;
let first_part = EndpointId::Part(PartName::new("tool/1")?);
let second_part = EndpointId::Part(PartName::new("tool/2")?);
let mut bindings = Bindings::default();
bindings.register(binding(
first_role.clone(),
DeviceEndpoint {
device: saved.clone(),
id: first_part.clone(),
},
))?;
bindings.register(binding(
second_role.clone(),
DeviceEndpoint {
device: saved.clone(),
id: second_part.clone(),
},
))?;
bindings.validate_device_readdress(&saved, &adopted)?;
bindings.readdress_device(&saved, adopted.clone())?;
assert_eq!(
bindings.binding(&first_role)?.endpoint,
DeviceEndpoint {
device: adopted.clone(),
id: first_part,
}
);
assert_eq!(
bindings.binding(&second_role)?.endpoint,
DeviceEndpoint {
device: adopted.clone(),
id: second_part,
}
);
assert_eq!(
bindings.roles_for(&saved).collect::<Vec<_>>(),
Vec::<&RoleKey>::new()
);
assert_eq!(bindings.roles_for(&adopted).count(), 2);
Ok(())
}
#[test]
fn device_readdress_capacity_refusal_changes_no_role_or_index() -> Result<(), Box<dyn Error>> {
let saved = device_key("saved-camera")?;
let adopted = device_key("adopted-camera")?;
let first_role = RoleKey::new("camera/first-clone")?;
let second_role = RoleKey::new("camera/second-clone")?;
let first_endpoint = DeviceEndpoint {
device: saved.clone(),
id: EndpointId::Part(PartName::new("tool/1")?),
};
let second_endpoint = DeviceEndpoint {
device: saved.clone(),
id: EndpointId::Part(PartName::new("tool/2")?),
};
let mut bindings = Bindings::default();
bindings.register(binding(first_role.clone(), first_endpoint.clone()))?;
bindings.register(binding(second_role.clone(), second_endpoint.clone()))?;
bindings.set_pending_transition_capacity(
NonZeroUsize::new(3).ok_or("nonzero transition capacity")?,
)?;
assert_eq!(
bindings.validate_device_readdress(&saved, &adopted),
Err(BindingError::PendingTransitionCapacityReached)
);
assert_eq!(
bindings.readdress_device(&saved, adopted.clone()),
Err(BindingError::PendingTransitionCapacityReached)
);
assert_eq!(bindings.binding(&first_role)?.endpoint, first_endpoint);
assert_eq!(bindings.binding(&second_role)?.endpoint, second_endpoint);
assert_eq!(bindings.roles_for(&saved).count(), 2);
assert_eq!(bindings.roles_for(&adopted).count(), 0);
Ok(())
}
#[test]
fn default_transition_capacity_rejects_another_registration_without_index_mutation()
-> Result<(), Box<dyn Error>> {
let mut bindings = Bindings::default();
for index in 0..super::DEFAULT_PENDING_TRANSITION_CAPACITY {
let role = RoleKey::new(format!("default-capacity-role-{index}"))?;
let endpoint = display_endpoint(&format!("default-capacity-device-{index}"))?;
bindings.register(binding(role, endpoint))?;
}
let overflow_role = RoleKey::new("default-capacity-overflow")?;
let overflow_endpoint = display_endpoint("default-capacity-overflow-device")?;
assert_eq!(
bindings.register(binding(overflow_role.clone(), overflow_endpoint.clone())),
Err(BindingError::PendingTransitionCapacityReached)
);
assert!(matches!(
bindings.binding(&overflow_role),
Err(BindingError::RoleNotBound { .. })
));
assert!(!bindings.owner_by_endpoint.contains_key(&overflow_endpoint));
assert!(
!bindings
.roles_by_device
.contains_key(&overflow_endpoint.device)
);
assert_eq!(
bindings.pending_transitions.queue.len(),
super::DEFAULT_PENDING_TRANSITION_CAPACITY
);
Ok(())
}
#[test]
fn transition_sequence_exhaustion_keeps_all_binding_indexes_unchanged()
-> Result<(), Box<dyn Error>> {
let first_role = RoleKey::new("last-sequence-role")?;
let first_endpoint = display_endpoint("last-sequence-device")?;
let second_role = RoleKey::new("exhausted-sequence-role")?;
let second_endpoint = display_endpoint("exhausted-sequence-device")?;
let mut bindings = Bindings {
next_transition_sequence: u64::MAX - 1,
..Default::default()
};
bindings.register(binding(first_role.clone(), first_endpoint.clone()))?;
assert!(matches!(
bindings.pending_transitions.queue.front(),
Some(BindingTransition::Registered { sequence, role })
if sequence.0 == u64::MAX - 1 && role == &first_role
));
assert_eq!(bindings.next_transition_sequence, u64::MAX);
assert_eq!(
bindings.register(binding(second_role.clone(), second_endpoint.clone())),
Err(BindingError::TransitionSequenceExhausted)
);
assert_eq!(bindings.binding(&first_role)?.endpoint, first_endpoint);
assert!(matches!(
bindings.binding(&second_role),
Err(BindingError::RoleNotBound { .. })
));
assert_eq!(
bindings.owner_by_endpoint.get(&first_endpoint),
Some(&first_role)
);
assert!(!bindings.owner_by_endpoint.contains_key(&second_endpoint));
assert_eq!(
bindings.roles_by_device.get(&first_endpoint.device),
Some(&vec![first_role])
);
assert!(
!bindings
.roles_by_device
.contains_key(&second_endpoint.device)
);
assert_eq!(bindings.next_transition_sequence, u64::MAX);
Ok(())
}
#[test]
fn in_service_apply_keeps_requested_and_readback_configuration_distinct()
-> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let mut bindings = Bindings::default();
let hardware_inventory = HardwareInventory::default();
let applied_configurations = Arc::new(Mutex::new(Vec::new()));
let mut drivers = Drivers::new();
let driver = drivers.add(RecordingDriver {
applied_configurations: Arc::clone(&applied_configurations),
});
assert_eq!(driver, DriverId(0));
let mut configured_binding = binding(role.clone(), display_endpoint("studio-display")?);
configured_binding.last_known_good =
LastKnownGoodConfiguration::known(TestConfiguration(1));
bindings.register(configured_binding)?;
assert!(matches!(bindings.role_view(&role)?, RoleView::Waiting(_)));
assert!(matches!(
match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Hardware(requesting_role)) => requesting_role
.start_requested_apply(
AttemptId::default(),
ApplyPermit::restore_only(),
&hardware_inventory,
),
_ => return Err("new binding must select waiting view".into()),
},
Err(BindingError::RequestedConfigurationRequiresInServicePermit)
));
{
let apply_request = match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Hardware(requesting_role)) => requesting_role
.start_requested_apply(
AttemptId::default(),
ApplyPermit::in_service(),
&hardware_inventory,
)?,
_ => return Err("new binding must select waiting view".into()),
};
assert_eq!(
drivers.start_apply(&mut World::new(), apply_request),
Ok(())
);
}
assert_eq!(
*applied_configurations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
vec![3]
);
match bindings.role_view(&role)? {
RoleView::Applying(mut applying_role) => {
applying_role.finish(AttemptOutcome::Succeeded);
},
_ => return Err("apply request must select applying view".into()),
}
match bindings.role_view(&role)? {
RoleView::Ready(mut ready_role) => {
ready_role.record_capture(CaptureOutcome::Read(LastKnownGoodConfiguration::known(
TestConfiguration(7),
)));
},
_ => return Err("successful apply must select ready view".into()),
}
match bindings.configuration_for(&role)? {
AvailableConfiguration::LastKnownGood(configuration) => {
assert_eq!(
configuration
.as_any()
.downcast_ref::<TestConfiguration>()
.map(|test_configuration| test_configuration.0),
Some(7)
);
},
AvailableConfiguration::Requested(_) => {
return Err("safe readback must take precedence over requested intent".into());
},
}
Ok(())
}
#[test]
fn restore_only_apply_uses_last_known_good_configuration() -> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let hardware_inventory = HardwareInventory::default();
let applied_configurations = Arc::new(Mutex::new(Vec::new()));
let mut drivers = Drivers::new();
let driver = drivers.add(RecordingDriver {
applied_configurations: Arc::clone(&applied_configurations),
});
let mut configured_binding = binding(role.clone(), display_endpoint("studio-display")?);
configured_binding.driver = driver;
configured_binding.last_known_good =
LastKnownGoodConfiguration::known(TestConfiguration(7));
let mut bindings = Bindings::default();
bindings.register(configured_binding)?;
bindings.set_waiting_work(&role, WaitingWork::RestorationOwed);
assert!(matches!(
match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Restoration(restoring_role)) => restoring_role
.start_last_known_good_restore(
AttemptId::default(),
ApplyPermit::in_service(),
&hardware_inventory,
),
_ => return Err("registered binding must select waiting view".into()),
},
Err(BindingError::LastKnownGoodConfigurationRequiresRestoreOnlyPermit)
));
let apply_request = match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Restoration(restoring_role)) => restoring_role
.start_last_known_good_restore(
AttemptId::default(),
ApplyPermit::restore_only(),
&hardware_inventory,
)?,
_ => return Err("restore authorization failure must retain waiting state".into()),
};
assert_eq!(
drivers.start_apply(&mut World::new(), apply_request),
Ok(())
);
assert_eq!(
*applied_configurations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
vec![7]
);
assert!(matches!(bindings.role_view(&role)?, RoleView::Applying(_)));
Ok(())
}
#[test]
fn dropped_start_apply_request_leaves_the_binding_waiting() -> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let hardware_inventory = HardwareInventory::default();
let mut bindings = Bindings::default();
bindings.register(binding(role.clone(), display_endpoint("studio-display")?))?;
let _ = match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Hardware(requesting_role)) => requesting_role
.start_requested_apply(
AttemptId::default(),
ApplyPermit::in_service(),
&hardware_inventory,
)?,
_ => return Err("registered binding must select waiting view".into()),
};
assert!(matches!(bindings.role_view(&role)?, RoleView::Waiting(_)));
Ok(())
}
#[test]
fn unregistered_driver_dispatch_leaves_the_binding_waiting() -> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let hardware_inventory = HardwareInventory::default();
let mut bindings = Bindings::default();
bindings.register(binding(role.clone(), display_endpoint("studio-display")?))?;
let apply_request = match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Hardware(requesting_role)) => requesting_role
.start_requested_apply(
AttemptId::default(),
ApplyPermit::in_service(),
&hardware_inventory,
)?,
_ => return Err("registered binding must select waiting view".into()),
};
let start_apply_result = Drivers::new().start_apply(&mut World::new(), apply_request);
assert!(matches!(
start_apply_result,
Err(DriverContractError::DriverNotRegistered { .. })
));
assert!(matches!(bindings.role_view(&role)?, RoleView::Waiting(_)));
Ok(())
}
#[test]
fn type_mismatch_dispatch_leaves_the_binding_waiting() -> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let hardware_inventory = HardwareInventory::default();
let mut bindings = Bindings::default();
let mut drivers = Drivers::new();
let driver = drivers.add(MismatchedDriver);
let mut configured_binding = binding(role.clone(), display_endpoint("studio-display")?);
configured_binding.driver = driver;
bindings.register(configured_binding)?;
let apply_request = match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Hardware(requesting_role)) => requesting_role
.start_requested_apply(
AttemptId::default(),
ApplyPermit::in_service(),
&hardware_inventory,
)?,
_ => return Err("registered binding must select waiting view".into()),
};
let start_apply_result = drivers.start_apply(&mut World::new(), apply_request);
assert!(matches!(
start_apply_result,
Err(DriverContractError::ConfigurationTypeMismatch { .. })
));
assert!(matches!(bindings.role_view(&role)?, RoleView::Waiting(_)));
Ok(())
}
#[test]
fn substituted_apply_returns_the_role_to_ready_for_safe_readback() -> Result<(), Box<dyn Error>>
{
let role = RoleKey::new("primary-window")?;
let hardware_inventory = HardwareInventory::default();
let mut drivers = Drivers::new();
let driver = drivers.add(RecordingDriver {
applied_configurations: Arc::new(Mutex::new(Vec::new())),
});
let mut configured_binding = binding(role.clone(), display_endpoint("studio-display")?);
configured_binding.driver = driver;
let mut bindings = Bindings::default();
bindings.register(configured_binding)?;
let apply_request = match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Hardware(requesting_role)) => requesting_role
.start_requested_apply(
AttemptId::default(),
ApplyPermit::in_service(),
&hardware_inventory,
)?,
_ => return Err("registered binding must select waiting view".into()),
};
drivers.start_apply(&mut World::new(), apply_request)?;
match bindings.role_view(&role)? {
RoleView::Applying(mut applying_role) => {
applying_role.finish(AttemptOutcome::Substituted);
},
_ => return Err("dispatched apply must select applying view".into()),
}
assert!(matches!(bindings.role_view(&role)?, RoleView::Ready(_)));
Ok(())
}
#[test]
fn aborting_an_dispatched_apply_returns_the_role_to_waiting() -> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let hardware_inventory = HardwareInventory::default();
let mut drivers = Drivers::new();
let driver = drivers.add(RecordingDriver {
applied_configurations: Arc::new(Mutex::new(Vec::new())),
});
let mut configured_binding = binding(role.clone(), display_endpoint("studio-display")?);
configured_binding.driver = driver;
let mut bindings = Bindings::default();
bindings.register(configured_binding)?;
let apply_request = match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Hardware(requesting_role)) => requesting_role
.start_requested_apply(
AttemptId::default(),
ApplyPermit::in_service(),
&hardware_inventory,
)?,
_ => return Err("registered binding must select waiting view".into()),
};
drivers.start_apply(&mut World::new(), apply_request)?;
match bindings.role_view(&role)? {
RoleView::Applying(mut applying_role) => applying_role.abort(),
_ => return Err("dispatched apply must select applying view".into()),
}
assert!(matches!(bindings.role_view(&role)?, RoleView::Waiting(_)));
Ok(())
}
#[test]
fn a_completed_restoration_settles_the_debt_and_a_failed_one_keeps_it()
-> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let hardware_inventory = HardwareInventory::default();
let mut drivers = Drivers::new();
let driver = drivers.add(RecordingDriver {
applied_configurations: Arc::new(Mutex::new(Vec::new())),
});
let mut configured_binding = binding(role.clone(), display_endpoint("studio-display")?);
configured_binding.driver = driver;
configured_binding.last_known_good =
LastKnownGoodConfiguration::known(TestConfiguration(7));
let mut bindings = Bindings::default();
bindings.register(configured_binding)?;
bindings.set_waiting_work(&role, WaitingWork::RestorationOwed);
let restore_request = match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Restoration(restoring_role)) => restoring_role
.start_last_known_good_restore(
AttemptId::default(),
ApplyPermit::restore_only(),
&hardware_inventory,
)?,
_ => return Err("a role owing a restoration selects the restoring view".into()),
};
drivers.start_apply(&mut World::new(), restore_request)?;
match bindings.role_view(&role)? {
RoleView::Applying(mut applying_role) => {
applying_role.finish(AttemptOutcome::Failed(DeviceAccessError::Contended {
detail: String::from("another owner holds the display"),
}));
},
_ => return Err("a dispatched restore selects the applying view".into()),
}
assert_eq!(bindings.waiting_work(&role), WaitingWork::RestorationOwed);
let retried_request = match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Restoration(restoring_role)) => restoring_role
.start_last_known_good_restore(
AttemptId::default(),
ApplyPermit::restore_only(),
&hardware_inventory,
)?,
_ => return Err("a failed restore leaves the role owing one".into()),
};
drivers.start_apply(&mut World::new(), retried_request)?;
match bindings.role_view(&role)? {
RoleView::Applying(mut applying_role) => {
applying_role.finish(AttemptOutcome::Succeeded);
},
_ => return Err("a dispatched restore selects the applying view".into()),
}
assert_eq!(bindings.waiting_work(&role), WaitingWork::Nothing);
assert!(matches!(bindings.role_view(&role)?, RoleView::Ready(_)));
Ok(())
}
#[test]
fn an_ordinary_apply_completing_under_an_owed_restoration_leaves_the_debt_owed()
-> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let hardware_inventory = HardwareInventory::default();
let mut drivers = Drivers::new();
let driver = drivers.add(RecordingDriver {
applied_configurations: Arc::new(Mutex::new(Vec::new())),
});
let mut configured_binding = binding(role.clone(), display_endpoint("studio-display")?);
configured_binding.driver = driver;
configured_binding.last_known_good =
LastKnownGoodConfiguration::known(TestConfiguration(7));
let mut bindings = Bindings::default();
bindings.register(configured_binding)?;
let apply_request = match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Hardware(requesting_role)) => requesting_role
.start_requested_apply(
AttemptId::default(),
ApplyPermit::in_service(),
&hardware_inventory,
)?,
_ => return Err("a role owing nothing selects the requesting view".into()),
};
drivers.start_apply(&mut World::new(), apply_request)?;
bindings.set_waiting_work(&role, WaitingWork::RestorationOwed);
match bindings.role_view(&role)? {
RoleView::Applying(mut applying_role) => {
applying_role.finish(AttemptOutcome::Succeeded);
},
_ => return Err("a dispatched requested apply selects the applying view".into()),
}
assert_eq!(bindings.waiting_work(&role), WaitingWork::RestorationOwed);
Ok(())
}
#[test]
fn read_failure_retains_prior_value_and_not_readable_stops_future_requests()
-> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let mut bindings = Bindings::default();
let hardware_inventory = HardwareInventory::default();
let mut drivers = Drivers::new();
let driver = drivers.add(RecordingDriver {
applied_configurations: Arc::new(Mutex::new(Vec::new())),
});
assert_eq!(driver, DriverId(0));
let mut configured_binding = binding(role.clone(), display_endpoint("studio-display")?);
configured_binding.state = RoleState::Ready;
configured_binding.last_known_good =
LastKnownGoodConfiguration::known(TestConfiguration(7));
bindings.register(configured_binding)?;
match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Hardware(requesting_role)) => {
let apply_request = requesting_role.start_requested_apply(
AttemptId::default(),
ApplyPermit::in_service(),
&hardware_inventory,
)?;
assert_eq!(
drivers.start_apply(&mut World::new(), apply_request),
Ok(())
);
},
_ => return Err("registration resets role state to waiting".into()),
}
match bindings.role_view(&role)? {
RoleView::Applying(mut applying_role) => {
applying_role.finish(AttemptOutcome::Succeeded);
},
_ => return Err("requested operation must select applying view".into()),
}
match bindings.role_view(&role)? {
RoleView::Ready(mut ready_role) => {
ready_role.record_capture(CaptureOutcome::ReadFailed(DeviceAccessError::Absent {
detail: String::from("test departure"),
}));
ready_role.record_capture(CaptureOutcome::NotReadable);
},
_ => return Err("successful operation must select ready view".into()),
}
match bindings.configuration_for(&role)? {
AvailableConfiguration::LastKnownGood(configuration) => assert_eq!(
configuration
.as_any()
.downcast_ref::<TestConfiguration>()
.map(|test_configuration| test_configuration.0),
Some(7)
),
AvailableConfiguration::Requested(_) => {
return Err("read failure must retain prior known value".into());
},
}
match bindings.role_view(&role)? {
RoleView::Ready(ready_role) => assert!(matches!(
ready_role.capture_request(&hardware_inventory),
Err(BindingError::ConfigurationNotReadable { .. })
)),
_ => return Err("readability test requires ready view".into()),
}
Ok(())
}
#[test]
fn stored_waiting_work_selects_the_only_request_a_waiting_role_is_owed()
-> Result<(), Box<dyn Error>> {
let owing_nothing = RoleKey::new("primary-window")?;
let owing_restoration = RoleKey::new("secondary-window")?;
let hardware_inventory = HardwareInventory::default();
let mut bindings = Bindings::default();
bindings.register(binding(
owing_nothing.clone(),
display_endpoint("studio-display")?,
))?;
let mut restoring_binding =
binding(owing_restoration.clone(), display_endpoint("edit-display")?);
restoring_binding.last_known_good = LastKnownGoodConfiguration::known(TestConfiguration(7));
bindings.register(restoring_binding)?;
bindings.set_waiting_work(&owing_restoration, WaitingWork::RestorationOwed);
match bindings.role_view(&owing_nothing)? {
RoleView::Waiting(WaitingRole::Hardware(requesting_role)) => {
requesting_role.start_requested_apply(
AttemptId::default(),
ApplyPermit::in_service(),
&hardware_inventory,
)?;
},
_ => return Err("a role owing nothing waits for hardware".into()),
}
match bindings.role_view(&owing_restoration)? {
RoleView::Waiting(WaitingRole::Restoration(restoring_role)) => {
restoring_role.start_last_known_good_restore(
AttemptId::default(),
ApplyPermit::restore_only(),
&hardware_inventory,
)?;
},
_ => return Err("a role owing a restoration waits for that restoration".into()),
}
Ok(())
}
#[test]
fn offline_waiting_role_cannot_mint_apply_requests() -> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let endpoint = display_endpoint("studio-display")?;
let driver_call_log = Arc::new(Mutex::new(DriverCallLog::default()));
let mut drivers = Drivers::new();
let driver = drivers.add(CallCountingDriver {
driver_call_log: Arc::clone(&driver_call_log),
});
let mut bindings = Bindings::default();
let mut configured_binding = binding(role.clone(), endpoint.clone());
configured_binding.driver = driver;
bindings.register(configured_binding)?;
let mut hardware_inventory = HardwareInventory::default();
hardware_inventory.configure(ConfiguredDevice {
key: endpoint.device.clone(),
mode: ConfiguredDeviceMode::Offline,
});
match bindings.configuration_for(&role)? {
AvailableConfiguration::Requested(configuration) => assert_eq!(
configuration
.as_any()
.downcast_ref::<TestConfiguration>()
.map(|test_configuration| test_configuration.0),
Some(3)
),
AvailableConfiguration::LastKnownGood(_) => {
return Err("no safe readback has established a configuration".into());
},
}
assert_eq!(
hardware_inventory.connection(&endpoint.device)?,
ConfiguredDeviceConnection::NotObserved
);
hardware_inventory.set_connection(&endpoint.device, ConfiguredDeviceConnection::Present)?;
assert_eq!(
hardware_inventory.connection(&endpoint.device)?,
ConfiguredDeviceConnection::Present
);
match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Hardware(requesting_role)) => assert!(matches!(
requesting_role.start_requested_apply(
AttemptId::default(),
ApplyPermit::in_service(),
&hardware_inventory,
),
Err(BindingError::ConfiguredDeviceOffline { .. })
)),
_ => return Err("registered offline binding must remain waiting".into()),
}
assert!(matches!(bindings.role_view(&role)?, RoleView::Waiting(_)));
bindings.set_waiting_work(&role, WaitingWork::RestorationOwed);
match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Restoration(restoring_role)) => assert!(matches!(
restoring_role.start_last_known_good_restore(
AttemptId::default(),
ApplyPermit::restore_only(),
&hardware_inventory,
),
Err(BindingError::ConfiguredDeviceOffline { .. })
)),
_ => return Err("offline requested-apply refusal must retain waiting state".into()),
}
assert!(matches!(bindings.role_view(&role)?, RoleView::Waiting(_)));
assert_eq!(
*driver_call_log
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
DriverCallLog::default()
);
Ok(())
}
#[test]
fn offline_inventory_blocks_ready_capture_and_applying_poll_requests()
-> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let endpoint = display_endpoint("studio-display")?;
let driver_call_log = Arc::new(Mutex::new(DriverCallLog::default()));
let mut drivers = Drivers::new();
let driver = drivers.add(CallCountingDriver {
driver_call_log: Arc::clone(&driver_call_log),
});
let mut bindings = Bindings::default();
let mut configured_binding = binding(role.clone(), endpoint.clone());
configured_binding.driver = driver;
bindings.register(configured_binding)?;
let mut hardware_inventory = HardwareInventory::default();
hardware_inventory.configure(ConfiguredDevice {
key: endpoint.device.clone(),
mode: ConfiguredDeviceMode::Managed,
});
let start_apply_request = match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Hardware(requesting_role)) => requesting_role
.start_requested_apply(
AttemptId::default(),
ApplyPermit::in_service(),
&hardware_inventory,
)?,
_ => return Err("managed binding must select waiting state before apply".into()),
};
drivers.start_apply(&mut World::new(), start_apply_request)?;
assert!(matches!(bindings.role_view(&role)?, RoleView::Applying(_)));
hardware_inventory.configure(ConfiguredDevice {
key: endpoint.device.clone(),
mode: ConfiguredDeviceMode::Offline,
});
match bindings.role_view(&role)? {
RoleView::Applying(applying_role) => assert!(matches!(
applying_role.poll_request(&hardware_inventory),
Err(BindingError::ConfiguredDeviceOffline { .. })
)),
_ => return Err("started apply must select applying state".into()),
}
assert!(matches!(bindings.role_view(&role)?, RoleView::Applying(_)));
assert_eq!(
*driver_call_log
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
DriverCallLog {
captures: 0,
applied_configurations: vec![3],
polls: 0,
}
);
hardware_inventory.configure(ConfiguredDevice {
key: endpoint.device.clone(),
mode: ConfiguredDeviceMode::Managed,
});
match bindings.role_view(&role)? {
RoleView::Applying(mut applying_role) => {
applying_role.finish(AttemptOutcome::Succeeded);
},
_ => return Err("applying role must remain finishable after poll refusal".into()),
}
assert!(matches!(bindings.role_view(&role)?, RoleView::Ready(_)));
hardware_inventory.configure(ConfiguredDevice {
key: endpoint.device,
mode: ConfiguredDeviceMode::Offline,
});
match bindings.role_view(&role)? {
RoleView::Ready(ready_role) => assert!(matches!(
ready_role.capture_request(&hardware_inventory),
Err(BindingError::ConfiguredDeviceOffline { .. })
)),
_ => return Err("successful apply must select ready state".into()),
}
assert!(matches!(bindings.role_view(&role)?, RoleView::Ready(_)));
assert_eq!(
*driver_call_log
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
DriverCallLog {
captures: 0,
applied_configurations: vec![3],
polls: 0,
}
);
Ok(())
}
#[test]
fn binding_inventory_and_reflected_configuration_types_register_automatically() {
let app = App::new();
let world = app.world();
let type_registry = world.resource::<AppTypeRegistry>().read();
for type_id in [
TypeId::of::<Bindings>(),
TypeId::of::<HardwareInventory>(),
TypeId::of::<Binding>(),
TypeId::of::<ConfiguredDevice>(),
] {
assert!(type_registry.contains(type_id));
}
drop(type_registry);
}
fn binding(role: RoleKey, endpoint: DeviceEndpoint) -> Binding {
Binding {
role,
endpoint,
driver: DriverId(0),
recovery: RecoveryPolicy::default(),
retry: RetryOn::NewRevision,
on_abort: OnAbort::default(),
on_loss: OnSessionLoss::default(),
state: RoleState::Ready,
requested: RequestedConfiguration::new(TestConfiguration(3)),
last_known_good: LastKnownGoodConfiguration::default(),
apply_deadline: ApplyDeadline::ProcessDefault,
}
}
fn display_endpoint(value: &str) -> Result<DeviceEndpoint, Box<dyn Error>> {
Ok(DeviceEndpoint {
device: device_key(value)?,
id: EndpointId::Whole,
})
}
fn device_key(value: &str) -> Result<DeviceKey, Box<dyn Error>> {
Ok(DeviceKey {
kind: DeviceKind::Display,
id: DeviceIdSource::Authored {
value: AuthoredId::new(value)?,
},
})
}
fn app_with_role(role: &str) -> Result<(App, RoleKey), Box<dyn Error>> {
let mut app = App::new();
app.add_plugins(RiggingPlugin);
let role = RoleKey::new(role)?;
app.world_mut()
.resource_mut::<Bindings>()
.register(test_binding(role.clone(), endpoint_named(role.as_str())?))?;
Ok((app, role))
}
fn endpoint_named(value: &str) -> Result<DeviceEndpoint, Box<dyn Error>> {
Ok(DeviceEndpoint {
device: DeviceKey {
kind: DeviceKind::Display,
id: DeviceIdSource::Authored {
value: AuthoredId::new(value)?,
},
},
id: EndpointId::Whole,
})
}
fn test_binding(role: RoleKey, endpoint: DeviceEndpoint) -> Binding {
Binding {
role,
endpoint,
driver: DriverId(0),
recovery: RecoveryPolicy::Forget,
retry: RetryOn::NewRevision,
on_abort: OnAbort::default(),
on_loss: OnSessionLoss::default(),
state: RoleState::default(),
requested: RequestedConfiguration::new(()),
last_known_good: LastKnownGoodConfiguration::default(),
apply_deadline: ApplyDeadline::ProcessDefault,
}
}
fn registered_entity(app: &App, role: &RoleKey) -> Entity {
match app.world().resource::<BindingEntities>().entity(role) {
BindingEntityLookup::Registered(entity) => entity,
BindingEntityLookup::Unregistered => {
panic!("role `{role}` has no binding entity")
},
}
}
#[derive(Default, Resource)]
struct ObservedBindingRetirements(Vec<(RoleKey, DeviceEndpoint)>);
fn observe_binding_retired(
binding_retired: On<BindingRetired>,
mut observed: ResMut<ObservedBindingRetirements>,
) {
observed.0.push((
binding_retired.role.clone(),
binding_retired.endpoint.clone(),
));
}
#[test]
fn registration_spawns_one_binding_entity_per_role_with_no_reporter_running()
-> Result<(), Box<dyn Error>> {
let (mut app, role) = app_with_role("window/main")?;
let second_role = RoleKey::new("window/inspector")?;
app.world_mut()
.resource_mut::<Bindings>()
.register(test_binding(
second_role.clone(),
endpoint_named(second_role.as_str())?,
))?;
app.update();
let binding_entities = app.world().resource::<BindingEntities>();
assert_eq!(binding_entities.count(), 2);
assert_ne!(
registered_entity(&app, &role),
registered_entity(&app, &second_role)
);
assert_eq!(
binding_entities.entity(&RoleKey::new("window/never-registered")?),
BindingEntityLookup::Unregistered
);
Ok(())
}
#[test]
fn a_binding_entity_outlives_every_frame_in_which_its_role_has_no_device()
-> Result<(), Box<dyn Error>> {
let (mut app, role) = app_with_role("window/main")?;
app.update();
let entity = registered_entity(&app, &role);
for _ in 0..4 {
app.update();
}
assert_eq!(registered_entity(&app, &role), entity);
assert!(app.world().get_entity(entity).is_ok());
assert_eq!(
app.world().get::<RoleState>(entity),
Some(&RoleState::Waiting)
);
Ok(())
}
#[test]
fn a_binding_entity_stripped_of_its_mirrors_is_repaired_and_stays_indexed()
-> Result<(), Box<dyn Error>> {
let (mut app, role) = app_with_role("window/main")?;
app.update();
let entity = registered_entity(&app, &role);
app.world_mut()
.entity_mut(entity)
.remove::<(RoleKey, RecoveryPolicy, RoleState)>();
app.update();
assert_eq!(registered_entity(&app, &role), entity);
assert_eq!(app.world().get::<RoleKey>(entity), Some(&role));
assert_eq!(
app.world().get::<RecoveryPolicy>(entity),
Some(&RecoveryPolicy::Forget)
);
assert_eq!(
app.world().get::<RoleState>(entity),
Some(&RoleState::Waiting)
);
Ok(())
}
#[test]
fn retirement_despawns_the_binding_entity_on_a_frame_with_no_reporter_completion()
-> Result<(), Box<dyn Error>> {
let (mut app, role) = app_with_role("window/main")?;
app.update();
let entity = registered_entity(&app, &role);
app.world_mut().resource_mut::<Bindings>().retire(&role)?;
app.update();
assert_eq!(
app.world().resource::<BindingEntities>().entity(&role),
BindingEntityLookup::Unregistered
);
assert!(app.world().get_entity(entity).is_err());
Ok(())
}
#[test]
fn retirement_announces_the_role_and_endpoint_when_its_queued_transition_applies()
-> Result<(), Box<dyn Error>> {
let (mut app, role) = app_with_role("window/main")?;
app.init_resource::<ObservedBindingRetirements>()
.add_observer(observe_binding_retired);
app.update();
let endpoint = app
.world()
.resource::<Bindings>()
.binding(&role)?
.endpoint
.clone();
app.world_mut().resource_mut::<Bindings>().retire(&role)?;
assert!(
app.world()
.resource::<ObservedBindingRetirements>()
.0
.is_empty()
);
app.update();
assert_eq!(
app.world().resource::<ObservedBindingRetirements>().0,
vec![(role, endpoint)]
);
Ok(())
}
#[test]
fn one_drain_moves_every_pending_transition_in_sequence_and_later_work_waits_a_frame()
-> Result<(), Box<dyn Error>> {
let (mut app, role) = app_with_role("window/main")?;
let late_role = RoleKey::new("window/late")?;
app.world_mut().resource_mut::<Bindings>().retire(&role)?;
app.init_resource::<ObservedBatches>().add_systems(
Update,
observe_batch
.after(project_binding_entities)
.before(crate::reconcile::reconcile),
);
app.update();
let sequences: Vec<u64> = app.world().resource::<ObservedBatches>().0[0]
.iter()
.map(|sequence| sequence.0)
.collect();
assert_eq!(sequences, vec![0, 1]);
assert!(
app.world_mut()
.resource_mut::<Bindings>()
.take_pending_transitions()
.is_empty()
);
app.world_mut()
.resource_mut::<Bindings>()
.register(test_binding(
late_role.clone(),
endpoint_named(late_role.as_str())?,
))?;
assert_eq!(
app.world().resource::<BindingEntities>().entity(&late_role),
BindingEntityLookup::Unregistered
);
app.update();
assert_eq!(app.world().resource::<ObservedBatches>().0[1].len(), 1);
assert!(matches!(
app.world().resource::<BindingEntities>().entity(&late_role),
BindingEntityLookup::Registered(_)
));
Ok(())
}
#[derive(Default, Resource)]
struct FramesWithChangedBindings(usize);
fn count_frames_with_changed_bindings(
bindings: Res<Bindings>,
mut frames_with_changed_bindings: ResMut<FramesWithChangedBindings>,
) {
if bindings.is_changed() {
frames_with_changed_bindings.0 += 1;
}
}
#[test]
fn a_frame_with_no_submitted_binding_operation_leaves_bindings_unchanged()
-> Result<(), Box<dyn Error>> {
let (mut app, _) = app_with_role("window/main")?;
app.init_resource::<FramesWithChangedBindings>()
.add_systems(
Update,
count_frames_with_changed_bindings.after(drain_binding_transitions),
);
app.update();
assert_eq!(app.world().resource::<FramesWithChangedBindings>().0, 1);
for _ in 0..3 {
app.update();
}
assert_eq!(app.world().resource::<FramesWithChangedBindings>().0, 1);
Ok(())
}
#[derive(Default, Resource)]
struct ObservedBatches(Vec<Vec<BindingTransitionSequence>>);
fn observe_batch(
binding_transition_batch: Res<BindingTransitionBatch>,
mut observed_batches: ResMut<ObservedBatches>,
) {
observed_batches.0.push(
binding_transition_batch
.transitions()
.iter()
.map(|binding_transition| match binding_transition {
BindingTransition::Registered { sequence, .. }
| BindingTransition::Replaced { sequence, .. }
| BindingTransition::Retired { sequence, .. } => *sequence,
})
.collect(),
);
}
#[test]
fn entity_lifecycle_attempts_and_events_observe_one_identical_ordered_batch()
-> Result<(), Box<dyn Error>> {
let (mut app, _) = app_with_role("window/main")?;
app.init_resource::<ObservedBatches>().add_systems(
Update,
(observe_batch, observe_batch, observe_batch)
.chain()
.after(project_binding_entities)
.before(crate::reconcile::reconcile),
);
app.update();
let observed_batches = app.world().resource::<ObservedBatches>();
assert_eq!(observed_batches.0.len(), 3);
assert!(
observed_batches
.0
.iter()
.all(|observed| observed == &observed_batches.0[0])
);
assert_eq!(observed_batches.0[0].len(), 1);
assert!(
app.world()
.resource::<BindingTransitionBatch>()
.transitions()
.is_empty()
);
Ok(())
}
#[test]
fn a_reflection_write_to_the_mirrored_recovery_policy_is_overwritten_next_reconcile()
-> Result<(), Box<dyn Error>> {
let (mut app, role) = app_with_role("window/main")?;
app.update();
let entity = registered_entity(&app, &role);
assert_eq!(
app.world().get::<RecoveryPolicy>(entity),
Some(&RecoveryPolicy::Forget)
);
*app.world_mut()
.get_mut::<RecoveryPolicy>(entity)
.expect("the binding entity mirrors its recovery policy") =
RecoveryPolicy::ReapplyOnReturn;
app.update();
assert_eq!(
app.world().get::<RecoveryPolicy>(entity),
Some(&RecoveryPolicy::Forget)
);
assert_eq!(
app.world().resource::<Bindings>().binding(&role)?.recovery,
RecoveryPolicy::Forget
);
Ok(())
}
#[test]
fn resolving_and_replacing_the_link_maintains_the_device_reverse_collection()
-> Result<(), Box<dyn Error>> {
let (mut app, role) = app_with_role("window/main")?;
app.update();
let entity = registered_entity(&app, &role);
let first_device = app.world_mut().spawn_empty().id();
let second_device = app.world_mut().spawn_empty().id();
app.world_mut()
.entity_mut(entity)
.insert(<ResolvedToDevice as Relationship>::from(first_device));
assert_eq!(
resolved_binding_entities(app.world(), first_device),
vec![entity]
);
app.world_mut()
.entity_mut(entity)
.insert(<ResolvedToDevice as Relationship>::from(second_device));
assert!(resolved_binding_entities(app.world(), first_device).is_empty());
assert_eq!(
resolved_binding_entities(app.world(), second_device),
vec![entity]
);
Ok(())
}
fn resolved_binding_entities(world: &World, device: Entity) -> Vec<Entity> {
world
.get::<ResolvedBindings>(device)
.map(|resolved_bindings| resolved_bindings.iter().collect())
.unwrap_or_default()
}
#[test]
fn despawning_a_live_device_removes_the_link_and_leaves_its_binding_entities_alive()
-> Result<(), Box<dyn Error>> {
let (mut app, role) = app_with_role("window/main")?;
app.update();
let entity = registered_entity(&app, &role);
let device = app.world_mut().spawn_empty().id();
app.world_mut()
.entity_mut(entity)
.insert(<ResolvedToDevice as Relationship>::from(device));
app.world_mut().entity_mut(device).despawn();
assert!(app.world().get_entity(entity).is_ok());
assert!(app.world().get::<ResolvedToDevice>(entity).is_none());
assert_eq!(
app.world().resource::<BindingEntities>().entity(&role),
BindingEntityLookup::Registered(entity)
);
assert!(app.world().resource::<Bindings>().binding(&role).is_ok());
Ok(())
}
#[test]
fn two_roles_on_one_device_share_a_reverse_collection_while_duplicates_stay_rejected()
-> Result<(), Box<dyn Error>> {
let device_key = DeviceKey {
kind: DeviceKind::Display,
id: DeviceIdSource::Authored {
value: AuthoredId::new("stream-deck")?,
},
};
let key_endpoint = DeviceEndpoint {
device: device_key.clone(),
id: EndpointId::Part(PartName::new("key/3")?),
};
let dial_endpoint = DeviceEndpoint {
device: device_key,
id: EndpointId::Part(PartName::new("dial/1")?),
};
let key_role = RoleKey::new("deck/key")?;
let dial_role = RoleKey::new("deck/dial")?;
let duplicate_role = RoleKey::new("deck/duplicate")?;
let mut app = App::new();
app.add_plugins(RiggingPlugin);
{
let mut bindings = app.world_mut().resource_mut::<Bindings>();
bindings.register(test_binding(key_role.clone(), key_endpoint.clone()))?;
bindings.register(test_binding(dial_role.clone(), dial_endpoint))?;
assert!(matches!(
bindings.register(test_binding(duplicate_role, key_endpoint)),
Err(BindingError::EndpointAlreadyOwned { .. })
));
}
app.update();
let device = app.world_mut().spawn_empty().id();
for role in [&key_role, &dial_role] {
let entity = registered_entity(&app, role);
app.world_mut()
.entity_mut(entity)
.insert(<ResolvedToDevice as Relationship>::from(device));
}
let resolved = resolved_binding_entities(app.world(), device);
assert_eq!(resolved.len(), 2);
assert!(resolved.contains(®istered_entity(&app, &key_role)));
assert!(resolved.contains(®istered_entity(&app, &dial_role)));
Ok(())
}
#[test]
fn binding_entity_components_register_reflection_metadata() {
let app = App::new();
let type_registry = app.world().resource::<AppTypeRegistry>().read();
for type_id in [
TypeId::of::<RoleKey>(),
TypeId::of::<RecoveryPolicy>(),
TypeId::of::<RoleState>(),
TypeId::of::<ResolvedToDevice>(),
TypeId::of::<ResolvedBindings>(),
] {
assert!(type_registry.contains(type_id));
assert!(
type_registry
.get_type_data::<ReflectComponent>(type_id)
.is_some()
);
}
drop(type_registry);
}
fn changed(device_revision: crate::DeviceRevisionLookup) -> crate::DeviceRevisionLookup {
match device_revision {
DeviceRevisionLookup::Retired => {
DeviceRevisionLookup::Retained(crate::DeviceRevision::default())
},
DeviceRevisionLookup::Retained(device_revision) => {
DeviceRevisionLookup::Retained(device_revision.advanced())
},
}
}
fn blocked() -> DeviceAccessError {
DeviceAccessError::Blocked {
detail: "the platform refused access".to_owned(),
}
}
fn measurable() -> FrameClockReading {
FrameClockReading::Measurable(bevy::platform::time::Instant::now())
}
fn generation_now(bindings: &Bindings, role: &RoleKey) -> super::BindingGeneration {
bindings
.generation(role)
.expect("the test registered a binding for this role")
}
#[test]
fn an_aborted_attempt_gates_its_retry_without_counting_toward_escalation()
-> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let mut bindings = Bindings::default();
let device_revision = DeviceRevisionLookup::Retained(crate::DeviceRevision::default());
bindings.register(binding(role.clone(), display_endpoint("studio-display")?))?;
bindings.record_attempt_ending(
&role,
generation_now(&bindings, &role),
AttemptOutcome::Aborted,
device_revision,
measurable(),
);
assert!(
!bindings
.retry_pacing(&role)
.permits_dispatch(device_revision, measurable())
);
assert!(
bindings
.retry_pacing(&role)
.permits_dispatch(changed(device_revision), measurable())
);
for _ in 0..2 {
bindings.record_attempt_ending(
&role,
generation_now(&bindings, &role),
AttemptOutcome::Aborted,
device_revision,
measurable(),
);
}
assert_eq!(bindings.binding(&role)?.state, RoleState::Waiting);
bindings.record_attempt_ending(
&role,
generation_now(&bindings, &role),
AttemptOutcome::Failed(blocked()),
device_revision,
measurable(),
);
let super::RetryPacing::AwaitingGate(retry_gate) = bindings.retry_pacing(&role) else {
return Err("a failed attempt under RetryOn::NewRevision must install a gate".into());
};
assert!(!retry_gate.opened(device_revision, measurable()));
assert!(retry_gate.opened(changed(device_revision), measurable()));
Ok(())
}
#[test]
fn an_interval_retry_policy_waits_on_the_clock_rather_than_on_a_new_revision()
-> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let mut bindings = Bindings::default();
let device_revision = DeviceRevisionLookup::Retained(crate::DeviceRevision::default());
let mut configured_binding = binding(role.clone(), display_endpoint("studio-display")?);
configured_binding.retry = RetryOn::Interval(std::time::Duration::from_hours(1));
bindings.register(configured_binding)?;
bindings.record_attempt_ending(
&role,
generation_now(&bindings, &role),
AttemptOutcome::Failed(blocked()),
device_revision,
measurable(),
);
assert!(
!bindings
.retry_pacing(&role)
.permits_dispatch(changed(device_revision), measurable())
);
Ok(())
}
#[test]
fn three_consecutive_failures_stop_dispatch_until_a_restart_or_a_success()
-> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let mut bindings = Bindings::default();
let mut device_revision = DeviceRevisionLookup::Retained(crate::DeviceRevision::default());
bindings.register(binding(role.clone(), display_endpoint("studio-display")?))?;
for _ in 0..2 {
bindings.record_attempt_ending(
&role,
generation_now(&bindings, &role),
AttemptOutcome::Failed(blocked()),
device_revision,
measurable(),
);
device_revision = changed(device_revision);
assert_eq!(bindings.binding(&role)?.state, RoleState::Waiting);
}
bindings.record_attempt_ending(
&role,
generation_now(&bindings, &role),
AttemptOutcome::Failed(blocked()),
device_revision,
measurable(),
);
assert_eq!(
bindings.binding(&role)?.state,
RoleState::StoppedAfterRepeatedFailures
);
assert!(matches!(
bindings.role_view(&role)?,
RoleView::StoppedAfterRepeatedFailures
));
bindings.restart_after_repeated_failures(&role)?;
assert_eq!(bindings.binding(&role)?.state, RoleState::Waiting);
assert_eq!(bindings.retry_pacing(&role), super::RetryPacing::Ready);
Ok(())
}
#[test]
fn a_stopped_role_waits_for_its_device_to_leave_and_return_before_another_attempt()
-> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let mut bindings = Bindings::default();
let device_revision = DeviceRevisionLookup::Retained(crate::DeviceRevision::default());
bindings.register(binding(role.clone(), display_endpoint("studio-display")?))?;
for _ in 0..3 {
bindings.record_attempt_ending(
&role,
generation_now(&bindings, &role),
AttemptOutcome::Failed(blocked()),
device_revision,
measurable(),
);
}
assert_eq!(
bindings.binding(&role)?.state,
RoleState::StoppedAfterRepeatedFailures
);
for _ in 0..3 {
bindings.observe_stopped_role_endpoint(&role, super::EndpointAvailability::Available);
}
assert_eq!(
bindings.binding(&role)?.state,
RoleState::StoppedAfterRepeatedFailures
);
bindings.observe_stopped_role_endpoint(&role, super::EndpointAvailability::Gone);
assert_eq!(
bindings.binding(&role)?.state,
RoleState::StoppedAfterRepeatedFailures
);
bindings.observe_stopped_role_endpoint(&role, super::EndpointAvailability::Available);
assert_eq!(bindings.binding(&role)?.state, RoleState::Waiting);
assert_eq!(bindings.retry_pacing(&role), super::RetryPacing::Ready);
bindings.record_attempt_ending(
&role,
generation_now(&bindings, &role),
AttemptOutcome::Succeeded,
device_revision,
measurable(),
);
for _ in 0..2 {
bindings.record_attempt_ending(
&role,
generation_now(&bindings, &role),
AttemptOutcome::Failed(blocked()),
device_revision,
measurable(),
);
}
assert_eq!(bindings.binding(&role)?.state, RoleState::Waiting);
Ok(())
}
#[test]
fn a_successful_attempt_clears_the_failures_counted_before_it() -> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let mut bindings = Bindings::default();
let device_revision = DeviceRevisionLookup::Retained(crate::DeviceRevision::default());
bindings.register(binding(role.clone(), display_endpoint("studio-display")?))?;
for _ in 0..2 {
bindings.record_attempt_ending(
&role,
generation_now(&bindings, &role),
AttemptOutcome::Failed(blocked()),
device_revision,
measurable(),
);
}
bindings.record_attempt_ending(
&role,
generation_now(&bindings, &role),
AttemptOutcome::Succeeded,
device_revision,
measurable(),
);
for _ in 0..2 {
bindings.record_attempt_ending(
&role,
generation_now(&bindings, &role),
AttemptOutcome::Failed(blocked()),
device_revision,
measurable(),
);
}
assert_eq!(bindings.binding(&role)?.state, RoleState::Waiting);
Ok(())
}
#[test]
fn a_restart_is_refused_for_a_role_that_was_never_stopped() -> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let mut bindings = Bindings::default();
bindings.register(binding(role.clone(), display_endpoint("studio-display")?))?;
assert!(matches!(
bindings.restart_after_repeated_failures(&role),
Err(BindingError::RoleNotStopped { .. })
));
Ok(())
}
#[test]
fn three_failed_readbacks_suspend_capture_until_one_succeeds() -> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let hardware_inventory = HardwareInventory::default();
let mut drivers = Drivers::new();
let driver = drivers.add(RecordingDriver {
applied_configurations: Arc::new(Mutex::new(Vec::new())),
});
let mut bindings = Bindings::default();
let mut configured_binding = binding(role.clone(), display_endpoint("studio-display")?);
configured_binding.driver = driver;
bindings.register(configured_binding)?;
let start_apply_request = match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Hardware(requesting_role)) => requesting_role
.start_requested_apply(
AttemptId::default(),
ApplyPermit::in_service(),
&hardware_inventory,
)?,
_ => return Err("a new binding must select the waiting view".into()),
};
drivers.start_apply(&mut World::new(), start_apply_request)?;
match bindings.role_view(&role)? {
RoleView::Applying(mut applying_role) => {
applying_role.finish(AttemptOutcome::Succeeded);
},
_ => return Err("a dispatched apply must select the applying view".into()),
}
for _ in 0..2 {
match bindings.role_view(&role)? {
RoleView::Ready(mut ready_role) => {
ready_role.record_capture(CaptureOutcome::ReadFailed(blocked()));
},
_ => return Err("a ready role must select the ready view".into()),
}
assert_eq!(
bindings.capture_dispatch(&role),
super::CaptureDispatch::Eligible
);
}
match bindings.role_view(&role)? {
RoleView::Ready(mut ready_role) => {
ready_role.record_capture(CaptureOutcome::ReadFailed(blocked()));
},
_ => return Err("a ready role must select the ready view".into()),
}
assert_eq!(
bindings.capture_dispatch(&role),
super::CaptureDispatch::SuspendedAfterRepeatedFailures
);
match bindings.role_view(&role)? {
RoleView::Ready(mut ready_role) => {
ready_role.record_capture(CaptureOutcome::Read(LastKnownGoodConfiguration::known(
TestConfiguration(7),
)));
},
_ => return Err("a ready role must select the ready view".into()),
}
assert_eq!(
bindings.capture_dispatch(&role),
super::CaptureDispatch::Eligible
);
Ok(())
}
#[test]
fn a_restore_only_permit_drives_authored_intent_only_until_a_readback_establishes_one()
-> Result<(), Box<dyn Error>> {
let role = RoleKey::new("primary-window")?;
let hardware_inventory = HardwareInventory::default();
let mut bindings = Bindings::default();
bindings.register(binding(role.clone(), display_endpoint("studio-display")?))?;
let start_apply_request = match bindings.role_view(&role)? {
RoleView::Waiting(WaitingRole::Hardware(requesting_role)) => requesting_role
.start_requested_apply(
AttemptId::default(),
ApplyPermit::restore_only(),
&hardware_inventory,
)?,
_ => return Err("a new binding must select the waiting view".into()),
};
assert_eq!(
start_apply_request.configuration_source,
super::ApplyConfigurationSource::Requested
);
Ok(())
}
}