ailloli_ui_terminal_pty/
lib.rs1mod batch;
21mod error;
22mod handle;
23mod mock;
24mod model;
25
26#[cfg(feature = "portable")]
27mod portable;
28
29pub use batch::{PtyBatchConfig, PtyOutputBatcher};
30pub use error::PtyError;
31pub use handle::{PtyBackend, PtyHandle};
32pub use mock::MockPtyBackend;
33pub use model::{PtyEvent, PtyExitStatus, PtySize, PtySpawnConfig};
34
35#[cfg(feature = "portable")]
36pub use portable::PortablePtyBackend;
37
38#[cfg(test)]
39mod tests {
41 use std::thread;
42 use std::time::Duration;
43
44 use super::*;
45
46 #[test]
47 fn mock_spawn_records_config_and_drains_events_in_order() {
48 let backend = MockPtyBackend::default();
49 backend.push_event(PtyEvent::Output(b"hello".to_vec()));
50 backend.push_event(PtyEvent::Exit(PtyExitStatus::success(0)));
51
52 let config = PtySpawnConfig::default();
53 let handle = backend.spawn(config.clone()).expect("spawn");
54
55 assert_eq!(backend.spawned_configs(), vec![config]);
56 assert_eq!(
57 handle.drain_events(),
58 vec![
59 PtyEvent::Output(b"hello".to_vec()),
60 PtyEvent::Exit(PtyExitStatus::success(0)),
61 ]
62 );
63 assert!(handle.drain_events().is_empty());
64 }
65
66 #[test]
67 fn mock_spawn_error_is_deterministic() {
68 let backend = MockPtyBackend::default().with_spawn_error(PtyError::Spawn("boom".into()));
69
70 assert_eq!(
71 backend.spawn(PtySpawnConfig::default()).expect_err("error"),
72 PtyError::Spawn("boom".into())
73 );
74 }
75
76 #[test]
77 fn mock_write_and_resize_are_recorded() {
78 let backend = MockPtyBackend::default();
79 let handle = backend.spawn(PtySpawnConfig::default()).expect("spawn");
80 let size = PtySize::new(40, 100, 800, 600);
81
82 handle.write(b"ls\r").expect("write");
83 handle.resize(size).expect("resize");
84
85 assert_eq!(backend.writes(), vec![b"ls\r".to_vec()]);
86 assert_eq!(backend.resizes(), vec![size]);
87 }
88
89 #[test]
90 fn mock_shutdown_is_idempotent_and_write_after_shutdown_is_closed() {
91 let backend = MockPtyBackend::default();
92 let handle = backend.spawn(PtySpawnConfig::default()).expect("spawn");
93
94 assert!(!handle.is_shutdown());
95 handle.shutdown().expect("shutdown");
96 handle.shutdown().expect("shutdown twice");
97 assert!(handle.is_shutdown());
98 assert_eq!(handle.write(b"nope").expect_err("closed"), PtyError::Closed);
99 }
100
101 #[test]
102 fn batcher_flushes_by_size_newline_timeout_and_preserves_order() {
103 let mut batcher = PtyOutputBatcher::with_config(PtyBatchConfig {
104 max_bytes: 4,
105 flush_timeout: Duration::from_millis(5),
106 });
107
108 assert!(batcher.push(b"ab").is_empty());
109 assert_eq!(
110 batcher.push(b"cd"),
111 vec![PtyEvent::Output(b"abcd".to_vec())]
112 );
113 assert_eq!(
114 batcher.push(b"e\n"),
115 vec![PtyEvent::Output(b"e\n".to_vec())]
116 );
117
118 assert!(batcher.push(b"fg").is_empty());
119 thread::sleep(Duration::from_millis(20));
120 assert_eq!(batcher.tick(), Some(PtyEvent::Output(b"fg".to_vec())));
121 }
122
123 #[test]
124 fn pty_size_clamps_rows_and_cols() {
125 let size = PtySize::new(0, 0, 20, 30);
126 assert_eq!(size.rows, 1);
127 assert_eq!(size.cols, 1);
128 assert_eq!(size.pixel_width, 20);
129 assert_eq!(size.pixel_height, 30);
130 }
131
132 #[cfg(feature = "portable")]
133 #[test]
134 #[ignore]
135 fn portable_backend_can_spawn_echo_resize_and_shutdown() {
136 let backend = PortablePtyBackend::new();
137 let mut config = PtySpawnConfig {
138 size: PtySize::new(12, 80, 0, 0),
139 ..PtySpawnConfig::default()
140 };
141 #[cfg(windows)]
142 {
143 config.program = Some("cmd".into());
144 config.args = vec![
145 "/C".into(),
146 "echo ailloli_ui-pty && ping -n 5 127.0.0.1 >NUL".into(),
147 ];
148 }
149 #[cfg(not(windows))]
150 {
151 config.program = Some("sh".into());
152 config.args = vec!["-lc".into(), "printf ailloli_ui-pty; sleep 5".into()];
153 }
154
155 let handle = backend.spawn(config).expect("spawn");
156 let deadline = std::time::Instant::now() + Duration::from_secs(2);
157 let mut output = Vec::new();
158 while std::time::Instant::now() < deadline {
159 for event in handle.drain_events() {
160 if let PtyEvent::Output(bytes) = event {
161 output.extend(bytes);
162 }
163 }
164 if output
165 .windows(b"ailloli_ui-pty".len())
166 .any(|w| w == b"ailloli_ui-pty")
167 {
168 break;
169 }
170 thread::sleep(Duration::from_millis(20));
171 }
172 handle.resize(PtySize::new(24, 100, 0, 0)).expect("resize");
173 handle.shutdown().expect("shutdown");
174 assert!(
175 output
176 .windows(b"ailloli_ui-pty".len())
177 .any(|w| w == b"ailloli_ui-pty"),
178 "output was {:?}",
179 String::from_utf8_lossy(&output)
180 );
181 }
182}