use std::time::{Duration, Instant};
use anyhow::Result;
use crossterm::event::{Event, EventStream};
use ratatui::{Terminal, backend::CrosstermBackend};
use tokio_stream::StreamExt;
use crate::Component;
use crate::event::set_current_event;
use crate::fiber_tree::{FiberTree, clear_fiber_tree, set_fiber_tree};
use crate::global_events::process_global_event;
use crate::panic_handler::setup_panic_handler;
use crate::render_context::{clear_render_context, init_render_context};
pub type FiberTerminal = Terminal<CrosstermBackend<std::io::Stdout>>;
#[derive(Debug, Clone)]
pub struct RenderOptions {
pub frame_interval_ms: u64,
pub strict_mode: bool,
}
impl Default for RenderOptions {
fn default() -> Self {
Self {
frame_interval_ms: 16,
strict_mode: false,
}
}
}
static EXIT_REQUESTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
static IN_RENDER_PHASE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
pub fn request_exit() {
EXIT_REQUESTED.store(true, std::sync::atomic::Ordering::SeqCst);
}
pub fn should_exit() -> bool {
EXIT_REQUESTED.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn reset_exit() {
EXIT_REQUESTED.store(false, std::sync::atomic::Ordering::SeqCst);
}
pub fn is_in_render_phase() -> bool {
IN_RENDER_PHASE.load(std::sync::atomic::Ordering::SeqCst)
}
fn set_render_phase(in_render: bool) {
IN_RENDER_PHASE.store(in_render, std::sync::atomic::Ordering::SeqCst);
}
#[cfg(debug_assertions)]
pub fn warn_if_effect_during_render(effect_name: &str) {
if is_in_render_phase() {
tracing::warn!(
"Effect '{}' executed during render phase! Effects should run after commit. \
This may cause inconsistent behavior.",
effect_name
);
}
}
#[cfg(not(debug_assertions))]
pub fn warn_if_effect_during_render(_effect_name: &str) {}
fn setup_terminal() -> Result<FiberTerminal> {
use crossterm::{
execute,
terminal::{EnterAlternateScreen, enable_raw_mode},
};
enable_raw_mode()?;
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> Result<()> {
use crossterm::{
execute,
terminal::{LeaveAlternateScreen, disable_raw_mode},
};
disable_raw_mode()?;
execute!(std::io::stdout(), LeaveAlternateScreen)?;
Ok(())
}
pub async fn render<C, F>(initializer: F) -> Result<()>
where
C: Component,
F: Fn() -> C + 'static,
{
render_with_options(initializer, RenderOptions::default()).await
}
pub async fn render_with_options<C, F>(initializer: F, options: RenderOptions) -> Result<()>
where
C: Component,
F: Fn() -> C + 'static,
{
setup_panic_handler();
reset_exit();
let mut terminal = setup_terminal()?;
let fiber_tree = FiberTree::new();
set_fiber_tree(fiber_tree);
init_render_context();
crate::scheduler::batch::init_main_thread();
crate::strict_mode::set_strict_mode_enabled(options.strict_mode);
let mut last_frame_time = Instant::now();
let mut events = EventStream::new();
let frame_interval = Duration::from_millis(options.frame_interval_ms);
loop {
let current_time = Instant::now();
let _delta = current_time.duration_since(last_frame_time);
last_frame_time = current_time;
let timeout = tokio::time::sleep(frame_interval);
tokio::pin!(timeout);
let current_event: Option<Event> = tokio::select! {
Some(Ok(event)) = events.next() => {
Some(event)
}
_ = &mut timeout => {
None
}
};
if let Some(ref event) = current_event {
set_current_event(Some(std::sync::Arc::new(event.clone())));
} else {
set_current_event(None);
}
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.prepare_for_render();
});
crate::component::reset_component_position_counter();
crate::global_events::clear_global_handlers();
set_render_phase(true);
let component = initializer();
set_render_phase(false);
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.mark_unseen_for_unmount();
});
terminal.draw(|frame| {
let area = frame.area();
let wrapper = crate::component::ComponentWrapper::new(component);
wrapper.render_with_fiber(area, frame.buffer_mut());
})?;
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.process_unmounts();
});
crate::scheduler::batch::begin_batch();
if let Some(Event::Key(key_event)) = ¤t_event {
process_global_event(key_event);
}
crate::scheduler::batch::drain_cross_thread_updates();
let _dirty_fibers = crate::scheduler::batch::end_batch();
set_current_event(None);
if should_exit() {
break;
}
crate::scheduler::effect_queue::flush_effects();
crate::scheduler::effect_queue::flush_async_effects().await;
}
set_current_event(None);
clear_fiber_tree();
clear_render_context();
restore_terminal()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fiber::FiberId;
use crate::fiber_tree::{clear_fiber_tree, set_fiber_tree, with_fiber_tree_mut};
use crate::scheduler::batch::{
StateUpdate, StateUpdateKind, begin_batch, clear_state_batch, end_batch, queue_update,
};
fn setup_test_environment() -> FiberId {
clear_state_batch();
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
tree.get_mut(fiber_id).unwrap().set_hook(0, 0i32);
tree.mark_clean(fiber_id);
set_fiber_tree(tree);
fiber_id
}
fn cleanup_test_environment() {
clear_fiber_tree();
clear_state_batch();
}
#[test]
fn test_event_phase_batching_multiple_updates() {
let fiber_id = setup_test_environment();
begin_batch();
queue_update(
fiber_id,
StateUpdate {
hook_index: 0,
update: StateUpdateKind::Value(Box::new(1i32)),
},
);
queue_update(
fiber_id,
StateUpdate {
hook_index: 0,
update: StateUpdateKind::Value(Box::new(2i32)),
},
);
queue_update(
fiber_id,
StateUpdate {
hook_index: 0,
update: StateUpdateKind::Value(Box::new(3i32)),
},
);
let dirty_fibers = end_batch();
assert_eq!(dirty_fibers.len(), 1);
assert!(dirty_fibers.contains(&fiber_id));
with_fiber_tree_mut(|tree| {
let value = tree.get(fiber_id).unwrap().get_hook::<i32>(0);
assert_eq!(value, Some(3));
});
cleanup_test_environment();
}
#[test]
fn test_event_phase_batching_functional_updates() {
let fiber_id = setup_test_environment();
with_fiber_tree_mut(|tree| {
tree.get_mut(fiber_id).unwrap().set_hook(0, 0i32);
});
begin_batch();
for _ in 0..5 {
queue_update(
fiber_id,
StateUpdate {
hook_index: 0,
update: StateUpdateKind::Updater(Box::new(|current| {
let val = current.downcast_ref::<i32>().unwrap();
Box::new(val + 1)
})),
},
);
}
let dirty_fibers = end_batch();
assert_eq!(dirty_fibers.len(), 1);
with_fiber_tree_mut(|tree| {
let value = tree.get(fiber_id).unwrap().get_hook::<i32>(0);
assert_eq!(value, Some(5));
});
cleanup_test_environment();
}
#[test]
fn test_event_phase_batching_multiple_fibers() {
clear_state_batch();
let mut tree = FiberTree::new();
let fiber1 = tree.mount(None, None);
let fiber2 = tree.mount(Some(fiber1), None);
let fiber3 = tree.mount(Some(fiber1), None);
tree.get_mut(fiber1).unwrap().set_hook(0, 0i32);
tree.get_mut(fiber2).unwrap().set_hook(0, 0i32);
tree.get_mut(fiber3).unwrap().set_hook(0, 0i32);
tree.mark_clean(fiber1);
tree.mark_clean(fiber2);
tree.mark_clean(fiber3);
set_fiber_tree(tree);
begin_batch();
queue_update(
fiber1,
StateUpdate {
hook_index: 0,
update: StateUpdateKind::Value(Box::new(10i32)),
},
);
queue_update(
fiber2,
StateUpdate {
hook_index: 0,
update: StateUpdateKind::Value(Box::new(20i32)),
},
);
queue_update(
fiber3,
StateUpdate {
hook_index: 0,
update: StateUpdateKind::Value(Box::new(30i32)),
},
);
let dirty_fibers = end_batch();
assert_eq!(dirty_fibers.len(), 3);
assert!(dirty_fibers.contains(&fiber1));
assert!(dirty_fibers.contains(&fiber2));
assert!(dirty_fibers.contains(&fiber3));
with_fiber_tree_mut(|tree| {
assert_eq!(tree.get(fiber1).unwrap().get_hook::<i32>(0), Some(10));
assert_eq!(tree.get(fiber2).unwrap().get_hook::<i32>(0), Some(20));
assert_eq!(tree.get(fiber3).unwrap().get_hook::<i32>(0), Some(30));
});
cleanup_test_environment();
}
#[test]
fn test_event_phase_no_updates_no_dirty_fibers() {
let _fiber_id = setup_test_environment();
begin_batch();
let dirty_fibers = end_batch();
assert!(dirty_fibers.is_empty());
cleanup_test_environment();
}
#[test]
fn test_event_phase_equality_check_skips_unchanged() {
let fiber_id = setup_test_environment();
with_fiber_tree_mut(|tree| {
tree.get_mut(fiber_id).unwrap().set_hook(0, 42i32);
tree.mark_clean(fiber_id);
});
begin_batch();
queue_update(
fiber_id,
StateUpdate {
hook_index: 0,
update: StateUpdateKind::ValueIfChanged {
value: Box::new(42i32), eq_check: Box::new(|old, new| {
let old = old.downcast_ref::<i32>().unwrap();
let new = new.downcast_ref::<i32>().unwrap();
old == new
}),
},
},
);
let dirty_fibers = end_batch();
assert!(dirty_fibers.is_empty());
cleanup_test_environment();
}
#[test]
fn test_render_options_default() {
let options = RenderOptions::default();
assert_eq!(options.frame_interval_ms, 16);
assert!(!options.strict_mode);
}
#[test]
fn test_exit_flag_operations() {
reset_exit();
assert!(!should_exit());
request_exit();
assert!(should_exit());
reset_exit();
assert!(!should_exit());
}
#[test]
fn test_render_phase_flag() {
assert!(!is_in_render_phase());
set_render_phase(true);
assert!(is_in_render_phase());
set_render_phase(false);
assert!(!is_in_render_phase());
}
#[test]
fn test_render_phase_isolation() {
set_render_phase(false);
assert!(!is_in_render_phase());
set_render_phase(true);
assert!(is_in_render_phase());
set_render_phase(false);
assert!(!is_in_render_phase());
}
#[test]
fn test_warn_if_effect_during_render_outside_render() {
set_render_phase(false);
warn_if_effect_during_render("test_effect");
}
#[test]
fn test_warn_if_effect_during_render_inside_render() {
set_render_phase(true);
warn_if_effect_during_render("test_effect");
set_render_phase(false);
}
#[test]
fn test_prepare_for_render_resets_hook_indices() {
let fiber_id = setup_test_environment();
with_fiber_tree_mut(|tree| {
tree.begin_render(fiber_id);
let fiber = tree.get_mut(fiber_id).unwrap();
fiber.next_hook_index();
fiber.next_hook_index();
fiber.next_hook_index();
tree.end_render();
});
with_fiber_tree_mut(|tree| {
assert_eq!(tree.get(fiber_id).unwrap().hook_index, 3);
});
with_fiber_tree_mut(|tree| {
tree.prepare_for_render();
});
with_fiber_tree_mut(|tree| {
assert_eq!(tree.get(fiber_id).unwrap().hook_index, 0);
});
cleanup_test_environment();
}
#[test]
fn test_effect_execution_timing_after_commit() {
use crate::fiber::PendingEffect;
use crate::scheduler::effect_queue::{
clear_effect_queue, flush_effects_with_tree, queue_effect,
};
use std::sync::Arc;
let fiber_id = setup_test_environment();
clear_effect_queue();
let execution_order = Arc::new(std::sync::Mutex::new(Vec::new()));
let order_clone = execution_order.clone();
queue_effect(
fiber_id,
PendingEffect {
effect: Box::new(move || {
order_clone.lock().unwrap().push("effect");
None
}),
hook_index: 0,
},
);
execution_order.lock().unwrap().push("commit");
with_fiber_tree_mut(|tree| {
flush_effects_with_tree(tree);
});
let order = execution_order.lock().unwrap();
assert_eq!(order.len(), 2);
assert_eq!(order[0], "commit");
assert_eq!(order[1], "effect");
cleanup_test_environment();
clear_effect_queue();
}
#[test]
fn test_cleanup_runs_before_new_effects() {
use crate::fiber::PendingEffect;
use crate::scheduler::effect_queue::{
clear_effect_queue, flush_effects_with_tree, queue_cleanup, queue_effect,
};
use std::sync::Arc;
let fiber_id = setup_test_environment();
clear_effect_queue();
let execution_order = Arc::new(std::sync::Mutex::new(Vec::new()));
let order_clone = execution_order.clone();
queue_cleanup(Box::new(move || {
order_clone.lock().unwrap().push("cleanup");
}));
let order_clone = execution_order.clone();
queue_effect(
fiber_id,
PendingEffect {
effect: Box::new(move || {
order_clone.lock().unwrap().push("new_effect");
None
}),
hook_index: 0,
},
);
with_fiber_tree_mut(|tree| {
flush_effects_with_tree(tree);
});
let order = execution_order.lock().unwrap();
assert_eq!(order.len(), 2);
assert_eq!(order[0], "cleanup");
assert_eq!(order[1], "new_effect");
cleanup_test_environment();
clear_effect_queue();
}
#[test]
fn test_multiple_cleanups_run_in_reverse_order() {
use crate::scheduler::effect_queue::{
clear_effect_queue, flush_effects_with_tree, queue_cleanup,
};
use std::sync::Arc;
let _fiber_id = setup_test_environment();
clear_effect_queue();
let execution_order = Arc::new(std::sync::Mutex::new(Vec::new()));
for i in 1..=3 {
let order_clone = execution_order.clone();
queue_cleanup(Box::new(move || {
order_clone.lock().unwrap().push(i);
}));
}
with_fiber_tree_mut(|tree| {
flush_effects_with_tree(tree);
});
let order = execution_order.lock().unwrap();
assert_eq!(*order, vec![3, 2, 1]);
cleanup_test_environment();
clear_effect_queue();
}
#[test]
fn test_effects_run_in_declaration_order() {
use crate::fiber::PendingEffect;
use crate::scheduler::effect_queue::{
clear_effect_queue, flush_effects_with_tree, queue_effect,
};
use std::sync::Arc;
let fiber_id = setup_test_environment();
clear_effect_queue();
let execution_order = Arc::new(std::sync::Mutex::new(Vec::new()));
for i in 1..=3 {
let order_clone = execution_order.clone();
queue_effect(
fiber_id,
PendingEffect {
effect: Box::new(move || {
order_clone.lock().unwrap().push(i);
None
}),
hook_index: i,
},
);
}
with_fiber_tree_mut(|tree| {
flush_effects_with_tree(tree);
});
let order = execution_order.lock().unwrap();
assert_eq!(*order, vec![1, 2, 3]);
cleanup_test_environment();
clear_effect_queue();
}
#[test]
fn test_use_event_available_during_render_phase() {
use crate::event::{clear_current_event, set_current_event};
use crate::hooks::use_event;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use std::sync::Arc;
clear_current_event();
let fiber_id = setup_test_environment();
let key_event = Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('j'),
KeyModifiers::NONE,
KeyEventKind::Press,
));
set_current_event(Some(Arc::new(key_event)));
set_render_phase(true);
with_fiber_tree_mut(|tree| {
tree.begin_render(fiber_id);
});
let event = use_event();
with_fiber_tree_mut(|tree| {
tree.end_render();
});
set_render_phase(false);
assert!(
event.is_some(),
"use_event() should return event during render phase"
);
if let Some(Event::Key(key)) = event {
assert_eq!(key.code, KeyCode::Char('j'));
assert_eq!(key.kind, KeyEventKind::Press);
} else {
panic!("Expected Key event with 'j'");
}
clear_current_event();
cleanup_test_environment();
}
#[test]
fn test_use_event_returns_none_when_no_event() {
use crate::event::clear_current_event;
use crate::hooks::use_event;
let fiber_id = setup_test_environment();
clear_current_event();
set_render_phase(true);
with_fiber_tree_mut(|tree| {
tree.begin_render(fiber_id);
});
let event = use_event();
with_fiber_tree_mut(|tree| {
tree.end_render();
});
set_render_phase(false);
assert!(
event.is_none(),
"use_event() should return None when no event is set"
);
cleanup_test_environment();
}
#[test]
fn test_event_cleared_after_render_cycle() {
use crate::event::{clear_current_event, peek_current_event, set_current_event};
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use std::sync::Arc;
let _fiber_id = setup_test_environment();
let key_event = Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('k'),
KeyModifiers::NONE,
KeyEventKind::Press,
));
set_current_event(Some(Arc::new(key_event)));
assert!(peek_current_event().is_some());
clear_current_event();
assert!(
peek_current_event().is_none(),
"Event should be cleared after render cycle"
);
cleanup_test_environment();
}
#[test]
fn test_complete_render_cycle_with_use_event() {
use crate::event::{clear_current_event, set_current_event};
use crate::hooks::use_event;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use std::sync::Arc;
let fiber_id = setup_test_environment();
let event_a = Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('a'),
KeyModifiers::NONE,
KeyEventKind::Press,
));
set_current_event(Some(Arc::new(event_a)));
set_render_phase(true);
with_fiber_tree_mut(|tree| {
tree.prepare_for_render();
tree.begin_render(fiber_id);
});
let event1 = use_event();
with_fiber_tree_mut(|tree| {
tree.end_render();
});
set_render_phase(false);
assert!(event1.is_some());
if let Some(Event::Key(key)) = event1 {
assert_eq!(key.code, KeyCode::Char('a'));
}
clear_current_event();
let event_b = Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('b'),
KeyModifiers::NONE,
KeyEventKind::Press,
));
set_current_event(Some(Arc::new(event_b)));
set_render_phase(true);
with_fiber_tree_mut(|tree| {
tree.prepare_for_render();
tree.begin_render(fiber_id);
});
let event2 = use_event();
with_fiber_tree_mut(|tree| {
tree.end_render();
});
set_render_phase(false);
assert!(event2.is_some());
if let Some(Event::Key(key)) = event2 {
assert_eq!(key.code, KeyCode::Char('b'));
}
clear_current_event();
set_render_phase(true);
with_fiber_tree_mut(|tree| {
tree.prepare_for_render();
tree.begin_render(fiber_id);
});
let event3 = use_event();
with_fiber_tree_mut(|tree| {
tree.end_render();
});
set_render_phase(false);
assert!(
event3.is_none(),
"Should return None when no event in frame"
);
cleanup_test_environment();
}
#[test]
fn test_position_based_component_id_stability() {
use crate::component::reset_component_position_counter;
use std::any::TypeId;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
fn generate_id<C: 'static>(position: u64) -> u64 {
let type_id = TypeId::of::<C>();
let mut hasher = DefaultHasher::new();
type_id.hash(&mut hasher);
position.hash(&mut hasher);
hasher.finish()
}
struct ComponentA;
struct ComponentB;
let mut frame_ids: Vec<(u64, u64)> = Vec::new();
for _frame in 0..5 {
reset_component_position_counter();
let id_a = generate_id::<ComponentA>(0); let id_b = generate_id::<ComponentB>(1);
frame_ids.push((id_a, id_b));
}
for i in 1..frame_ids.len() {
assert_eq!(
frame_ids[0].0, frame_ids[i].0,
"ComponentA should have stable ID across frames"
);
assert_eq!(
frame_ids[0].1, frame_ids[i].1,
"ComponentB should have stable ID across frames"
);
}
let id_a_pos0 = generate_id::<ComponentA>(0);
let id_b_pos0 = generate_id::<ComponentB>(0);
assert_ne!(
id_a_pos0, id_b_pos0,
"Different component types at same position should have different IDs"
);
let id_a_pos0 = generate_id::<ComponentA>(0);
let id_a_pos1 = generate_id::<ComponentA>(1);
assert_ne!(
id_a_pos0, id_a_pos1,
"Same component type at different positions should have different IDs"
);
}
#[test]
fn test_runtime_resets_position_counter() {
use crate::component::reset_component_position_counter;
use std::cell::RefCell;
thread_local! {
static POSITIONS: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
}
for _frame in 0..3 {
reset_component_position_counter();
crate::component::reset_component_position_counter();
}
}
#[test]
fn test_cross_thread_updates_applied_after_drain() {
use crate::scheduler::batch::{
clear_cross_thread_updates, drain_cross_thread_updates, has_cross_thread_updates,
init_main_thread, reset_main_thread,
};
reset_main_thread();
clear_state_batch();
clear_cross_thread_updates();
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
tree.get_mut(fiber_id).unwrap().set_hook(0, 0i32);
tree.mark_clean(fiber_id);
set_fiber_tree(tree);
init_main_thread();
let handle = std::thread::spawn(move || {
queue_update(
fiber_id,
StateUpdate {
hook_index: 0,
update: StateUpdateKind::Value(Box::new(42i32)),
},
);
});
handle.join().unwrap();
assert!(
has_cross_thread_updates(),
"Update from background thread should be in cross-thread queue"
);
begin_batch();
drain_cross_thread_updates();
let dirty_fibers = end_batch();
assert!(
dirty_fibers.contains(&fiber_id),
"Fiber should be marked dirty after cross-thread update"
);
with_fiber_tree_mut(|tree| {
let value = tree.get(fiber_id).unwrap().get_hook::<i32>(0);
assert_eq!(
value,
Some(42),
"Cross-thread update should have been applied"
);
});
clear_fiber_tree();
clear_state_batch();
clear_cross_thread_updates();
reset_main_thread();
}
#[test]
fn test_concurrent_cross_thread_updates_from_multiple_threads() {
use crate::scheduler::batch::{
CrossThreadUpdate, CrossThreadUpdateKind, StateUpdaterFn, clear_cross_thread_updates,
drain_cross_thread_updates, init_main_thread, queue_cross_thread_update,
reset_main_thread,
};
use std::sync::{Arc, Mutex};
reset_main_thread();
clear_state_batch();
clear_cross_thread_updates();
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
tree.get_mut(fiber_id).unwrap().set_hook(0, 0i32);
set_fiber_tree(tree);
init_main_thread();
let barrier = Arc::new(std::sync::Barrier::new(10));
let handles: Vec<_> = (1..=10)
.map(|i| {
let barrier = Arc::clone(&barrier);
std::thread::spawn(move || {
barrier.wait(); let updater: StateUpdaterFn = Box::new(move |any| {
let n = any.downcast_ref::<i32>().unwrap();
Box::new(n + i)
});
queue_cross_thread_update(CrossThreadUpdate {
fiber_id,
hook_index: 0,
update: CrossThreadUpdateKind::Updater(Arc::new(Mutex::new(Some(updater)))),
});
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
begin_batch();
drain_cross_thread_updates();
let dirty_fibers = end_batch();
assert!(dirty_fibers.contains(&fiber_id));
with_fiber_tree_mut(|tree| {
let value = tree.get(fiber_id).unwrap().get_hook::<i32>(0);
assert_eq!(
value,
Some(55),
"All concurrent updates should have been applied"
);
});
clear_fiber_tree();
clear_state_batch();
clear_cross_thread_updates();
reset_main_thread();
}
#[test]
fn test_runtime_render_loop_with_cross_thread_updates() {
use crate::scheduler::batch::{
clear_cross_thread_updates, drain_cross_thread_updates, init_main_thread,
reset_main_thread,
};
reset_main_thread();
clear_state_batch();
clear_cross_thread_updates();
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
tree.get_mut(fiber_id).unwrap().set_hook(0, 100i32);
tree.mark_clean(fiber_id);
set_fiber_tree(tree);
init_main_thread();
let handle = std::thread::spawn(move || {
queue_update(
fiber_id,
StateUpdate {
hook_index: 0,
update: StateUpdateKind::Updater(Box::new(|any| {
let n = any.downcast_ref::<i32>().unwrap();
Box::new(n + 1) })),
},
);
});
handle.join().unwrap();
begin_batch();
drain_cross_thread_updates();
let dirty_fibers = end_batch();
assert!(
dirty_fibers.contains(&fiber_id),
"Fiber should be dirty after cross-thread update"
);
with_fiber_tree_mut(|tree| {
let value = tree.get(fiber_id).unwrap().get_hook::<i32>(0);
assert_eq!(value, Some(101), "Counter should have been incremented");
});
clear_fiber_tree();
clear_state_batch();
clear_cross_thread_updates();
reset_main_thread();
}
#[test]
fn test_reentrant_update_integration() {
use crate::scheduler::batch::{
clear_cross_thread_updates, drain_cross_thread_updates, has_cross_thread_updates,
init_main_thread, reset_main_thread, test_simulate_reentrant_update,
};
reset_main_thread();
clear_state_batch();
clear_cross_thread_updates();
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
tree.get_mut(fiber_id).unwrap().set_hook(0, 0i32);
set_fiber_tree(tree);
init_main_thread();
test_simulate_reentrant_update(fiber_id, 0, Box::new(999i32));
assert!(
has_cross_thread_updates(),
"Re-entrant update should fall back to cross-thread queue"
);
begin_batch();
drain_cross_thread_updates();
let dirty_fibers = end_batch();
assert!(dirty_fibers.contains(&fiber_id));
with_fiber_tree_mut(|tree| {
let value = tree.get(fiber_id).unwrap().get_hook::<i32>(0);
assert_eq!(value, Some(999), "Re-entrant update should be applied");
});
clear_fiber_tree();
clear_state_batch();
clear_cross_thread_updates();
reset_main_thread();
}
}