use crate::helpers::Clock;
use crate::{App, Cmd};
use std::cell::Cell;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::time::Duration;
type BoxFuture<Msg> = Pin<Box<dyn Future<Output = Msg> + Send + 'static>>;
#[derive(Clone)]
pub struct FakeClock {
current: Rc<Cell<Duration>>,
}
impl FakeClock {
pub fn new() -> Self {
Self {
current: Rc::new(Cell::new(Duration::ZERO)),
}
}
pub fn advance(&self, duration: Duration) {
self.current.set(self.current.get() + duration);
}
pub fn set(&self, time: Duration) {
self.current.set(time);
}
pub fn get(&self) -> Duration {
self.current.get()
}
pub fn reset(&self) {
self.current.set(Duration::ZERO);
}
}
impl Default for FakeClock {
fn default() -> Self {
Self::new()
}
}
impl Clock for FakeClock {
fn now(&self) -> Duration {
self.current.get()
}
}
pub struct TestRunner<A: App> {
model: A::Model,
commands: Vec<CmdRecord<A::Msg>>,
pending_tasks: Vec<BoxFuture<A::Msg>>,
}
#[derive(Debug)]
pub enum CmdRecord<Msg> {
None,
Task,
Msg(Msg),
Batch(usize),
}
impl<A: App> TestRunner<A> {
pub fn new() -> Self {
let (model, init_cmd) = A::init();
let mut runner = Self {
model,
commands: Vec::new(),
pending_tasks: Vec::new(),
};
runner.record_cmd(init_cmd);
runner
}
pub fn with_model(model: A::Model) -> Self {
Self {
model,
commands: Vec::new(),
pending_tasks: Vec::new(),
}
}
pub fn send(&mut self, msg: A::Msg) -> &mut Self {
let cmd = A::update(&mut self.model, msg);
self.record_cmd(cmd);
self
}
pub fn send_all(&mut self, msgs: impl IntoIterator<Item = A::Msg>) -> &mut Self {
for msg in msgs {
self.send(msg);
}
self
}
pub fn model(&self) -> &A::Model {
&self.model
}
pub fn model_mut(&mut self) -> &mut A::Model {
&mut self.model
}
pub fn last_cmd(&self) -> Option<&CmdRecord<A::Msg>> {
self.commands.last()
}
pub fn commands(&self) -> &[CmdRecord<A::Msg>] {
&self.commands
}
pub fn clear_commands(&mut self) -> &mut Self {
self.commands.clear();
self
}
pub fn last_was_none(&self) -> bool {
matches!(self.last_cmd(), Some(CmdRecord::None))
}
pub fn last_was_task(&self) -> bool {
matches!(self.last_cmd(), Some(CmdRecord::Task))
}
pub fn last_was_msg(&self) -> bool {
matches!(self.last_cmd(), Some(CmdRecord::Msg(_)))
}
fn last_cmd_kind(&self) -> &'static str {
match self.last_cmd() {
Some(CmdRecord::None) => "None",
Some(CmdRecord::Task) => "Task",
Some(CmdRecord::Msg(_)) => "Msg",
Some(CmdRecord::Batch(_)) => "Batch",
None => "<no command>",
}
}
fn record_cmd(&mut self, cmd: Cmd<A::Msg>) {
let record = match cmd {
Cmd::None => CmdRecord::None,
Cmd::Task(future) => {
self.pending_tasks.push(future);
CmdRecord::Task
}
Cmd::Msg(msg) => CmdRecord::Msg(msg),
Cmd::Batch(cmds) => {
let len = cmds.len();
for cmd in cmds {
self.extract_tasks(cmd);
}
CmdRecord::Batch(len)
}
};
self.commands.push(record);
}
fn extract_tasks(&mut self, cmd: Cmd<A::Msg>) {
match cmd {
Cmd::None | Cmd::Msg(_) => {}
Cmd::Task(future) => {
self.pending_tasks.push(future);
}
Cmd::Batch(cmds) => {
for cmd in cmds {
self.extract_tasks(cmd);
}
}
}
}
pub fn pending_task_count(&self) -> usize {
self.pending_tasks.len()
}
pub fn has_pending_tasks(&self) -> bool {
!self.pending_tasks.is_empty()
}
pub async fn process_task(&mut self) -> bool {
if let Some(task) = self.pending_tasks.pop() {
let msg = task.await;
self.send(msg);
true
} else {
false
}
}
pub async fn process_tasks(&mut self) -> &mut Self {
while let Some(task) = self.pending_tasks.pop() {
let msg = task.await;
self.send(msg);
}
self
}
pub async fn process_n_tasks(&mut self, n: usize) -> &mut Self {
for i in 0..n {
assert!(
!self.pending_tasks.is_empty(),
"process_n_tasks: expected {} tasks but only {} were available",
n,
i
);
let task = self.pending_tasks.remove(0);
let msg = task.await;
self.send(msg);
}
self
}
pub fn expect_model(&mut self, predicate: impl FnOnce(&A::Model) -> bool) -> &mut Self {
assert!(
predicate(&self.model),
"expect_model: predicate returned false"
);
self
}
pub fn expect_model_msg(
&mut self,
predicate: impl FnOnce(&A::Model) -> bool,
msg: &str,
) -> &mut Self {
assert!(predicate(&self.model), "expect_model: {}", msg);
self
}
pub fn expect_cmd_none(&mut self) -> &mut Self {
assert!(
self.last_was_none(),
"expect_cmd_none: last command was {}, expected None",
self.last_cmd_kind()
);
self
}
pub fn expect_cmd_task(&mut self) -> &mut Self {
assert!(
self.last_was_task(),
"expect_cmd_task: last command was {}, expected Task",
self.last_cmd_kind()
);
self
}
pub fn expect_cmd_msg(&mut self) -> &mut Self {
assert!(
self.last_was_msg(),
"expect_cmd_msg: last command was {}, expected Msg",
self.last_cmd_kind()
);
self
}
pub fn expect_cmd_msg_eq(&mut self, expected: A::Msg) -> &mut Self
where
A::Msg: PartialEq + std::fmt::Debug,
{
match self.last_cmd() {
Some(CmdRecord::Msg(msg)) => {
assert_eq!(msg, &expected, "expect_cmd_msg_eq: message mismatch");
}
_ => {
panic!(
"expect_cmd_msg_eq: last command was {}, expected Msg({:?})",
self.last_cmd_kind(),
expected
);
}
}
self
}
pub fn expect_cmd_batch(&mut self) -> &mut Self {
assert!(
matches!(self.last_cmd(), Some(CmdRecord::Batch(_))),
"expect_cmd_batch: last command was {}, expected Batch",
self.last_cmd_kind()
);
self
}
pub fn expect_cmd_batch_size(&mut self, expected_size: usize) -> &mut Self {
match self.last_cmd() {
Some(CmdRecord::Batch(size)) => {
assert_eq!(
*size, expected_size,
"expect_cmd_batch_size: batch size mismatch (got {}, expected {})",
size, expected_size
);
}
_ => {
panic!(
"expect_cmd_batch_size: last command was {}, expected Batch({})",
self.last_cmd_kind(),
expected_size
);
}
}
self
}
}
impl<A: App> Default for TestRunner<A> {
fn default() -> Self {
Self::new()
}
}
pub trait ModelAssert<T> {
fn assert_that(&self, predicate: impl FnOnce(&T) -> bool, msg: &str);
}
impl<A: App> ModelAssert<A::Model> for TestRunner<A> {
fn assert_that(&self, predicate: impl FnOnce(&A::Model) -> bool, msg: &str) {
assert!(predicate(&self.model), "{}", msg);
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestApp;
#[derive(Default)]
struct TestModel {
value: i32,
}
#[derive(Clone, Debug, PartialEq)]
enum TestMsg {
Inc,
Dec,
Set(i32),
Delayed,
MultiBatch,
AsyncFetch,
FetchResult(i32),
}
impl App for TestApp {
type Model = TestModel;
type Msg = TestMsg;
fn init() -> (Self::Model, Cmd<Self::Msg>) {
(TestModel::default(), Cmd::none())
}
fn update(model: &mut Self::Model, msg: Self::Msg) -> Cmd<Self::Msg> {
match msg {
TestMsg::Inc => model.value += 1,
TestMsg::Dec => model.value -= 1,
TestMsg::Set(v) => model.value = v,
TestMsg::Delayed => {
return Cmd::msg(TestMsg::Inc);
}
TestMsg::MultiBatch => {
return Cmd::batch([Cmd::msg(TestMsg::Inc), Cmd::msg(TestMsg::Inc)]);
}
TestMsg::AsyncFetch => {
return Cmd::task(async { TestMsg::FetchResult(42) });
}
TestMsg::FetchResult(v) => model.value = v,
}
Cmd::none()
}
fn view(_model: &Self::Model, _ctx: &mut crate::ViewCtx<Self::Msg>) {
}
}
#[test]
fn test_runner_basic() {
let mut runner = TestRunner::<TestApp>::new();
runner.send(TestMsg::Inc);
assert_eq!(runner.model().value, 1);
runner.send(TestMsg::Inc).send(TestMsg::Inc);
assert_eq!(runner.model().value, 3);
runner.send(TestMsg::Dec);
assert_eq!(runner.model().value, 2);
}
#[test]
fn test_runner_cmd_tracking() {
let mut runner = TestRunner::<TestApp>::new();
runner.send(TestMsg::Inc);
assert!(runner.last_was_none());
runner.send(TestMsg::Delayed);
assert!(runner.last_was_msg());
}
#[test]
fn test_runner_send_all() {
let mut runner = TestRunner::<TestApp>::new();
runner.send_all([TestMsg::Inc, TestMsg::Inc, TestMsg::Inc]);
assert_eq!(runner.model().value, 3);
}
#[test]
fn test_expect_model() {
let mut runner = TestRunner::<TestApp>::new();
runner
.send(TestMsg::Inc)
.expect_model(|m| m.value == 1)
.send(TestMsg::Inc)
.expect_model(|m| m.value == 2)
.send(TestMsg::Set(100))
.expect_model(|m| m.value == 100);
}
#[test]
fn test_expect_cmd_none() {
let mut runner = TestRunner::<TestApp>::new();
runner.send(TestMsg::Inc).expect_cmd_none();
}
#[test]
fn test_expect_cmd_msg() {
let mut runner = TestRunner::<TestApp>::new();
runner.send(TestMsg::Delayed).expect_cmd_msg();
}
#[test]
fn test_expect_cmd_msg_eq() {
let mut runner = TestRunner::<TestApp>::new();
runner
.send(TestMsg::Delayed)
.expect_cmd_msg_eq(TestMsg::Inc);
}
#[test]
fn test_expect_cmd_batch() {
let mut runner = TestRunner::<TestApp>::new();
runner
.send(TestMsg::MultiBatch)
.expect_cmd_batch()
.expect_cmd_batch_size(2);
}
#[test]
fn test_expect_chaining() {
let mut runner = TestRunner::<TestApp>::new();
runner
.send(TestMsg::Inc)
.expect_model(|m| m.value == 1)
.expect_cmd_none()
.send(TestMsg::Inc)
.expect_model(|m| m.value == 2)
.expect_cmd_none()
.send(TestMsg::Delayed)
.expect_model(|m| m.value == 2) .expect_cmd_msg_eq(TestMsg::Inc);
}
#[cfg(feature = "tokio")]
fn block_on<F: std::future::Future>(f: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(f)
}
#[test]
#[cfg(feature = "tokio")]
fn test_process_task() {
block_on(async {
let mut runner = TestRunner::<TestApp>::new();
runner.send(TestMsg::AsyncFetch);
assert!(runner.last_was_task());
assert!(runner.has_pending_tasks());
assert_eq!(runner.pending_task_count(), 1);
assert_eq!(runner.model().value, 0);
runner.process_task().await;
assert!(!runner.has_pending_tasks());
assert_eq!(runner.model().value, 42);
});
}
#[test]
#[cfg(feature = "tokio")]
fn test_process_tasks() {
block_on(async {
let mut runner = TestRunner::<TestApp>::new();
runner.send(TestMsg::AsyncFetch);
runner.send(TestMsg::AsyncFetch);
assert_eq!(runner.pending_task_count(), 2);
runner.process_tasks().await;
assert!(!runner.has_pending_tasks());
assert_eq!(runner.model().value, 42);
});
}
#[test]
#[cfg(feature = "tokio")]
fn test_async_expect_chaining() {
block_on(async {
let mut runner = TestRunner::<TestApp>::new();
runner
.send(TestMsg::Inc)
.expect_model(|m| m.value == 1)
.expect_cmd_none()
.send(TestMsg::AsyncFetch)
.expect_cmd_task();
runner.process_tasks().await;
runner.expect_model(|m| m.value == 42);
});
}
#[test]
fn test_fake_clock_basic() {
let clock = super::FakeClock::new();
assert_eq!(clock.get(), Duration::ZERO);
clock.advance(Duration::from_millis(100));
assert_eq!(clock.get(), Duration::from_millis(100));
clock.advance(Duration::from_millis(50));
assert_eq!(clock.get(), Duration::from_millis(150));
}
#[test]
fn test_fake_clock_set_and_reset() {
let clock = super::FakeClock::new();
clock.set(Duration::from_secs(10));
assert_eq!(clock.get(), Duration::from_secs(10));
clock.reset();
assert_eq!(clock.get(), Duration::ZERO);
}
#[test]
fn test_fake_clock_shared() {
let clock1 = super::FakeClock::new();
let clock2 = clock1.clone();
clock1.advance(Duration::from_millis(100));
assert_eq!(clock2.get(), Duration::from_millis(100));
}
#[test]
#[cfg(feature = "tokio")]
fn test_debouncer_with_fake_clock() {
use crate::helpers::DebouncerWithClock;
let clock = super::FakeClock::new();
let mut debouncer = DebouncerWithClock::new(clock.clone());
let _cmd = debouncer.trigger(Duration::from_millis(500), ());
assert!(debouncer.is_pending());
assert!(!debouncer.should_fire());
clock.advance(Duration::from_millis(300));
assert!(!debouncer.should_fire());
clock.advance(Duration::from_millis(100));
assert!(!debouncer.should_fire());
clock.advance(Duration::from_millis(150));
assert!(debouncer.should_fire());
assert!(!debouncer.is_pending());
}
#[test]
#[cfg(feature = "tokio")]
fn test_debouncer_reset_with_fake_clock() {
use crate::helpers::DebouncerWithClock;
let clock = super::FakeClock::new();
let mut debouncer = DebouncerWithClock::new(clock.clone());
let _cmd = debouncer.trigger(Duration::from_millis(500), ());
clock.advance(Duration::from_millis(300));
assert!(!debouncer.should_fire());
let _cmd = debouncer.trigger(Duration::from_millis(500), ());
clock.advance(Duration::from_millis(300));
assert!(!debouncer.should_fire());
clock.advance(Duration::from_millis(250));
assert!(debouncer.should_fire());
}
#[test]
fn test_debouncer_with_fake_clock_mark_trigger() {
use crate::helpers::DebouncerWithClock;
let clock = super::FakeClock::new();
let mut debouncer = DebouncerWithClock::new(clock.clone());
debouncer.mark_trigger(Duration::from_millis(500));
assert!(debouncer.is_pending());
assert!(!debouncer.should_fire());
clock.advance(Duration::from_millis(550));
assert!(debouncer.should_fire());
assert!(!debouncer.is_pending());
}
#[test]
fn test_throttler_with_fake_clock() {
use crate::helpers::ThrottlerWithClock;
let clock = super::FakeClock::new();
let mut throttler = ThrottlerWithClock::new(clock.clone());
let interval = Duration::from_millis(100);
let cmd1 = throttler.run(interval, || Cmd::Msg(1));
assert!(cmd1.is_msg());
let cmd2 = throttler.run(interval, || Cmd::Msg(2));
assert!(cmd2.is_none());
clock.advance(Duration::from_millis(50));
let cmd3 = throttler.run(interval, || Cmd::Msg(3));
assert!(cmd3.is_none());
clock.advance(Duration::from_millis(60));
let cmd4 = throttler.run(interval, || Cmd::Msg(4));
assert!(cmd4.is_msg());
}
#[test]
fn test_throttler_time_remaining_with_fake_clock() {
use crate::helpers::ThrottlerWithClock;
let clock = super::FakeClock::new();
let mut throttler = ThrottlerWithClock::new(clock.clone());
let interval = Duration::from_millis(100);
assert!(throttler.time_remaining(interval).is_none());
let _ = throttler.run(interval, || Cmd::Msg(1));
let remaining = throttler.time_remaining(interval);
assert_eq!(remaining, Some(Duration::from_millis(100)));
clock.advance(Duration::from_millis(30));
let remaining = throttler.time_remaining(interval);
assert_eq!(remaining, Some(Duration::from_millis(70)));
clock.advance(Duration::from_millis(80));
assert!(throttler.time_remaining(interval).is_none());
}
}