T36: programs report their status through OSC 7501, revision 0.4, checked as the spec checks them; each terminal keeps their records by id, replaced whole, cleared with those under them, 256 at most; a new prompt drops working, blocked and idle, a full reset all; the support query is answered with every state and kind, ahead of a DA1 sent after it; the pty's tests move to their own file
CI / platforms (windows-latest) (push) Successful in 1m31s
CI / platforms (ubuntu-24.04) (push) Successful in 1m46s
CI / macos (push) Successful in 11m59s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-17 00:46:07 -04:00
1 parent 99db56bab1
commit 42b272ece6
12 files changed
+755 -250

No files matched your search

+6 -220
View File
@@ -11,7 +11,7 @@ use alacritty_terminal::{
};
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded};
use super::{marks::Mark, mouse::MouseMode, tap::Tapped};
use super::{marks::Mark, mouse::MouseMode, status::Status, tap::Tapped};
use crate::settings::{CursorStyle, Settings};
/// What a session hears from its pty.
@@ -21,6 +21,10 @@ pub enum PtyEvent {
Folder(PathBuf),
Mouse(MouseMode),
Mark(Mark),
/// A program's status, OSC 7501.
Status(Status),
/// A full reset, `ESC c`.
Reset,
}
/// Hands the grid's events to the session; `pid` names the program in logs.
@@ -246,225 +250,7 @@ pub fn fit(columns: usize, lines: usize, cell: (u16, u16)) -> WindowSize {
}
#[cfg(all(test, unix))]
mod tests {
use alacritty_terminal::{
grid::Dimensions,
index::{Column, Line},
};
use futures::StreamExt;
use super::*;
struct Ran {
folders: Vec<PathBuf>,
marks: Vec<Mark>,
text: String,
/// The first line's cells that carry a prompt's link.
prompt: Vec<usize>,
success: bool,
pid: u32,
columns: usize,
}
fn run(script: &str, size: (usize, usize)) -> Ran {
crate::testlog::init();
let launch = Launch {
program: Some(("/bin/sh".into(), vec!["-c".into(), script.into()])),
cwd: std::env::temp_dir(),
env: vec![("ELY".into(), "ok".into())],
reports: false,
};
let (pty, mut events) =
spawn(&launch, size, (8, 16), config(&Settings::default())).expect("sh starts");
let (folders, marks, success) = futures::executor::block_on(async {
let (mut folders, mut marks, mut success) = (Vec::new(), Vec::new(), false);
while let Some(event) = events.next().await {
match event {
PtyEvent::Folder(folder) => folders.push(folder),
PtyEvent::Mark(mark) => marks.push(mark),
PtyEvent::Term(Event::ChildExit(status)) => success = status.success(),
PtyEvent::Term(Event::Exit) => break,
PtyEvent::Term(_) | PtyEvent::Mouse(_) => {}
}
}
(folders, marks, success)
});
let term = pty.term.lock();
let row = &term.grid()[Line(0)];
let text: String = (0..24).map(|column| row[Column(column)].c).collect();
let prompt = (0..24).filter(|&column| {
row[Column(column)]
.hyperlink()
.is_some_and(|link| link.uri() == crate::term::prompts::LINK)
});
Ran {
folders,
marks,
text: text.trim_end().to_string(),
prompt: prompt.collect(),
success,
pid: pty.pid,
columns: term.columns(),
}
}
#[test]
fn a_program_prints_into_the_grid_with_its_variables_and_id() {
let ran = run("printf 'ely %s %s' \"$ELY\" $$", (40, 4));
assert!(ran.folders.is_empty());
assert_eq!(ran.text, format!("ely ok {}", ran.pid));
assert!(ran.success);
}
#[test]
fn osc_7_reaches_the_session_as_a_folder() {
let ran = run(
"printf '\\033]7;file:///tmp/a%%20b\\007x\\033]7;file://elsewhere.invalid/c\\033\\\\'",
(40, 4),
);
assert_eq!(ran.folders, [PathBuf::from("/tmp/a b")]);
assert_eq!(ran.text, "x");
}
#[test]
fn osc_133_reaches_the_session_as_marks() {
let ran = run(
"printf '\\033]133;A\\007>\\033]133;B\\007\\033]133;C\\033\\\\x\\033]133;D;3\\007'",
(40, 4),
);
let marks = [Mark::Prompt, Mark::Input, Mark::Output, Mark::Done(Some(3))];
assert_eq!(ran.marks, marks);
assert_eq!(ran.text, ">x");
assert_eq!(ran.prompt, [0], "the prompt's cell, not the output's");
}
#[test]
fn a_long_escape_of_controls_keeps_the_pty_reading() {
let ran = run(
"printf '\\033'; head -c 3000000 /dev/zero; printf '[0mREADY'",
(40, 4),
);
assert_eq!(ran.text, "READY");
}
#[test]
fn the_foreground_is_the_program_s_own_group() {
let launch = Launch {
program: Some(("/bin/sh".into(), vec!["-c".into(), "exec sleep 30".into()])),
cwd: std::env::temp_dir(),
env: Vec::new(),
reports: false,
};
let (pty, _events) =
spawn(&launch, (20, 4), (8, 16), config(&Settings::default())).expect("sh starts");
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while pty.foreground().ok() != Some(pty.pid as i32) {
assert!(
std::time::Instant::now() < deadline,
"{:?}",
pty.foreground()
);
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
#[test]
fn a_size_past_the_kernel_is_one_size_for_grid_and_program() {
let ran = run("stty size", (9000, 8));
assert_eq!(ran.columns, usize::from(u16::MAX / 8));
assert_eq!(ran.text, format!("8 {}", u16::MAX / 8));
}
/// Starts `script`, waits for it to print READY, closes it, and waits for the reap's log.
fn close_after_ready(script: &str, reaped: &str) {
crate::testlog::init();
let launch = Launch {
program: Some(("/bin/sh".into(), vec!["-c".into(), script.into()])),
cwd: std::env::temp_dir(),
env: Vec::new(),
reports: false,
};
let (pty, mut events) =
spawn(&launch, (20, 4), (8, 16), config(&Settings::default())).expect("sh starts");
futures::executor::block_on(async {
while let Some(event) = events.next().await {
let term = pty.term.lock();
let text: String = (0..5)
.map(|column| term.grid()[Line(0)][Column(column)].c)
.collect();
drop(term);
if matches!(event, PtyEvent::Term(Event::Wakeup)) && text == "READY" {
break;
}
}
});
let pid = pty.pid;
drop(pty);
let hung_up = format!("pty {pid}: hung up, {reaped}");
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while !crate::testlog::logged(&hung_up) {
assert!(std::time::Instant::now() < deadline, "no `{hung_up}`");
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert!(crate::testlog::logged(&format!("pty {pid}: closing")));
let alive = unsafe { libc::kill(pid as libc::pid_t, 0) } == 0;
assert!(!alive, "the program was reaped");
}
#[test]
fn closing_a_session_hangs_up_its_program() {
close_after_ready("printf READY; exec cat", "signal: 1 (SIGHUP)");
}
#[test]
fn a_program_that_ignores_sighup_ends_when_the_pty_closes() {
// cat reads EOF on macOS and EIO on Linux; the shell's own exit holds on both.
close_after_ready("trap '' HUP; printf READY; cat; exit 7", "exit status: 7");
}
#[test]
fn a_missing_folder_stops_the_start() {
let launch = Launch {
program: None,
cwd: "/nonexistent/ely".into(),
env: Vec::new(),
reports: false,
};
let error = spawn(&launch, (20, 4), (8, 16), config(&Settings::default()))
.err()
.expect("a missing folder is an error");
assert!(error.to_string().contains("/nonexistent/ely"));
}
#[test]
fn a_folder_that_cannot_be_entered_stops_the_start() {
use std::os::unix::fs::PermissionsExt;
let folder = std::env::temp_dir().join(format!("elyterminal-shut-{}", std::process::id()));
std::fs::create_dir_all(&folder).expect("a temp folder");
std::fs::set_permissions(&folder, std::fs::Permissions::from_mode(0o600)).expect("600");
let launch = Launch {
program: Some(("/bin/sh".into(), vec!["-c".into(), "true".into()])),
cwd: folder.clone(),
env: Vec::new(),
reports: false,
};
let started = spawn(&launch, (20, 4), (8, 16), config(&Settings::default()));
std::fs::set_permissions(&folder, std::fs::Permissions::from_mode(0o700)).expect("700");
std::fs::remove_dir(&folder).expect("the temp folder goes");
let error = started.err().expect("a folder it cannot enter is an error");
let error = error.to_string();
assert!(error.contains("Permission denied"), "{error}");
assert!(error.contains(&folder.display().to_string()), "{error}");
}
#[test]
fn sizes_hold_at_least_two_by_one_and_cap_at_the_kernel() {
let size = fit(70_000, 24, (8, 16));
assert_eq!((size.num_cols, size.num_lines), (u16::MAX / 8, 24));
let size = fit(0, 0, (8, 16));
assert_eq!((size.num_cols, size.num_lines), (2, 1));
}
}
mod tests;
#[cfg(test)]
mod scrollback {