#![allow(dead_code)]
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use portable_pty::{CommandBuilder, MasterPty, PtySize, native_pty_system};
pub const ENTER: &str = "\r";
pub const DOWN: &str = "\x1b[B";
pub const LEFT: &str = "\x1b[D";
pub const ESC: &str = "\x1b";
fn timeout() -> Duration {
let secs = std::env::var("RPROJ_TEST_TIMEOUT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(30);
Duration::from_secs(secs)
}
const ROWS: u16 = 400;
const COLS: u16 = 160;
pub struct TempProject {
path: PathBuf,
}
impl TempProject {
pub fn new(label: &str) -> Self {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"rproj-test-{}-{label}-{unique}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path).expect("create temp project");
Self { path }
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn write(&self, relative: &str, contents: &str) {
let path = self.path.join(relative);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("create parent");
}
std::fs::write(path, contents).expect("write fixture");
}
pub fn read(&self, relative: &str) -> String {
std::fs::read_to_string(self.path.join(relative)).expect("read back")
}
pub fn exists(&self, relative: &str) -> bool {
self.path.join(relative).exists()
}
}
impl Drop for TempProject {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
pub struct Outcome {
pub text: String,
pub code: u32,
}
impl Outcome {
pub fn assert_contains(&self, needle: &str) {
assert!(
self.text.contains(needle),
"expected {needle:?} in:\n{}",
self.text
);
}
pub fn assert_lacks(&self, needle: &str) {
assert!(
!self.text.contains(needle),
"did not expect {needle:?} in:\n{}",
self.text
);
}
}
pub struct Session {
child: Box<dyn portable_pty::Child + Send + Sync>,
writer: Box<dyn Write + Send>,
raw: Arc<Mutex<Vec<u8>>>,
_master: Box<dyn MasterPty + Send>,
}
impl Session {
pub fn start(cwd: &Path, args: &[&str]) -> Self {
Self::start_with_env(cwd, args, &[])
}
pub fn start_with_env(cwd: &Path, args: &[&str], vars: &[(&str, &str)]) -> Self {
let binary = option_env!("CARGO_BIN_EXE_rproj")
.map(PathBuf::from)
.unwrap_or_else(|| std::env::current_exe().expect("unit test executable"));
Self::start_program(&binary, cwd, args, vars)
}
pub fn start_program(program: &Path, cwd: &Path, args: &[&str], vars: &[(&str, &str)]) -> Self {
let pair = native_pty_system()
.openpty(PtySize {
rows: ROWS,
cols: COLS,
pixel_width: 0,
pixel_height: 0,
})
.expect("open pty");
let mut cmd = CommandBuilder::new(program);
for arg in args {
cmd.arg(arg);
}
cmd.cwd(cwd);
for (key, value) in vars {
cmd.env(key, value);
}
let child = pair.slave.spawn_command(cmd).expect("spawn rproj");
drop(pair.slave);
let raw = Arc::new(Mutex::new(Vec::new()));
let mut reader = pair.master.try_clone_reader().expect("clone reader");
{
let raw = Arc::clone(&raw);
std::thread::spawn(move || {
let mut chunk = [0u8; 4096];
while let Ok(n) = reader.read(&mut chunk) {
if n == 0 {
break;
}
raw.lock().unwrap().extend_from_slice(&chunk[..n]);
}
});
}
let writer = pair.master.take_writer().expect("take writer");
Self {
child,
writer,
raw,
_master: pair.master,
}
}
pub fn text(&self) -> String {
let raw = self.raw.lock().unwrap();
let mut parser = vt100::Parser::new(ROWS, COLS, 0);
parser.process(&raw);
let contents = parser.screen().contents();
contents.trim_end().to_string()
}
pub fn send(&mut self, keys: &str) {
self.writer
.write_all(keys.as_bytes())
.expect("write to pty");
self.writer.flush().expect("flush pty");
}
pub fn wait_for(&self, needle: &str) {
let deadline = Instant::now() + timeout();
loop {
let text = self.text();
if text.contains(needle) {
return;
}
assert!(
Instant::now() < deadline,
"timed out waiting for {needle:?} in:\n{text}"
);
std::thread::sleep(Duration::from_millis(20));
}
}
pub fn wait_for_prompt(&self, heading: &str) {
self.wait_for(&format!("{heading}\n "));
}
pub fn enter_through(&mut self, headings: &[&str]) {
for heading in headings {
self.wait_for_prompt(heading);
self.send(ENTER);
}
}
pub fn finish(mut self) -> Outcome {
let deadline = Instant::now() + timeout();
let status = loop {
match self.child.try_wait().expect("wait on rproj") {
Some(status) => break status,
None if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(20)),
None => {
let text = self.text();
let _ = self.child.kill();
panic!("rproj never exited. Output so far:\n{text}");
}
}
};
std::thread::sleep(Duration::from_millis(150));
Outcome {
text: self.text(),
code: status.exit_code(),
}
}
}