#![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::{native_pty_system, CommandBuilder, MasterPty, PtySize};
pub const ENTER: &str = "\r";
pub const DOWN: &str = "\x1b[B";
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 {
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(env!("CARGO_BIN_EXE_rproj"));
for arg in args {
cmd.arg(arg);
}
cmd.cwd(cwd);
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() }
}
}