-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathlib.rs
More file actions
124 lines (112 loc) · 4.25 KB
/
lib.rs
File metadata and controls
124 lines (112 loc) · 4.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
use std::{
collections::VecDeque,
io::{BufReader, Read},
};
pub use portable_pty::CommandBuilder;
use pty_terminal::terminal::{PtyReader, Terminal};
pub use pty_terminal::{
ExitStatus,
geo::ScreenSize,
terminal::{ChildHandle, PtyWriter},
};
const MILESTONE_HYPERTEXT: char = '\u{200b}';
/// A test-oriented terminal that provides milestone-based synchronization.
///
/// Wraps a PTY terminal, splitting it into a [`PtyWriter`] for sending input
/// and a [`Reader`] that can wait for named milestones emitted by the child
/// process via [`pty_terminal_test_client::mark_milestone`].
pub struct TestTerminal {
pub writer: PtyWriter,
pub reader: Reader,
pub child_handle: ChildHandle,
}
/// The read half of a test terminal, wrapping [`PtyReader`] with milestone support.
pub struct Reader {
pty: BufReader<PtyReader>,
child_handle: ChildHandle,
/// OSC sequences taken from the PTY but not yet consumed by `expect_milestone`.
pending_osc: VecDeque<Vec<Vec<u8>>>,
}
impl TestTerminal {
/// Spawns a new child process in a test terminal.
///
/// # Errors
///
/// Returns an error if the PTY cannot be opened or the command fails to spawn.
pub fn spawn(size: ScreenSize, cmd: CommandBuilder) -> anyhow::Result<Self> {
let Terminal { pty_reader, pty_writer, child_handle, .. } = Terminal::spawn(size, cmd)?;
Ok(Self {
writer: pty_writer,
reader: Reader {
pty: BufReader::new(pty_reader),
child_handle: child_handle.clone(),
pending_osc: VecDeque::new(),
},
child_handle,
})
}
}
impl Reader {
/// Returns terminal screen contents with milestone hyperlink text removed.
#[must_use]
pub fn screen_contents(&self) -> String {
let mut contents = self.pty.get_ref().screen_contents();
contents.retain(|ch| ch != MILESTONE_HYPERTEXT);
contents
}
/// Reads from the PTY until a milestone with the given name is encountered.
///
/// Returns the terminal screen contents at the moment the milestone is detected.
///
/// Milestones use a uniform protocol across platforms: the milestone name
/// is encoded in an OSC 8 hyperlink URI. We parse unhandled OSC sequences
/// from the VT parser state (instead of raw byte matching), then decode the
/// milestone URI payload. The zero-width milestone hyperlink anchor is
/// stripped from returned screen contents.
///
/// # Panics
///
/// Panics if the child process exits (EOF) before the named milestone is received,
/// or if a read error occurs.
#[must_use]
pub fn expect_milestone(&mut self, name: &str) -> String {
let mut buf = [0u8; 4096];
loop {
// Drain new sequences from the PTY into our local buffer.
self.pending_osc.append(&mut self.pty.get_ref().take_unhandled_osc_sequences());
// Scan for the first matching milestone, keeping the rest.
let mut found = false;
let mut remaining = VecDeque::with_capacity(self.pending_osc.len());
for params in self.pending_osc.drain(..) {
if !found
&& pty_terminal_test_client::decode_milestone_from_osc8_params(¶ms)
.is_some_and(|decoded| decoded == name)
{
found = true;
continue;
}
remaining.push_back(params);
}
self.pending_osc = remaining;
if found {
return self.screen_contents();
}
let n = self.pty.read(&mut buf).expect("PTY read failed");
assert!(n > 0, "EOF reached before milestone '{name}'");
}
}
/// Reads all remaining PTY output until the child exits, then returns the exit status.
///
/// # Errors
///
/// Returns an error if waiting for the child process exit status fails.
///
/// # Panics
///
/// Panics if reading from the PTY fails.
pub fn wait_for_exit(&mut self) -> anyhow::Result<ExitStatus> {
let mut discard = Vec::new();
self.pty.read_to_end(&mut discard).expect("PTY read_to_end failed");
self.child_handle.wait()
}
}