1use std::collections::VecDeque;
2use std::fmt;
3use std::time::{Duration, Instant};
4
5use crate::cmd::Cmd;
6use crate::effect::{run_leaf, Effect};
7use crate::env::Environment;
8
9pub struct ExhaustiveTestStore<S, M> {
11 pub state: S,
12 update: fn(&mut S, M) -> Cmd<M>,
13 env: Environment,
14 pending_actions: VecDeque<M>,
15 pending_effects: Vec<Effect<M>>,
16 exhaustive: bool,
17}
18
19impl<S, M> ExhaustiveTestStore<S, M>
20where
21 S: fmt::Debug + PartialEq + Clone,
22 M: PartialEq + fmt::Debug + Send + 'static,
23{
24 pub fn new(state: S, update: fn(&mut S, M) -> Cmd<M>) -> Self {
25 Self {
26 state,
27 update,
28 env: Environment::test(),
29 pending_actions: VecDeque::new(),
30 pending_effects: Vec::new(),
31 exhaustive: true,
32 }
33 }
34
35 pub fn with_exhaustivity(mut self, on: bool) -> Self {
36 self.exhaustive = on;
37 self
38 }
39
40 pub fn send(&mut self, action: M) {
41 self.send_expect(action, None);
42 }
43
44 pub fn send_with(&mut self, action: M, expect: impl FnOnce(&mut S)) {
45 let mut expected = self.state.clone();
46 expect(&mut expected);
47 self.send_expect(action, Some(expected));
48 }
49
50 fn send_expect(&mut self, action: M, expected: Option<S>) {
51 if self.exhaustive {
52 self.assert_idle();
53 }
54 let cmd = (self.update)(&mut self.state, action);
55 if let Some(expected) = expected {
56 assert_state_eq(&expected, &self.state);
57 }
58 self.enqueue_effects(cmd.into_effects());
59 }
60
61 pub fn receive(&mut self, expected: M) {
62 let actual = self
63 .pending_actions
64 .pop_front()
65 .unwrap_or_else(|| panic!("expected action {expected:?}, but effect queue was empty"));
66 assert_eq!(actual, expected, "unexpected effect-produced action");
67 let cmd = (self.update)(&mut self.state, actual);
68 self.enqueue_effects(cmd.into_effects());
69 }
70
71 pub fn finish(&self) {
72 if self.exhaustive {
73 self.assert_idle();
74 }
75 }
76
77 fn assert_idle(&self) {
78 assert!(
79 self.pending_actions.is_empty(),
80 "unconsumed effect actions: {:?}",
81 self.pending_actions
82 );
83 assert!(
84 self.pending_effects.is_empty(),
85 "uninterpreted effects: {:?}",
86 self.pending_effects
87 );
88 }
89
90 fn enqueue_effects(&mut self, effects: Vec<Effect<M>>) {
91 for effect in effects {
92 self.pending_effects.push(effect);
93 }
94 while let Some(effect) = self.pending_effects.pop() {
95 self.run_effect_leaf(effect);
96 }
97 }
98
99 fn run_effect_leaf(&mut self, effect: Effect<M>) {
100 match effect {
101 Effect::None => {}
102 Effect::Batch(items) | Effect::Race(items) => {
103 for item in items {
104 self.run_effect_leaf(item);
105 }
106 }
107 Effect::Sequence(items) => {
108 for item in items {
109 self.run_effect_leaf(item);
110 }
111 }
112 leaf => {
113 let fut = run_leaf(leaf, &self.env);
114 let msg = tokio_test_block_on(fut);
115 match msg {
116 Ok(action) => self.pending_actions.push_back(action),
117 Err(err) => panic!("effect failed in test store: {err}"),
118 }
119 }
120 }
121 }
122
123 pub fn receive_timeout(&mut self, expected: M, timeout: Duration) -> Result<(), TestStoreError> {
124 let deadline = Instant::now() + timeout;
125 while Instant::now() < deadline {
126 if !self.pending_actions.is_empty() {
127 self.receive(expected);
128 return Ok(());
129 }
130 std::thread::sleep(Duration::from_millis(1));
131 }
132 Err(TestStoreError::ReceiveTimeout)
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub enum TestStoreError {
138 ReceiveTimeout,
139}
140
141fn assert_state_eq<S: fmt::Debug + PartialEq>(expected: &S, actual: &S) {
142 assert_eq!(
143 expected, actual,
144 "state mismatch after send\n expected: {expected:?}\n actual: {actual:?}"
145 );
146}
147
148fn tokio_test_block_on<M: Send + 'static>(
149 fut: std::pin::Pin<
150 Box<dyn std::future::Future<Output = Result<M, crate::EffectError>> + Send>,
151 >,
152) -> Result<M, crate::EffectError> {
153 tokio::runtime::Builder::new_current_thread()
154 .enable_all()
155 .build()
156 .expect("test tokio runtime")
157 .block_on(fut)
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::effect::Effect;
164 use crate::panic_on_state_clone;
165 use crate::test_support::allow_state_clones;
166
167 panic_on_state_clone! {
168 #[derive(Debug, PartialEq, Eq, Default)]
169 struct Counter {
170 n: i32,
171 }
172 }
173
174 fn update(s: &mut Counter, msg: i32) -> Cmd<i32> {
175 match msg {
176 0 => Cmd::single(Effect::task(1, || Box::pin(async { Ok(10) }))),
177 n => {
178 s.n = n;
179 Cmd::none()
180 }
181 }
182 }
183
184 #[test]
185 fn exhaustive_requires_effect_actions_be_received() {
186 let mut store = ExhaustiveTestStore::new(Counter::default(), update);
187 store.send(0);
188 assert_eq!(store.pending_actions.len(), 1);
189 store.receive(10);
190 store.finish();
191 assert_eq!(store.state.n, 10);
192 }
193
194 #[test]
195 fn send_with_validates_expected_state() {
196 let mut store = ExhaustiveTestStore::new(Counter::default(), update);
197 allow_state_clones(1, || {
198 store.send_with(5, |s| {
199 s.n = 5;
200 });
201 });
202 store.finish();
203 }
204
205 #[test]
206 #[should_panic(expected = "unconsumed effect actions")]
207 fn finish_fails_when_actions_remain() {
208 let mut store = ExhaustiveTestStore::new(Counter::default(), update);
209 store.send(0);
210 store.finish();
211 }
212
213 #[test]
214 fn non_exhaustive_allows_leftover_actions() {
215 let mut store = ExhaustiveTestStore::new(Counter::default(), update).with_exhaustivity(false);
216 store.send(0);
217 store.finish();
218 }
219}