Skip to main content

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}