ferroui_reactive_testing/
lib.rs1use ferroui_reactive::concurrency::{IScheduler, VirtualTimeClock, VirtualTimeScheduler};
24use ferroui_reactive::disposables::{CompositeDisposable, Disposable};
25use ferroui_reactive::{IDisposable, IObservable, IObserver, Notification, ObservableError};
26use std::cell::RefCell;
27use std::fmt;
28use std::ops::Deref;
29use std::rc::Rc;
30use std::time::{Duration, SystemTime};
31
32pub struct TestTime;
34
35impl VirtualTimeClock for TestTime {
36 type Absolute = i64;
37 type Relative = i64;
38
39 fn add(absolute: i64, relative: i64) -> i64 {
40 absolute + relative
41 }
42
43 fn to_system_time(absolute: i64) -> SystemTime {
45 SystemTime::UNIX_EPOCH + ticks_to_duration(absolute.max(0))
46 }
47
48 fn to_relative(duration: Duration) -> i64 {
49 duration_to_ticks(duration)
50 }
51
52 fn subtract(absolute: i64, duration: Duration) -> i64 {
53 absolute - duration_to_ticks(duration)
54 }
55
56 fn adjust_due_time(due_time: i64, clock: i64) -> i64 {
58 if due_time <= clock {
59 clock + 1
60 } else {
61 due_time
62 }
63 }
64}
65
66pub fn duration_to_ticks(duration: Duration) -> i64 {
68 (duration.as_nanos() / 100) as i64
69}
70
71pub fn ticks_to_duration(ticks: i64) -> Duration {
73 Duration::from_nanos(ticks.max(0) as u64 * 100)
74}
75
76pub fn ticks_to_time(ticks: i64) -> SystemTime {
78 TestTime::to_system_time(ticks)
79}
80
81#[derive(Clone)]
85pub struct TestScheduler {
86 inner: Rc<VirtualTimeScheduler<TestTime>>,
87}
88
89impl Default for TestScheduler {
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95impl Deref for TestScheduler {
96 type Target = VirtualTimeScheduler<TestTime>;
97
98 fn deref(&self) -> &Self::Target {
99 &self.inner
100 }
101}
102
103impl TestScheduler {
104 pub fn new() -> Self {
106 Self { inner: VirtualTimeScheduler::new(0) }
107 }
108
109 pub fn scheduler(&self) -> Rc<dyn IScheduler> {
111 self.inner.clone()
112 }
113
114 pub fn schedule_absolute_action(&self, due_time: i64, action: impl FnOnce() + 'static) -> Rc<dyn IDisposable> {
116 self.inner.schedule_absolute(
117 due_time,
118 Box::new(move |_| {
119 action();
120 Disposable::empty()
121 }),
122 )
123 }
124
125 pub fn start_with<T: Clone + 'static>(
129 &self,
130 create: impl FnOnce() -> Rc<dyn IObservable<T>> + 'static,
131 created: i64,
132 subscribed: i64,
133 disposed: i64,
134 ) -> Rc<MockObserver<T>> {
135 assert!(subscribed >= created, "Specified argument was out of the range of valid values. (Parameter 'subscribed')");
136 assert!(disposed >= created && disposed >= subscribed, "Specified argument was out of the range of valid values. (Parameter 'disposed')");
137 let observer = self.create_observer::<T>();
138 let source: Rc<RefCell<Option<Rc<dyn IObservable<T>>>>> = Rc::new(RefCell::new(None));
139 let subscription: Rc<RefCell<Option<Rc<dyn IDisposable>>>> = Rc::new(RefCell::new(None));
140 {
141 let source = source.clone();
142 self.schedule_absolute_action(created, move || *source.borrow_mut() = Some(create()));
143 }
144 {
145 let (source, subscription, observer) = (source.clone(), subscription.clone(), observer.clone());
146 self.schedule_absolute_action(subscribed, move || {
147 let source = source.borrow().clone().expect("the sequence was created");
148 *subscription.borrow_mut() = Some(source.subscribe(observer));
149 });
150 }
151 {
152 let subscription = subscription.clone();
153 self.schedule_absolute_action(disposed, move || {
154 let subscription = subscription.borrow().clone();
155 if let Some(subscription) = subscription {
156 subscription.dispose();
157 }
158 });
159 }
160 self.inner.start();
161 observer
162 }
163
164 pub fn start_disposed<T: Clone + 'static>(&self, create: impl FnOnce() -> Rc<dyn IObservable<T>> + 'static, disposed: i64) -> Rc<MockObserver<T>> {
167 self.start_with(create, ReactiveTest::CREATED, ReactiveTest::SUBSCRIBED, disposed)
168 }
169
170 pub fn start<T: Clone + 'static>(&self, create: impl FnOnce() -> Rc<dyn IObservable<T>> + 'static) -> Rc<MockObserver<T>> {
173 self.start_with(create, ReactiveTest::CREATED, ReactiveTest::SUBSCRIBED, ReactiveTest::DISPOSED)
174 }
175
176 pub fn run(&self) {
178 self.inner.start();
179 }
180
181 pub fn create_hot_observable<T: Clone + 'static>(&self, messages: Vec<Recorded<Notification<T>>>) -> Rc<HotObservable<T>> {
184 HotObservable::new(self.clone(), messages)
185 }
186
187 pub fn create_cold_observable<T: Clone + 'static>(&self, messages: Vec<Recorded<Notification<T>>>) -> Rc<ColdObservable<T>> {
190 Rc::new(ColdObservable { scheduler: self.clone(), messages, subscriptions: Rc::new(RefCell::new(Vec::new())) })
191 }
192
193 pub fn create_observer<T: 'static>(&self) -> Rc<MockObserver<T>> {
195 Rc::new(MockObserver { scheduler: self.clone(), messages: RefCell::new(Vec::new()) })
196 }
197}
198
199#[derive(Clone, PartialEq)]
201pub struct Recorded<T> {
202 pub time: i64,
204 pub value: T,
206}
207
208impl<T> Recorded<T> {
209 pub fn new(time: i64, value: T) -> Self {
210 Self { time, value }
211 }
212}
213
214impl<T: fmt::Debug> fmt::Debug for Recorded<T> {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 write!(f, "{:?}@{}", self.value, self.time)
217 }
218}
219
220#[derive(Clone, Copy, PartialEq, Eq, Hash)]
223pub struct Subscription {
224 pub subscribe: i64,
226 pub unsubscribe: i64,
228}
229
230impl Subscription {
231 pub const INFINITE: i64 = i64::MAX;
233
234 pub fn new(subscribe: i64, unsubscribe: i64) -> Self {
235 Self { subscribe, unsubscribe }
236 }
237
238 pub fn starting(subscribe: i64) -> Self {
239 Self { subscribe, unsubscribe: Self::INFINITE }
240 }
241}
242
243impl fmt::Debug for Subscription {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245 if self.unsubscribe == Self::INFINITE {
246 write!(f, "({}, Infinite)", self.subscribe)
247 } else {
248 write!(f, "({}, {})", self.subscribe, self.unsubscribe)
249 }
250 }
251}
252
253#[derive(Debug)]
255pub struct TestException(pub String);
256
257impl fmt::Display for TestException {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 f.write_str(&self.0)
260 }
261}
262
263impl std::error::Error for TestException {}
264
265pub fn new_error(message: &str) -> ObservableError {
267 Rc::new(TestException(message.to_owned()))
268}
269
270#[derive(Clone)]
273pub enum Expected<T> {
274 Exact(Notification<T>),
276 OnNextWhere(Rc<dyn Fn(&T) -> bool>),
278 OnErrorWhere(Rc<dyn Fn(&ObservableError) -> bool>),
280}
281
282impl<T: PartialEq> Expected<T> {
283 pub fn matches(&self, notification: &Notification<T>) -> bool {
285 match (self, notification) {
286 (Self::Exact(expected), actual) => expected == actual,
287 (Self::OnNextWhere(predicate), Notification::OnNext(value)) => predicate(value),
288 (Self::OnErrorWhere(predicate), Notification::OnError(error)) => predicate(error),
289 _ => false,
290 }
291 }
292}
293
294impl<T: fmt::Debug> fmt::Debug for Expected<T> {
295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296 match self {
297 Self::Exact(notification) => notification.fmt(f),
298 Self::OnNextWhere(_) => f.write_str("OnNext(<predicate>)"),
299 Self::OnErrorWhere(_) => f.write_str("OnError(<predicate>)"),
300 }
301 }
302}
303
304impl<T> From<Recorded<Notification<T>>> for Recorded<Expected<T>> {
305 fn from(recorded: Recorded<Notification<T>>) -> Self {
306 Recorded::new(recorded.time, Expected::Exact(recorded.value))
307 }
308}
309
310pub struct ReactiveTest;
313
314impl ReactiveTest {
315 pub const CREATED: i64 = 100;
317 pub const SUBSCRIBED: i64 = 200;
319 pub const DISPOSED: i64 = 1000;
321
322 pub fn on_next<T>(ticks: i64, value: T) -> Recorded<Notification<T>> {
324 Recorded::new(ticks, Notification::OnNext(value))
325 }
326
327 pub fn on_next_where<T>(ticks: i64, predicate: impl Fn(&T) -> bool + 'static) -> Recorded<Expected<T>> {
329 Recorded::new(ticks, Expected::OnNextWhere(Rc::new(predicate)))
330 }
331
332 pub fn on_completed<T>(ticks: i64) -> Recorded<Notification<T>> {
334 Recorded::new(ticks, Notification::OnCompleted)
335 }
336
337 pub fn on_error<T>(ticks: i64, error: ObservableError) -> Recorded<Notification<T>> {
339 Recorded::new(ticks, Notification::OnError(error))
340 }
341
342 pub fn on_error_where<T>(ticks: i64, predicate: impl Fn(&ObservableError) -> bool + 'static) -> Recorded<Expected<T>> {
344 Recorded::new(ticks, Expected::OnErrorWhere(Rc::new(predicate)))
345 }
346
347 pub fn subscribe(start: i64, end: i64) -> Subscription {
349 Subscription::new(start, end)
350 }
351
352 pub fn subscribe_from(start: i64) -> Subscription {
354 Subscription::starting(start)
355 }
356}
357
358pub fn assert_recorded_eq<T: PartialEq + fmt::Debug>(actual: &[Recorded<Notification<T>>], expected: &[Recorded<Expected<T>>]) {
361 let matches = actual.len() == expected.len()
362 && actual.iter().zip(expected).all(|(a, e)| a.time == e.time && e.value.matches(&a.value));
363 if !matches {
364 panic!("the recorded notifications differ\n expected: {expected:?}\n actual: {actual:?}");
365 }
366}
367
368#[macro_export]
371macro_rules! assert_messages {
372 ($actual:expr $(, $expected:expr)* $(,)?) => {{
373 let expected = ::std::vec![$($crate::Recorded::<$crate::Expected<_>>::from($expected)),*];
374 $crate::assert_recorded_eq(&$actual, &expected);
375 }};
376}
377
378#[macro_export]
380macro_rules! assert_subscriptions {
381 ($actual:expr $(, $expected:expr)* $(,)?) => {{
382 let expected: ::std::vec::Vec<$crate::Subscription> = ::std::vec![$($expected),*];
383 assert_eq!($actual, expected, "the subscriptions differ");
384 }};
385}
386
387pub fn assert_throws(action: impl FnOnce()) {
390 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(action));
391 assert!(result.is_err(), "expected the action to panic");
392}
393
394pub struct MockObserver<T> {
397 scheduler: TestScheduler,
398 messages: RefCell<Vec<Recorded<Notification<T>>>>,
399}
400
401impl<T: Clone> MockObserver<T> {
402 pub fn messages(&self) -> Vec<Recorded<Notification<T>>> {
404 self.messages.borrow().clone()
405 }
406}
407
408impl<T> IObserver<T> for MockObserver<T> {
409 fn on_next(&self, value: T) {
410 let time = self.scheduler.clock();
411 self.messages.borrow_mut().push(Recorded::new(time, Notification::OnNext(value)));
412 }
413
414 fn on_error(&self, error: ObservableError) {
415 let time = self.scheduler.clock();
416 self.messages.borrow_mut().push(Recorded::new(time, Notification::OnError(error)));
417 }
418
419 fn on_completed(&self) {
420 let time = self.scheduler.clock();
421 self.messages.borrow_mut().push(Recorded::new(time, Notification::OnCompleted));
422 }
423}
424
425pub struct HotObservable<T> {
428 scheduler: TestScheduler,
429 messages: Vec<Recorded<Notification<T>>>,
430 observers: Rc<RefCell<Vec<Rc<dyn IObserver<T>>>>>,
431 subscriptions: Rc<RefCell<Vec<Subscription>>>,
432}
433
434impl<T: Clone + 'static> HotObservable<T> {
435 fn new(scheduler: TestScheduler, messages: Vec<Recorded<Notification<T>>>) -> Rc<Self> {
436 let observers: Rc<RefCell<Vec<Rc<dyn IObserver<T>>>>> = Rc::new(RefCell::new(Vec::new()));
437 for message in &messages {
438 let observers = observers.clone();
439 let notification = message.value.clone();
440 scheduler.schedule_absolute_action(message.time, move || {
441 let current = observers.borrow().clone();
442 for observer in current {
443 notification.clone().accept(&*observer);
444 }
445 });
446 }
447 Rc::new(Self { scheduler, messages, observers, subscriptions: Rc::new(RefCell::new(Vec::new())) })
448 }
449
450 pub fn subscriptions(&self) -> Vec<Subscription> {
452 self.subscriptions.borrow().clone()
453 }
454
455 pub fn messages(&self) -> Vec<Recorded<Notification<T>>> {
457 self.messages.clone()
458 }
459
460 pub fn as_observable(self: &Rc<Self>) -> Rc<dyn IObservable<T>> {
462 self.clone()
463 }
464}
465
466impl<T: Clone + 'static> IObservable<T> for HotObservable<T> {
467 fn subscribe(&self, observer: Rc<dyn IObserver<T>>) -> Rc<dyn IDisposable> {
468 self.observers.borrow_mut().push(observer.clone());
469 let index = {
470 let mut subscriptions = self.subscriptions.borrow_mut();
471 subscriptions.push(Subscription::starting(self.scheduler.clock()));
472 subscriptions.len() - 1
473 };
474 let (observers, subscriptions, scheduler) = (self.observers.clone(), self.subscriptions.clone(), self.scheduler.clone());
475 Disposable::create(move || {
476 {
477 let mut observers = observers.borrow_mut();
478 if let Some(position) = observers.iter().position(|o| Rc::ptr_eq(o, &observer)) {
479 observers.remove(position);
480 }
481 }
482 let mut subscriptions = subscriptions.borrow_mut();
483 let subscribe = subscriptions[index].subscribe;
484 subscriptions[index] = Subscription::new(subscribe, scheduler.clock());
485 })
486 }
487}
488
489pub struct ColdObservable<T> {
492 scheduler: TestScheduler,
493 messages: Vec<Recorded<Notification<T>>>,
494 subscriptions: Rc<RefCell<Vec<Subscription>>>,
495}
496
497impl<T: Clone + 'static> ColdObservable<T> {
498 pub fn subscriptions(&self) -> Vec<Subscription> {
500 self.subscriptions.borrow().clone()
501 }
502
503 pub fn messages(&self) -> Vec<Recorded<Notification<T>>> {
505 self.messages.clone()
506 }
507
508 pub fn as_observable(self: &Rc<Self>) -> Rc<dyn IObservable<T>> {
510 self.clone()
511 }
512}
513
514impl<T: Clone + 'static> IObservable<T> for ColdObservable<T> {
515 fn subscribe(&self, observer: Rc<dyn IObserver<T>>) -> Rc<dyn IDisposable> {
516 let index = {
517 let mut subscriptions = self.subscriptions.borrow_mut();
518 subscriptions.push(Subscription::starting(self.scheduler.clock()));
519 subscriptions.len() - 1
520 };
521 let group = CompositeDisposable::new();
522 for message in &self.messages {
523 let notification = message.value.clone();
524 let target = observer.clone();
525 group.add(self.scheduler.schedule_relative(
526 message.time,
527 Box::new(move |_| {
528 notification.accept(&*target);
529 Disposable::empty()
530 }),
531 ));
532 }
533 let (subscriptions, scheduler) = (self.subscriptions.clone(), self.scheduler.clone());
534 Disposable::create(move || {
535 {
536 let mut subscriptions = subscriptions.borrow_mut();
537 let subscribe = subscriptions[index].subscribe;
538 subscriptions[index] = Subscription::new(subscribe, scheduler.clock());
539 }
540 group.dispose();
541 })
542 }
543}