#![cfg(all(unix, feature = "tui"))]
#![forbid(unsafe_code)]
use std::fs::File;
use std::io::Read;
use std::os::fd::OwnedFd;
use std::process::{Child, Command, Stdio};
use std::sync::mpsc;
use std::time::{Duration, Instant};
const ALTERNATE_SCREEN: &[u8] = b"\x1b[?1049h";
const DEADLINE: Duration = Duration::from_secs(20);
fn openpty() -> (OwnedFd, OwnedFd) {
let size = nix::pty::Winsize {
ws_row: 40,
ws_col: 120,
ws_xpixel: 0,
ws_ypixel: 0,
};
let pair = nix::pty::openpty(&size, None).expect("openpty");
(pair.master, pair.slave)
}
struct OnPty {
child: Child,
output: mpsc::Receiver<Vec<u8>>,
seen: Vec<u8>,
_config: tempfile::TempDir,
}
impl OnPty {
fn spawn(args: &[&str], no_tui_env: Option<&str>) -> Self {
let (master, slave) = openpty();
let config = tempfile::tempdir().unwrap();
let mut cmd = Command::new(env!("CARGO_BIN_EXE_bacnet"));
cmd.args(args)
.env("TERM", "xterm-256color")
.env("BACNET_CONFIG_DIR", config.path())
.env_remove("BACNET_NO_TUI")
.env_remove("RUST_LOG")
.stdin(Stdio::from(slave.try_clone().unwrap()))
.stdout(Stdio::from(slave.try_clone().unwrap()))
.stderr(Stdio::from(slave));
if let Some(value) = no_tui_env {
cmd.env("BACNET_NO_TUI", value);
}
let child = cmd.spawn().unwrap();
drop(cmd);
let (sender, output) = mpsc::channel();
let mut master = File::from(master);
std::thread::spawn(move || {
let mut buffer = [0; 4096];
while let Ok(read @ 1..) = master.read(&mut buffer) {
if sender.send(buffer[..read].to_vec()).is_err() {
break;
}
}
});
Self {
child,
output,
seen: Vec::new(),
_config: config,
}
}
fn wait_for(&mut self, needle: &[u8]) -> bool {
let deadline = Instant::now() + DEADLINE;
loop {
if self
.seen
.windows(needle.len())
.any(|window| window == needle)
{
return true;
}
let left = deadline.saturating_duration_since(Instant::now());
match self.output.recv_timeout(left) {
Ok(chunk) => self.seen.extend(chunk),
Err(mpsc::RecvTimeoutError::Disconnected) => return false,
Err(mpsc::RecvTimeoutError::Timeout) => panic!(
"no {needle:?} within {DEADLINE:?}; saw {:?}",
String::from_utf8_lossy(&self.seen)
),
}
}
}
fn text(&self) -> String {
String::from_utf8_lossy(&self.seen).into_owned()
}
}
impl Drop for OnPty {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
#[test]
fn plain_bacnet_on_a_terminal_opens_the_tui() {
for no_tui_env in [None, Some(""), Some("0")] {
let mut pty = OnPty::spawn(&[], no_tui_env);
assert!(
pty.wait_for(ALTERNATE_SCREEN),
"{no_tui_env:?}: {}",
pty.text()
);
assert!(pty.child.try_wait().unwrap().is_none(), "the TUI exited");
}
}
#[test]
fn no_tui_on_a_terminal_prints_the_usage_and_exits_2() {
for (args, no_tui_env) in [(&["--no-tui"][..], None), (&[][..], Some("1"))] {
let mut pty = OnPty::spawn(args, no_tui_env);
assert!(
pty.wait_for(b"the terminal UI won't open"),
"{args:?}: {}",
pty.text()
);
let status = pty.child.wait().unwrap();
assert_eq!(status.code(), Some(2), "{args:?} {no_tui_env:?}");
assert!(
!pty.wait_for(ALTERNATE_SCREEN),
"{args:?} opened the TUI: {}",
pty.text()
);
assert!(pty.text().contains("Usage:"), "{}", pty.text());
}
}