1use std::cell::RefCell;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use parking_lot::{Mutex, RwLock};
8
9use rust_dependencies::{Clock, ClockDep, DependencyError, DependencyValues};
10
11pub use rust_dependencies::{
12 ClockKey, DepRng, DependencyKey, LiveRng, LiveUuidGen, Now, NowDep, NowKey, RealClock,
13 RngDep, RngKey, SeededRng, SeededUuidGen, TestNow, UuidGen, UuidKey,
14};
15
16#[derive(Debug, Clone)]
18pub struct FakeClock {
19 inner: Arc<Mutex<Instant>>,
20}
21
22impl FakeClock {
23 pub fn new(start: Instant) -> Self {
24 Self {
25 inner: Arc::new(Mutex::new(start)),
26 }
27 }
28
29 pub fn now(&self) -> Instant {
30 *self.inner.lock()
31 }
32
33 pub fn advance(&self, duration: Duration) {
34 *self.inner.lock() += duration;
35 }
36}
37
38impl Clock for FakeClock {
39 fn now(&self) -> Instant {
40 FakeClock::now(self)
41 }
42}
43
44#[derive(Debug, Clone, Default)]
46pub struct MockHttp {
47 responses: Arc<Mutex<Vec<(String, String)>>>,
48}
49
50impl MockHttp {
51 pub fn new() -> Self {
52 Self::default()
53 }
54
55 pub fn stub(&self, url: impl Into<String>, body: impl Into<String>) {
56 self.responses.lock().push((url.into(), body.into()));
57 }
58
59 pub fn get(&self, url: &str) -> Option<String> {
60 self.responses
61 .lock()
62 .iter()
63 .find(|(u, _)| u == url)
64 .map(|(_, b)| b.clone())
65 }
66}
67
68#[derive(Clone)]
70pub struct Environment {
71 layers: Arc<Mutex<Vec<DependencyValues>>>,
72 merged_cache: Arc<RwLock<Option<DependencyValues>>>,
73}
74
75impl Default for Environment {
76 fn default() -> Self {
77 Self::live()
78 }
79}
80
81impl std::fmt::Debug for Environment {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct("Environment")
84 .field("layers", &self.layers.lock().len())
85 .finish()
86 }
87}
88
89impl Environment {
90 pub fn new() -> Self {
91 Self::live()
92 }
93
94 pub fn live() -> Self {
95 Self::from_values(DependencyValues::live())
96 }
97
98 pub fn test() -> Self {
99 Self::from_values(DependencyValues::test())
100 }
101
102 pub fn from_values(base: DependencyValues) -> Self {
103 Self {
104 layers: Arc::new(Mutex::new(vec![base])),
105 merged_cache: Arc::new(RwLock::new(None)),
106 }
107 }
108
109 fn invalidate_merged_cache(&self) {
110 *self.merged_cache.write() = None;
111 }
112
113 pub fn values(&self) -> DependencyValues {
114 if let Some(cached) = self.merged_cache.read().clone() {
115 return cached;
116 }
117 let layers = self.layers.lock();
118 let merged = DependencyValues::new();
119 for layer in layers.iter() {
120 merged.merge_from(layer);
121 }
122 *self.merged_cache.write() = Some(merged.clone());
123 merged
124 }
125
126 pub fn get<D: Send + Sync + 'static>(&self) -> Option<Arc<D>> {
127 let layers = self.layers.lock();
128 for layer in layers.iter().rev() {
129 if let Some(value) = layer.get::<D>() {
130 return Some(value);
131 }
132 }
133 None
134 }
135
136 pub fn require<D: Send + Sync + 'static>(&self) -> Result<Arc<D>, DependencyError> {
137 self.get::<D>()
138 .ok_or_else(|| DependencyError::missing(std::any::type_name::<D>()))
139 }
140
141 pub fn with<D: Send + Sync + 'static>(self, value: D) -> Self {
142 let overlay = DependencyValues::new();
143 overlay.insert(value);
144 self.push_values(overlay);
145 self
146 }
147
148 pub fn push_values(&self, overlay: DependencyValues) {
149 self.layers.lock().push(overlay);
150 self.invalidate_merged_cache();
151 }
152
153 pub fn scoped_with(&self, overlay: Environment) -> Self {
155 let mut layers = self.layers.lock().clone();
156 layers.extend(overlay.layers.lock().iter().cloned());
157 Self {
158 layers: Arc::new(Mutex::new(layers)),
159 merged_cache: Arc::new(RwLock::new(None)),
160 }
161 }
162
163 pub fn with_clock(self, clock: FakeClock) -> Self {
164 self.with(clock.clone()).with(ClockDep(Arc::new(clock)))
165 }
166
167 pub fn with_http(self, http: MockHttp) -> Self {
168 self.with(http)
169 }
170
171 pub fn clock(&self) -> Option<FakeClock> {
172 self.get::<FakeClock>().map(|c| c.as_ref().clone())
173 }
174
175 pub fn http(&self) -> Option<MockHttp> {
176 self.get::<MockHttp>().map(|arc| (*arc).clone())
177 }
178
179 pub fn push_layer(&self, overlay: Environment) {
181 self.layers
182 .lock()
183 .extend(overlay.layers.lock().iter().cloned());
184 self.invalidate_merged_cache();
185 }
186}
187
188thread_local! {
189 static BATCH_QUEUE: RefCell<Vec<Box<dyn FnOnce() + 'static>>> = RefCell::new(Vec::new());
190}
191
192pub fn batch<R>(f: impl FnOnce() -> R) -> R {
194 BATCH_QUEUE.with(|q| q.borrow_mut().clear());
195 let result = f();
196 BATCH_QUEUE.with(|q| {
197 let mut queue = q.borrow_mut();
198 while let Some(job) = queue.pop() {
199 job();
200 }
201 });
202 result
203}
204
205pub fn defer_batch(job: impl FnOnce() + 'static) {
206 BATCH_QUEUE.with(|q| q.borrow_mut().push(Box::new(job)));
207}
208
209pub type EnvFuture<M> = Pin<Box<dyn Future<Output = Result<M, crate::error::EffectError>> + Send>>;
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use rust_dependencies::{RngDep, SeededUuidGen, UuidDep};
215
216 #[test]
217 fn fake_clock_advances() {
218 let start = Instant::now();
219 let clock = FakeClock::new(start);
220 clock.advance(Duration::from_millis(100));
221 assert_eq!(clock.now(), start + Duration::from_millis(100));
222 }
223
224 #[test]
225 fn mock_http_stubs_responses() {
226 let http = MockHttp::new();
227 http.stub("https://example.com", "ok");
228 assert_eq!(http.get("https://example.com"), Some("ok".into()));
229 }
230
231 #[test]
232 fn test_env_is_deterministic() {
233 let env = Environment::test();
234 let Ok(uuid_dep) = env.require::<UuidDep>() else {
235 panic!("missing uuid dependency");
236 };
237 let uuid1 = uuid_dep.0.next();
238 let Ok(rng_dep) = env.require::<RngDep>() else {
239 panic!("missing rng dependency");
240 };
241 let rng1 = rng_dep.0.next_u64();
242 let env2 = Environment::test();
243 let Ok(uuid_dep2) = env2.require::<UuidDep>() else {
244 panic!("missing uuid dependency");
245 };
246 let uuid2 = uuid_dep2.0.next();
247 let Ok(rng_dep2) = env2.require::<RngDep>() else {
248 panic!("missing rng dependency");
249 };
250 let rng2 = rng_dep2.0.next_u64();
251 assert_eq!(uuid1, uuid2);
252 assert_eq!(rng1, rng2);
253 }
254
255 #[test]
256 fn scoped_override_replaces_dependency() {
257 let env = Environment::test();
258 let Ok(base_dep) = env.require::<UuidDep>() else {
259 panic!("missing uuid dependency");
260 };
261 let base = base_dep.0.next();
262 let scoped = env.scoped_with(Environment::new().with(UuidDep(Arc::new(SeededUuidGen::new(
263 99,
264 )))));
265 let Ok(overridden_dep) = scoped.require::<UuidDep>() else {
266 panic!("missing uuid dependency");
267 };
268 let overridden = overridden_dep.0.next();
269 assert_ne!(base, overridden);
270 assert_eq!(overridden, SeededUuidGen::new(99).next());
271 }
272}