ferroui_reactiveui_testing/
rx_test.rs1use ferroui_reactive::linq::TimeoutException;
4use ferroui_reactiveui::PanicException;
5use ferroui_splat::AppBuilder;
6use ferroui_system::threading::tasks::{run, Task, TaskCompletionSource};
7use ferroui_system::ObservableError;
8use std::cell::{Cell, RefCell};
9use std::collections::VecDeque;
10use std::future::{poll_fn, Future};
11use std::pin::Pin;
12use std::rc::Rc;
13use std::task::Poll;
14use std::time::Duration;
15
16const DEFAULT_MAX_WAIT_MS: u64 = 60_000;
18
19#[derive(Default)]
22struct TestGate {
23 held: Cell<bool>,
24 waiters: RefCell<VecDeque<TaskCompletionSource<()>>>,
25}
26
27ferroui_system::ferro_thread_local! {
28 static TEST_GATE: Rc<TestGate> = Rc::new(TestGate::default());
30}
31
32impl TestGate {
33 fn current() -> Rc<Self> {
34 TEST_GATE.with(Rc::clone)
35 }
36
37 async fn wait(self: Rc<Self>, max_wait: Duration) -> bool {
40 if !self.held.get() {
41 self.held.set(true);
42 return true;
43 }
44 let turn = TaskCompletionSource::<()>::new();
45 let granted = turn.task();
46 self.waiters.borrow_mut().push_back(turn);
47 match first_of(granted.clone(), delay(max_wait)).await {
48 First::Left(_) => true,
49 First::Right(_) => {
50 self.waiters.borrow_mut().retain(|waiter| waiter.task().id() != granted.id());
51 granted.is_completed()
52 }
53 }
54 }
55
56 fn release(&self) {
58 let next = self.waiters.borrow_mut().pop_front();
59 match next {
60 Some(next) => next.set_result(()),
61 None => self.held.set(false),
62 }
63 }
64}
65
66enum First<A, B> {
68 Left(A),
69 Right(B),
70}
71
72async fn first_of<A: Clone + 'static, B: Clone + 'static>(
74 mut left: Task<A>,
75 mut right: Task<B>,
76) -> First<Result<A, ObservableError>, Result<B, ObservableError>> {
77 poll_fn(move |context| {
78 if let Poll::Ready(outcome) = Pin::new(&mut left).poll(context) {
79 return Poll::Ready(First::Left(outcome));
80 }
81 if let Poll::Ready(outcome) = Pin::new(&mut right).poll(context) {
82 return Poll::Ready(First::Right(outcome));
83 }
84 Poll::Pending
85 })
86 .await
87}
88
89fn delay(duration: Duration) -> Task<()> {
94 run(move || {
95 std::thread::sleep(duration);
96 Ok(())
97 })
98}
99
100pub struct RxTest;
107
108impl RxTest {
109 pub fn app_builder_test_async(test_body: impl FnOnce() -> Task<()> + 'static) -> Task<()> {
112 Self::app_builder_test_async_with(test_body, DEFAULT_MAX_WAIT_MS)
113 }
114
115 pub fn app_builder_test_async_with(test_body: impl FnOnce() -> Task<()> + 'static, max_wait_ms: u64) -> Task<()> {
126 let max_wait = Duration::from_millis(max_wait_ms);
127 Task::spawn_local(async move {
128 let gate = TestGate::current();
131 if !gate.clone().wait(max_wait).await {
132 return Err(Rc::new(TimeoutException) as ObservableError);
133 }
134
135 AppBuilder::reset_builder_state_for_tests();
137
138 let test_task = match PanicException::catch(test_body) {
141 Ok(task) => task,
142 Err((error, _payload)) => Task::from_exception(error),
143 };
144 let outcome = match first_of(test_task, delay(max_wait)).await {
145 First::Left(outcome) => outcome,
146 First::Right(_) => Err(Rc::new(TimeoutException) as ObservableError),
147 };
148
149 AppBuilder::reset_builder_state_for_tests();
151 gate.release();
152 outcome
153 })
154 }
155}