use std::io::{self, Cursor, PipeWriter, Write};
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use crate::prelude::*;
use crate::runtime::Harness;
use crate::widgets::{Terminal, TerminalChange, TerminalSession};
use super::super::terminal_session::lock;
use super::TerminalCarrier;
const PATIENCE: Duration = Duration::from_secs(30);
#[derive(Debug, Default)]
struct Asked {
written: Vec<u8>,
sizes: Vec<(u16, u16)>,
ended: usize,
}
#[derive(Clone, Default)]
struct Recording {
asked: Arc<Mutex<Asked>>,
code: Option<u32>,
}
impl TerminalCarrier for Recording {
fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
lock(&self.asked).written.extend_from_slice(bytes);
Ok(())
}
fn resize(&mut self, columns: u16, rows: u16) -> io::Result<()> {
lock(&self.asked).sizes.push((columns, rows));
Ok(())
}
fn end(&mut self) {
lock(&self.asked).ended += 1;
}
fn exit_code(&mut self) -> Option<u32> {
self.code
}
}
fn wait_for(session: &TerminalSession, text: &str) {
let watch = session.watch();
let started = Instant::now();
while !session.parser().screen().contents().contains(text) {
let _ = watch.next_change_within(Duration::from_millis(200));
assert!(started.elapsed() < PATIENCE, "{text:?} never came:\n{}", session.parser().screen().contents());
}
}
fn wait_quiet(session: &TerminalSession) {
let started = Instant::now();
while session.last_output().elapsed() < Duration::from_millis(300) {
std::thread::sleep(Duration::from_millis(50));
assert!(started.elapsed() < PATIENCE, "the program never went quiet");
}
}
fn everything(session: &TerminalSession) -> Vec<Vec<u8>> {
let parser = session.parser();
let mut screen = parser.screen().clone();
let columns = screen.size().1;
let mut out = vec![
screen.contents_formatted(),
screen.input_mode_formatted(),
screen.cursor_state_formatted(),
vec![u8::from(screen.alternate_screen())],
format!("{:?} {:?}", parser.callbacks().keyboard.pushed(false), parser.callbacks().keyboard.pushed(true))
.into_bytes(),
];
screen.set_scrollback(usize::MAX);
for back in (1..=screen.scrollback()).rev() {
screen.set_scrollback(back);
out.push(screen.rows_formatted(0, columns).next().unwrap_or_default());
}
out
}
const PROGRAM: &str = "i=1; while [ $i -le 30 ]; do printf 'line %s\\n' $i; i=$((i+1)); done; \
printf '\\033[31mred\\033[m \\033[1mbold\\033[m'; \
printf '\\033[?2004h\\033[?1000h\\033[?1006h\\033[?1004h\\033[>1u'; printf ' ready'; read a; \
printf '\\033[?1049h\\033[2;3Halt screen\\033[>3u'; read b; \
printf '\\033[?1049lback'; read c";
#[test]
fn a_snapshot_and_the_bytes_after_it_draw_what_the_program_draws() {
let relay: Arc<Mutex<Option<PipeWriter>>> = Arc::default();
let tee = Arc::clone(&relay);
let real = TerminalSession::builder("/bin/sh")
.args(["-c", PROGRAM])
.folder(Path::new("/"))
.size(40, 10)
.scrollback(100)
.tee(move |bytes| {
if let Some(relay) = lock(&tee).as_mut() {
relay.write_all(bytes).expect("the attached screen reads");
}
})
.spawn()
.expect("pty");
wait_for(&real, "ready");
wait_quiet(&real);
let (output, mut relayed) = io::pipe().expect("a pipe");
{
let mut relay = lock(&relay);
relayed.write_all(&real.snapshot()).expect("the attached screen reads");
*relay = Some(relayed);
}
let attached = TerminalSession::attach(output, Recording::default()).size(40, 10).scrollback(100).start();
wait_for(&attached, "ready");
assert_eq!(everything(&attached), everything(&real), "the same screen, scrollback, cursor and modes");
real.write(b"\r").expect("the program reads");
wait_for(&real, "alt screen");
wait_for(&attached, "alt screen");
wait_quiet(&real);
assert_eq!(everything(&attached), everything(&real), "the alternate screen too");
real.kill();
}
#[test]
fn a_snapshot_taken_on_the_alternate_screen_keeps_the_screen_beneath_it() {
let relay: Arc<Mutex<Option<PipeWriter>>> = Arc::default();
let tee = Arc::clone(&relay);
let real = TerminalSession::builder("/bin/sh")
.args(["-c", PROGRAM])
.folder(Path::new("/"))
.size(40, 10)
.scrollback(100)
.tee(move |bytes| {
if let Some(relay) = lock(&tee).as_mut() {
relay.write_all(bytes).expect("the attached screen reads");
}
})
.spawn()
.expect("pty");
wait_for(&real, "ready");
real.write(b"\r").expect("the program reads");
wait_for(&real, "alt screen");
wait_quiet(&real);
let (output, mut relayed) = io::pipe().expect("a pipe");
{
let mut relay = lock(&relay);
relayed.write_all(&real.snapshot()).expect("the attached screen reads");
*relay = Some(relayed);
}
let attached = TerminalSession::attach(output, Recording::default()).size(40, 10).scrollback(100).start();
wait_for(&attached, "alt screen");
assert_eq!(everything(&attached), everything(&real), "the alternate screen as the program drew it");
real.write(b"\r").expect("the program reads");
wait_for(&real, "back");
wait_for(&attached, "back");
wait_quiet(&real);
assert!(attached.parser().screen().contents().contains("bold"), "{}", attached.parser().screen().contents());
assert_eq!(everything(&attached), everything(&real), "the screen beneath, its scrollback and its modes");
real.kill();
}
struct Screen {
session: TerminalSession,
}
impl App for Screen {
type Msg = ();
fn update(&mut self, (): ()) -> Command<()> {
Command::none()
}
fn view(&self, ui: &mut View<'_, ()>) {
ui.add(Terminal::new(&self.session)).fill().id("terminal");
}
}
#[test]
fn what_a_person_types_and_the_size_the_screen_takes_reach_the_carrier() {
let (output, mut program) = io::pipe().expect("a pipe");
let carrier = Recording::default();
let asked = Arc::clone(&carrier.asked);
let session = TerminalSession::attached(output, carrier);
program.write_all(b"$ ").expect("the screen reads");
wait_for(&session, "$");
let mut h = Harness::new(Screen { session: session.clone() }, 50, 12);
h.click_text("$").type_text("ls").press("enter");
assert_eq!(lock(&asked).written, b"ls\r", "the keys went to the program through the carrier");
let watch = session.watch();
let started = Instant::now();
while lock(&asked).sizes.is_empty() {
let _ = watch.next_change_within(Duration::from_millis(100));
assert!(started.elapsed() < PATIENCE, "the size never reached the carrier");
}
assert_eq!(lock(&asked).sizes, [(50, 12)], "the screen's size, columns first");
assert_eq!(lock(&asked).ended, 0);
}
#[test]
fn a_programs_question_is_left_to_the_session_it_runs_in() {
let (output, mut program) = io::pipe().expect("a pipe");
let carrier = Recording::default();
let asked = Arc::clone(&carrier.asked);
let session = TerminalSession::attached(output, carrier);
program.write_all(b"\x1b[6n\x1b[c\x1b[?u\x1b]11;?\x1b\\asked").expect("the screen reads");
wait_for(&session, "asked");
assert!(lock(&asked).written.is_empty(), "nothing was written back: {:?}", lock(&asked).written);
}
#[test]
fn the_end_of_the_output_is_the_end_of_the_program_with_the_code_the_carrier_gives() {
let carrier = Recording { code: Some(3), ..Recording::default() };
let session = TerminalSession::attached(Cursor::new(b"bye".to_vec()), carrier);
let watch = session.watch();
let started = Instant::now();
loop {
match watch.next_change_within(Duration::from_millis(200)) {
Some(TerminalChange::Exited(code)) => {
assert_eq!(code, Some(3), "the code the carrier was told");
break;
}
_ => assert!(started.elapsed() < PATIENCE, "the end never came"),
}
}
assert!(session.parser().screen().contents().contains("bye"), "what it wrote before is drawn");
assert!(session.write(b"x").is_err(), "a program that ended takes no keys");
}
#[test]
fn letting_the_screen_go_leaves_the_program_running_and_kill_ends_it() {
let (output, _program) = io::pipe().expect("a pipe");
let carrier = Recording::default();
let asked = Arc::clone(&carrier.asked);
let session = TerminalSession::attached(output, carrier);
let kept = session.clone();
drop(session);
assert_eq!(lock(&asked).ended, 0, "a screen that goes is not a program that ends");
kept.terminate(Duration::from_secs(1));
assert_eq!(lock(&asked).ended, 1, "terminate asks the carrier, since the program is not here");
assert_eq!(kept.pid(), None, "and there is no process id to signal");
kept.kill();
assert_eq!(lock(&asked).ended, 2);
drop(kept);
assert_eq!(lock(&asked).ended, 2, "and the last handle going asks nothing more");
}
#[test]
fn a_listener_hears_every_byte_the_program_writes_before_it_is_drawn() {
let heard: Arc<Mutex<Vec<u8>>> = Arc::default();
let into = Arc::clone(&heard);
let session = TerminalSession::builder("/bin/sh")
.args(["-c", "printf 'one\\033[31mtwo\\033[m'; read a"])
.folder(Path::new("/"))
.tee(move |bytes| lock(&into).extend_from_slice(bytes))
.spawn()
.expect("pty");
wait_for(&session, "onetwo");
let heard = String::from_utf8_lossy(&lock(&heard)).into_owned();
assert!(heard.contains("one\x1b[31mtwo\x1b[m"), "the raw bytes, escapes and all: {heard:?}");
session.kill();
}
#[test]
fn a_session_no_widget_shows_takes_the_size_a_screen_asks_for() {
let real = TerminalSession::builder("/bin/sh")
.args(["-c", "printf 'ready'; read a; stty size; read b"])
.folder(Path::new("/"))
.spawn()
.expect("pty");
wait_for(&real, "ready");
real.resize(91, 17);
assert_eq!(real.size(), (91, 17), "the screen kept here");
real.write(b"\r").expect("the program reads");
wait_for(&real, "17 91");
real.kill();
let (output, _program) = io::pipe().expect("a pipe");
let carrier = Recording::default();
let asked = Arc::clone(&carrier.asked);
let attached = TerminalSession::attached(output, carrier);
attached.resize(70, 20);
assert_eq!(attached.size(), (70, 20));
assert_eq!(lock(&asked).sizes, [(70, 20)], "the carrier is told, columns first");
}
#[test]
fn a_chunk_read_while_a_screen_attaches_reaches_the_snapshot_or_the_relay() {
let relay: Arc<Mutex<Option<PipeWriter>>> = Arc::default();
let tee = Arc::clone(&relay);
let (in_between, held) = std::sync::mpsc::channel::<()>();
let (release, go_on) = std::sync::mpsc::channel::<()>();
let go_on = Mutex::new(go_on);
let real = TerminalSession::builder("/bin/sh")
.args(["-c", "printf 'early'; read a; printf ' late'; read b"])
.folder(Path::new("/"))
.size(40, 10)
.tee(move |bytes| {
if let Some(relay) = lock(&tee).as_mut() {
relay.write_all(bytes).expect("the attached screen reads");
}
if String::from_utf8_lossy(bytes).contains("late") {
let _ = in_between.send(());
let _ = lock(&go_on).recv_timeout(PATIENCE);
}
})
.spawn()
.expect("pty");
wait_for(&real, "early");
real.write(b"\r").expect("the program reads");
held.recv_timeout(PATIENCE).expect("the listener heard the late chunk");
let (output, relayed) = io::pipe().expect("a pipe");
let attaching = {
let (real, relay) = (real.clone(), Arc::clone(&relay));
std::thread::spawn(move || {
real.snapshot_then(|snapshot| {
let mut relayed = relayed;
relayed.write_all(snapshot).expect("the attached screen reads");
*lock(&relay) = Some(relayed);
});
})
};
std::thread::sleep(Duration::from_millis(300));
release.send(()).expect("the listener waits");
attaching.join().expect("the attach ends");
let attached = TerminalSession::attach(output, Recording::default()).size(40, 10).start();
wait_for(&real, " late");
let started = Instant::now();
while !attached.parser().screen().contents().contains(" late") {
let _ = attached.watch().next_change_within(Duration::from_millis(200));
assert!(
started.elapsed() < PATIENCE,
"the late chunk never reached the attached screen:\n{}",
attached.parser().screen().contents()
);
}
real.kill();
}