summaryrefslogtreecommitdiff
path: root/test_util/src
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2021-09-20 22:15:44 -0400
committerGitHub <noreply@github.com>2021-09-20 22:15:44 -0400
commit0f23d926019d333572366a4de4f291b848fa6ded (patch)
tree7767a39d72cabca060740198dc7a3c23f97bf363 /test_util/src
parent60b68e63f1045a36496257912ef4f32e716a2440 (diff)
chore(tests): windows pty tests (#12091)
Diffstat (limited to 'test_util/src')
-rw-r--r--test_util/src/lib.rs85
-rw-r--r--test_util/src/pty.rs442
2 files changed, 501 insertions, 26 deletions
diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs
index 5eaedbaa0..8bfe5caa0 100644
--- a/test_util/src/lib.rs
+++ b/test_util/src/lib.rs
@@ -44,10 +44,8 @@ use tokio_rustls::rustls::{self, Session};
use tokio_rustls::TlsAcceptor;
use tokio_tungstenite::accept_async;
-#[cfg(unix)]
-pub use pty;
-
pub mod lsp;
+pub mod pty;
const PORT: u16 = 4545;
const TEST_AUTH_TOKEN: &str = "abcdef123456789";
@@ -1589,62 +1587,97 @@ pub enum PtyData {
Output(&'static str),
}
-#[cfg(unix)]
pub fn test_pty2(args: &str, data: Vec<PtyData>) {
- use pty::fork::Fork;
use std::io::BufRead;
- let tests_path = testdata_path();
- let fork = Fork::from_ptmx().unwrap();
- if let Ok(master) = fork.is_parent() {
- let mut buf_reader = std::io::BufReader::new(master);
- for d in data {
+ with_pty(&args.split_whitespace().collect::<Vec<_>>(), |console| {
+ let mut buf_reader = std::io::BufReader::new(console);
+ for d in data.iter() {
match d {
PtyData::Input(s) => {
println!("INPUT {}", s.escape_debug());
- buf_reader.get_mut().write_all(s.as_bytes()).unwrap();
+ buf_reader.get_mut().write_text(s);
// Because of tty echo, we should be able to read the same string back.
assert!(s.ends_with('\n'));
let mut echo = String::new();
buf_reader.read_line(&mut echo).unwrap();
println!("ECHO: {}", echo.escape_debug());
- assert!(echo.starts_with(&s.trim()));
+
+ // Windows may also echo the previous line, so only check the end
+ assert!(normalize_text(&echo).ends_with(&normalize_text(s)));
}
PtyData::Output(s) => {
let mut line = String::new();
if s.ends_with('\n') {
buf_reader.read_line(&mut line).unwrap();
} else {
- while s != line {
+ // assumes the buffer won't have overlapping virtual terminal sequences
+ while normalize_text(&line).len() < normalize_text(s).len() {
let mut buf = [0; 64 * 1024];
- let _n = buf_reader.read(&mut buf).unwrap();
+ let bytes_read = buf_reader.read(&mut buf).unwrap();
+ assert!(bytes_read > 0);
let buf_str = std::str::from_utf8(&buf)
.unwrap()
.trim_end_matches(char::from(0));
line += buf_str;
- assert!(s.starts_with(&line));
}
}
println!("OUTPUT {}", line.escape_debug());
- assert_eq!(line, s);
+ assert_eq!(normalize_text(&line), normalize_text(s));
}
}
}
+ });
- fork.wait().unwrap();
- } else {
- deno_cmd()
- .current_dir(tests_path)
- .env("NO_COLOR", "1")
- .args(args.split_whitespace())
- .spawn()
- .unwrap()
- .wait()
- .unwrap();
+ // This normalization function is not comprehensive
+ // and may need to updated as new scenarios emerge.
+ fn normalize_text(text: &str) -> String {
+ lazy_static! {
+ static ref MOVE_CURSOR_RIGHT_ONE_RE: Regex =
+ Regex::new(r"\x1b\[1C").unwrap();
+ static ref FOUND_SEQUENCES_RE: Regex =
+ Regex::new(r"(\x1b\]0;[^\x07]*\x07)*(\x08)*(\x1b\[\d+X)*").unwrap();
+ static ref CARRIAGE_RETURN_RE: Regex =
+ Regex::new(r"[^\n]*\r([^\n])").unwrap();
+ }
+
+ // any "move cursor right" sequences should just be a space
+ let text = MOVE_CURSOR_RIGHT_ONE_RE.replace_all(text, " ");
+ // replace additional virtual terminal sequences that strip ansi codes doesn't catch
+ let text = FOUND_SEQUENCES_RE.replace_all(&text, "");
+ // strip any ansi codes, which also strips more terminal sequences
+ let text = strip_ansi_codes(&text);
+ // get rid of any text that is overwritten with only a carriage return
+ let text = CARRIAGE_RETURN_RE.replace_all(&text, "$1");
+ // finally, trim surrounding whitespace
+ text.trim().to_string()
}
}
+pub fn with_pty(deno_args: &[&str], mut action: impl FnMut(Box<dyn pty::Pty>)) {
+ if !atty::is(atty::Stream::Stdin) || !atty::is(atty::Stream::Stderr) {
+ eprintln!("Ignoring non-tty environment.");
+ return;
+ }
+
+ let deno_dir = new_deno_dir();
+ let mut env_vars = std::collections::HashMap::new();
+ env_vars.insert("NO_COLOR".to_string(), "1".to_string());
+ env_vars.insert(
+ "DENO_DIR".to_string(),
+ deno_dir.path().to_string_lossy().to_string(),
+ );
+ let pty = pty::create_pty(
+ &deno_exe_path().to_string_lossy().to_string(),
+ deno_args,
+ testdata_path(),
+ Some(env_vars),
+ );
+
+ action(pty);
+}
+
pub struct WrkOutput {
pub latency: f64,
pub requests: u64,
diff --git a/test_util/src/pty.rs b/test_util/src/pty.rs
new file mode 100644
index 000000000..2fa2ed4cd
--- /dev/null
+++ b/test_util/src/pty.rs
@@ -0,0 +1,442 @@
+use std::collections::HashMap;
+use std::io::Read;
+use std::path::Path;
+
+pub trait Pty: Read {
+ fn write_text(&mut self, text: &str);
+
+ fn write_line(&mut self, text: &str) {
+ self.write_text(&format!("{}\n", text));
+ }
+
+ /// Reads the output to the EOF.
+ fn read_all_output(&mut self) -> String {
+ let mut text = String::new();
+ self.read_to_string(&mut text).unwrap();
+ text
+ }
+}
+
+#[cfg(unix)]
+pub fn create_pty(
+ program: impl AsRef<Path>,
+ args: &[&str],
+ cwd: impl AsRef<Path>,
+ env_vars: Option<HashMap<String, String>>,
+) -> Box<dyn Pty> {
+ let fork = pty::fork::Fork::from_ptmx().unwrap();
+ if fork.is_parent().is_ok() {
+ Box::new(unix::UnixPty { fork })
+ } else {
+ std::process::Command::new(program.as_ref())
+ .current_dir(cwd)
+ .args(args)
+ .envs(env_vars.unwrap_or_default())
+ .spawn()
+ .unwrap()
+ .wait()
+ .unwrap();
+ unreachable!();
+ }
+}
+
+#[cfg(unix)]
+mod unix {
+ use std::io::Read;
+ use std::io::Write;
+
+ use super::Pty;
+
+ pub struct UnixPty {
+ pub fork: pty::fork::Fork,
+ }
+
+ impl Drop for UnixPty {
+ fn drop(&mut self) {
+ self.fork.wait().unwrap();
+ }
+ }
+
+ impl Pty for UnixPty {
+ fn write_text(&mut self, text: &str) {
+ let mut master = self.fork.is_parent().unwrap();
+ master.write_all(text.as_bytes()).unwrap();
+ }
+ }
+
+ impl Read for UnixPty {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ let mut master = self.fork.is_parent().unwrap();
+ master.read(buf)
+ }
+ }
+}
+
+#[cfg(target_os = "windows")]
+pub fn create_pty(
+ program: impl AsRef<Path>,
+ args: &[&str],
+ cwd: impl AsRef<Path>,
+ env_vars: Option<HashMap<String, String>>,
+) -> Box<dyn Pty> {
+ let pty = windows::WinPseudoConsole::new(
+ program,
+ args,
+ &cwd.as_ref().to_string_lossy().to_string(),
+ env_vars,
+ );
+ Box::new(pty)
+}
+
+#[cfg(target_os = "windows")]
+mod windows {
+ use std::collections::HashMap;
+ use std::io::Read;
+ use std::io::Write;
+ use std::path::Path;
+ use std::ptr;
+ use std::time::Duration;
+
+ use winapi::shared::minwindef::FALSE;
+ use winapi::shared::minwindef::LPVOID;
+ use winapi::shared::minwindef::TRUE;
+ use winapi::shared::winerror::S_OK;
+ use winapi::um::consoleapi::ClosePseudoConsole;
+ use winapi::um::consoleapi::CreatePseudoConsole;
+ use winapi::um::fileapi::ReadFile;
+ use winapi::um::fileapi::WriteFile;
+ use winapi::um::handleapi::DuplicateHandle;
+ use winapi::um::handleapi::INVALID_HANDLE_VALUE;
+ use winapi::um::namedpipeapi::CreatePipe;
+ use winapi::um::processthreadsapi::CreateProcessW;
+ use winapi::um::processthreadsapi::DeleteProcThreadAttributeList;
+ use winapi::um::processthreadsapi::GetCurrentProcess;
+ use winapi::um::processthreadsapi::InitializeProcThreadAttributeList;
+ use winapi::um::processthreadsapi::UpdateProcThreadAttribute;
+ use winapi::um::processthreadsapi::LPPROC_THREAD_ATTRIBUTE_LIST;
+ use winapi::um::processthreadsapi::PROCESS_INFORMATION;
+ use winapi::um::synchapi::WaitForSingleObject;
+ use winapi::um::winbase::CREATE_UNICODE_ENVIRONMENT;
+ use winapi::um::winbase::EXTENDED_STARTUPINFO_PRESENT;
+ use winapi::um::winbase::INFINITE;
+ use winapi::um::winbase::STARTUPINFOEXW;
+ use winapi::um::wincontypes::COORD;
+ use winapi::um::wincontypes::HPCON;
+ use winapi::um::winnt::DUPLICATE_SAME_ACCESS;
+ use winapi::um::winnt::HANDLE;
+
+ use super::Pty;
+
+ macro_rules! assert_win_success {
+ ($expression:expr) => {
+ let success = $expression;
+ if success != TRUE {
+ panic!("{}", std::io::Error::last_os_error().to_string())
+ }
+ };
+ }
+
+ pub struct WinPseudoConsole {
+ stdin_write_handle: WinHandle,
+ stdout_read_handle: WinHandle,
+ // keep these alive for the duration of the pseudo console
+ _process_handle: WinHandle,
+ _thread_handle: WinHandle,
+ _attribute_list: ProcThreadAttributeList,
+ }
+
+ impl WinPseudoConsole {
+ pub fn new(
+ program: impl AsRef<Path>,
+ args: &[&str],
+ cwd: &str,
+ maybe_env_vars: Option<HashMap<String, String>>,
+ ) -> Self {
+ // https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session
+ unsafe {
+ let mut size: COORD = std::mem::zeroed();
+ size.X = 800;
+ size.Y = 500;
+ let mut console_handle = std::ptr::null_mut();
+ let (stdin_read_handle, stdin_write_handle) = create_pipe();
+ let (stdout_read_handle, stdout_write_handle) = create_pipe();
+
+ let result = CreatePseudoConsole(
+ size,
+ stdin_read_handle.as_raw_handle(),
+ stdout_write_handle.as_raw_handle(),
+ 0,
+ &mut console_handle,
+ );
+ assert_eq!(result, S_OK);
+
+ let mut environment_vars = maybe_env_vars.map(get_env_vars);
+ let mut attribute_list = ProcThreadAttributeList::new(console_handle);
+ let mut startup_info: STARTUPINFOEXW = std::mem::zeroed();
+ startup_info.StartupInfo.cb =
+ std::mem::size_of::<STARTUPINFOEXW>() as u32;
+ startup_info.lpAttributeList = attribute_list.as_mut_ptr();
+
+ let mut proc_info: PROCESS_INFORMATION = std::mem::zeroed();
+ let command = format!(
+ "\"{}\" {}",
+ program.as_ref().to_string_lossy(),
+ args.join(" ")
+ )
+ .trim()
+ .to_string();
+ let mut application_str =
+ to_windows_str(&program.as_ref().to_string_lossy());
+ let mut command_str = to_windows_str(&command);
+ let mut cwd = to_windows_str(cwd);
+
+ assert_win_success!(CreateProcessW(
+ application_str.as_mut_ptr(),
+ command_str.as_mut_ptr(),
+ ptr::null_mut(),
+ ptr::null_mut(),
+ FALSE,
+ EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT,
+ environment_vars
+ .as_mut()
+ .map(|v| v.as_mut_ptr() as LPVOID)
+ .unwrap_or(ptr::null_mut()),
+ cwd.as_mut_ptr(),
+ &mut startup_info.StartupInfo,
+ &mut proc_info,
+ ));
+
+ // close the handles that the pseudoconsole now has
+ drop(stdin_read_handle);
+ drop(stdout_write_handle);
+
+ // start a thread that will close the pseudoconsole on process exit
+ let thread_handle = WinHandle::new(proc_info.hThread);
+ std::thread::spawn({
+ let thread_handle = thread_handle.duplicate();
+ let console_handle = WinHandle::new(console_handle);
+ move || {
+ WaitForSingleObject(thread_handle.as_raw_handle(), INFINITE);
+ // wait for the reading thread to catch up
+ std::thread::sleep(Duration::from_millis(200));
+ // close the console handle which will close the
+ // stdout pipe for the reader
+ ClosePseudoConsole(console_handle.into_raw_handle());
+ }
+ });
+
+ Self {
+ stdin_write_handle,
+ stdout_read_handle,
+ _process_handle: WinHandle::new(proc_info.hProcess),
+ _thread_handle: thread_handle,
+ _attribute_list: attribute_list,
+ }
+ }
+ }
+ }
+
+ impl Read for WinPseudoConsole {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ unsafe {
+ loop {
+ let mut bytes_read = 0;
+ let success = ReadFile(
+ self.stdout_read_handle.as_raw_handle(),
+ buf.as_mut_ptr() as _,
+ buf.len() as u32,
+ &mut bytes_read,
+ ptr::null_mut(),
+ );
+
+ // ignore zero-byte writes
+ let is_zero_byte_write = bytes_read == 0 && success == TRUE;
+ if !is_zero_byte_write {
+ return Ok(bytes_read as usize);
+ }
+ }
+ }
+ }
+ }
+
+ impl Pty for WinPseudoConsole {
+ fn write_text(&mut self, text: &str) {
+ // windows psuedo console requires a \r\n to do a newline
+ let newline_re = regex::Regex::new("\r?\n").unwrap();
+ self
+ .write_all(newline_re.replace_all(text, "\r\n").as_bytes())
+ .unwrap();
+ }
+ }
+
+ impl std::io::Write for WinPseudoConsole {
+ fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
+ unsafe {
+ let mut bytes_written = 0;
+ assert_win_success!(WriteFile(
+ self.stdin_write_handle.as_raw_handle(),
+ buffer.as_ptr() as *const _,
+ buffer.len() as u32,
+ &mut bytes_written,
+ ptr::null_mut(),
+ ));
+ Ok(bytes_written as usize)
+ }
+ }
+
+ fn flush(&mut self) -> std::io::Result<()> {
+ Ok(())
+ }
+ }
+
+ struct WinHandle {
+ inner: HANDLE,
+ }
+
+ impl WinHandle {
+ pub fn new(handle: HANDLE) -> Self {
+ WinHandle { inner: handle }
+ }
+
+ pub fn duplicate(&self) -> WinHandle {
+ unsafe {
+ let process_handle = GetCurrentProcess();
+ let mut duplicate_handle = ptr::null_mut();
+ assert_win_success!(DuplicateHandle(
+ process_handle,
+ self.inner,
+ process_handle,
+ &mut duplicate_handle,
+ 0,
+ 0,
+ DUPLICATE_SAME_ACCESS,
+ ));
+
+ WinHandle::new(duplicate_handle)
+ }
+ }
+
+ pub fn as_raw_handle(&self) -> HANDLE {
+ self.inner
+ }
+
+ pub fn into_raw_handle(self) -> HANDLE {
+ let handle = self.inner;
+ // skip the drop implementation in order to not close the handle
+ std::mem::forget(self);
+ handle
+ }
+ }
+
+ unsafe impl Send for WinHandle {}
+ unsafe impl Sync for WinHandle {}
+
+ impl Drop for WinHandle {
+ fn drop(&mut self) {
+ unsafe {
+ if !self.inner.is_null() && self.inner != INVALID_HANDLE_VALUE {
+ winapi::um::handleapi::CloseHandle(self.inner);
+ }
+ }
+ }
+ }
+
+ struct ProcThreadAttributeList {
+ buffer: Vec<u8>,
+ }
+
+ impl ProcThreadAttributeList {
+ pub fn new(console_handle: HPCON) -> Self {
+ unsafe {
+ // discover size required for the list
+ let mut size = 0;
+ let attribute_count = 1;
+ assert_eq!(
+ InitializeProcThreadAttributeList(
+ ptr::null_mut(),
+ attribute_count,
+ 0,
+ &mut size
+ ),
+ FALSE
+ );
+
+ let mut buffer = vec![0u8; size];
+ let attribute_list_ptr = buffer.as_mut_ptr() as _;
+
+ assert_win_success!(InitializeProcThreadAttributeList(
+ attribute_list_ptr,
+ attribute_count,
+ 0,
+ &mut size,
+ ));
+
+ const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE: usize = 0x00020016;
+ assert_win_success!(UpdateProcThreadAttribute(
+ attribute_list_ptr,
+ 0,
+ PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
+ console_handle,
+ std::mem::size_of::<HPCON>(),
+ ptr::null_mut(),
+ ptr::null_mut(),
+ ));
+
+ ProcThreadAttributeList { buffer }
+ }
+ }
+
+ pub fn as_mut_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST {
+ self.buffer.as_mut_slice().as_mut_ptr() as *mut _
+ }
+ }
+
+ impl Drop for ProcThreadAttributeList {
+ fn drop(&mut self) {
+ unsafe { DeleteProcThreadAttributeList(self.as_mut_ptr()) };
+ }
+ }
+
+ fn create_pipe() -> (WinHandle, WinHandle) {
+ unsafe {
+ let mut read_handle = std::ptr::null_mut();
+ let mut write_handle = std::ptr::null_mut();
+
+ assert_win_success!(CreatePipe(
+ &mut read_handle,
+ &mut write_handle,
+ ptr::null_mut(),
+ 0
+ ));
+
+ (WinHandle::new(read_handle), WinHandle::new(write_handle))
+ }
+ }
+
+ fn to_windows_str(str: &str) -> Vec<u16> {
+ use std::os::windows::prelude::OsStrExt;
+ std::ffi::OsStr::new(str)
+ .encode_wide()
+ .chain(Some(0))
+ .collect()
+ }
+
+ fn get_env_vars(env_vars: HashMap<String, String>) -> Vec<u16> {
+ // See lpEnvironment: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
+ let mut parts = env_vars
+ .into_iter()
+ // each environment variable is in the form `name=value\0`
+ .map(|(key, value)| format!("{}={}\0", key, value))
+ .collect::<Vec<_>>();
+
+ // all strings in an environment block must be case insensitively
+ // sorted alphabetically by name
+ // https://docs.microsoft.com/en-us/windows/win32/procthread/changing-environment-variables
+ parts.sort_by_key(|part| part.to_lowercase());
+
+ // the entire block is terminated by NULL (\0)
+ format!("{}\0", parts.join(""))
+ .encode_utf16()
+ .collect::<Vec<_>>()
+ }
+}