cranpose_testing/
testing.rs1use cranpose_core::{
2 location_key, ApplierGuard, Composition, Key, MemoryApplier, NodeError, NodeId, RuntimeHandle,
3 ROOT_RENDER_REPLAY_LIMIT,
4};
5use cranpose_ui::{request_render_invalidation, reset_render_state_for_tests};
6
7#[cfg(test)]
8use cranpose_core::{
9 pop_parent, push_parent, with_current_composer, with_node_mut, MutableState, Node,
10};
11#[cfg(test)]
12use std::cell::Cell;
13#[cfg(test)]
14use std::rc::Rc;
15
16pub struct ComposeTestRule {
23 composition: Composition<MemoryApplier>,
24 content: Option<Box<dyn FnMut()>>, initial_root_key: Key,
26}
27
28impl ComposeTestRule {
29 pub fn new() -> Self {
31 reset_render_state_for_tests();
32 Self {
33 composition: Composition::new(MemoryApplier::new()),
34 content: None,
35 initial_root_key: location_key(file!(), line!(), column!()),
36 }
37 }
38
39 fn root_key(&self) -> Key {
40 self.composition.root_key().unwrap_or(self.initial_root_key)
41 }
42
43 pub fn set_content(&mut self, content: impl FnMut() + 'static) -> Result<(), NodeError> {
46 self.content = Some(Box::new(content));
47 self.render()
48 }
49
50 pub fn recomposition(&mut self) -> Result<(), NodeError> {
52 self.render()
53 }
54
55 pub fn advance_frame(&mut self, frame_time_nanos: u64) -> Result<(), NodeError> {
58 let handle = self.composition.runtime_handle();
59 handle.drain_frame_callbacks(frame_time_nanos);
60 self.pump_until_idle()
61 }
62
63 pub fn pump_until_idle(&mut self) -> Result<(), NodeError> {
66 let mut i = 0;
67 loop {
68 let mut progressed = false;
69 i += 1;
70 if i > ROOT_RENDER_REPLAY_LIMIT {
71 panic!(
72 "pump_until_idle exceeded {ROOT_RENDER_REPLAY_LIMIT} iterations — reentrant render or recomposition bug"
73 );
74 }
75
76 if self.composition.should_render() {
77 self.render()?;
78 progressed = true;
79 }
80
81 let handle = self.composition.runtime_handle();
82 if handle.has_updates() {
83 self.composition.flush_pending_node_updates()?;
84 progressed = true;
85 }
86
87 if handle.has_invalid_scopes() {
88 let changed = self.composition.process_invalid_scopes()?;
89 if changed {
90 request_render_invalidation();
92 }
93 if self.composition.take_root_render_request() {
94 self.render()?;
95 }
96 progressed = true;
97 }
98
99 if !progressed {
100 break;
101 }
102 }
103 Ok(())
104 }
105
106 pub fn runtime_handle(&self) -> RuntimeHandle {
109 self.composition.runtime_handle()
110 }
111
112 pub fn applier_mut(&mut self) -> ApplierGuard<'_, MemoryApplier> {
115 self.composition.applier_mut()
116 }
117
118 pub fn dump_tree(&mut self) -> String {
120 let root = self.composition.root();
121 let applier = self.composition.applier_mut();
122 applier.dump_tree(root)
123 }
124
125 pub fn has_content(&self) -> bool {
127 self.content.is_some()
128 }
129
130 pub fn root_id(&self) -> Option<NodeId> {
132 self.composition.root()
133 }
134
135 pub fn composition(&mut self) -> &mut Composition<MemoryApplier> {
137 &mut self.composition
138 }
139
140 fn render(&mut self) -> Result<(), NodeError> {
141 let key = self.root_key();
142 if let Some(content) = self.content.as_mut() {
143 self.composition.render(key, &mut **content)?;
144 self.drain_root_render_requests()?;
145 request_render_invalidation();
148 }
149 Ok(())
150 }
151
152 fn drain_root_render_requests(&mut self) -> Result<(), NodeError> {
153 let key = self.root_key();
154 for _ in 0..ROOT_RENDER_REPLAY_LIMIT {
155 if !self.composition.take_root_render_request() {
156 return Ok(());
157 }
158 let content = self
159 .content
160 .as_mut()
161 .expect("root render replay requires installed content");
162 self.composition.render(key, &mut **content)?;
163 request_render_invalidation();
164 }
165
166 panic!(
167 "root render replay exceeded {ROOT_RENDER_REPLAY_LIMIT} iterations — reentrant render bug"
168 );
169 }
170}
171
172impl Default for ComposeTestRule {
173 fn default() -> Self {
174 Self::new()
175 }
176}
177
178pub fn run_test_composition<R>(f: impl FnOnce(&mut ComposeTestRule) -> R) -> R {
181 let mut rule = ComposeTestRule::new();
182 f(&mut rule)
183}
184
185#[cfg(test)]
186#[path = "tests/testing_tests.rs"]
187mod tests;
188
189#[cfg(test)]
190#[path = "tests/recomposition_tests.rs"]
191mod recomposition_tests;