ratatui_testlib/error.rs
1//! Error types for ratatui_testlib.
2//!
3//! This module defines all error types that can occur during TUI testing operations.
4//! The main error type [`TermTestError`] is an enum covering all possible failure modes,
5//! and [`Result<T>`] is a type alias for convenience.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use ratatui_testlib::{Result, TermTestError};
11//!
12//! fn may_fail() -> Result<()> {
13//! Err(TermTestError::Timeout { timeout_ms: 5000 })
14//! }
15//!
16//! match may_fail() {
17//! Ok(_) => println!("Success"),
18//! Err(TermTestError::Timeout { timeout_ms }) => {
19//! eprintln!("Timed out after {}ms", timeout_ms);
20//! }
21//! Err(e) => eprintln!("Error: {}", e),
22//! }
23//! ```
24
25use std::io;
26use thiserror::Error;
27
28/// Result type alias for ratatui_testlib operations.
29///
30/// This is a convenience alias for `std::result::Result<T, TermTestError>`.
31/// Most public APIs in this crate return this type.
32///
33/// # Examples
34///
35/// ```rust
36/// use ratatui_testlib::{Result, TuiTestHarness};
37///
38/// fn create_harness() -> Result<TuiTestHarness> {
39/// TuiTestHarness::new(80, 24)
40/// }
41/// ```
42pub type Result<T> = std::result::Result<T, TermTestError>;
43
44/// Errors that can occur during TUI testing.
45///
46/// This enum represents all possible error conditions in the ratatui_testlib library.
47/// Each variant provides specific context about the failure.
48///
49/// # Variants
50///
51/// - [`TermTestError::Pty`]: Low-level PTY operation failures
52/// - [`TermTestError::Io`]: Standard I/O errors (file, network, etc.)
53/// - [`TermTestError::Timeout`]: Wait operations that exceed their deadline
54/// - [`TermTestError::Parse`]: Terminal escape sequence parsing errors
55/// - `SnapshotMismatch`: Snapshot testing failures (requires `snapshot-insta` feature)
56/// - `SixelValidation`: Sixel graphics validation failures (requires `sixel` feature)
57/// - [`TermTestError::SpawnFailed`]: Process spawning failures
58/// - [`TermTestError::ProcessAlreadyRunning`]: Attempt to spawn when a process is already running
59/// - [`TermTestError::NoProcessRunning`]: Attempt to interact with a non-existent process
60/// - [`TermTestError::InvalidDimensions`]: Invalid terminal size parameters
61/// - `Bevy`: Bevy ECS-related errors (requires `bevy` feature)
62#[derive(Debug, Error)]
63pub enum TermTestError {
64 /// Error from PTY (pseudo-terminal) operations.
65 ///
66 /// This error occurs when low-level PTY operations fail, such as:
67 /// - PTY allocation failures
68 /// - PTY configuration errors
69 /// - PTY system unavailability
70 #[error("PTY error: {0}")]
71 Pty(String),
72
73 /// Standard I/O error.
74 ///
75 /// This wraps [`std::io::Error`] and occurs for file operations, network I/O,
76 /// or other system-level I/O failures. Automatically converted via `From` trait.
77 #[error("I/O error: {0}")]
78 Io(#[from] io::Error),
79
80 /// Timeout waiting for a condition.
81 ///
82 /// This error is returned when a wait operation (like `TuiTestHarness::wait_for`)
83 /// exceeds its configured timeout duration. The error includes the timeout value
84 /// for debugging purposes.
85 ///
86 /// # Example
87 ///
88 /// ```rust,no_run
89 /// use ratatui_testlib::{TuiTestHarness, TermTestError};
90 /// use std::time::Duration;
91 ///
92 /// # fn test() -> ratatui_testlib::Result<()> {
93 /// let mut harness = TuiTestHarness::new(80, 24)?
94 /// .with_timeout(Duration::from_secs(1));
95 ///
96 /// match harness.wait_for_text("Never appears") {
97 /// Err(TermTestError::Timeout { timeout_ms }) => {
98 /// eprintln!("Timed out after {}ms", timeout_ms);
99 /// }
100 /// _ => {}
101 /// }
102 /// # Ok(())
103 /// # }
104 /// ```
105 #[error("Timeout waiting for condition after {timeout_ms}ms")]
106 Timeout {
107 /// Timeout duration in milliseconds.
108 timeout_ms: u64,
109 },
110
111 /// Error parsing terminal escape sequences.
112 ///
113 /// This occurs when the terminal emulator encounters malformed or unexpected
114 /// escape sequences in the PTY output.
115 #[error("Parse error: {0}")]
116 Parse(String),
117
118 /// Snapshot comparison mismatch.
119 ///
120 /// This error is returned when using the `snapshot-insta` feature and a
121 /// snapshot assertion fails. Requires the `snapshot-insta` feature flag.
122 #[cfg(feature = "snapshot-insta")]
123 #[error("Snapshot mismatch: {0}")]
124 SnapshotMismatch(String),
125
126 /// Sixel validation failed.
127 ///
128 /// This error occurs when Sixel graphics validation fails, such as:
129 /// - Sixel graphics outside expected bounds
130 /// - Invalid Sixel sequence format
131 /// - Sixel position validation failures
132 ///
133 /// Requires the `sixel` feature flag.
134 #[cfg(feature = "sixel")]
135 #[error("Sixel validation failed: {0}")]
136 SixelValidation(String),
137
138 /// Process spawn failed.
139 ///
140 /// This error occurs when attempting to spawn a process in the PTY fails,
141 /// typically due to:
142 /// - Command not found
143 /// - Permission denied
144 /// - Resource limits exceeded
145 #[error("Failed to spawn process: {0}")]
146 SpawnFailed(String),
147
148 /// Process already running.
149 ///
150 /// This error is returned when attempting to spawn a process while another
151 /// process is still running in the PTY. Only one process can run at a time
152 /// in a given `TestTerminal`.
153 #[error("Process is already running")]
154 ProcessAlreadyRunning,
155
156 /// No process running.
157 ///
158 /// This error occurs when attempting to interact with a process (e.g., wait,
159 /// kill) when no process is currently running in the PTY.
160 #[error("No process is running")]
161 NoProcessRunning,
162
163 /// Invalid terminal dimensions.
164 ///
165 /// This error is returned when attempting to create or resize a terminal
166 /// with invalid dimensions (e.g., zero width or height, or dimensions that
167 /// exceed system limits).
168 #[error("Invalid terminal dimensions: width={width}, height={height}")]
169 InvalidDimensions {
170 /// Terminal width in columns.
171 width: u16,
172 /// Terminal height in rows.
173 height: u16,
174 },
175
176 /// Bevy ECS-specific errors.
177 ///
178 /// This error occurs for Bevy-related failures when using the `bevy` feature,
179 /// such as:
180 /// - Entity query failures
181 /// - System execution errors
182 /// - Plugin initialization failures
183 ///
184 /// Requires the `bevy` feature flag.
185 #[cfg(feature = "bevy")]
186 #[error("Bevy error: {0}")]
187 Bevy(String),
188}
189
190// Conversion from anyhow::Error (used by portable-pty)
191impl From<anyhow::Error> for TermTestError {
192 fn from(err: anyhow::Error) -> Self {
193 TermTestError::Pty(err.to_string())
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_io_error_conversion() {
203 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "test error");
204 let term_err: TermTestError = io_err.into();
205
206 assert!(matches!(term_err, TermTestError::Io(_)));
207 assert!(term_err.to_string().contains("test error"));
208 }
209
210 #[test]
211 fn test_timeout_error_message() {
212 let err = TermTestError::Timeout { timeout_ms: 5000 };
213 let msg = err.to_string();
214
215 assert!(msg.contains("5000"));
216 assert!(msg.contains("Timeout"));
217 }
218
219 #[test]
220 fn test_invalid_dimensions_error() {
221 let err = TermTestError::InvalidDimensions {
222 width: 0,
223 height: 24,
224 };
225 let msg = err.to_string();
226
227 assert!(msg.contains("Invalid"));
228 assert!(msg.contains("width=0"));
229 assert!(msg.contains("height=24"));
230 }
231
232 #[test]
233 fn test_spawn_failed_error() {
234 let err = TermTestError::SpawnFailed("command not found".to_string());
235 let msg = err.to_string();
236
237 assert!(msg.contains("Failed to spawn"));
238 assert!(msg.contains("command not found"));
239 }
240
241 #[test]
242 fn test_process_already_running_error() {
243 let err = TermTestError::ProcessAlreadyRunning;
244 let msg = err.to_string();
245
246 assert!(msg.contains("already running"));
247 }
248
249 #[test]
250 fn test_no_process_running_error() {
251 let err = TermTestError::NoProcessRunning;
252 let msg = err.to_string();
253
254 assert!(msg.contains("No process"));
255 }
256
257 #[test]
258 fn test_anyhow_error_conversion() {
259 let anyhow_err = anyhow::anyhow!("test anyhow error");
260 let term_err: TermTestError = anyhow_err.into();
261
262 assert!(matches!(term_err, TermTestError::Pty(_)));
263 assert!(term_err.to_string().contains("test anyhow error"));
264 }
265
266 #[cfg(feature = "sixel")]
267 #[test]
268 fn test_sixel_validation_error() {
269 let err = TermTestError::SixelValidation("out of bounds".to_string());
270 let msg = err.to_string();
271
272 assert!(msg.contains("Sixel"));
273 assert!(msg.contains("out of bounds"));
274 }
275}