use core::future::Future;
use core::pin::Pin;
use core::task::{Context, Poll, ready};
use std::collections::VecDeque;
use std::time::Duration;
use thiserror::Error;
use tokio_stream::wrappers::BroadcastStream;
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
use tokio_stream::{Stream, StreamExt};
use crate::protocol::{BatteryStatus, BoardEvent, Command, PieceStatus};
#[cfg(doc)]
use crate::transport;
use crate::transport::{
DecodeNotificationError, DecodedNotification, MAX_NOTIFICATION_LEN, Notification,
NotificationSource, decode_notification,
};
pub trait TokioTransport: Send + 'static {
type Error: Send + 'static;
fn subscribe(
&mut self,
source: NotificationSource,
) -> impl Future<Output = Result<(), Self::Error>> + Send + '_;
fn unsubscribe(
&mut self,
_source: NotificationSource,
) -> impl Future<Output = Result<(), Self::Error>> + Send + '_ {
async { Ok(()) }
}
fn write_command<'a>(
&'a mut self,
command: &'a Command,
) -> impl Future<Output = Result<(), Self::Error>> + Send + 'a;
fn next_notification<'a>(
&'a mut self,
buffer: &'a mut [u8],
) -> impl Future<Output = Result<Notification<'a>, Self::Error>> + Send + 'a;
fn close(&mut self) -> impl Future<Output = Result<(), Self::Error>> + Send + '_ {
async { Ok(()) }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ActorConfig {
pub command_capacity: usize,
pub event_capacity: usize,
pub query_capacity: usize,
pub request_timeout: Duration,
}
impl Default for ActorConfig {
fn default() -> Self {
Self {
command_capacity: 32,
event_capacity: 64,
query_capacity: 32,
request_timeout: Duration::from_secs(5),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
pub enum SpawnError {
#[error("command channel capacity must be greater than zero")]
ZeroCommandCapacity,
#[error("event channel capacity must be greater than zero")]
ZeroEventCapacity,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum LifecycleState {
Starting,
Running,
ShuttingDown,
Stopped,
Faulted,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
pub enum HandleError {
#[error("the board actor has stopped")]
ActorStopped,
#[error("the board actor is shutting down")]
ShuttingDown,
#[error("the transport operation failed; inspect BoardTask for the underlying error")]
TransportFailed,
#[error("the board request timed out")]
RequestTimedOut,
#[error("the pending query queue is full")]
QueryQueueFull,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
pub enum EventStreamError {
#[error("event subscriber lagged and missed {0} events")]
Lagged(u64),
#[error(transparent)]
Decode(#[from] DecodeNotificationError),
#[error("the board event stream is closed")]
Closed,
}
#[derive(Debug, Error)]
pub enum ActorError<E> {
#[error("transport error: {0}")]
Transport(E),
}
#[derive(Clone)]
pub struct BoardHandle {
request_tx: ::tokio::sync::mpsc::Sender<Message>,
lifecycle_rx: ::tokio::sync::watch::Receiver<LifecycleState>,
}
impl BoardHandle {
pub fn lifecycle(&self) -> LifecycleState {
*self.lifecycle_rx.borrow()
}
pub fn subscribe_lifecycle(&self) -> ::tokio::sync::watch::Receiver<LifecycleState> {
self.lifecycle_rx.clone()
}
pub async fn send(&self, command: Command) -> Result<(), HandleError> {
trace_event!(
command_len = command.bytes().len(),
write_kind = ?command.write_kind(),
"submitting command to board actor"
);
let (reply_tx, reply_rx) = ::tokio::sync::oneshot::channel();
self
.send_message(Message::Send {
command,
reply: reply_tx,
})
.await?;
receive_reply(reply_rx).await?
}
pub async fn battery_status(&self) -> Result<BatteryStatus, HandleError> {
debug_event!(query = "battery_status", "submitting board query");
let (reply_tx, reply_rx) = ::tokio::sync::oneshot::channel();
self
.send_message(Message::Query(QueryRequest::Battery(reply_tx)))
.await?;
receive_reply(reply_rx).await?
}
pub async fn piece_status(&self) -> Result<PieceStatus, HandleError> {
debug_event!(query = "piece_status", "submitting board query");
let (reply_tx, reply_rx) = ::tokio::sync::oneshot::channel();
self
.send_message(Message::Query(QueryRequest::Pieces(reply_tx)))
.await?;
receive_reply(reply_rx).await?
}
pub async fn subscribe_events(&self) -> Result<EventStream, HandleError> {
debug_event!("subscribing to board actor events");
let (reply_tx, reply_rx) = ::tokio::sync::oneshot::channel();
self
.send_message(Message::SubscribeEvents { reply: reply_tx })
.await?;
receive_reply(reply_rx).await?
}
pub async fn shutdown(&self) -> Result<(), HandleError> {
info_event!("requesting graceful board actor shutdown");
let (reply_tx, reply_rx) = ::tokio::sync::oneshot::channel();
self
.send_message(Message::Shutdown { reply: reply_tx })
.await?;
receive_reply(reply_rx).await?
}
async fn send_message(&self, message: Message) -> Result<(), HandleError> {
let lifecycle = self.lifecycle();
match lifecycle {
LifecycleState::Starting | LifecycleState::Running => {}
LifecycleState::ShuttingDown => {
debug_event!(?lifecycle, "board actor rejected handle request");
return Err(HandleError::ShuttingDown);
}
LifecycleState::Stopped | LifecycleState::Faulted => {
debug_event!(?lifecycle, "board actor rejected handle request");
return Err(HandleError::ActorStopped);
}
}
self.request_tx.send(message).await.map_err(|_| {
debug_event!("board actor request channel is closed");
HandleError::ActorStopped
})
}
}
pub struct EventStream {
inner: BroadcastStream<Result<BoardEvent, DecodeNotificationError>>,
}
impl EventStream {
fn new(
receiver: ::tokio::sync::broadcast::Receiver<Result<BoardEvent, DecodeNotificationError>>,
) -> Self {
Self {
inner: BroadcastStream::new(receiver),
}
}
pub async fn recv(&mut self) -> Result<BoardEvent, EventStreamError> {
self.next().await.unwrap_or(Err(EventStreamError::Closed))
}
}
impl Stream for EventStream {
type Item = Result<BoardEvent, EventStreamError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let item = ready!(Pin::new(&mut self.inner).poll_next(cx));
Poll::Ready(match item {
Some(Ok(Ok(event))) => Some(Ok(event)),
Some(Ok(Err(error))) => Some(Err(EventStreamError::Decode(error))),
Some(Err(BroadcastStreamRecvError::Lagged(count))) => {
warn_event!(missed_events = count, "board event subscriber lagged");
Some(Err(EventStreamError::Lagged(count)))
}
None => {
debug_event!("board event stream closed");
None
}
})
}
}
pub struct ActorExit<T: TokioTransport> {
transport: T,
result: Result<(), ActorError<T::Error>>,
}
impl<T: TokioTransport> ActorExit<T> {
pub const fn transport(&self) -> &T {
&self.transport
}
pub fn into_transport(self) -> T {
self.transport
}
pub fn into_parts(self) -> (T, Result<(), ActorError<T::Error>>) {
(self.transport, self.result)
}
pub const fn result(&self) -> Result<(), &ActorError<T::Error>> {
match &self.result {
Ok(()) => Ok(()),
Err(error) => Err(error),
}
}
pub fn into_result(self) -> Result<(), ActorError<T::Error>> {
self.result
}
}
pub struct BoardTask<T: TokioTransport> {
join: ::tokio::task::JoinHandle<ActorExit<T>>,
}
impl<T: TokioTransport> BoardTask<T> {
pub fn abort(&self) {
self.join.abort();
}
pub fn is_finished(&self) -> bool {
self.join.is_finished()
}
}
impl<T: TokioTransport> Future for BoardTask<T> {
type Output = Result<ActorExit<T>, ::tokio::task::JoinError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.join).poll(cx)
}
}
pub fn spawn<T: TokioTransport>(
transport: T,
config: ActorConfig,
) -> Result<(BoardHandle, BoardTask<T>), SpawnError> {
if config.command_capacity == 0 {
warn_event!(
field = "command_capacity",
"invalid board actor configuration"
);
return Err(SpawnError::ZeroCommandCapacity);
}
if config.event_capacity == 0 {
warn_event!(
field = "event_capacity",
"invalid board actor configuration"
);
return Err(SpawnError::ZeroEventCapacity);
}
info_event!(
command_capacity = config.command_capacity,
event_capacity = config.event_capacity,
query_capacity = config.query_capacity,
request_timeout = ?config.request_timeout,
"spawning board actor"
);
let (request_tx, request_rx) = ::tokio::sync::mpsc::channel(config.command_capacity);
let (event_tx, _) = ::tokio::sync::broadcast::channel(config.event_capacity);
let (lifecycle_tx, lifecycle_rx) = ::tokio::sync::watch::channel(LifecycleState::Starting);
let actor = run_actor(transport, config, request_rx, event_tx, lifecycle_tx);
#[cfg(feature = "tracing")]
let actor = {
use ::tracing::Instrument as _;
actor.instrument(::tracing::info_span!("board_actor"))
};
let join = ::tokio::spawn(actor);
Ok((
BoardHandle {
request_tx,
lifecycle_rx,
},
BoardTask { join },
))
}
enum Message {
Send {
command: Command,
reply: ::tokio::sync::oneshot::Sender<Result<(), HandleError>>,
},
Query(QueryRequest),
SubscribeEvents {
reply: ::tokio::sync::oneshot::Sender<Result<EventStream, HandleError>>,
},
Shutdown {
reply: ::tokio::sync::oneshot::Sender<Result<(), HandleError>>,
},
}
enum QueryRequest {
Battery(::tokio::sync::oneshot::Sender<Result<BatteryStatus, HandleError>>),
Pieces(::tokio::sync::oneshot::Sender<Result<PieceStatus, HandleError>>),
}
struct PendingQuery {
request: QueryRequest,
deadline: ::tokio::time::Instant,
}
impl QueryRequest {
#[cfg(feature = "tracing")]
const fn kind(&self) -> &'static str {
match self {
Self::Battery(_) => "battery_status",
Self::Pieces(_) => "piece_status",
}
}
fn command(&self) -> Command {
match self {
Self::Battery(_) => Command::read_battery_level(),
Self::Pieces(_) => Command::read_piece_status(),
}
}
fn fail(self, error: HandleError) {
match self {
Self::Battery(reply) => {
let _ = reply.send(Err(error));
}
Self::Pieces(reply) => {
let _ = reply.send(Err(error));
}
}
}
}
impl PendingQuery {
fn fail(self, error: HandleError) {
self.request.fail(error);
}
}
async fn receive_reply<T>(
reply: ::tokio::sync::oneshot::Receiver<Result<T, HandleError>>,
) -> Result<Result<T, HandleError>, HandleError> {
reply.await.map_err(|_| HandleError::ActorStopped)
}
async fn run_actor<T: TokioTransport>(
mut transport: T,
config: ActorConfig,
mut request_rx: ::tokio::sync::mpsc::Receiver<Message>,
event_tx: ::tokio::sync::broadcast::Sender<Result<BoardEvent, DecodeNotificationError>>,
lifecycle_tx: ::tokio::sync::watch::Sender<LifecycleState>,
) -> ActorExit<T> {
info_event!("board actor is starting");
let mut notification_buffer = [0; MAX_NOTIFICATION_LEN];
let mut pending_query: Option<PendingQuery> = None;
let mut queued_queries: VecDeque<QueryRequest> = VecDeque::new();
let mut shutdown_reply = None;
let mut result = initialize_transport(&mut transport)
.await
.map_err(ActorError::Transport);
if result.is_ok() {
lifecycle_tx.send_replace(LifecycleState::Running);
info_event!("board actor is running");
result = loop {
if pending_query.is_none()
&& let Some(query) = queued_queries.pop_front()
{
debug_event!(
query = query.kind(),
queued_queries = queued_queries.len(),
"starting queued board query"
);
match start_query(&mut transport, query, config.request_timeout).await {
Ok(query) => pending_query = Some(query),
Err(error) => break Err(error),
}
}
let deadline = pending_query
.as_ref()
.map(|query| query.deadline)
.unwrap_or_else(::tokio::time::Instant::now);
::tokio::select! {
message = request_rx.recv() => {
match message {
Some(Message::Send { command, reply }) => {
trace_event!(
command_len = command.bytes().len(),
write_kind = ?command.write_kind(),
"board actor is writing a command"
);
match transport.write_command(&command).await {
Ok(()) => {
let _ = reply.send(Ok(()));
}
Err(error) => {
error_event!(operation = "write_command", "board transport failed");
let _ = reply.send(Err(HandleError::TransportFailed));
break Err(ActorError::Transport(error));
}
}
}
Some(Message::Query(query)) => {
debug_event!(
query = query.kind(),
queued_queries = queued_queries.len(),
has_active_query = pending_query.is_some(),
"board actor received a query"
);
if pending_query.is_none() && queued_queries.is_empty() {
match start_query(&mut transport, query, config.request_timeout).await {
Ok(query) => pending_query = Some(query),
Err(error) => break Err(error),
}
} else if queued_queries.len() < config.query_capacity {
queued_queries.push_back(query);
} else {
warn_event!(
query = query.kind(),
query_capacity = config.query_capacity,
"board query queue is full"
);
query.fail(HandleError::QueryQueueFull);
}
}
Some(Message::SubscribeEvents { reply }) => {
debug_event!(
subscribers = event_tx.receiver_count() + 1,
"creating board event subscription"
);
let _ = reply.send(Ok(EventStream::new(event_tx.subscribe())));
}
Some(Message::Shutdown { reply }) => {
info_event!("board actor received shutdown request");
shutdown_reply = Some(reply);
break Ok(());
}
None => {
info_event!("all board handles were dropped");
break Ok(());
}
}
}
notification = transport.next_notification(&mut notification_buffer) => {
let notification = match notification {
Ok(notification) => notification,
Err(error) => {
error_event!(operation = "next_notification", "board transport failed");
break Err(ActorError::Transport(error));
}
};
trace_event!(
source = ?notification.source(),
notification_len = notification.bytes().len(),
"board actor received a notification"
);
let event = match decode_notification(notification) {
Ok(DecodedNotification::Event(event)) => event,
Ok(DecodedNotification::RealtimeUpdatesAcknowledged) => continue,
Err(error) => {
warn_event!(error = ?error, "board actor skipped a malformed notification");
let _ = event_tx.send(Err(error));
continue;
}
};
resolve_query(&mut pending_query, event);
let _ = event_tx.send(Ok(event));
}
_ = ::tokio::time::sleep_until(deadline), if pending_query.is_some() => {
if let Some(query) = pending_query.take() {
warn_event!(query = query.request.kind(), "board query timed out");
query.fail(HandleError::RequestTimedOut);
}
}
}
};
} else {
error_event!(
operation = "initialize",
"board actor initialization failed"
);
}
lifecycle_tx.send_replace(LifecycleState::ShuttingDown);
info_event!("board actor is shutting down");
request_rx.close();
if let Some(query) = pending_query.take() {
query.fail(HandleError::ShuttingDown);
}
for query in queued_queries {
query.fail(HandleError::ShuttingDown);
}
while let Ok(message) = request_rx.try_recv() {
fail_message(message, HandleError::ShuttingDown);
}
let cleanup_result = shutdown_transport(&mut transport)
.await
.map_err(ActorError::Transport);
if cleanup_result.is_err() {
error_event!(operation = "shutdown", "board transport cleanup failed");
}
if result.is_ok() {
result = cleanup_result;
}
let final_state = if result.is_ok() {
LifecycleState::Stopped
} else {
LifecycleState::Faulted
};
lifecycle_tx.send_replace(final_state);
match final_state {
LifecycleState::Stopped => info_event!("board actor stopped"),
LifecycleState::Faulted => error_event!("board actor stopped after a fatal error"),
_ => {}
}
if let Some(reply) = shutdown_reply {
let reply_result = if result.is_ok() {
Ok(())
} else {
Err(HandleError::TransportFailed)
};
let _ = reply.send(reply_result);
}
ActorExit { transport, result }
}
async fn initialize_transport<T: TokioTransport>(transport: &mut T) -> Result<(), T::Error> {
trace_event!(
operation = "subscribe",
source = ?NotificationSource::Position,
"initializing board transport"
);
transport.subscribe(NotificationSource::Position).await?;
trace_event!(
operation = "enable_realtime_updates",
"initializing board transport"
);
transport
.write_command(&Command::enable_realtime_updates())
.await?;
trace_event!(
operation = "subscribe",
source = ?NotificationSource::CommandResponse,
"initializing board transport"
);
transport
.subscribe(NotificationSource::CommandResponse)
.await
}
async fn shutdown_transport<T: TokioTransport>(transport: &mut T) -> Result<(), T::Error> {
trace_event!(
operation = "unsubscribe",
source = ?NotificationSource::CommandResponse,
"shutting down board transport"
);
transport
.unsubscribe(NotificationSource::CommandResponse)
.await?;
trace_event!(
operation = "unsubscribe",
source = ?NotificationSource::Position,
"shutting down board transport"
);
transport.unsubscribe(NotificationSource::Position).await?;
trace_event!(operation = "close", "shutting down board transport");
transport.close().await
}
async fn start_query<T: TokioTransport>(
transport: &mut T,
query: QueryRequest,
timeout: Duration,
) -> Result<PendingQuery, ActorError<T::Error>> {
debug_event!(
query = query.kind(),
timeout = ?timeout,
"writing serialized board query"
);
if let Err(error) = transport.write_command(&query.command()).await {
error_event!(
operation = "write_query",
query = query.kind(),
"board transport failed"
);
query.fail(HandleError::TransportFailed);
return Err(ActorError::Transport(error));
}
Ok(PendingQuery {
request: query,
deadline: ::tokio::time::Instant::now() + timeout,
})
}
fn resolve_query(pending: &mut Option<PendingQuery>, event: BoardEvent) {
let matches = matches!(
(pending.as_ref().map(|query| &query.request), event),
(Some(QueryRequest::Battery(_)), BoardEvent::BatteryStatus(_))
| (Some(QueryRequest::Pieces(_)), BoardEvent::PieceStatus(_))
);
if !matches {
return;
}
let query = pending.take().expect("matching pending query exists");
debug_event!(
query = query.request.kind(),
"board query received its response"
);
match (query.request, event) {
(QueryRequest::Battery(reply), BoardEvent::BatteryStatus(status)) => {
let _ = reply.send(Ok(status));
}
(QueryRequest::Pieces(reply), BoardEvent::PieceStatus(status)) => {
let _ = reply.send(Ok(status));
}
_ => unreachable!("query and event variants were checked before taking the query"),
}
}
fn fail_message(message: Message, error: HandleError) {
match message {
Message::Send { reply, .. } | Message::Shutdown { reply } => {
let _ = reply.send(Err(error));
}
Message::Query(query) => query.fail(error),
Message::SubscribeEvents { reply } => {
let _ = reply.send(Err(error));
}
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use super::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
#[error("mock notification channel closed")]
struct MockError;
struct OwnedNotification {
source: NotificationSource,
bytes: Vec<u8>,
}
#[derive(Debug, PartialEq, Eq)]
enum Operation {
Subscribe(NotificationSource),
Write(Vec<u8>),
}
#[derive(Default)]
struct Record {
subscriptions: Vec<NotificationSource>,
unsubscriptions: Vec<NotificationSource>,
writes: Vec<Vec<u8>>,
operations: Vec<Operation>,
closed: bool,
}
struct MockTransport {
record: Arc<Mutex<Record>>,
notification_rx: ::tokio::sync::mpsc::Receiver<OwnedNotification>,
}
impl MockTransport {
fn new() -> (
Self,
Arc<Mutex<Record>>,
::tokio::sync::mpsc::Sender<OwnedNotification>,
) {
let record = Arc::new(Mutex::new(Record::default()));
let (notification_tx, notification_rx) = ::tokio::sync::mpsc::channel(8);
(
Self {
record: Arc::clone(&record),
notification_rx,
},
record,
notification_tx,
)
}
}
impl TokioTransport for MockTransport {
type Error = MockError;
async fn subscribe(&mut self, source: NotificationSource) -> Result<(), Self::Error> {
let mut record = self.record.lock().unwrap();
record.subscriptions.push(source);
record.operations.push(Operation::Subscribe(source));
Ok(())
}
async fn unsubscribe(&mut self, source: NotificationSource) -> Result<(), Self::Error> {
self.record.lock().unwrap().unsubscriptions.push(source);
Ok(())
}
async fn write_command(&mut self, command: &Command) -> Result<(), Self::Error> {
let bytes = command.bytes().to_vec();
let mut record = self.record.lock().unwrap();
record.writes.push(bytes.clone());
record.operations.push(Operation::Write(bytes));
Ok(())
}
async fn next_notification<'a>(
&'a mut self,
buffer: &'a mut [u8],
) -> Result<Notification<'a>, Self::Error> {
let notification = self.notification_rx.recv().await.ok_or(MockError)?;
buffer[..notification.bytes.len()].copy_from_slice(¬ification.bytes);
Ok(Notification::new(
notification.source,
&buffer[..notification.bytes.len()],
))
}
async fn close(&mut self) -> Result<(), Self::Error> {
self.record.lock().unwrap().closed = true;
Ok(())
}
}
#[::tokio::test]
async fn actor_correlates_queries_without_stealing_events() {
let (transport, record, notification_tx) = MockTransport::new();
let (handle, task) = spawn(transport, ActorConfig::default()).unwrap();
let mut events = handle.subscribe_events().await.unwrap();
wait_until_running(&handle).await;
assert_eq!(
record.lock().unwrap().operations[..3],
[
Operation::Subscribe(NotificationSource::Position),
Operation::Write(Command::enable_realtime_updates().bytes().to_vec()),
Operation::Subscribe(NotificationSource::CommandResponse),
]
);
notification_tx
.send(OwnedNotification {
source: NotificationSource::CommandResponse,
bytes: vec![0x23, 0x01, 0x00],
})
.await
.unwrap();
let query_handle = handle.clone();
let query = ::tokio::spawn(async move { query_handle.battery_status().await });
wait_for_write(&record, Command::read_battery_level().bytes()).await;
notification_tx
.send(OwnedNotification {
source: NotificationSource::Position,
bytes: vec![0; 34],
})
.await
.unwrap();
assert!(matches!(
events.recv().await,
Ok(BoardEvent::PositionChanged(_))
));
assert!(!query.is_finished());
notification_tx
.send(OwnedNotification {
source: NotificationSource::CommandResponse,
bytes: vec![0x41, 0x03, 0x0c, 0x01, 88],
})
.await
.unwrap();
assert_eq!(
query.await.unwrap(),
Ok(BatteryStatus {
charging: true,
percentage: 88,
})
);
assert_eq!(
events.recv().await,
Ok(BoardEvent::BatteryStatus(BatteryStatus {
charging: true,
percentage: 88,
}))
);
let query_handle = handle.clone();
let query = ::tokio::spawn(async move { query_handle.piece_status().await });
wait_for_write(&record, Command::read_piece_status().bytes()).await;
notification_tx
.send(OwnedNotification {
source: NotificationSource::CommandResponse,
bytes: piece_status_response(),
})
.await
.unwrap();
let piece_status = query.await.unwrap().unwrap();
assert_eq!(piece_status.pieces[0].battery_percentage, Some(50));
assert_eq!(piece_status.pieces[33].battery_percentage, Some(83));
assert!(matches!(
events.recv().await,
Ok(BoardEvent::PieceStatus(_))
));
handle.shutdown().await.unwrap();
let exit = task.await.unwrap();
assert!(exit.result().is_ok());
assert_eq!(handle.lifecycle(), LifecycleState::Stopped);
assert_eq!(events.recv().await, Err(EventStreamError::Closed));
let record = record.lock().unwrap();
assert_eq!(
record.subscriptions,
[
NotificationSource::Position,
NotificationSource::CommandResponse,
]
);
assert_eq!(
record.unsubscriptions,
[
NotificationSource::CommandResponse,
NotificationSource::Position,
]
);
assert!(record.closed);
}
#[::tokio::test(start_paused = true)]
async fn request_timeout_does_not_stop_the_actor() {
let (transport, record, _notification_tx) = MockTransport::new();
let config = ActorConfig {
request_timeout: Duration::from_secs(5),
..ActorConfig::default()
};
let (handle, task) = spawn(transport, config).unwrap();
wait_until_running(&handle).await;
let query_handle = handle.clone();
let query = ::tokio::spawn(async move { query_handle.battery_status().await });
wait_for_write(&record, Command::read_battery_level().bytes()).await;
::tokio::time::advance(Duration::from_secs(6)).await;
assert_eq!(query.await.unwrap(), Err(HandleError::RequestTimedOut));
assert_eq!(handle.lifecycle(), LifecycleState::Running);
handle.shutdown().await.unwrap();
assert!(task.await.unwrap().result().is_ok());
}
#[::tokio::test]
async fn malformed_notification_is_reported_without_stopping_actor() {
let (transport, _record, notification_tx) = MockTransport::new();
let (handle, task) = spawn(transport, ActorConfig::default()).unwrap();
let mut events = handle.subscribe_events().await.unwrap();
wait_until_running(&handle).await;
notification_tx
.send(OwnedNotification {
source: NotificationSource::Position,
bytes: vec![0; 29],
})
.await
.unwrap();
assert!(matches!(
events.recv().await,
Err(EventStreamError::Decode(DecodeNotificationError::Position(
crate::protocol::DecodePositionNotificationError::NotificationTooShort(
crate::protocol::NotificationTooShortError {
expected: 34,
actual: 29,
}
)
)))
));
assert_eq!(handle.lifecycle(), LifecycleState::Running);
handle.shutdown().await.unwrap();
assert!(task.await.unwrap().result().is_ok());
}
#[::tokio::test]
async fn event_stream_reports_lag() {
let (sender, receiver) = ::tokio::sync::broadcast::channel(1);
let mut events = EventStream::new(receiver);
sender
.send(Ok(BoardEvent::BatteryStatus(BatteryStatus {
charging: false,
percentage: 10,
})))
.unwrap();
sender
.send(Ok(BoardEvent::BatteryStatus(BatteryStatus {
charging: false,
percentage: 11,
})))
.unwrap();
assert_eq!(events.recv().await, Err(EventStreamError::Lagged(1)));
assert_eq!(
events.recv().await,
Ok(BoardEvent::BatteryStatus(BatteryStatus {
charging: false,
percentage: 11,
}))
);
}
async fn wait_until_running(handle: &BoardHandle) {
let mut lifecycle = handle.subscribe_lifecycle();
while *lifecycle.borrow() != LifecycleState::Running {
lifecycle.changed().await.unwrap();
}
}
async fn wait_for_write(record: &Arc<Mutex<Record>>, expected: &[u8]) {
loop {
if record
.lock()
.unwrap()
.writes
.iter()
.any(|write| write == expected)
{
return;
}
::tokio::task::yield_now().await;
}
}
fn piece_status_response() -> Vec<u8> {
const IDENTITIES: [u8; 34] = [
1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 9, 9, 10,
10, 11, 11, 12,
];
let mut response = vec![0; MAX_NOTIFICATION_LEN];
response[..3].copy_from_slice(&[0x41, 0x89, 0x0b]);
for (index, identity) in IDENTITIES.iter().copied().enumerate() {
let offset = 3 + index * 4;
response[offset] = identity;
response[offset + 1] = index as u8;
response[offset + 2] = u8::MAX - index as u8;
response[offset + 3] = 50 + index as u8;
}
response
}
}