use color_eyre::eyre::Result;
use futures::stream::StreamExt;
use ratatui::prelude::Backend;
use tokio::time::{Duration, Instant};
use crate::{application::Application, command::Command};
mod app_input;
mod core;
pub mod frame_rate;
mod frame_scheduler;
mod keyed_commands;
mod pending_work;
use app_input::AppInput;
use frame_rate::FrameRate;
use frame_scheduler::FrameScheduler;
use keyed_commands::{CommandOutput, ReceiverEvent};
use self::core::RuntimeCore;
pub struct Runtime<App: Application> {
core: RuntimeCore<App>,
scheduler: FrameScheduler,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum BatchOutcome {
Continue,
Quit,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum InputOutcome {
Updated,
NoUpdate,
Quit,
}
impl<App: Application> Runtime<App> {
#[must_use]
pub fn new(flags: App::Flags, frame_rate: FrameRate) -> Self {
let core = RuntimeCore::new(flags);
Self {
core,
scheduler: FrameScheduler::new(frame_rate),
}
}
#[cfg(test)]
fn process_message_batch(&mut self, first_msg: App::Message) {
let _ = self.process_input_batch(AppInput::Shared(first_msg));
}
fn process_input_batch(&mut self, first_input: AppInput<App::Message>) -> BatchOutcome {
let mut processed = match self.process_app_input(first_input) {
InputOutcome::Updated => 1usize,
InputOutcome::NoUpdate => 0usize,
InputOutcome::Quit => return BatchOutcome::Quit,
};
let batch_deadline = Instant::now() + Duration::from_micros(100);
while Instant::now() < batch_deadline {
match self.core.app_inputs.try_next_ready() {
Some(input) => match self.process_app_input(input) {
InputOutcome::Updated => processed += 1,
InputOutcome::NoUpdate => {}
InputOutcome::Quit => {
if processed > 0 {
self.scheduler.mark_subscriptions_dirty();
}
return BatchOutcome::Quit;
}
},
None => break, }
}
tracing::trace!(target: "tears::runtime", messages = processed, "processed message batch");
if processed > 0 {
self.scheduler.mark_subscriptions_dirty();
}
BatchOutcome::Continue
}
fn process_app_input(&mut self, input: AppInput<App::Message>) -> InputOutcome {
match input {
AppInput::Shared(msg)
| AppInput::Keyed(ReceiverEvent::Output(CommandOutput::Message(msg))) => {
let cmd = self.core.app.update(msg);
self.dispatch_update_command(cmd);
InputOutcome::Updated
}
AppInput::Keyed(ReceiverEvent::Output(CommandOutput::Quit)) => InputOutcome::Quit,
AppInput::Keyed(ReceiverEvent::Closed) => InputOutcome::NoUpdate,
}
}
fn dispatch_update_command(&mut self, cmd: Command<App::Message>) {
let parts = cmd.into_runtime_parts();
self.scheduler.record_redraw(parts.requests_redraw());
self.core.enqueue_command(parts);
}
fn process_frame_tick<B: Backend>(
&mut self,
terminal: &mut ratatui::Terminal<B>,
) -> Result<(), <B as Backend>::Error> {
tracing::trace!(target: "tears::runtime::frame", "frame tick");
if self.scheduler.take_redraw() {
self.core.render(terminal)?;
tracing::trace!(target: "tears::runtime", "frame rendered");
}
if self.scheduler.take_subscriptions_dirty() {
self.update_subscriptions();
}
Ok(())
}
fn update_subscriptions(&mut self) {
let subscriptions = self.core.app.subscriptions();
let count = subscriptions.len();
self.core.subscription_manager.update(subscriptions);
tracing::debug!(target: "tears::runtime", count, "subscriptions re-evaluated");
}
pub async fn run<B: Backend>(
mut self,
terminal: &mut ratatui::Terminal<B>,
) -> Result<(), <B as Backend>::Error> {
self.core.initialize_subscriptions();
tracing::debug!(target: "tears::runtime", "runtime started");
loop {
tokio::select! {
Some(input) = self.core.app_inputs.next() => {
if self.process_input_batch(input) == BatchOutcome::Quit {
tracing::debug!(target: "tears::runtime", "keyed quit signal received");
break;
}
}
() = self.scheduler.next_work_frame() => {
self.process_frame_tick(terminal)?;
}
_ = self.core.quit_rx.recv() => {
tracing::debug!(target: "tears::runtime", "quit signal received");
break;
}
}
}
tracing::debug!(target: "tears::runtime", "runtime shutting down");
self.core.shutdown();
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::future::pending;
use std::num::NonZeroU32;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use futures::stream::{self, BoxStream, iter};
use ratatui::backend::TestBackend;
use ratatui::prelude::*;
use tokio::sync::oneshot;
use tokio::time::{Duration, sleep, timeout};
use crate::application::Application;
use crate::command::{CancelPolicy, Command, CommandId};
use crate::subscription::{Subscription, SubscriptionSource};
use crate::test_support::{TestApp, TestMessage, TraceRecorder, wait_until};
fn frame_rate(value: u32) -> FrameRate {
FrameRate::new(NonZeroU32::new(value).expect("frame rate must be non-zero"))
.expect("frame rate must be valid")
}
struct RedrawControlApp;
#[derive(Debug, Clone)]
enum RedrawControlMessage {
Redraw,
Skip,
}
impl Application for RedrawControlApp {
type Message = RedrawControlMessage;
type Flags = ();
fn new((): ()) -> (Self, Command<Self::Message>) {
(Self, Command::none())
}
fn update(&mut self, msg: Self::Message) -> Command<Self::Message> {
match msg {
RedrawControlMessage::Redraw => Command::none(),
RedrawControlMessage::Skip => Command::none().without_redraw(),
}
}
fn view(&self, _frame: &mut Frame<'_>) {}
fn subscriptions(&self) -> Vec<Subscription<Self::Message>> {
vec![]
}
}
#[tokio::test]
async fn test_event_loop_new() {
let runtime = Runtime::<TestApp>::new(0, frame_rate(60));
assert_eq!(runtime.core.app.counter, 0);
}
#[tokio::test]
async fn test_event_loop_new_with_different_frame_rates() {
let _runtime1 = Runtime::<TestApp>::new(0, frame_rate(30));
let _runtime2 = Runtime::<TestApp>::new(0, frame_rate(144));
}
#[tokio::test]
async fn test_runtime_frame_scheduler_period_is_accurate() {
let runtime = Runtime::<TestApp>::new(0, frame_rate(60));
let period = runtime.scheduler.frame_period();
assert!(
period >= Duration::from_micros(16_600) && period <= Duration::from_micros(16_700),
"60 FPS period should be ~16.667ms, got {period:?}",
);
let runtime = Runtime::<TestApp>::new(0, frame_rate(144));
let period = runtime.scheduler.frame_period();
assert!(
period >= Duration::from_micros(6_900) && period <= Duration::from_micros(6_950),
"144 FPS period should be ~6.944ms, got {period:?}",
);
}
#[tokio::test]
async fn test_event_loop_process_message_batch_single_message() {
let mut runtime = Runtime::<TestApp>::new(0, frame_rate(60));
runtime.process_message_batch(TestMessage::Increment);
assert_eq!(runtime.core.app.counter, 1);
assert!(runtime.scheduler.pending.needs_redraw);
}
#[tokio::test]
async fn test_process_message_batch_without_redraw_leaves_redraw_unchanged() {
let mut runtime = Runtime::<RedrawControlApp>::new((), frame_rate(60));
runtime.scheduler.pending.needs_redraw = false;
runtime.scheduler.pending.subscriptions_dirty = false;
runtime.process_message_batch(RedrawControlMessage::Skip);
assert!(!runtime.scheduler.pending.needs_redraw);
assert!(
runtime.scheduler.pending.subscriptions_dirty,
"redraw suppression must not suppress subscription re-evaluation"
);
}
#[tokio::test]
async fn test_process_message_batch_mixed_commands_redraws() {
let mut runtime = Runtime::<RedrawControlApp>::new((), frame_rate(60));
runtime.scheduler.pending.needs_redraw = false;
runtime.scheduler.pending.subscriptions_dirty = false;
let _ = runtime.core.msg_tx.send(RedrawControlMessage::Redraw);
runtime.process_message_batch(RedrawControlMessage::Skip);
assert!(runtime.scheduler.pending.needs_redraw);
assert!(runtime.scheduler.pending.subscriptions_dirty);
}
#[tokio::test]
async fn test_process_message_batch_redraw_recovers_after_suppression() {
let mut runtime = Runtime::<RedrawControlApp>::new((), frame_rate(60));
runtime.scheduler.pending.needs_redraw = false;
runtime.process_message_batch(RedrawControlMessage::Skip);
assert!(!runtime.scheduler.pending.needs_redraw);
runtime.scheduler.pending.subscriptions_dirty = false;
runtime.process_message_batch(RedrawControlMessage::Redraw);
assert!(runtime.scheduler.pending.needs_redraw);
assert!(runtime.scheduler.pending.subscriptions_dirty);
}
#[tokio::test]
async fn test_process_message_batch_emits_tracing_event() {
let recorder = TraceRecorder::new().with_target("tears::runtime");
let _guard = recorder.set_default();
let mut runtime = Runtime::<TestApp>::new(0, frame_rate(60));
runtime.process_message_batch(TestMessage::Increment);
assert!(
recorder.event_count() >= 1,
"processing a message batch should emit a tracing event"
);
}
#[tokio::test(start_paused = true)]
async fn test_event_loop_process_message_batch_with_batching() {
let mut runtime = Runtime::<TestApp>::new(0, frame_rate(60));
let _ = runtime.core.msg_tx.send(TestMessage::Increment);
let _ = runtime.core.msg_tx.send(TestMessage::Increment);
let _ = runtime.core.msg_tx.send(TestMessage::Increment);
runtime.process_message_batch(TestMessage::Increment);
assert_eq!(runtime.core.app.counter, 4);
}
#[tokio::test]
async fn test_process_input_batch_drains_shared_inputs_in_fifo_order() {
struct BatchOrderApp {
messages: Vec<i32>,
}
impl Application for BatchOrderApp {
type Message = i32;
type Flags = ();
fn new((): ()) -> (Self, Command<Self::Message>) {
(
Self {
messages: Vec::new(),
},
Command::none(),
)
}
fn update(&mut self, msg: Self::Message) -> Command<Self::Message> {
self.messages.push(msg);
Command::none()
}
fn view(&self, _frame: &mut Frame<'_>) {}
fn subscriptions(&self) -> Vec<Subscription<Self::Message>> {
vec![]
}
}
let mut runtime = Runtime::<BatchOrderApp>::new((), frame_rate(60));
runtime.scheduler.pending.subscriptions_dirty = false;
runtime
.core
.msg_tx
.send(2)
.expect("receiver should be open");
runtime
.core
.msg_tx
.send(3)
.expect("receiver should be open");
runtime.process_input_batch(AppInput::Shared(1));
assert_eq!(runtime.core.app.messages, vec![1, 2, 3]);
assert!(runtime.scheduler.pending.subscriptions_dirty);
}
#[tokio::test]
async fn test_event_loop_process_frame_tick_renders_when_needed() -> Result<()> {
let mut runtime = Runtime::<TestApp>::new(0, frame_rate(60));
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend)?;
assert!(runtime.scheduler.pending.needs_redraw);
runtime.process_frame_tick(&mut terminal)?;
assert!(!runtime.scheduler.pending.needs_redraw);
Ok(())
}
#[tokio::test]
async fn test_event_loop_process_frame_tick_skips_render_when_not_needed() -> Result<()> {
let mut runtime = Runtime::<TestApp>::new(0, frame_rate(60));
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend)?;
runtime.scheduler.pending.needs_redraw = false;
runtime.process_frame_tick(&mut terminal)?;
assert!(!runtime.scheduler.pending.needs_redraw);
Ok(())
}
#[tokio::test]
async fn test_event_loop_run_quits_on_quit_action() -> Result<()> {
let runtime = Runtime::<TestApp>::new(0, frame_rate(60));
let _ = runtime.core.msg_tx.send(TestMessage::Quit);
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend)?;
timeout(Duration::from_secs(5), runtime.run(&mut terminal))
.await
.expect("run() should quit before the timeout")?;
Ok(())
}
#[tokio::test]
async fn shared_cancel_drops_ready_keyed_output_before_batch_delivery() {
enum Message {
Leave,
Result(i32),
}
struct CancellationApp {
results: Vec<i32>,
}
impl Application for CancellationApp {
type Message = Message;
type Flags = ();
fn new((): ()) -> (Self, Command<Self::Message>) {
(
Self {
results: Vec::new(),
},
Command::none(),
)
}
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message {
Message::Leave => Command::cancel(CommandId::new("search")),
Message::Result(value) => {
self.results.push(value);
Command::none()
}
}
}
fn view(&self, _frame: &mut Frame<'_>) {}
fn subscriptions(&self) -> Vec<Subscription<Self::Message>> {
Vec::new()
}
}
let id = CommandId::new("search");
let mut runtime = Runtime::<CancellationApp>::new((), frame_rate(60));
runtime.core.enqueue_command(
Command::stream(iter([Message::Result(1), Message::Result(2)]))
.cancellable(id.clone())
.into_runtime_parts(),
);
wait_until(
|| runtime.core.app_inputs.has_closed_buffered(&id),
"keyed results should be buffered before cancellation",
)
.await;
runtime
.core
.msg_tx
.send(Message::Leave)
.expect("message receiver should be open");
let first = runtime
.core
.app_inputs
.next()
.await
.expect("shared input should be ready");
assert!(matches!(first, AppInput::Shared(Message::Leave)));
assert_eq!(runtime.process_input_batch(first), BatchOutcome::Continue);
assert!(runtime.core.app.results.is_empty());
assert!(runtime.core.app_inputs.try_next_ready().is_none());
}
#[tokio::test]
async fn keep_in_flight_applies_redraw_and_cancels_before_dropping_new_stream() {
struct DropGuard(Arc<AtomicBool>);
impl Drop for DropGuard {
fn drop(&mut self) {
self.0.store(true, Ordering::SeqCst);
}
}
let keep_id = CommandId::new("kept");
let cancel_id = CommandId::new("cancelled");
let kept_dropped = Arc::new(AtomicBool::new(false));
let cancelled_dropped = Arc::new(AtomicBool::new(false));
let arrival_dropped = Arc::new(AtomicBool::new(false));
let (kept_started_tx, kept_started_rx) = oneshot::channel();
let (cancelled_started_tx, cancelled_started_rx) = oneshot::channel();
let mut runtime = Runtime::<TestApp>::new(0, frame_rate(60));
let kept_guard = DropGuard(Arc::clone(&kept_dropped));
runtime.core.enqueue_command(
Command::future(async move {
let _guard = kept_guard;
let _ = kept_started_tx.send(());
pending::<TestMessage>().await
})
.cancellable(keep_id.clone())
.into_runtime_parts(),
);
let cancelled_guard = DropGuard(Arc::clone(&cancelled_dropped));
runtime.core.enqueue_command(
Command::future(async move {
let _guard = cancelled_guard;
let _ = cancelled_started_tx.send(());
pending::<TestMessage>().await
})
.cancellable(cancel_id.clone())
.into_runtime_parts(),
);
timeout(Duration::from_secs(1), kept_started_rx)
.await
.expect("kept command should start before the timeout")
.expect("kept command should signal that it started");
timeout(Duration::from_secs(1), cancelled_started_rx)
.await
.expect("cancelled command should start before the timeout")
.expect("cancelled command should signal that it started");
runtime.scheduler.pending.needs_redraw = false;
let arrival_guard = DropGuard(Arc::clone(&arrival_dropped));
let dropped_arrival = Command::future(async move {
let _guard = arrival_guard;
pending::<TestMessage>().await
});
let command = Command::batch([Command::cancel(cancel_id), dropped_arrival])
.cancellable_with(keep_id, CancelPolicy::KeepInFlight);
runtime.dispatch_update_command(command);
assert!(runtime.scheduler.pending.needs_redraw);
assert!(arrival_dropped.load(Ordering::SeqCst));
assert!(!kept_dropped.load(Ordering::SeqCst));
wait_until(
|| cancelled_dropped.load(Ordering::SeqCst),
"explicit cancels should be applied before KeepInFlight drops the arrival",
)
.await;
runtime.core.shutdown();
wait_until(
|| kept_dropped.load(Ordering::SeqCst),
"shutdown should clean up the kept command",
)
.await;
}
#[tokio::test]
async fn live_keyed_quit_exits_the_runtime() -> Result<()> {
struct KeyedQuitApp;
impl Application for KeyedQuitApp {
type Message = ();
type Flags = ();
fn new((): ()) -> (Self, Command<Self::Message>) {
(Self, Command::quit().cancellable(CommandId::new("quit")))
}
fn update(&mut self, (): ()) -> Command<Self::Message> {
Command::none()
}
fn view(&self, _frame: &mut Frame<'_>) {}
fn subscriptions(&self) -> Vec<Subscription<Self::Message>> {
Vec::new()
}
}
let runtime = Runtime::<KeyedQuitApp>::new((), frame_rate(60));
let mut terminal = Terminal::new(TestBackend::new(80, 24))?;
timeout(Duration::from_secs(1), runtime.run(&mut terminal))
.await
.expect("live keyed quit should stop the runtime")?;
Ok(())
}
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn test_event_loop_run_quits_while_idle() -> Result<()> {
let runtime = Runtime::<TestApp>::new(0, frame_rate(60));
let quit_tx = runtime.core.quit_tx.clone();
tokio::spawn(async move {
sleep(Duration::from_secs(1)).await;
let _ = quit_tx.send(());
});
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend)?;
timeout(Duration::from_secs(5), runtime.run(&mut terminal))
.await
.expect("run() should quit while idle before the timeout")?;
Ok(())
}
#[tokio::test]
async fn test_frame_branch_gated_off_when_idle() -> Result<()> {
let mut runtime = Runtime::<TestApp>::new(0, frame_rate(60));
let mut terminal = Terminal::new(TestBackend::new(80, 24))?;
runtime.process_frame_tick(&mut terminal)?;
assert!(!runtime.scheduler.pending.has_pending_work());
Ok(())
}
#[tokio::test]
async fn test_frame_branch_reenabled_after_message() -> Result<()> {
let mut runtime = Runtime::<TestApp>::new(0, frame_rate(60));
let mut terminal = Terminal::new(TestBackend::new(80, 24))?;
runtime.process_frame_tick(&mut terminal)?;
assert!(!runtime.scheduler.pending.has_pending_work());
runtime.process_message_batch(TestMessage::Increment);
assert!(runtime.scheduler.pending.has_pending_work());
Ok(())
}
#[tokio::test]
async fn test_event_loop_process_frame_tick_updates_subscriptions() -> Result<()> {
struct ParkedSource {
spawns: Arc<AtomicUsize>,
}
impl SubscriptionSource for ParkedSource {
type Output = ();
type Key = ();
fn stream(&self) -> BoxStream<'static, ()> {
self.spawns.fetch_add(1, Ordering::SeqCst);
stream::pending().boxed()
}
fn key(&self) -> Self::Key {}
}
struct App {
spawns: Arc<AtomicUsize>,
}
impl Application for App {
type Message = ();
type Flags = Arc<AtomicUsize>;
fn new(spawns: Arc<AtomicUsize>) -> (Self, Command<Self::Message>) {
(Self { spawns }, Command::none())
}
fn update(&mut self, (): ()) -> Command<Self::Message> {
Command::none()
}
fn view(&self, _frame: &mut Frame<'_>) {}
fn subscriptions(&self) -> Vec<Subscription<Self::Message>> {
vec![Subscription::new(ParkedSource {
spawns: self.spawns.clone(),
})]
}
}
let spawns = Arc::new(AtomicUsize::new(0));
let mut runtime = Runtime::<App>::new(spawns.clone(), frame_rate(60));
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend)?;
runtime.scheduler.pending.needs_redraw = false;
assert_eq!(spawns.load(Ordering::SeqCst), 0);
runtime.scheduler.pending.subscriptions_dirty = true;
runtime.process_frame_tick(&mut terminal)?;
assert!(!runtime.scheduler.pending.subscriptions_dirty);
wait_until(
|| spawns.load(Ordering::SeqCst) == 1,
"a dirty frame tick must actually start the requested subscription",
)
.await;
Ok(())
}
struct SubCountingApp {
sub_calls: Arc<AtomicUsize>,
}
impl Application for SubCountingApp {
type Message = ();
type Flags = Arc<AtomicUsize>;
fn new(sub_calls: Arc<AtomicUsize>) -> (Self, Command<Self::Message>) {
(Self { sub_calls }, Command::none())
}
fn update(&mut self, (): ()) -> Command<Self::Message> {
Command::none()
}
fn view(&self, _frame: &mut Frame<'_>) {}
fn subscriptions(&self) -> Vec<Subscription<Self::Message>> {
self.sub_calls.fetch_add(1, Ordering::SeqCst);
vec![]
}
}
#[tokio::test]
async fn test_subscriptions_not_reevaluated_on_idle_frames() -> Result<()> {
let counter = Arc::new(AtomicUsize::new(0));
let mut runtime = Runtime::<SubCountingApp>::new(counter.clone(), frame_rate(60));
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend)?;
runtime.process_frame_tick(&mut terminal)?;
runtime.process_frame_tick(&mut terminal)?;
runtime.process_frame_tick(&mut terminal)?;
assert_eq!(
counter.load(Ordering::SeqCst),
0,
"subscriptions() should not be called on idle frames"
);
Ok(())
}
#[tokio::test]
async fn test_subscriptions_reevaluated_after_message() -> Result<()> {
let counter = Arc::new(AtomicUsize::new(0));
let mut runtime = Runtime::<SubCountingApp>::new(counter.clone(), frame_rate(60));
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend)?;
runtime.process_frame_tick(&mut terminal)?;
assert_eq!(counter.load(Ordering::SeqCst), 0);
runtime.process_message_batch(());
runtime.process_frame_tick(&mut terminal)?;
assert_eq!(counter.load(Ordering::SeqCst), 1);
runtime.process_frame_tick(&mut terminal)?;
runtime.process_frame_tick(&mut terminal)?;
assert_eq!(counter.load(Ordering::SeqCst), 1);
Ok(())
}
#[tokio::test]
async fn test_finished_subscription_restarted_with_unchanged_id() -> Result<()> {
#[derive(Clone)]
struct RestartCounters {
spawns: Arc<AtomicUsize>,
completions: Arc<AtomicUsize>,
}
struct OneshotSource {
counters: RestartCounters,
}
impl SubscriptionSource for OneshotSource {
type Output = ();
type Key = ();
fn stream(&self) -> BoxStream<'static, ()> {
self.counters.spawns.fetch_add(1, Ordering::SeqCst);
let completions = self.counters.completions.clone();
stream::unfold(false, move |emitted| {
let completions = completions.clone();
async move {
if emitted {
completions.fetch_add(1, Ordering::SeqCst);
None
} else {
Some(((), true))
}
}
})
.boxed()
}
fn key(&self) -> Self::Key {
}
}
struct RestartApp {
counters: RestartCounters,
}
impl Application for RestartApp {
type Message = ();
type Flags = RestartCounters;
fn new(counters: RestartCounters) -> (Self, Command<Self::Message>) {
(Self { counters }, Command::none())
}
fn update(&mut self, (): ()) -> Command<Self::Message> {
Command::none()
}
fn view(&self, _frame: &mut Frame<'_>) {}
fn subscriptions(&self) -> Vec<Subscription<Self::Message>> {
vec![Subscription::new(OneshotSource {
counters: self.counters.clone(),
})]
}
}
let counters = RestartCounters {
spawns: Arc::new(AtomicUsize::new(0)),
completions: Arc::new(AtomicUsize::new(0)),
};
let mut runtime = Runtime::<RestartApp>::new(counters.clone(), frame_rate(60));
let mut terminal = Terminal::new(TestBackend::new(80, 24))?;
runtime.core.initialize_subscriptions();
wait_until(
|| counters.spawns.load(Ordering::SeqCst) >= 1,
"initial subscription spawn should start before the timeout",
)
.await;
for round in 2..=3 {
wait_until(
|| counters.completions.load(Ordering::SeqCst) >= round - 1,
"current one-shot subscription should finish before re-evaluation",
)
.await;
runtime.process_message_batch(());
runtime.process_frame_tick(&mut terminal)?;
wait_until(
|| counters.spawns.load(Ordering::SeqCst) >= round,
"finished subscription should restart before the timeout",
)
.await;
}
Ok(())
}
}