Skip to main content

rust_elm/
test_support.rs

1//! Test helpers for asserting store/reducer paths do not clone feature state.
2//!
3//! Wrap reducer state types with [`panic_on_state_clone!`] and allow only explicit
4//! clones via [`allow_state_clones`].
5
6use std::cell::Cell;
7
8use crate::optics::Casepath;
9use crate::store::{ScopedStateSubscriber, StateSubscriber};
10
11thread_local! {
12    static CLONE_BUDGET: Cell<usize> = const { Cell::new(0) };
13}
14
15/// Panics unless the current thread has remaining clone budget from [`allow_state_clones`].
16pub fn on_state_clone(type_name: &str) {
17    CLONE_BUDGET.with(|budget| {
18        let remaining = budget.get();
19        assert!(
20            remaining > 0,
21            "unexpected clone of {type_name} (clone budget exhausted)"
22        );
23        budget.set(remaining - 1);
24    });
25}
26
27/// Temporarily allow `count` feature-state clones on this thread.
28pub fn allow_state_clones<T>(count: usize, f: impl FnOnce() -> T) -> T {
29    CLONE_BUDGET.with(|budget| {
30        let prev = budget.get();
31        budget.set(prev + count);
32        let out = f();
33        budget.set(prev);
34        out
35    })
36}
37
38/// [`Store::state`] clones feature state once — use in tests that intentionally read a snapshot.
39pub fn store_state<S, M>(store: &crate::Store<S, M>) -> S
40where
41    S: Send + Sync + Clone + 'static,
42    M: Send + 'static,
43{
44    allow_state_clones(1, || store.state())
45}
46
47/// [`Store::subscribe_state`] clones once when the subscriber is created.
48pub fn subscribe_state<S, M>(
49    store: &crate::Store<S, M>,
50) -> crate::StateSubscriber<S>
51where
52    S: Clone + PartialEq + Send + Sync + 'static,
53    M: Send + 'static,
54{
55    allow_state_clones(1, || store.subscribe_state())
56}
57
58/// [`ScopedStore::child_state`] clones parent state once via [`Store::state`].
59pub fn scoped_child_state<S, M, CS, CM, AK, SK>(
60    scoped: &crate::ScopedStore<S, M, CS, CM, AK, SK>,
61) -> Option<CS>
62where
63    S: Send + Sync + Clone,
64    M: Send,
65    CS: Clone + PartialEq + Send + Sync,
66    CM: Clone + Send,
67    AK: Casepath<M, CM> + Clone + Send + Sync + 'static,
68    SK: crate::StateLens<S, CS> + Clone,
69{
70    allow_state_clones(1, || scoped.child_state())
71}
72
73/// [`ScopedStore::subscribe_state`] clones parent state once when the subscriber is created.
74pub fn scoped_subscribe_state<S, M, CS, CM, AK, SK>(
75    scoped: &crate::ScopedStore<S, M, CS, CM, AK, SK>,
76) -> ScopedStateSubscriber<S, CS, SK>
77where
78    S: Send + Sync + Clone + PartialEq,
79    M: Send,
80    CS: Clone + PartialEq + Send + Sync,
81    CM: Clone + Send,
82    AK: Casepath<M, CM> + Clone + Send + Sync + 'static,
83    SK: crate::StateLens<S, CS> + Clone,
84{
85    allow_state_clones(1, || scoped.subscribe_state())
86}
87
88/// One [`StateSubscriber::next`] / [`StateSubscriber::wait_next`] may clone feature state once.
89pub fn subscriber_wait_next<S>(
90    sub: &mut StateSubscriber<S>,
91    timeout: std::time::Duration,
92) -> Option<std::sync::Arc<S>>
93where
94    S: PartialEq + Clone,
95{
96    allow_state_clones(1, || sub.wait_next(timeout))
97}
98
99/// One [`ScopedStateSubscriber::next`] may clone parent feature state once.
100pub fn scoped_subscriber_next<S, CS, SK>(
101    sub: &mut ScopedStateSubscriber<S, CS, SK>,
102) -> Option<CS>
103where
104    S: PartialEq + Clone,
105    CS: Clone + PartialEq,
106    SK: crate::StateLens<S, CS> + Clone,
107{
108    allow_state_clones(1, || sub.next())
109}
110
111/// [`ReplayHarness::snapshot`] clones feature state once.
112pub fn replay_snapshot<S, M>(harness: &crate::ReplayHarness<S, M>) -> S
113where
114    S: Clone,
115    M: Clone,
116{
117    allow_state_clones(1, || harness.snapshot())
118}
119
120/// [`Shared::with_mut`] clones feature state once to detect changes.
121pub fn shared_with_mut<T, R>(shared: &crate::Shared<T>, f: impl FnOnce(&mut T) -> R) -> R
122where
123    T: Clone + PartialEq,
124{
125    allow_state_clones(1, || shared.with_mut(f))
126}
127
128/// [`Shared::get`] clones feature state once.
129pub fn shared_get<S>(shared: &crate::Shared<S>) -> S
130where
131    S: Clone,
132{
133    allow_state_clones(1, || shared.get())
134}
135
136/// Define a reducer/store state struct whose [`Clone`] panics unless budget is available.
137#[macro_export]
138macro_rules! panic_on_state_clone {
139    (
140        $(#[$meta:meta])*
141        $vis:vis struct $name:ident {
142            $($field:ident: $ty:ty),* $(,)?
143        }
144    ) => {
145        $(#[$meta])*
146        $vis struct $name {
147            $($field: $ty),*
148        }
149
150        impl Clone for $name {
151            fn clone(&self) -> Self {
152                $crate::test_support::on_state_clone(concat!(module_path!(), "::", stringify!($name)));
153                Self {
154                    $($field: self.$field.clone()),*
155                }
156            }
157        }
158    };
159}
160
161pub use panic_on_state_clone;