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 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 match tokio::runtime::Builder::new_current_thread()
154 .enable_all()
155 .build()
156 {
157 Ok(rt) => rt.block_on(fut),
158 Err(err) => Err(crate::EffectError::Other(format!(
159 "failed to create test tokio runtime: {err}"
160 ))),
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::effect::Effect;
168 use crate::panic_on_state_clone;
169 use crate::test_support::allow_state_clones;
170
171 panic_on_state_clone! {
172 #[derive(Debug, PartialEq, Eq, Default)]
173 struct Counter {
174 n: i32,
175 }
176 }
177
178 fn update(s: &mut Counter, msg: i32) -> Cmd<i32> {
179 match msg {
180 0 => Cmd::single(Effect::task(1, || Box::pin(async { Ok(10) }))),
181 n => {
182 s.n = n;
183 Cmd::none()
184 }
185 }
186 }
187
188 #[test]
189 fn exhaustive_requires_effect_actions_be_received() {
190 let mut store = ExhaustiveTestStore::new(Counter::default(), update);
191 store.send(0);
192 assert_eq!(store.pending_actions.len(), 1);
193 store.receive(10);
194 store.finish();
195 assert_eq!(store.state.n, 10);
196 }
197
198 #[test]
199 fn send_with_validates_expected_state() {
200 let mut store = ExhaustiveTestStore::new(Counter::default(), update);
201 allow_state_clones(1, || {
202 store.send_with(5, |s| {
203 s.n = 5;
204 });
205 });
206 store.finish();
207 }
208
209 #[test]
210 #[should_panic(expected = "unconsumed effect actions")]
211 fn finish_fails_when_actions_remain() {
212 let mut store = ExhaustiveTestStore::new(Counter::default(), update);
213 store.send(0);
214 store.finish();
215 }
216
217 #[test]
218 fn non_exhaustive_allows_leftover_actions() {
219 let mut store = ExhaustiveTestStore::new(Counter::default(), update).with_exhaustivity(false);
220 store.send(0);
221 store.finish();
222 }
223}