ferroui_reactiveui_testing/test_sequencer.rs
1//! `TestSequencer` (`ReactiveUI.Testing/TestSequencer.cs`).
2
3use ferroui_system::threading::tasks::{Task, TaskCompletionSource};
4use ferroui_system::IDisposable;
5use std::cell::{Cell, RefCell};
6use std::fmt;
7
8/// Two participants of a test that wait for each other phase by phase: each calls
9/// [`advance_phase_async`](Self::advance_phase_async) at the end of its part of a phase, and the
10/// phase completes when both have (`TestSequencer`, a `Barrier` of two participants).
11///
12/// Deviation (DEVIATIONS.md, ReactiveUI): the observables are single-threaded, so the two
13/// participants are two tasks of the current thread instead of two threads: the task of the first
14/// participant to arrive ends when the second one arrives, and the task of the second one has
15/// ended when [`advance_phase_async`](Self::advance_phase_async) returns. The continuations of a
16/// task run when it ends, so upstream's `Task.Yield()` after the barrier has no counterpart.
17#[derive(Default)]
18pub struct TestSequencer {
19 /// The phases both participants completed (`Barrier.CurrentPhaseNumber`).
20 completed_phases: Cell<i64>,
21 /// The phase the participants are in (`CurrentPhase`).
22 current_phase: Cell<i64>,
23 /// The first participant of the current phase, waiting for the second one.
24 waiting: RefCell<Option<TaskCompletionSource<()>>>,
25 disposed: Cell<bool>,
26}
27
28impl fmt::Debug for TestSequencer {
29 /// `{CompletedPhases}, {CurrentPhase}` (`DebuggerDisplay`).
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 write!(f, "{}, {}", self.completed_phases(), self.current_phase())
32 }
33}
34
35impl TestSequencer {
36 /// A sequencer before its first phase.
37 pub fn new() -> Self {
38 Self::default()
39 }
40
41 /// The number of phases both participants completed (`CompletedPhases`).
42 pub fn completed_phases(&self) -> i64 {
43 self.completed_phases.get()
44 }
45
46 /// The phase the participants are in, set when the first of them arrives (`CurrentPhase`).
47 pub fn current_phase(&self) -> i64 {
48 self.current_phase.get()
49 }
50
51 /// Ends the part of the current phase of the calling participant; the task ends when the other
52 /// participant has ended its part too (`AdvancePhaseAsync()`).
53 pub fn advance_phase_async(&self) -> Task<()> {
54 self.advance_phase_async_with("")
55 }
56
57 /// [`advance_phase_async`](Self::advance_phase_async), with a comment that names the phase for
58 /// the reader of the test (`AdvancePhaseAsync(string)`).
59 ///
60 /// # Panics
61 /// When the sequencer is disposed, where upstream's barrier throws `ObjectDisposedException`.
62 pub fn advance_phase_async_with(&self, comment: &str) -> Task<()> {
63 let _ = comment;
64 assert!(!self.disposed.get(), "Cannot access a disposed object. Object name: 'TestSequencer'.");
65 let waiting = self.waiting.borrow_mut().take();
66 match waiting {
67 // The first participant to arrive starts the phase and waits for the other one.
68 None => {
69 self.current_phase.set(self.completed_phases.get() + 1);
70 let source = TaskCompletionSource::new();
71 let task = source.task();
72 *self.waiting.borrow_mut() = Some(source);
73 task
74 }
75 // The second one completes the phase and releases the first.
76 Some(first) => {
77 self.completed_phases.set(self.completed_phases.get() + 1);
78 first.set_result(());
79 Task::from_result(())
80 }
81 }
82 }
83}
84
85impl IDisposable for TestSequencer {
86 /// Disposes the sequencer (`Dispose`); a participant still waiting has its task cancelled.
87 fn dispose(&self) {
88 if self.disposed.replace(true) {
89 return;
90 }
91 let waiting = self.waiting.borrow_mut().take();
92 if let Some(waiting) = waiting {
93 let _ = waiting.try_set_canceled();
94 }
95 }
96}