use std::fmt::Debug;
use std::task::Poll;
use std::thread;
use std::time::Duration;
use futures::stream::BoxStream;
use tokio::runtime::{Builder, Handle, Runtime};
use tokio::time;
use crate::application::Application;
use crate::command::{Action, CancelPolicy, CommandId, RuntimeCommandParts};
use crate::noop_waker::noop_context;
use crate::subscription::core::SubscriptionId;
struct PendingLeaf<Msg> {
stream: Option<BoxStream<'static, Action<Msg>>>,
buffered: Option<Action<Msg>>,
key: Option<CommandId>,
position: usize,
}
impl<Msg: Send + 'static> PendingLeaf<Msg> {
fn poll_once(&mut self) -> Poll<Option<Action<Msg>>> {
let Some(stream) = self.stream.as_mut() else {
return Poll::Ready(None);
};
let mut context = noop_context();
let poll = stream.as_mut().poll_next(&mut context);
if matches!(poll, Poll::Ready(None)) {
self.stream = None;
}
poll
}
}
pub struct TestStore<App: Application>
where
App::Message: Debug,
{
app: App,
context: Runtime,
pending: Vec<PendingLeaf<App::Message>>,
redraw_requested: bool,
quit_observed: bool,
enqueued_leaves: usize,
finished: bool,
}
#[expect(
clippy::panic,
reason = "assertion failures in a test harness are panics by design"
)]
impl<App: Application> TestStore<App>
where
App::Message: Debug,
{
#[must_use]
#[track_caller]
pub fn new(flags: App::Flags) -> Self {
assert!(
Handle::try_current().is_err(),
"TestStore::new: a Tokio runtime is already entered; TestStore owns its \
controlled time context and must be constructed on a plain #[test], not \
#[tokio::test] (RFC 0008 §4.3)"
);
let context = Builder::new_current_thread()
.enable_time()
.start_paused(true)
.build()
.expect("controlled time context construction should not fail");
let (app, init_command) = App::new(flags);
let mut store = Self {
app,
context,
pending: Vec::new(),
redraw_requested: true,
quit_observed: false,
enqueued_leaves: 0,
finished: false,
};
store.enqueue_command(init_command.into_runtime_parts());
store
}
pub const fn state(&self) -> &App {
&self.app
}
#[track_caller]
pub fn send(&mut self, msg: App::Message) {
self.assert_running("send");
self.apply_update(msg);
}
#[track_caller]
pub fn advance(&mut self, duration: Duration) {
self.assert_running("advance");
{
let _context = self.context.enter();
for leaf in &mut self.pending {
if leaf.buffered.is_some() {
continue;
}
if let Poll::Ready(Some(action)) = leaf.poll_once() {
leaf.buffered = Some(action);
}
}
}
self.pending
.retain(|leaf| leaf.stream.is_some() || leaf.buffered.is_some());
self.context.block_on(time::advance(duration));
}
#[track_caller]
#[expect(
clippy::needless_pass_by_value,
reason = "the expected value is consumed by the assertion (RFC 0008 §2.1)"
)]
pub fn receive(&mut self, expected: App::Message)
where
App::Message: PartialEq,
{
self.assert_running("receive");
let actual = self.next_message("receive");
assert!(
actual == expected,
"TestStore::receive: message mismatch\n expected: {expected:?}\n actual: {actual:?}"
);
self.apply_update(actual);
}
#[track_caller]
pub fn receive_matching(&mut self, matches: impl FnOnce(&App::Message) -> bool) {
self.assert_running("receive_matching");
let actual = self.next_message("receive_matching");
assert!(
matches(&actual),
"TestStore::receive_matching: predicate rejected the delivered message: {actual:?}"
);
self.apply_update(actual);
}
#[track_caller]
pub fn receive_quit(&mut self) {
self.assert_running("receive_quit");
match self.next_deliverable("receive_quit") {
Action::Quit => self.quit_observed = true,
Action::Message(msg) => panic!(
"TestStore::receive_quit: expected a quit request, but the next deliverable output is a message: {msg:?}"
),
}
}
#[must_use]
pub const fn redraw_requested(&self) -> bool {
self.redraw_requested
}
#[must_use]
pub fn subscription_ids(&self) -> Vec<SubscriptionId> {
let mut ids: Vec<SubscriptionId> = Vec::new();
for subscription in self.app.subscriptions() {
let id = subscription.id();
if !ids.contains(id) {
ids.push(id.clone());
}
}
ids
}
#[track_caller]
pub fn finish(mut self) {
self.finished = true;
self.check_exhaustive("finish");
}
fn apply_update(&mut self, msg: App::Message) {
let command = self.app.update(msg);
self.enqueue_command(command.into_runtime_parts());
}
fn enqueue_command(&mut self, parts: RuntimeCommandParts<App::Message>) {
self.redraw_requested = parts.requests_redraw();
let (cancels, key, leaves) = parts.into_execution_parts();
for id in &cancels {
self.cancel_id(id);
}
if leaves.is_empty() {
return;
}
match key {
None => self.push_leaves(None, leaves),
Some(key) => match key.policy {
CancelPolicy::CancelInFlight => {
self.cancel_id(&key.id);
self.push_leaves(Some(&key.id), leaves);
}
CancelPolicy::KeepInFlight => {
if self.reconcile_is_occupied(&key.id) {
drop(leaves);
} else {
self.push_leaves(Some(&key.id), leaves);
}
}
},
}
}
fn cancel_id(&mut self, id: &CommandId) {
self.pending.retain(|leaf| leaf.key.as_ref() != Some(id));
}
fn reconcile_is_occupied(&mut self, id: &CommandId) -> bool {
if self
.pending
.iter()
.any(|leaf| leaf.key.as_ref() == Some(id) && leaf.buffered.is_some())
{
return true;
}
let _context = self.context.enter();
for leaf in &mut self.pending {
if leaf.key.as_ref() != Some(id) || leaf.stream.is_none() {
continue;
}
match leaf.poll_once() {
Poll::Ready(Some(action)) => {
leaf.buffered = Some(action);
return true;
}
Poll::Ready(None) => {}
Poll::Pending => return true,
}
}
false
}
fn push_leaves(
&mut self,
key: Option<&CommandId>,
leaves: Vec<BoxStream<'static, Action<App::Message>>>,
) {
for stream in leaves {
let position = self.enqueued_leaves;
self.enqueued_leaves += 1;
self.pending.push(PendingLeaf {
stream: Some(stream),
buffered: None,
key: key.cloned(),
position,
});
}
}
#[track_caller]
fn assert_running(&self, method: &str) {
assert!(
!self.quit_observed,
"TestStore::{method}: the application has quit; remaining output is discarded and no further steps run"
);
}
#[track_caller]
fn next_message(&mut self, method: &str) -> App::Message {
match self.next_deliverable(method) {
Action::Message(msg) => msg,
Action::Quit => panic!(
"TestStore::{method}: the next deliverable output is a quit request; assert it with TestStore::receive_quit"
),
}
}
#[track_caller]
fn next_deliverable(&mut self, method: &str) -> Action<App::Message> {
let mut found = None;
let mut any_open = false;
let context = self.context.enter();
for leaf in &mut self.pending {
if let Some(action) = leaf.buffered.take() {
found = Some(action);
break;
}
match leaf.poll_once() {
Poll::Ready(Some(action)) => {
found = Some(action);
break;
}
Poll::Ready(None) => {}
Poll::Pending => any_open = true,
}
}
drop(context);
self.pending
.retain(|leaf| leaf.stream.is_some() || leaf.buffered.is_some());
if let Some(action) = found {
return action;
}
assert!(
!any_open,
"TestStore::{method}: no deliverable output: effects are pending but none is ready"
);
panic!("TestStore::{method}: no deliverable output: no pending effects");
}
#[track_caller]
fn check_exhaustive(&mut self, site: &str) {
enum Leak {
Deliverable { rendered: String, position: usize },
Unfinished { position: usize },
}
if self.quit_observed {
return;
}
let mut leak = None;
let _context = self.context.enter();
for leaf in &mut self.pending {
if let Some(action) = &leaf.buffered {
leak = Some(Leak::Deliverable {
rendered: render_action(action),
position: leaf.position,
});
break;
}
match leaf.poll_once() {
Poll::Ready(Some(action)) => {
leak = Some(Leak::Deliverable {
rendered: render_action(&action),
position: leaf.position,
});
break;
}
Poll::Ready(None) => {}
Poll::Pending => {
leak = Some(Leak::Unfinished {
position: leaf.position,
});
break;
}
}
}
match leak {
None => {}
Some(Leak::Deliverable { rendered, position }) => panic!(
"TestStore::{site}: deliverable output was never received: {rendered} (leaf enqueued at position {position})"
),
Some(Leak::Unfinished { position }) => {
let unfinished = self
.pending
.iter()
.filter(|leaf| leaf.stream.is_some())
.count();
panic!(
"TestStore::{site}: {unfinished} effect leaf(s) not driven to completion; first still pending at enqueue position {position}"
);
}
}
}
}
impl<App: Application> Drop for TestStore<App>
where
App::Message: Debug,
{
fn drop(&mut self) {
if self.finished || thread::panicking() {
return;
}
self.check_exhaustive("drop check");
}
}
fn render_action<Msg: Debug>(action: &Action<Msg>) -> String {
match action {
Action::Message(msg) => format!("{msg:?}"),
Action::Quit => "a quit request".to_owned(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::any::Any;
use std::future::pending;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::Duration;
use std::num::NonZeroUsize;
use futures::channel::oneshot;
use futures::stream;
use ratatui::Frame;
#[cfg(not(loom))]
use tokio::net::TcpStream;
use tracing::Level;
use crate::command::{Command, RetryPolicy};
use crate::subscription::core::Subscription;
use crate::subscription::mock::MockSource;
use crate::test_support::TraceRecorder;
type UpdateFn<Msg> = Box<dyn FnMut(Msg) -> Command<Msg> + Send>;
struct Harness<Msg: Send + Debug + 'static> {
log: Vec<String>,
update: UpdateFn<Msg>,
}
struct HarnessFlags<Msg: Send + 'static> {
init: Command<Msg>,
update: UpdateFn<Msg>,
}
impl<Msg: Send + Debug + 'static> Application for Harness<Msg> {
type Message = Msg;
type Flags = HarnessFlags<Msg>;
fn new(flags: HarnessFlags<Msg>) -> (Self, Command<Msg>) {
(
Self {
log: Vec::new(),
update: flags.update,
},
flags.init,
)
}
fn update(&mut self, msg: Msg) -> Command<Msg> {
self.log.push(format!("{msg:?}"));
(self.update)(msg)
}
fn view(&self, _frame: &mut Frame<'_>) {}
fn subscriptions(&self) -> Vec<Subscription<Msg>> {
Vec::new()
}
}
fn store_with<Msg: Send + Debug + 'static>(
init: Command<Msg>,
update: impl FnMut(Msg) -> Command<Msg> + Send + 'static,
) -> TestStore<Harness<Msg>> {
TestStore::new(HarnessFlags {
init,
update: Box::new(update),
})
}
#[derive(Debug, PartialEq)]
enum Msg {
N(u32),
Keyed(u32),
Start,
StartRetry,
Restart,
TryKeep,
Cancel,
Unrelated,
Loud,
StartQuit,
}
#[cfg(not(loom))]
fn io_command() -> Command<Msg> {
Command::perform(
async {
drop(TcpStream::connect("127.0.0.1:9").await);
},
|()| Msg::N(99),
)
}
fn timeout_command(secs: u64) -> Command<Msg> {
Command::future(pending()).timeout(Duration::from_secs(secs), || Msg::N(99))
}
fn failure_message<T>(result: Result<T, Box<dyn Any + Send>>) -> String {
let payload = result.err().expect("the call should have failed");
match payload.downcast::<String>() {
Ok(message) => *message,
Err(payload) => (*payload
.downcast::<&str>()
.expect("panic payload should be a string"))
.to_owned(),
}
}
#[derive(Debug)]
enum Opaque {
Ping,
Pong,
}
#[test]
fn store_bounds_are_debug_only() {
let mut store = store_with(Command::none(), |msg| match msg {
Opaque::Ping => Command::message(Opaque::Pong),
Opaque::Pong => Command::none(),
});
store.send(Opaque::Ping);
assert_eq!(store.state().log, vec!["Ping".to_owned()]);
store.receive_matching(|msg| matches!(msg, Opaque::Pong));
store.finish();
}
fn deterministic_program_transcript() -> Vec<String> {
let id = CommandId::new("det");
let mut store = store_with(
Command::batch([
Command::stream(stream::iter([Msg::N(1), Msg::N(2)])),
Command::message(Msg::N(3)),
]),
move |msg| match msg {
Msg::Start => Command::stream(stream::iter([Msg::Keyed(1), Msg::Keyed(2)]))
.cancellable(id.clone()),
Msg::Cancel => Command::cancel(id.clone()),
_ => Command::none(),
},
);
store.receive(Msg::N(1));
store.receive(Msg::N(2));
store.receive(Msg::N(3));
store.send(Msg::Start);
store.receive(Msg::Keyed(1));
store.send(Msg::Cancel);
let transcript = store.state().log.clone();
store.finish();
transcript
}
#[test]
fn deterministic_program_repeats_its_transcript() {
assert_eq!(
deterministic_program_transcript(),
deterministic_program_transcript()
);
}
#[test]
fn checks_poll_each_reached_leaf_exactly_once() {
let polls = Arc::new(AtomicUsize::new(0));
let done = Arc::new(AtomicBool::new(false));
let counting = {
let polls = Arc::clone(&polls);
let done = Arc::clone(&done);
stream::poll_fn(move |_| {
polls.fetch_add(1, Ordering::SeqCst);
if done.load(Ordering::SeqCst) {
Poll::Ready(None)
} else {
Poll::Pending::<Option<Msg>>
}
})
};
let mut store = store_with(
Command::batch([Command::stream(counting), Command::message(Msg::N(1))]),
|_| Command::none(),
);
assert_eq!(polls.load(Ordering::SeqCst), 0);
store.receive(Msg::N(1));
assert_eq!(polls.load(Ordering::SeqCst), 1);
store.send(Msg::Unrelated);
assert_eq!(polls.load(Ordering::SeqCst), 1);
store.advance(Duration::from_millis(1));
assert_eq!(polls.load(Ordering::SeqCst), 2);
done.store(true, Ordering::SeqCst);
store.finish();
assert_eq!(polls.load(Ordering::SeqCst), 3);
}
#[test]
fn one_leaf_delivers_in_stream_order() {
let mut store = store_with(
Command::stream(stream::iter([Msg::N(1), Msg::N(2), Msg::N(3)])),
|_| Command::none(),
);
store.receive(Msg::N(1));
store.receive(Msg::N(2));
store.receive(Msg::N(3));
store.finish();
}
#[test]
fn ready_leaves_deliver_in_declaration_order() {
let mut store = store_with(
Command::batch([
Command::message(Msg::N(1)),
Command::message(Msg::N(2)),
Command::stream(stream::iter([Msg::N(3), Msg::N(4)])),
]),
|_| Command::none(),
);
store.receive(Msg::N(1));
store.receive(Msg::N(2));
store.receive(Msg::N(3));
store.receive(Msg::N(4));
store.finish();
}
#[test]
fn late_ready_leaf_delivers_at_its_enqueue_position() {
let (tx, rx) = oneshot::channel::<u32>();
let mut store = store_with(
Command::batch([
Command::future(async move { Msg::N(rx.await.expect("sender completes")) }),
Command::message(Msg::N(2)),
Command::message(Msg::N(3)),
]),
|_| Command::none(),
);
store.receive(Msg::N(2));
tx.send(1).expect("receiver is alive");
store.receive(Msg::N(1));
store.receive(Msg::N(3));
store.finish();
}
#[test]
fn cancel_in_flight_supersedes_pending_keyed_output() {
let id = CommandId::new("k");
let mut store = store_with(Command::none(), move |msg| match msg {
Msg::Start => Command::stream(stream::iter([Msg::Keyed(1), Msg::Keyed(2)]))
.cancellable(id.clone()),
Msg::Restart => Command::message(Msg::Keyed(9)).cancellable(id.clone()),
_ => Command::none(),
});
store.send(Msg::Start);
store.send(Msg::Restart);
store.receive(Msg::Keyed(9));
store.finish();
}
#[cfg(not(loom))]
#[test]
fn cancel_in_flight_supersedes_an_io_dependent_occupant() {
let id = CommandId::new("k");
let mut store = store_with(Command::none(), move |msg| match msg {
Msg::Start => io_command().cancellable(id.clone()),
Msg::Restart => Command::message(Msg::Keyed(9)).cancellable(id.clone()),
_ => Command::none(),
});
store.send(Msg::Start);
store.send(Msg::Restart);
store.receive(Msg::Keyed(9));
store.finish();
}
#[test]
fn keep_in_flight_is_discarded_while_occupied() {
let id = CommandId::new("k");
let mut store = store_with(Command::none(), move |msg| match msg {
Msg::Start => Command::stream(stream::iter([Msg::Keyed(1), Msg::Keyed(2)]))
.cancellable(id.clone()),
Msg::TryKeep => Command::message(Msg::Keyed(9))
.cancellable_with(id.clone(), CancelPolicy::KeepInFlight),
_ => Command::none(),
});
store.send(Msg::Start);
store.receive(Msg::Keyed(1));
store.send(Msg::TryKeep);
store.receive(Msg::Keyed(2));
store.finish();
}
#[test]
fn keep_in_flight_is_admitted_after_occupant_exhaustion() {
let id = CommandId::new("k");
let mut store = store_with(Command::none(), move |msg| match msg {
Msg::Start => Command::message(Msg::Keyed(1)).cancellable(id.clone()),
Msg::TryKeep => Command::message(Msg::Keyed(2))
.cancellable_with(id.clone(), CancelPolicy::KeepInFlight),
_ => Command::none(),
});
store.send(Msg::Start);
store.receive(Msg::Keyed(1));
store.send(Msg::TryKeep);
store.receive(Msg::Keyed(2));
store.finish();
}
#[test]
fn explicit_cancel_is_strict_and_idempotent() {
let id = CommandId::new("k");
let mut store = store_with(Command::none(), move |msg| match msg {
Msg::Start => Command::stream(stream::iter([Msg::Keyed(1)])).cancellable(id.clone()),
Msg::Cancel => Command::cancel(id.clone()),
_ => Command::none(),
});
store.send(Msg::Start);
store.send(Msg::Cancel);
store.send(Msg::Cancel);
store.finish();
}
#[test]
fn same_command_cancel_then_spawn_reclaims_the_id() {
let id = CommandId::new("k");
let mut store = store_with(Command::none(), move |msg| match msg {
Msg::Start => Command::stream(stream::iter([Msg::Keyed(1), Msg::Keyed(2)]))
.cancellable(id.clone()),
Msg::Restart => {
Command::batch([Command::cancel(id.clone()), Command::message(Msg::Keyed(9))])
.cancellable(id.clone())
}
_ => Command::none(),
});
store.send(Msg::Start);
store.send(Msg::Restart);
store.receive(Msg::Keyed(9));
store.finish();
}
#[test]
fn unkeyed_output_is_unaffected_by_cancellation() {
let id = CommandId::new("k");
let mut store = store_with(Command::message(Msg::N(1)), move |msg| match msg {
Msg::Start => Command::stream(stream::iter([Msg::Keyed(1)])).cancellable(id.clone()),
Msg::Cancel => Command::cancel(id.clone()),
_ => Command::none(),
});
store.send(Msg::Start);
store.send(Msg::Cancel);
store.receive(Msg::N(1));
store.finish();
}
#[test]
fn cancelled_keyed_quit_is_suppressed() {
let id = CommandId::new("k");
let mut store = store_with(Command::none(), move |msg| match msg {
Msg::StartQuit => Command::quit().cancellable(id.clone()),
Msg::Cancel => Command::cancel(id.clone()),
_ => Command::none(),
});
store.send(Msg::StartQuit);
store.send(Msg::Cancel);
let failure = catch_unwind(AssertUnwindSafe(|| store.receive_quit()));
assert!(
failure_message(failure).contains("no pending effects"),
"the suppressed quit must not be deliverable"
);
store.send(Msg::Unrelated);
store.finish();
}
#[test]
#[should_panic(expected = "deliverable output was never received: N(7)")]
fn finish_fails_on_an_unreceived_ready_message() {
let store = store_with(Command::message(Msg::N(7)), |_| Command::none());
store.finish();
}
#[test]
#[should_panic(
expected = "1 effect leaf(s) not driven to completion; first still pending at enqueue position 0"
)]
fn finish_fails_on_an_unfinished_leaf() {
let store = store_with(Command::stream(stream::pending::<Msg>()), |_| {
Command::none()
});
store.finish();
}
#[test]
fn drop_without_finish_fails_on_leaked_output() {
let failure = catch_unwind(AssertUnwindSafe(|| {
let store = store_with(Command::message(Msg::N(7)), |_| Command::none());
drop(store);
}));
let message = failure_message(failure);
assert!(
message.contains("drop check")
&& message.contains("deliverable output was never received: N(7)"),
"the drop check should name the leaked value: {message}"
);
}
#[test]
fn send_does_not_block_on_pending_keyed_output() {
let id = CommandId::new("k");
let mut store = store_with(Command::none(), move |msg| match msg {
Msg::Start => Command::stream(stream::iter([Msg::Keyed(1)])).cancellable(id.clone()),
_ => Command::none(),
});
store.send(Msg::Start);
store.send(Msg::Unrelated);
store.receive(Msg::Keyed(1));
store.finish();
}
#[test]
fn send_does_not_block_on_pending_unkeyed_output() {
let mut store = store_with(Command::message(Msg::N(1)), |_| Command::none());
store.send(Msg::Unrelated);
store.receive(Msg::N(1));
store.finish();
}
#[test]
fn send_does_not_block_on_keyed_init_output() {
let id = CommandId::new("k");
let mut store = store_with(
Command::stream(stream::iter([Msg::Keyed(1)])).cancellable(id),
|_| Command::none(),
);
store.send(Msg::Unrelated);
store.receive(Msg::Keyed(1));
store.finish();
}
#[test]
fn send_does_not_block_on_unkeyed_step_output() {
let mut store = store_with(Command::none(), |msg| match msg {
Msg::Start => Command::message(Msg::N(1)),
_ => Command::none(),
});
store.send(Msg::Start);
store.send(Msg::Unrelated);
store.receive(Msg::N(1));
store.finish();
}
#[tokio::test]
#[should_panic(expected = "a Tokio runtime is already entered")]
async fn new_panics_inside_an_entered_runtime() {
let _ = store_with(Command::<Msg>::none(), |_| Command::none());
}
#[cfg(not(loom))]
#[test]
fn quit_is_terminal_and_discards_remaining_output() {
let mut store = store_with(Command::none(), |msg| match msg {
Msg::StartQuit => Command::batch([Command::quit(), io_command()]),
_ => Command::none(),
});
store.send(Msg::StartQuit);
store.receive_quit();
for failure in [
catch_unwind(AssertUnwindSafe(|| store.send(Msg::Unrelated))),
catch_unwind(AssertUnwindSafe(|| store.advance(Duration::from_secs(1)))),
catch_unwind(AssertUnwindSafe(|| store.receive(Msg::N(1)))),
catch_unwind(AssertUnwindSafe(|| store.receive_matching(|_| true))),
catch_unwind(AssertUnwindSafe(|| store.receive_quit())),
] {
assert!(
failure_message(failure).contains("the application has quit"),
"post-quit calls fail on the quit state, not by polling a leaf"
);
}
let _ = store.state();
let _ = store.subscription_ids();
assert!(store.redraw_requested(), "the quit step requested a redraw");
store.finish();
}
#[test]
fn redraw_reports_the_init_directive_before_the_first_step() {
let defaulted = store_with(Command::message(Msg::N(1)), |_| Command::none());
assert!(
defaulted.redraw_requested(),
"constructors default to redraw"
);
drop(catch_unwind(AssertUnwindSafe(move || defaulted.finish())));
let opted_out = store_with(Command::<Msg>::none().without_redraw(), |_| Command::none());
assert!(
!opted_out.redraw_requested(),
"without_redraw is observable"
);
opted_out.finish();
}
#[test]
fn redraw_tracks_steps_and_receive_quit_is_not_a_step() {
let mut store = store_with(Command::message(Msg::N(1)), |msg| match msg {
Msg::N(_) => Command::none().without_redraw(),
Msg::StartQuit => Command::quit().without_redraw(),
_ => Command::none(),
});
assert!(
store.redraw_requested(),
"init directive defaults to redraw"
);
store.receive(Msg::N(1));
assert!(!store.redraw_requested(), "the receive step opted out");
store.send(Msg::Loud);
assert!(store.redraw_requested(), "the send step defaults to redraw");
store.send(Msg::StartQuit);
assert!(!store.redraw_requested(), "the quit step opted out");
store.receive_quit();
assert!(!store.redraw_requested(), "receive_quit is not a step");
store.finish();
}
struct SubApp {
first: MockSource<()>,
second: MockSource<()>,
both: bool,
}
impl Application for SubApp {
type Message = ();
type Flags = (MockSource<()>, MockSource<()>);
fn new((first, second): Self::Flags) -> (Self, Command<()>) {
(
Self {
first,
second,
both: true,
},
Command::none(),
)
}
fn update(&mut self, (): ()) -> Command<()> {
self.both = false;
Command::none()
}
fn view(&self, _frame: &mut Frame<'_>) {}
fn subscriptions(&self) -> Vec<Subscription<()>> {
if self.both {
vec![
Subscription::new(self.first.clone()),
Subscription::new(self.second.clone()),
]
} else {
vec![Subscription::new(self.first.clone())]
}
}
}
#[test]
fn subscription_ids_observe_the_declared_set_in_declaration_order() {
let first = MockSource::<()>::new();
let second = MockSource::<()>::new();
let first_id = Subscription::new(first.clone()).id().clone();
let second_id = Subscription::new(second.clone()).id().clone();
let mut store = TestStore::<SubApp>::new((first, second));
assert_eq!(
store.subscription_ids(),
vec![first_id.clone(), second_id],
"declared ids in declaration order"
);
store.send(());
assert_eq!(
store.subscription_ids(),
vec![first_id],
"the declared set follows the state after a send"
);
store.finish();
}
struct DupApp {
first: MockSource<()>,
second: MockSource<()>,
}
impl Application for DupApp {
type Message = ();
type Flags = (MockSource<()>, MockSource<()>);
fn new((first, second): Self::Flags) -> (Self, Command<()>) {
(Self { first, second }, Command::none())
}
fn update(&mut self, (): ()) -> Command<()> {
Command::none()
}
fn view(&self, _frame: &mut Frame<'_>) {}
fn subscriptions(&self) -> Vec<Subscription<()>> {
vec![
Subscription::new(self.first.clone()),
Subscription::new(self.second.clone()),
Subscription::new(self.first.clone()),
]
}
}
#[test]
fn subscription_ids_dedup_is_first_occurrence_stable() {
let first = MockSource::<()>::new();
let second = MockSource::<()>::new();
let first_id = Subscription::new(first.clone()).id().clone();
let second_id = Subscription::new(second.clone()).id().clone();
let store = TestStore::<DupApp>::new((first, second));
assert_eq!(
store.subscription_ids(),
vec![first_id, second_id],
"duplicates collapse to the first occurrence in declaration order"
);
store.finish();
}
#[test]
fn subscription_ids_starts_no_source() {
let first = MockSource::<()>::new();
let second = MockSource::<()>::new();
let store = TestStore::<DupApp>::new((first.clone(), second.clone()));
let _ = store.subscription_ids();
assert_eq!(first.receiver_count(), 0, "no declared source is started");
assert_eq!(second.receiver_count(), 0, "no declared source is started");
store.finish();
}
#[test]
fn subscription_ids_emits_no_duplicate_ignored_warning() {
let recorder = TraceRecorder::new()
.with_target("tears::subscription")
.with_level(Level::WARN);
let _guard = recorder.set_default();
let store = TestStore::<DupApp>::new((MockSource::new(), MockSource::new()));
let _ = store.subscription_ids();
assert_eq!(
recorder.event_count(),
0,
"the duplicate-ignored warning is reconciliation's side effect, not the store's"
);
store.finish();
}
#[test]
#[should_panic(expected = "message mismatch")]
fn receive_fails_on_a_mismatch() {
let mut store = store_with(Command::message(Msg::N(1)), |_| Command::none());
store.receive(Msg::N(2));
}
#[test]
#[should_panic(expected = "assert it with TestStore::receive_quit")]
fn receive_fails_when_the_next_output_is_a_quit_request() {
let mut store = store_with(Command::<Msg>::quit(), |_| Command::none());
store.receive(Msg::N(1));
}
#[test]
#[should_panic(expected = "the next deliverable output is a message: N(1)")]
fn receive_quit_fails_when_the_next_output_is_a_message() {
let mut store = store_with(Command::message(Msg::N(1)), |_| Command::none());
store.receive_quit();
}
#[test]
#[should_panic(expected = "no deliverable output: no pending effects")]
fn receive_fails_with_no_pending_effects() {
let mut store = store_with(Command::<Msg>::none(), |_| Command::none());
store.receive(Msg::N(1));
}
#[test]
#[should_panic(expected = "no deliverable output: effects are pending but none is ready")]
fn receive_fails_with_effects_pending_but_not_ready() {
let mut store = store_with(Command::stream(stream::pending::<Msg>()), |_| {
Command::none()
});
store.receive(Msg::N(1));
}
#[test]
#[should_panic(expected = "predicate rejected the delivered message: N(1)")]
fn receive_matching_fails_when_the_predicate_rejects() {
let mut store = store_with(Command::message(Msg::N(1)), |_| Command::none());
store.receive_matching(|msg| matches!(msg, Msg::N(2)));
}
#[cfg(not(loom))]
#[test]
#[should_panic(expected = "IO is disabled")]
fn polling_an_io_dependent_leaf_fails_the_test() {
let mut store = store_with(
Command::batch([io_command(), Command::message(Msg::N(1))]),
|_| Command::none(),
);
store.receive(Msg::N(1));
}
#[test]
fn timeout_leaf_is_pending_until_advance_reaches_its_deadline() {
let mut store = store_with(Command::none(), |msg| match msg {
Msg::Start => Command::batch([
timeout_command(60),
Command::stream(stream::iter([Msg::N(1), Msg::N(2), Msg::N(3)])),
]),
_ => Command::none(),
});
store.send(Msg::Start);
store.receive(Msg::N(1));
store.advance(Duration::from_secs(30));
store.receive(Msg::N(2));
store.advance(Duration::from_secs(29));
store.receive(Msg::N(3));
store.advance(Duration::from_secs(1));
store.receive(Msg::N(99));
store.finish();
}
#[test]
fn timeout_deadline_anchors_at_first_poll_not_construction() {
let mut store = store_with(Command::none(), |msg| match msg {
Msg::Start => Command::batch([
timeout_command(60),
Command::stream(stream::iter([Msg::N(1)])),
]),
_ => Command::none(),
});
store.advance(Duration::from_secs(10));
store.send(Msg::Start);
store.advance(Duration::from_secs(59));
store.receive(Msg::N(1));
store.advance(Duration::from_secs(1));
store.receive(Msg::N(99));
store.finish();
}
#[test]
fn retry_backoff_delivers_after_an_advance_spanning_the_backoff() {
let mut store = store_with(Command::none(), |msg| match msg {
Msg::StartRetry => Command::batch([
Command::retry(
RetryPolicy::new(NonZeroUsize::new(2).expect("non-zero"))
.with_fixed_backoff(Duration::from_secs(5)),
|ctx| async move {
if ctx.attempt().get() == 1 {
Err("first attempt fails")
} else {
Ok(42)
}
},
|result| Msg::N(result.expect("the second attempt succeeds")),
),
Command::stream(stream::iter([Msg::N(1)])),
]),
_ => Command::none(),
});
store.send(Msg::StartRetry);
store.advance(Duration::from_secs(4));
store.receive(Msg::N(1));
store.advance(Duration::from_secs(1));
store.receive(Msg::N(42));
store.finish();
}
#[test]
fn equal_deadline_timeout_leaves_deliver_in_enqueue_order() {
let mut store = store_with(
Command::batch([
Command::future(pending()).timeout(Duration::from_secs(5), || Msg::N(1)),
Command::future(pending()).timeout(Duration::from_secs(5), || Msg::N(2)),
]),
|_| Command::none(),
);
store.advance(Duration::from_secs(5));
store.receive(Msg::N(1));
store.receive(Msg::N(2));
store.finish();
}
#[test]
fn advance_buffers_ready_output_without_delivering() {
let mut store = store_with(Command::message(Msg::N(1)), |_| Command::none());
store.advance(Duration::ZERO);
store.receive(Msg::N(1));
store.finish();
}
#[test]
#[should_panic(expected = "effect leaf(s) not driven to completion")]
fn finish_fails_on_an_unadvanced_timeout_leaf() {
let store = store_with(timeout_command(60), |_| Command::none());
store.finish();
}
}