use std::io;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use futures_util::stream::{self, Stream};
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use tokio::sync::mpsc;
use super::*;
use crate::tui::event_loop::{run, LoopIo, LoopStats, INTERNAL_ERROR, WORKER_STOPPED};
use crate::tui::terminal::{chain_panic_hook, PanicHook, Signals};
type Input = std::pin::Pin<Box<dyn Stream<Item = io::Result<Event>>>>;
struct Harness {
keys: mpsc::UnboundedSender<Event>,
events: mpsc::Sender<WorkerEvent>,
commands: mpsc::Receiver<Command>,
io: LoopIo<Input>,
}
fn harness() -> Harness {
let (keys, key_rx) = mpsc::unbounded_channel();
let input: Input = Box::pin(stream::unfold(key_rx, |mut rx| async move {
rx.recv().await.map(|event| (Ok(event), rx))
}));
let (events, event_rx) = mpsc::channel(64);
let (command_tx, commands) = mpsc::channel(16);
Harness {
keys,
events,
commands,
io: LoopIo {
input,
events: event_rx,
commands: command_tx,
dropped: Arc::new(AtomicU64::new(0)),
fps: 20,
signals: Signals::none(),
alive: || true,
},
}
}
fn key(code: KeyCode, modifiers: KeyModifiers, kind: KeyEventKind) -> Event {
let mut event = KeyEvent::new(code, modifiers);
event.kind = kind;
Event::Key(event)
}
fn press_event(code: KeyCode) -> Event {
key(code, KeyModifiers::NONE, KeyEventKind::Press)
}
async fn run_app(app: &mut App, io: &mut LoopIo<Input>) -> LoopStats {
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
tokio::time::timeout(Duration::from_secs(60), run(&mut terminal, app, io))
.await
.expect("loop did not finish")
.unwrap()
}
#[tokio::test(start_paused = true)]
async fn an_idle_screen_is_drawn_once_and_q_quits() {
let mut h = harness();
let keys = h.keys.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(10)).await;
keys.send(press_event(KeyCode::Char('q'))).unwrap();
});
let mut app = new_app(None);
let stats = run_app(&mut app, &mut h.io).await;
assert!(app.quit);
assert_eq!(stats.frames, 1, "no redraw without a change");
}
#[tokio::test(start_paused = true)]
async fn a_burst_of_worker_events_costs_one_frame() {
let mut h = harness();
let keys = h.keys.clone();
let events = h.events.clone();
tokio::spawn(async move {
events
.send(WorkerEvent::Connected {
local: "10.0.0.1:47808".into(),
})
.await
.unwrap();
for i in 0..50 {
let address = format!("10.0.0.{i}:47808");
events
.send(WorkerEvent::Discovered(row(i, &address, 1, 0)))
.await
.unwrap();
}
tokio::time::sleep(Duration::from_millis(500)).await;
keys.send(press_event(KeyCode::Char('q'))).unwrap();
});
let mut app = new_app(None);
let stats = run_app(&mut app, &mut h.io).await;
assert_eq!(app.devices.len(), 50);
assert!(
(2..=3).contains(&stats.frames),
"initial frame plus one or two for the burst, got {}",
stats.frames
);
}
#[tokio::test(start_paused = true)]
async fn only_presses_count_and_ctrl_c_twice_quits() {
let mut h = harness();
h.keys
.send(key(
KeyCode::Char('q'),
KeyModifiers::NONE,
KeyEventKind::Release,
))
.unwrap();
let ctrl_c = key(
KeyCode::Char('c'),
KeyModifiers::CONTROL,
KeyEventKind::Press,
);
h.keys.send(ctrl_c.clone()).unwrap();
h.keys.send(ctrl_c).unwrap();
let mut app = connected_app();
run_app(&mut app, &mut h.io).await;
assert!(app.quit);
assert_eq!(app.exit_error, None);
}
#[tokio::test(start_paused = true)]
async fn commands_from_keys_reach_the_worker() {
let mut h = harness();
for event in [
press_event(KeyCode::Char('d')),
press_event(KeyCode::Tab),
press_event(KeyCode::Char('7')),
press_event(KeyCode::Enter),
press_event(KeyCode::Char('q')),
] {
h.keys.send(event).unwrap();
}
let mut app = connected_app();
run_app(&mut app, &mut h.io).await;
let Ok(Command::WhoIs { spec, .. }) = h.commands.try_recv() else {
panic!("no WhoIs command");
};
assert_eq!(
spec.range,
Some(bacnet_services::who_is::DeviceInstanceRange::single(7).unwrap())
);
}
#[tokio::test(start_paused = true)]
async fn the_loop_ends_when_input_ends() {
let mut h = harness();
let keys = h.keys;
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(1)).await;
drop(keys);
});
let mut app = connected_app();
run_app(&mut app, &mut h.io).await;
assert!(app.quit, "end of input quits");
assert_eq!(app.exit_error, None);
}
#[tokio::test(start_paused = true)]
async fn a_worker_that_stops_early_ends_the_loop_with_an_error() {
let mut h = harness();
let (worker_tx, worker_rx) = mpsc::channel(1);
h.io.events = worker_rx;
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(1)).await;
drop(worker_tx);
});
let mut app = connected_app();
run_app(&mut app, &mut h.io).await;
assert!(app.quit);
assert_eq!(app.exit_error.as_deref(), Some(WORKER_STOPPED));
assert_eq!(app.exit_code, Some(1));
}
static TEST_TERMINAL_ACTIVE: AtomicBool = AtomicBool::new(true);
fn test_terminal_active() -> bool {
TEST_TERMINAL_ACTIVE.load(Ordering::SeqCst)
}
#[tokio::test(start_paused = true)]
async fn a_panic_in_another_task_stops_drawing_and_ends_the_loop() {
let mut h = harness();
h.io.alive = test_terminal_active;
let original = std::panic::take_hook();
let silent: PanicHook = Box::new(|_: &std::panic::PanicHookInfo<'_>| {});
std::panic::set_hook(chain_panic_hook(silent, || {
TEST_TERMINAL_ACTIVE.store(false, Ordering::SeqCst)
}));
let panicked = tokio::spawn(async {
tokio::time::sleep(Duration::from_secs(1)).await;
panic!("a bug in a client task");
});
let events = h.events.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(2)).await;
let _ = events
.send(WorkerEvent::Discovered(row(9, "10.0.0.9:47808", 1, 0)))
.await;
});
let mut app = connected_app();
let stats = run_app(&mut app, &mut h.io).await;
std::panic::set_hook(original);
assert!(panicked.await.unwrap_err().is_panic());
assert!(app.quit);
assert_eq!(app.exit_error.as_deref(), Some(INTERNAL_ERROR));
assert_eq!(
stats.frames, 1,
"nothing drawn after the terminal was restored"
);
}