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}