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