Support tests that need a terminal emulator

This commit is contained in:
YizhePKU 2024-05-31 21:30:45 +08:00
parent f3cf693ec7
commit a2f5268e70
5 changed files with 240 additions and 2 deletions

108
Cargo.lock generated
View File

@ -56,6 +56,30 @@ dependencies = [
"memchr",
]
[[package]]
name = "alacritty_terminal"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db5ad9e2d4c1f7e17fccec9493eeb4e9c1f00e1167519d3940272b708ed8a069"
dependencies = [
"base64 0.22.1",
"bitflags 2.5.0",
"home",
"libc",
"log",
"miow",
"parking_lot",
"piper",
"polling",
"regex-automata",
"rustix-openpty",
"serde",
"signal-hook",
"unicode-width",
"vte 0.13.0",
"windows-sys 0.48.0",
]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
@ -297,6 +321,12 @@ version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ae037714f313c1353189ead58ef9eec30a8e8dc101b2622d461418fd59e28a9"
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
version = "1.2.0"
@ -888,6 +918,15 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "console"
version = "0.15.8"
@ -1129,6 +1168,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "cursor-icon"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
[[package]]
name = "deranged"
version = "0.3.11"
@ -2632,6 +2677,15 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "miow"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "359f76430b20a79f9e20e115b3428614e654f04fab314482fc0fda0ebd3c6044"
dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "mockito"
version = "1.4.0"
@ -2772,6 +2826,7 @@ dependencies = [
name = "nu"
version = "0.94.1"
dependencies = [
"alacritty_terminal",
"assert_cmd",
"crossterm",
"ctrlc",
@ -3304,6 +3359,7 @@ dependencies = [
name = "nu-test-support"
version = "0.94.1"
dependencies = [
"alacritty_terminal",
"nu-glob",
"nu-path",
"nu-utils",
@ -3981,6 +4037,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464db0c665917b13ebb5d453ccdec4add5658ee1adc7affc7677615356a8afaf"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]]
name = "pkg-config"
version = "0.3.30"
@ -4417,6 +4484,21 @@ dependencies = [
"version_check",
]
[[package]]
name = "polling"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
"rustix",
"tracing",
"windows-sys 0.52.0",
]
[[package]]
name = "pori"
version = "0.0.0"
@ -5123,11 +5205,23 @@ checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags 2.5.0",
"errno",
"itoa",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "rustix-openpty"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a25c3aad9fc1424eb82c88087789a7d938e1829724f3e4043163baf0d13cfc12"
dependencies = [
"errno",
"libc",
"rustix",
]
[[package]]
name = "rustversion"
version = "1.0.15"
@ -6543,6 +6637,20 @@ dependencies = [
"vte_generate_state_changes",
]
[[package]]
name = "vte"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40eb22ae96f050e0c0d6f7ce43feeae26c348fc4dea56928ca81537cfaa6188b"
dependencies = [
"bitflags 2.5.0",
"cursor-icon",
"log",
"serde",
"utf8parse",
"vte_generate_state_changes",
]
[[package]]
name = "vte_generate_state_changes"
version = "0.1.1"

View File

@ -61,6 +61,7 @@ members = [
]
[workspace.dependencies]
alacritty_terminal = "0.24"
alphanumeric-sort = "1.5"
ansi-str = "0.8"
anyhow = "1.0.82"
@ -221,6 +222,7 @@ nix = { workspace = true, default-features = false, features = [
nu-test-support = { path = "./crates/nu-test-support", version = "0.94.1" }
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.94.1" }
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.94.1" }
alacritty_terminal = { workspace = true }
assert_cmd = "2.0"
dirs-next = { workspace = true }
tango-bench = "0.5"

View File

@ -19,3 +19,4 @@ nu-utils = { path = "../nu-utils", version = "0.94.1" }
num-format = { workspace = true }
which = { workspace = true }
tempfile = { workspace = true }
alacritty_terminal = { workspace = true }

View File

@ -3,6 +3,7 @@ pub mod fs;
pub mod locale_override;
pub mod macros;
pub mod playground;
pub mod terminal;
use std::process::ExitStatus;
// Needs to be reexported for `nu!` macro

View File

@ -0,0 +1,126 @@
//! Helper functions for tests that requires a terminal emulator.
use alacritty_terminal::{
event::{Event, EventListener, WindowSize},
grid::Indexed,
term::{test::TermSize, Config},
tty::{self, EventedReadWrite, Options, Pty, Shell},
vte::ansi::{Processor, StdSyncHandler},
Term,
};
use std::{
collections::HashMap,
io::{ErrorKind, Read, Write},
path::PathBuf,
sync::mpsc,
time::Duration,
};
pub struct EventProxy(mpsc::Sender<Event>);
impl EventListener for EventProxy {
fn send_event(&self, event: Event) {
let _ = self.0.send(event);
}
}
/// Creates a 24x80 terminal with default configurations. Returns the terminal
/// and a `mpsc::Receiver` that receives terminal events.
pub fn default_terminal() -> (Term<EventProxy>, mpsc::Receiver<Event>) {
let config = Config::default();
let size = TermSize {
screen_lines: 24,
columns: 80,
};
let (tx, rx) = mpsc::channel();
(Term::new(config, &size, EventProxy(tx)), rx)
}
/// Creates a PTY and connect the slave end to a Nushell process. If `pwd` is
/// None, the Nushell process will inherit PWD from the current process.
pub fn pty_with_nushell(args: Vec<String>, pwd: Option<PathBuf>) -> Pty {
let executable = crate::fs::executable_path().to_string_lossy().to_string();
let options = Options {
shell: Some(Shell::new(executable, args)),
working_directory: pwd,
hold: false,
env: HashMap::new(),
};
let window_size = WindowSize {
num_lines: 24,
num_cols: 80,
cell_width: 0,
cell_height: 0,
};
tty::new(&options, window_size, 0).unwrap()
}
/// Reads from `pty` until no more data is available. Will periodically call
/// `event_handler` to handle terminal events.
pub fn read_to_end<T: EventListener>(
terminal: &mut Term<T>,
pty: &mut Pty,
events: &mut mpsc::Receiver<Event>,
mut event_handler: impl FnMut(&mut Term<T>, &mut Pty, Event),
) {
let mut parser: Processor<StdSyncHandler> = Processor::new();
loop {
// Read from the PTY.
let mut buf = [0; 512];
match pty.reader().read(&mut buf) {
Ok(n) => {
if n == 0 {
return;
} else {
// Update the terminal state.
for byte in &buf[..n] {
parser.advance(terminal, *byte);
}
// Handle terminal events.
while let Ok(event) = events.try_recv() {
event_handler(terminal, pty, event);
}
// Poll again after 100ms. The delay is necessary so that
// the child process can respond to any new data we might
// have sent in the event handler.
std::thread::sleep(Duration::from_millis(100));
}
}
Err(err) => {
if let ErrorKind::Interrupted = err.kind() {
// retry
} else {
return;
}
}
}
}
}
/// An event handler that only responds to `Event::PtyWrite`. This is the
/// minimum amount of event handling you need to get Nushell working.
pub fn pty_write_handler<T: EventListener>(_terminal: &mut Term<T>, pty: &mut Pty, event: Event) {
if let Event::PtyWrite(text) = event {
pty.writer().write_all(text.as_bytes()).unwrap();
}
}
/// Extracts the current cursor position.
pub fn extract_cursor<T>(terminal: &Term<T>) -> (usize, usize) {
let cursor = terminal.grid().cursor.point;
(cursor.line.0 as usize, cursor.column.0 as usize)
}
/// Extracts all visible text, ignoring text styles.
pub fn extract_text<T>(terminal: &Term<T>) -> Vec<String> {
let mut text: Vec<String> = vec![];
for Indexed { point, cell } in terminal.grid().display_iter() {
if point.column == 0 {
text.push(String::new());
}
text.last_mut().unwrap().push(cell.c);
}
text
}