Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/oxide-core/tests/state_comprehensive_tests.rs
StratoSDK / crates / oxide-core / tests / state_comprehensive_tests.rs
1//! Comprehensive tests for the state management system
2//!
3//! This test suite aims to achieve >80% coverage of the state module
4 
5use oxide_core::state::{Signal, ReactiveContext, Disposable};
6use oxide_core::Result;
7use std::sync::{Arc, Mutex};
8use std::thread;
9use std::time::Duration;
10 
11#[test]
12fn test_signal_creation_and_basic_operations() {
13 let signal = Signal::new(42);
14 assert_eq!(signal.get(), 42);
15
16 signal.set(100);
17 assert_eq!(signal.get(), 100);
18}
19 
20#[test]
21fn test_signal_peek_does_not_track_dependency() {
22 let signal = Signal::new(10);
23
24 // peek should not track dependency
25 let value = signal.peek();
26 assert_eq!(value, 10);
27
28 // get should track dependency
29 let value = signal.get();
30 assert_eq!(value, 10);
31}
32 
33#[test]
34fn test_signal_update_with_function() {
35 let signal = Signal::new(0);
36
37 signal.update(|v| *v += 10);
38 assert_eq!(signal.get(), 10);
39
40 signal.update(|v| *v *= 2);
41 assert_eq!(signal.get(), 20);
42}
43 
44#[test]
45fn test_signal_subscription() {
46 let signal = Signal::new(0);
47 let received = Arc::new(Mutex::new(Vec::new()));
48 let received_clone = received.clone();
49
50 let _disposable = signal.subscribe(Box::new(move |value| {
51 if let Some(v) = value.downcast_ref::<i32>() {
52 received_clone.lock().unwrap().push(*v);
53 }
54 }));
55
56 signal.set(1);
57 signal.set(2);
58 signal.set(3);
59
60 // Give time for notifications
61 thread::sleep(Duration::from_millis(10));
62
63 let values = received.lock().unwrap();
64 assert_eq!(*values, vec![1, 2, 3]);
65}
66 
67#[test]
68fn test_signal_multiple_subscribers() {
69 let signal = Signal::new(0);
70 let count1 = Arc::new(Mutex::new(0));
71 let count2 = Arc::new(Mutex::new(0));
72
73 let count1_clone = count1.clone();
74 let _disposable1 = signal.subscribe(Box::new(move |_| {
75 *count1_clone.lock().unwrap() += 1;
76 }));
77
78 let count2_clone = count2.clone();
79 let _disposable2 = signal.subscribe(Box::new(move |_| {
80 *count2_clone.lock().unwrap() += 1;
81 }));
82
83 signal.set(1);
84 signal.set(2);
85
86 thread::sleep(Duration::from_millis(10));
87
88 assert_eq!(*count1.lock().unwrap(), 2);
89 assert_eq!(*count2.lock().unwrap(), 2);
90}
91 
92#[test]
93fn test_signal_disposable_unsubscribe() {
94 let signal = Signal::new(0);
95 let count = Arc::new(Mutex::new(0));
96 let count_clone = count.clone();
97
98 let disposable = signal.subscribe(Box::new(move |_| {
99 *count_clone.lock().unwrap() += 1;
100 }));
101
102 signal.set(1);
103 thread::sleep(Duration::from_millis(10));
104 assert_eq!(*count.lock().unwrap(), 1);
105
106 // Dispose subscription
107 disposable.dispose();
108
109 signal.set(2);
110 thread::sleep(Duration::from_millis(10));
111
112 // Count should not increase after disposal
113 assert_eq!(*count.lock().unwrap(), 1);
114}
115 
116#[test]
117fn test_signal_computed() {
118 let signal = Signal::new(10);
119 let doubled = signal.computed(|v| v * 2);
120
121 assert_eq!(doubled.get(), 20);
122
123 signal.set(20);
124 thread::sleep(Duration::from_millis(10));
125
126 assert_eq!(doubled.get(), 40);
127}
128 
129#[test]
130fn test_signal_map() {
131 let signal = Signal::new(5);
132 let squared = signal.map(|v| v * v);
133
134 assert_eq!(squared.get(), 25);
135
136 signal.set(10);
137 thread::sleep(Duration::from_millis(10));
138
139 assert_eq!(squared.get(), 100);
140}
141 
142#[test]
143fn test_signal_effect() {
144 let signal = Signal::new(0);
145 let effect_count = Arc::new(Mutex::new(0));
146 let last_value = Arc::new(Mutex::new(0));
147
148 let effect_count_clone = effect_count.clone();
149 let last_value_clone = last_value.clone();
150
151 let _disposable = signal.effect(move |v| {
152 *effect_count_clone.lock().unwrap() += 1;
153 *last_value_clone.lock().unwrap() = *v;
154 });
155
156 // Effect should run immediately
157 assert_eq!(*effect_count.lock().unwrap(), 1);
158 assert_eq!(*last_value.lock().unwrap(), 0);
159
160 signal.set(42);
161 thread::sleep(Duration::from_millis(10));
162
163 assert_eq!(*effect_count.lock().unwrap(), 2);
164 assert_eq!(*last_value.lock().unwrap(), 42);
165}
166 
167#[test]
168fn test_signal_with_string() {
169 let signal = Signal::new(String::from("Hello"));
170 assert_eq!(signal.get(), "Hello");
171
172 signal.set(String::from("World"));
173 assert_eq!(signal.get(), "World");
174}
175 
176#[test]
177fn test_signal_with_vec() {
178 let signal = Signal::new(vec![1, 2, 3]);
179 assert_eq!(signal.get(), vec![1, 2, 3]);
180
181 signal.update(|v| v.push(4));
182 assert_eq!(signal.get(), vec![1, 2, 3, 4]);
183}
184 
185#[test]
186fn test_signal_with_option() {
187 let signal = Signal::new(Some(42));
188 assert_eq!(signal.get(), Some(42));
189
190 signal.set(None);
191 assert_eq!(signal.get(), None);
192}
193 
194#[test]
195fn test_signal_clone() {
196 let signal1 = Signal::new(10);
197 let signal2 = signal1.clone();
198
199 assert_eq!(signal1.get(), 10);
200 assert_eq!(signal2.get(), 10);
201
202 signal1.set(20);
203 assert_eq!(signal1.get(), 20);
204 assert_eq!(signal2.get(), 20);
205}
206 
207#[test]
208fn test_signal_thread_safety() {
209 let signal = Arc::new(Signal::new(0));
210 let mut handles = vec![];
211
212 // Spawn multiple threads that increment the signal
213 for _ in 0..10 {
214 let signal_clone = Arc::clone(&signal);
215 let handle = thread::spawn(move || {
216 for _ in 0..100 {
217 let current = signal_clone.get();
218 signal_clone.set(current + 1);
219 }
220 });
221 handles.push(handle);
222 }
223
224 // Wait for all threads
225 for handle in handles {
226 handle.join().unwrap();
227 }
228
229 // Final value should be 1000 (10 threads * 100 increments)
230 // Note: Due to race conditions, this might not always be exactly 1000
231 // but it should be close and the test should not panic
232 let final_value = signal.get();
233 assert!(final_value > 0);
234 assert!(final_value <= 1000);
235}
236 
237#[test]
238fn test_reactive_context_creation() {
239 let context = ReactiveContext::new();
240 // Just verify it can be created
241 drop(context);
242}
243 
244#[test]
245fn test_signal_with_custom_context() {
246 let context = Arc::new(ReactiveContext::new());
247 let signal = Signal::with_context(42, context);
248
249 assert_eq!(signal.get(), 42);
250}
251 
252#[test]
253fn test_signal_chain_of_computations() {
254 let base = Signal::new(2);
255 let doubled = base.computed(|v| v * 2);
256 let quadrupled = doubled.computed(|v| v * 2);
257
258 assert_eq!(base.get(), 2);
259 assert_eq!(doubled.get(), 4);
260 assert_eq!(quadrupled.get(), 8);
261
262 base.set(5);
263 thread::sleep(Duration::from_millis(20));
264
265 assert_eq!(base.get(), 5);
266 assert_eq!(doubled.get(), 10);
267 assert_eq!(quadrupled.get(), 20);
268}
269 
270#[test]
271fn test_signal_with_complex_type() {
272 #[derive(Clone, Debug, PartialEq)]
273 struct User {
274 name: String,
275 age: u32,
276 }
277
278 let signal = Signal::new(User {
279 name: "Alice".to_string(),
280 age: 30,
281 });
282
283 let user = signal.get();
284 assert_eq!(user.name, "Alice");
285 assert_eq!(user.age, 30);
286
287 signal.set(User {
288 name: "Bob".to_string(),
289 age: 25,
290 });
291
292 let user = signal.get();
293 assert_eq!(user.name, "Bob");
294 assert_eq!(user.age, 25);
295}
296 
297#[test]
298fn test_signal_update_preserves_subscribers() {
299 let signal = Signal::new(0);
300 let count = Arc::new(Mutex::new(0));
301 let count_clone = count.clone();
302
303 let _disposable = signal.subscribe(Box::new(move |_| {
304 *count_clone.lock().unwrap() += 1;
305 }));
306
307 // Multiple updates
308 for i in 1..=5 {
309 signal.set(i);
310 }
311
312 thread::sleep(Duration::from_millis(50));
313
314 // Should have received all 5 updates
315 assert_eq!(*count.lock().unwrap(), 5);
316}
317 
318#[test]
319fn test_signal_peek_vs_get_performance() {
320 let signal = Signal::new(42);
321
322 // Both should return the same value
323 assert_eq!(signal.peek(), signal.get());
324
325 // peek should be slightly faster as it doesn't track dependencies
326 // but we can't easily test performance in a unit test
327 // This test just verifies they both work
328}
329 
330#[test]
331fn test_signal_with_bool() {
332 let signal = Signal::new(true);
333 assert_eq!(signal.get(), true);
334
335 signal.set(false);
336 assert_eq!(signal.get(), false);
337
338 signal.update(|v| *v = !*v);
339 assert_eq!(signal.get(), true);
340}
341 
342#[test]
343fn test_multiple_effects_on_same_signal() {
344 let signal = Signal::new(0);
345 let count1 = Arc::new(Mutex::new(0));
346 let count2 = Arc::new(Mutex::new(0));
347 let count3 = Arc::new(Mutex::new(0));
348
349 let count1_clone = count1.clone();
350 let _d1 = signal.effect(move |_| {
351 *count1_clone.lock().unwrap() += 1;
352 });
353
354 let count2_clone = count2.clone();
355 let _d2 = signal.effect(move |_| {
356 *count2_clone.lock().unwrap() += 1;
357 });
358
359 let count3_clone = count3.clone();
360 let _d3 = signal.effect(move |_| {
361 *count3_clone.lock().unwrap() += 1;
362 });
363
364 // All effects should run immediately
365 assert_eq!(*count1.lock().unwrap(), 1);
366 assert_eq!(*count2.lock().unwrap(), 1);
367 assert_eq!(*count3.lock().unwrap(), 1);
368
369 signal.set(42);
370 thread::sleep(Duration::from_millis(10));
371
372 // All effects should run again
373 assert_eq!(*count1.lock().unwrap(), 2);
374 assert_eq!(*count2.lock().unwrap(), 2);
375 assert_eq!(*count3.lock().unwrap(), 2);
376}
377 
378#[test]
379fn test_signal_rapid_updates() {
380 let signal = Signal::new(0);
381 let final_values = Arc::new(Mutex::new(Vec::new()));
382 let final_values_clone = final_values.clone();
383
384 let _disposable = signal.subscribe(Box::new(move |value| {
385 if let Some(v) = value.downcast_ref::<i32>() {
386 final_values_clone.lock().unwrap().push(*v);
387 }
388 }));
389
390 // Rapid updates
391 for i in 1..=100 {
392 signal.set(i);
393 }
394
395 thread::sleep(Duration::from_millis(50));
396
397 let values = final_values.lock().unwrap();
398 assert_eq!(values.len(), 100);
399 assert_eq!(*values.last().unwrap(), 100);
400}
401 
402#[cfg(test)]
403mod property_tests {
404 use super::*;
405
406 #[test]
407 fn test_signal_always_returns_latest_value() {
408 let signal = Signal::new(0);
409
410 for i in 0..1000 {
411 signal.set(i);
412 assert_eq!(signal.get(), i);
413 }
414 }
415
416 #[test]
417 fn test_signal_update_is_atomic() {
418 let signal = Signal::new(vec![1, 2, 3]);
419
420 signal.update(|v| {
421 v.push(4);
422 v.push(5);
423 });
424
425 assert_eq!(signal.get(), vec![1, 2, 3, 4, 5]);
426 }
427}
428 
429#[cfg(test)]
430mod edge_cases {
431 use super::*;
432
433 #[test]
434 fn test_signal_with_empty_string() {
435 let signal = Signal::new(String::new());
436 assert_eq!(signal.get(), "");
437
438 signal.set("test".to_string());
439 assert_eq!(signal.get(), "test");
440 }
441
442 #[test]
443 fn test_signal_with_empty_vec() {
444 let signal = Signal::new(Vec::<i32>::new());
445 assert_eq!(signal.get(), Vec::<i32>::new());
446
447 signal.update(|v| v.push(1));
448 assert_eq!(signal.get(), vec![1]);
449 }
450
451 #[test]
452 fn test_signal_with_zero_values() {
453 let signal_i32 = Signal::new(0i32);
454 assert_eq!(signal_i32.get(), 0);
455
456 let signal_f64 = Signal::new(0.0f64);
457 assert_eq!(signal_f64.get(), 0.0);
458 }
459}
460