Skip to content

Commit 628ac98

Browse files
committed
test(macros): Add integration tests for global signals and memory cleanup, test the dependencies between local and global signals, verify memory cleanup functionality, and ensure objects are released correctly
1 parent f6eaea9 commit 628ac98

File tree

2 files changed

+198
-1
lines changed

2 files changed

+198
-1
lines changed

macros/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,18 @@ pub fn signal(input: TokenStream) -> TokenStream {
200200
/// 42
201201
/// }
202202
///
203+
/// #[memo]
204+
/// pub fn get_string() -> String {
205+
/// "Hello, World!".to_string()
206+
/// }
207+
///
203208
/// fn main() {
204209
/// // First call computes and caches the value
205210
/// assert_eq!(get_number(), 42);
206211
/// // Subsequent calls return the cached value without re-running the block
207212
/// assert_eq!(get_number(), 42);
213+
///
214+
/// assert_eq!(get_string(), "Hello, World!");
208215
/// }
209216
/// ```
210217
///
@@ -274,7 +281,7 @@ pub fn memo(_attr: TokenStream, item: TokenStream) -> TokenStream {
274281
/// # Examples
275282
///
276283
/// ```rust
277-
/// use reactive_macros::evaluate;
284+
/// use reactive_macros::{evaluate, ref_signal};
278285
///
279286
/// fn print(msg: String) {
280287
/// println!("{}", msg);

macros/tests/integration.rs

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
use std::{
2+
cell::Cell,
3+
rc::{Rc, Weak},
4+
};
5+
6+
use reactive_cache::{Effect, Memo, Signal};
7+
use reactive_macros::{memo, ref_signal, signal};
8+
9+
// ----------------------
10+
// Global signals
11+
// ----------------------
12+
13+
// Non-Copy global string signal
14+
ref_signal!(static mut GLOBAL_STR: String = "hello".to_string(););
15+
16+
// Copy global numeric signal
17+
signal!(static mut GLOBAL_NUM: i32 = 10;);
18+
19+
// ----------------------
20+
// Global memos
21+
// ----------------------
22+
23+
#[memo]
24+
pub fn get_global_number() -> i32 {
25+
// Getter for the global numeric signal
26+
GLOBAL_NUM()
27+
}
28+
29+
#[memo]
30+
pub fn get_global_string() -> String {
31+
// Getter for the global string signal
32+
GLOBAL_STR_get().clone()
33+
}
34+
35+
// ----------------------
36+
// ViewModel definition
37+
// ----------------------
38+
39+
struct ViewModel {
40+
counter: Rc<Signal<i32>>,
41+
double: Rc<Memo<i32>>,
42+
effect: Rc<Effect>,
43+
}
44+
45+
impl ViewModel {
46+
/// Create a new ViewModel with a local counter signal.
47+
/// The memo depends on the local counter and optionally reads the global numeric signal.
48+
/// The effect increments `effect_run_count` and reads the memo to establish reactive dependencies.
49+
fn new(initial_counter: i32, use_global_num: bool, effect_run_count: Rc<Cell<u32>>) -> Self {
50+
let counter = Signal::new(initial_counter);
51+
52+
// Memo depends on `counter`, optionally reading the global numeric signal
53+
let double = Memo::new({
54+
let counter_clone = counter.clone();
55+
move || {
56+
let base = *counter_clone.get();
57+
let global_val = if use_global_num {
58+
GLOBAL_NUM()
59+
} else {
60+
0
61+
};
62+
base * 2 + global_val
63+
}
64+
});
65+
66+
// Effect observes `double` and increments the counter
67+
let effect_run_count_clone = effect_run_count.clone();
68+
let double_for_effect = double.clone();
69+
let effect = Effect::new(move || {
70+
effect_run_count_clone.set(effect_run_count_clone.get() + 1);
71+
let _ = double_for_effect.get(); // subscribe to memo
72+
});
73+
74+
Self {
75+
counter,
76+
double,
77+
effect,
78+
}
79+
}
80+
}
81+
82+
// ----------------------
83+
// Comprehensive integration test
84+
// ----------------------
85+
86+
#[test]
87+
fn test_global_and_vm_interaction_with_memory_cleanup() {
88+
// ----------------------
89+
// Arrange: run counters
90+
// ----------------------
91+
let global_effect_runs = Rc::new(Cell::new(0));
92+
let vm1_effect_runs = Rc::new(Cell::new(0));
93+
let vm2_effect_runs = Rc::new(Cell::new(0));
94+
95+
// ----------------------
96+
// Global effect observing global memos
97+
// ----------------------
98+
let _global_effect = Effect::new({
99+
let runs = global_effect_runs.clone();
100+
move || {
101+
runs.set(runs.get() + 1);
102+
let _ = get_global_number();
103+
let _ = get_global_string();
104+
}
105+
});
106+
107+
// Effect runs once upon creation
108+
assert_eq!(global_effect_runs.get(), 1);
109+
110+
// ----------------------
111+
// Create two ViewModels
112+
// vm1 uses global numeric signal in its memo, vm2 does not
113+
// ----------------------
114+
let vm1 = ViewModel::new(1, true, vm1_effect_runs.clone());
115+
let vm2 = ViewModel::new(2, false, vm2_effect_runs.clone());
116+
117+
// Keep weak references to check for leaks after dropping
118+
let vm1_counter_weak: Weak<Signal<i32>> = Rc::downgrade(&vm1.counter);
119+
let vm1_double_weak: Weak<Memo<i32>> = Rc::downgrade(&vm1.double);
120+
let vm1_effect_weak: Weak<Effect> = Rc::downgrade(&vm1.effect);
121+
122+
let vm2_counter_weak: Weak<Signal<i32>> = Rc::downgrade(&vm2.counter);
123+
let vm2_double_weak: Weak<Memo<i32>> = Rc::downgrade(&vm2.double);
124+
let vm2_effect_weak: Weak<Effect> = Rc::downgrade(&vm2.effect);
125+
126+
// Each VM effect runs once on creation
127+
assert_eq!(vm1_effect_runs.get(), 1);
128+
assert_eq!(vm2_effect_runs.get(), 1);
129+
130+
// Global effect may have run once more depending on memo initialization
131+
assert_eq!(global_effect_runs.get(), 1);
132+
133+
// ----------------------
134+
// Trigger updates and verify effects
135+
// ----------------------
136+
// Update vm1 counter -> vm1 effect should re-run
137+
vm1.counter.set(10);
138+
assert_eq!(vm1_effect_runs.get(), 2);
139+
assert_eq!(vm2_effect_runs.get(), 1); // vm2 unaffected
140+
141+
// Update vm2 counter -> vm2 effect should re-run
142+
vm2.counter.set(7);
143+
assert_eq!(vm2_effect_runs.get(), 2);
144+
assert_eq!(vm1_effect_runs.get(), 2); // vm1 unaffected
145+
146+
// Update global numeric signal -> global effect should re-run
147+
// vm1's memo depends on global numeric, so its effect should also run
148+
GLOBAL_NUM_set(100);
149+
assert_eq!(global_effect_runs.get(), 2);
150+
assert_eq!(vm1_effect_runs.get(), 3);
151+
assert_eq!(vm2_effect_runs.get(), 2); // vm2 does not depend on global
152+
153+
// ----------------------
154+
// Verify memo values
155+
// vm1.double = counter*2 + global_num (10*2 + 100)
156+
// vm2.double = counter*2 (7*2)
157+
// ----------------------
158+
assert_eq!(vm1.double.get(), 10 * 2 + 100);
159+
assert_eq!(vm2.double.get(), 7 * 2);
160+
161+
// ----------------------
162+
// Drop vm1 and check memory cleanup
163+
// ----------------------
164+
drop(vm1);
165+
assert!(vm1_counter_weak.upgrade().is_none());
166+
assert!(vm1_double_weak.upgrade().is_none());
167+
assert!(vm1_effect_weak.upgrade().is_none());
168+
169+
// vm2 still alive
170+
assert!(vm2_counter_weak.upgrade().is_some());
171+
172+
// ----------------------
173+
// Drop vm2 and verify cleanup
174+
// ----------------------
175+
drop(vm2);
176+
assert!(vm2_counter_weak.upgrade().is_none());
177+
assert!(vm2_double_weak.upgrade().is_none());
178+
assert!(vm2_effect_weak.upgrade().is_none());
179+
180+
// ----------------------
181+
// Globals still alive after VMs are dropped
182+
// ----------------------
183+
let _n = get_global_number();
184+
let _s = get_global_string();
185+
186+
// Sanity check on global values
187+
assert_eq!(GLOBAL_NUM(), 100);
188+
assert_eq!(get_global_number(), 100);
189+
assert_eq!(get_global_string(), "hello".to_string());
190+
}

0 commit comments

Comments
 (0)