diff --git a/crates/filesystem/src/error.rs b/crates/filesystem/src/error.rs index 63d92f7c..ff268a25 100644 --- a/crates/filesystem/src/error.rs +++ b/crates/filesystem/src/error.rs @@ -8,15 +8,28 @@ //! conditions that can occur during filesystem operations. It uses the `snafu` crate //! for ergonomic error handling with context. //! +//! The error type covers: +//! - Path existence errors ([`NotFound`][Error::NotFound], [`AlreadyExists`][Error::AlreadyExists]) +//! - Permission errors ([`PermissionDenied`][Error::PermissionDenied]) +//! - Type mismatch errors ([`NotAFile`][Error::NotAFile], [`NotADirectory`][Error::NotADirectory]) +//! - Content errors ([`NotEmpty`][Error::NotEmpty], [`InvalidUtf8`][Error::InvalidUtf8]) +//! - I/O errors with context ([`Io`][Error::Io]) +//! - Timeout errors ([`Timeout`][Error::Timeout]) +//! //! ## How //! //! The error type is built using `snafu`'s derive macro, which automatically generates: -//! - `Error` and `Display` implementations -//! - Context selectors for each variant -//! - Backtrace capture (when enabled) +//! - `std::error::Error` and `Display` implementations +//! - Context selectors for each variant (e.g., `NotFoundSnafu`, `IoSnafu`) +//! - Source error chaining for the `Io` variant +//! +//! Each variant includes the path that caused the error and, where applicable, +//! the operation being performed and the underlying system error. This provides +//! rich debugging information for troubleshooting filesystem issues. //! -//! Each variant includes the path that caused the error and, where applicable, the -//! underlying system error. This provides rich debugging information. +//! Context selectors are generated with `pub(crate)` visibility to keep them +//! internal to the crate while still allowing convenient error construction +//! within the crate's modules. //! //! ## Why //! @@ -24,19 +37,404 @@ //! - **Consistency**: All filesystem operations return the same error type //! - **Context Preservation**: Every error includes the path and operation that failed //! - **Type Safety**: Compile-time guarantees about error handling -//! - **Ergonomics**: Easy error conversion with the `?` operator +//! - **Ergonomics**: Easy error conversion with the `?` operator via snafu's `.context()` method +//! - **Thread Safety**: Error is `Send + Sync` for use in async contexts //! //! ## Example //! //! ```rust,ignore //! use workspace_fs::{Error, Result}; +//! use workspace_fs::error::NotFoundSnafu; +//! use snafu::ResultExt; //! use std::path::Path; //! -//! async fn read_config(path: &Path) -> Result { +//! async fn read_config(fs: &impl FileSystem, path: &Path) -> Result { //! // Errors automatically include the path that failed //! fs.read_to_string(path).await //! } +//! +//! fn check_path_exists(path: &Path) -> Result<()> { +//! if !path.exists() { +//! return Err(Error::NotFound { path: path.to_path_buf() }); +//! } +//! Ok(()) +//! } //! ``` -// TODO: will be implemented on epic workspace-node-tools-906 (Error Module) -#![allow(clippy::todo)] +use snafu::Snafu; +use std::path::PathBuf; +use std::time::Duration; + +// ============================================================================= +// Type Aliases +// ============================================================================= + +/// A specialized `Result` type for filesystem operations. +/// +/// This type alias provides a convenient shorthand for functions that return +/// filesystem errors, reducing boilerplate in function signatures. +/// +/// # Example +/// +/// ```rust,ignore +/// use workspace_fs::Result; +/// +/// async fn read_file_contents(path: &Path) -> Result { +/// // Implementation that may fail with filesystem errors +/// } +/// ``` +pub type Result = std::result::Result; + +// ============================================================================= +// Error Enum +// ============================================================================= + +/// Unified error type for all filesystem operations. +/// +/// This enum captures all possible error conditions that can occur during +/// filesystem operations in the `workspace-fs` crate. Each variant includes +/// the path that caused the error for debugging purposes. +/// +/// The error type implements: +/// - [`std::error::Error`] for standard error handling +/// - [`std::fmt::Display`] for user-friendly error messages +/// - [`Send`] and [`Sync`] for thread-safe async operations +/// +/// # Variants +/// +/// | Variant | Description | PRD Reference | +/// |---------|-------------|---------------| +/// | [`NotFound`][Self::NotFound] | Path does not exist | FR-6.2.1 | +/// | [`PermissionDenied`][Self::PermissionDenied] | Insufficient permissions | FR-6.2.2 | +/// | [`AlreadyExists`][Self::AlreadyExists] | Path already exists | FR-6.2.3 | +/// | [`NotAFile`][Self::NotAFile] | Expected file, found directory | FR-6.2.4 | +/// | [`NotADirectory`][Self::NotADirectory] | Expected directory, found file | FR-6.2.5 | +/// | [`NotEmpty`][Self::NotEmpty] | Directory is not empty | FR-6.2.6 | +/// | [`InvalidUtf8`][Self::InvalidUtf8] | Invalid UTF-8 content | FR-6.2.7 | +/// | [`Io`][Self::Io] | Wrapped I/O error | FR-6.2.8 | +/// | [`Timeout`][Self::Timeout] | Operation timed out | FR-6.2.9 | +/// +/// # Example +/// +/// ```rust,ignore +/// use workspace_fs::Error; +/// use std::path::PathBuf; +/// +/// // Creating an error directly +/// let err = Error::NotFound { +/// path: PathBuf::from("/missing/file.txt"), +/// }; +/// assert_eq!(format!("{}", err), "path not found: /missing/file.txt"); +/// +/// // Using context selectors with snafu +/// use snafu::ResultExt; +/// use workspace_fs::error::IoSnafu; +/// +/// let result: Result = std::fs::read_to_string("config.json") +/// .context(IoSnafu { +/// path: PathBuf::from("config.json"), +/// operation: "read", +/// }); +/// ``` +#[derive(Debug, Snafu)] +#[snafu(visibility(pub(crate)))] +pub enum Error { + /// Path does not exist. + /// + /// This error occurs when attempting to access a file or directory + /// that does not exist on the filesystem. + /// + /// # Fields + /// + /// * `path` - The path that was not found + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::Error; + /// use std::path::PathBuf; + /// + /// let err = Error::NotFound { + /// path: PathBuf::from("/nonexistent/file.txt"), + /// }; + /// println!("{}", err); // "path not found: /nonexistent/file.txt" + /// ``` + #[snafu(display("path not found: {}", path.display()))] + NotFound { + /// The path that was not found. + path: PathBuf, + }, + + /// Insufficient permissions to access path. + /// + /// This error occurs when the current process does not have the + /// required permissions to perform the requested operation on the path. + /// + /// # Fields + /// + /// * `path` - The path that could not be accessed + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::Error; + /// use std::path::PathBuf; + /// + /// let err = Error::PermissionDenied { + /// path: PathBuf::from("/root/secret.txt"), + /// }; + /// println!("{}", err); // "permission denied: /root/secret.txt" + /// ``` + #[snafu(display("permission denied: {}", path.display()))] + PermissionDenied { + /// The path that access was denied for. + path: PathBuf, + }, + + /// Path already exists when it shouldn't. + /// + /// This error occurs when attempting to create a file or directory + /// that already exists, when the operation requires it not to exist. + /// + /// # Fields + /// + /// * `path` - The path that already exists + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::Error; + /// use std::path::PathBuf; + /// + /// let err = Error::AlreadyExists { + /// path: PathBuf::from("/existing/file.txt"), + /// }; + /// println!("{}", err); // "path already exists: /existing/file.txt" + /// ``` + #[snafu(display("path already exists: {}", path.display()))] + AlreadyExists { + /// The path that already exists. + path: PathBuf, + }, + + /// Expected a file but found a directory. + /// + /// This error occurs when an operation expects a file but the + /// path points to a directory instead. + /// + /// # Fields + /// + /// * `path` - The path that is a directory instead of a file + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::Error; + /// use std::path::PathBuf; + /// + /// let err = Error::NotAFile { + /// path: PathBuf::from("/some/directory"), + /// }; + /// println!("{}", err); // "expected file, found directory: /some/directory" + /// ``` + #[snafu(display("expected file, found directory: {}", path.display()))] + NotAFile { + /// The path that is a directory instead of a file. + path: PathBuf, + }, + + /// Expected a directory but found a file. + /// + /// This error occurs when an operation expects a directory but the + /// path points to a file instead. + /// + /// # Fields + /// + /// * `path` - The path that is a file instead of a directory + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::Error; + /// use std::path::PathBuf; + /// + /// let err = Error::NotADirectory { + /// path: PathBuf::from("/some/file.txt"), + /// }; + /// println!("{}", err); // "expected directory, found file: /some/file.txt" + /// ``` + #[snafu(display("expected directory, found file: {}", path.display()))] + NotADirectory { + /// The path that is a file instead of a directory. + path: PathBuf, + }, + + /// Directory is not empty. + /// + /// This error occurs when attempting to remove a directory that + /// is not empty, when the operation requires an empty directory. + /// + /// # Fields + /// + /// * `path` - The path to the non-empty directory + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::Error; + /// use std::path::PathBuf; + /// + /// let err = Error::NotEmpty { + /// path: PathBuf::from("/some/directory"), + /// }; + /// println!("{}", err); // "directory not empty: /some/directory" + /// ``` + #[snafu(display("directory not empty: {}", path.display()))] + NotEmpty { + /// The path to the non-empty directory. + path: PathBuf, + }, + + /// File content is not valid UTF-8. + /// + /// This error occurs when attempting to read a file as a string + /// but the content contains invalid UTF-8 byte sequences. + /// + /// # Fields + /// + /// * `path` - The path to the file with invalid UTF-8 content + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::Error; + /// use std::path::PathBuf; + /// + /// let err = Error::InvalidUtf8 { + /// path: PathBuf::from("/binary/file.bin"), + /// }; + /// println!("{}", err); // "invalid UTF-8 content in file: /binary/file.bin" + /// ``` + #[snafu(display("invalid UTF-8 content in file: {}", path.display()))] + InvalidUtf8 { + /// The path to the file with invalid UTF-8 content. + path: PathBuf, + }, + + /// Wrapped I/O error with context. + /// + /// This error wraps an underlying [`std::io::Error`] with additional + /// context about which path and operation caused the failure. + /// + /// # Fields + /// + /// * `path` - The path where the I/O error occurred + /// * `operation` - A description of the operation being performed + /// * `source` - The underlying I/O error + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::Error; + /// use std::path::PathBuf; + /// use std::io; + /// + /// let io_error = io::Error::new(io::ErrorKind::Other, "disk full"); + /// let err = Error::Io { + /// path: PathBuf::from("/large/file.bin"), + /// operation: "write".to_string(), + /// source: io_error, + /// }; + /// println!("{}", err); // "write failed for '/large/file.bin': disk full" + /// ``` + #[snafu(display("{} failed for '{}': {}", operation, path.display(), source))] + Io { + /// The path where the I/O error occurred. + path: PathBuf, + /// The operation that was being performed when the error occurred. + operation: String, + /// The underlying I/O error. + source: std::io::Error, + }, + + /// Operation timed out. + /// + /// This error occurs when a filesystem operation exceeds its + /// configured timeout duration. + /// + /// # Fields + /// + /// * `path` - The path involved in the operation + /// * `operation` - A description of the operation that timed out + /// * `duration` - The timeout duration that was exceeded + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::Error; + /// use std::path::PathBuf; + /// use std::time::Duration; + /// + /// let err = Error::Timeout { + /// path: PathBuf::from("/slow/network/file.txt"), + /// operation: "read".to_string(), + /// duration: Duration::from_secs(30), + /// }; + /// println!("{}", err); // "read timed out after 30s for '/slow/network/file.txt'" + /// ``` + #[snafu(display("{} timed out after {:?} for '{}'", operation, duration, path.display()))] + Timeout { + /// The path involved in the timed-out operation. + path: PathBuf, + /// The operation that timed out. + operation: String, + /// The timeout duration that was exceeded. + duration: Duration, + }, +} + +// ============================================================================= +// Trait Implementations +// ============================================================================= + +/// Provides variant name introspection for error handling and logging. +/// +/// This implementation returns the qualified variant name as a static string, +/// which is useful for: +/// - Error categorization in logging systems +/// - Metrics and telemetry +/// - Pattern matching on error types without accessing fields +/// +/// # Example +/// +/// ```rust +/// use workspace_fs::Error; +/// use std::path::PathBuf; +/// +/// let err = Error::NotFound { path: PathBuf::from("/test") }; +/// assert_eq!(err.as_ref(), "Error::NotFound"); +/// ``` +impl AsRef for Error { + fn as_ref(&self) -> &str { + match self { + Error::NotFound { .. } => "Error::NotFound", + Error::PermissionDenied { .. } => "Error::PermissionDenied", + Error::AlreadyExists { .. } => "Error::AlreadyExists", + Error::NotAFile { .. } => "Error::NotAFile", + Error::NotADirectory { .. } => "Error::NotADirectory", + Error::NotEmpty { .. } => "Error::NotEmpty", + Error::InvalidUtf8 { .. } => "Error::InvalidUtf8", + Error::Io { .. } => "Error::Io", + Error::Timeout { .. } => "Error::Timeout", + } + } +} + +// Static assertions to ensure Error is Send + Sync (FR-6.1.4) +// These will fail to compile if Error is not Send + Sync +const _: () = { + const fn assert_send() {} + const fn assert_sync() {} + assert_send::(); + assert_sync::(); +}; diff --git a/crates/filesystem/src/lib.rs b/crates/filesystem/src/lib.rs index 77df8213..d1d4ea29 100644 --- a/crates/filesystem/src/lib.rs +++ b/crates/filesystem/src/lib.rs @@ -68,9 +68,9 @@ /// Error types for filesystem operations. /// -/// Provides a unified [`Error`](error) enum that captures all possible error +/// Provides a unified [`Error`](error::Error) enum that captures all possible error /// conditions with path context for debugging. -pub(crate) mod error; +pub mod error; /// Configuration types for filesystem behavior. /// @@ -116,9 +116,11 @@ mod tests; // Public Re-exports // ============================================================================= +// Error types +pub use error::{Error, Result}; + // TODO: Re-exports will be added as modules are implemented // pub use config::{FileSystemConfig, FileSystemConfigBuilder}; -// pub use error::{Error, Result}; // pub use mock::MockFileSystem; // pub use path_ext::PathExt; // pub use real::RealFileSystem; diff --git a/crates/filesystem/src/tests.rs b/crates/filesystem/src/tests.rs index c3d5f70a..2bb8b1f9 100644 --- a/crates/filesystem/src/tests.rs +++ b/crates/filesystem/src/tests.rs @@ -49,7 +49,348 @@ #[cfg(test)] mod error { //! Tests for the error module. - // TODO: will be implemented on epic workspace-node-tools-906 (Error Module) + //! + //! This module contains unit tests that verify: + //! - Display messages for all error variants + //! - Error trait implementation + //! - Send + Sync trait bounds + //! - Error source chaining for Io variant + + use crate::error::Error; + use std::error::Error as StdError; + use std::io; + use std::path::PathBuf; + use std::time::Duration; + + // ========================================================================= + // Display Message Tests (FR-6.2.1 - FR-6.2.9) + // ========================================================================= + + #[test] + fn test_not_found_display() { + let path = PathBuf::from("/missing/file.txt"); + let err = Error::NotFound { path }; + let display = format!("{err}"); + assert_eq!(display, "path not found: /missing/file.txt"); + } + + #[test] + fn test_permission_denied_display() { + let path = PathBuf::from("/root/secret.txt"); + let err = Error::PermissionDenied { path }; + let display = format!("{err}"); + assert_eq!(display, "permission denied: /root/secret.txt"); + } + + #[test] + fn test_already_exists_display() { + let path = PathBuf::from("/existing/file.txt"); + let err = Error::AlreadyExists { path }; + let display = format!("{err}"); + assert_eq!(display, "path already exists: /existing/file.txt"); + } + + #[test] + fn test_not_a_file_display() { + let path = PathBuf::from("/some/directory"); + let err = Error::NotAFile { path }; + let display = format!("{err}"); + assert_eq!(display, "expected file, found directory: /some/directory"); + } + + #[test] + fn test_not_a_directory_display() { + let path = PathBuf::from("/some/file.txt"); + let err = Error::NotADirectory { path }; + let display = format!("{err}"); + assert_eq!(display, "expected directory, found file: /some/file.txt"); + } + + #[test] + fn test_not_empty_display() { + let path = PathBuf::from("/non/empty/directory"); + let err = Error::NotEmpty { path }; + let display = format!("{err}"); + assert_eq!(display, "directory not empty: /non/empty/directory"); + } + + #[test] + fn test_invalid_utf8_display() { + let path = PathBuf::from("/binary/file.bin"); + let err = Error::InvalidUtf8 { path }; + let display = format!("{err}"); + assert_eq!(display, "invalid UTF-8 content in file: /binary/file.bin"); + } + + #[test] + fn test_io_error_display() { + let path = PathBuf::from("/failed/operation.txt"); + let io_error = io::Error::other("disk full"); + let err = Error::Io { path, operation: "write".to_string(), source: io_error }; + let display = format!("{err}"); + assert_eq!(display, "write failed for '/failed/operation.txt': disk full"); + } + + #[test] + fn test_timeout_display() { + let path = PathBuf::from("/slow/file.txt"); + let err = Error::Timeout { + path, + operation: "read".to_string(), + duration: Duration::from_secs(30), + }; + let display = format!("{err}"); + assert_eq!(display, "read timed out after 30s for '/slow/file.txt'"); + } + + #[test] + fn test_timeout_display_with_millis() { + let path = PathBuf::from("/slow/file.txt"); + let err = Error::Timeout { + path, + operation: "metadata".to_string(), + duration: Duration::from_millis(500), + }; + let display = format!("{err}"); + assert_eq!(display, "metadata timed out after 500ms for '/slow/file.txt'"); + } + + // ========================================================================= + // std::error::Error Trait Tests (FR-6.1.3) + // ========================================================================= + + #[test] + fn test_error_implements_std_error() { + fn assert_std_error() {} + assert_std_error::(); + } + + #[test] + fn test_io_error_has_source() { + let path = PathBuf::from("/test/file.txt"); + let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found"); + let err = Error::Io { path, operation: "read".to_string(), source: io_error }; + + // Verify the source is accessible through std::error::Error + let source = err.source(); + assert!(source.is_some()); + } + + #[test] + fn test_non_io_errors_have_no_source() { + let not_found = Error::NotFound { path: PathBuf::from("/test") }; + assert!(not_found.source().is_none()); + + let permission_denied = Error::PermissionDenied { path: PathBuf::from("/test") }; + assert!(permission_denied.source().is_none()); + + let already_exists = Error::AlreadyExists { path: PathBuf::from("/test") }; + assert!(already_exists.source().is_none()); + + let not_a_file = Error::NotAFile { path: PathBuf::from("/test") }; + assert!(not_a_file.source().is_none()); + + let not_a_directory = Error::NotADirectory { path: PathBuf::from("/test") }; + assert!(not_a_directory.source().is_none()); + + let not_empty = Error::NotEmpty { path: PathBuf::from("/test") }; + assert!(not_empty.source().is_none()); + + let invalid_utf8 = Error::InvalidUtf8 { path: PathBuf::from("/test") }; + assert!(invalid_utf8.source().is_none()); + + let timeout = Error::Timeout { + path: PathBuf::from("/test"), + operation: "read".to_string(), + duration: Duration::from_secs(1), + }; + assert!(timeout.source().is_none()); + } + + // ========================================================================= + // Send + Sync Tests (FR-6.1.4) + // ========================================================================= + + #[test] + fn test_error_is_send() { + fn assert_send() {} + assert_send::(); + } + + #[test] + fn test_error_is_sync() { + fn assert_sync() {} + assert_sync::(); + } + + // ========================================================================= + // AsRef Trait Tests (Variant Name Introspection) + // ========================================================================= + + #[test] + fn test_as_ref_not_found() { + let err = Error::NotFound { path: PathBuf::from("/test") }; + assert_eq!(err.as_ref(), "Error::NotFound"); + } + + #[test] + fn test_as_ref_permission_denied() { + let err = Error::PermissionDenied { path: PathBuf::from("/test") }; + assert_eq!(err.as_ref(), "Error::PermissionDenied"); + } + + #[test] + fn test_as_ref_already_exists() { + let err = Error::AlreadyExists { path: PathBuf::from("/test") }; + assert_eq!(err.as_ref(), "Error::AlreadyExists"); + } + + #[test] + fn test_as_ref_not_a_file() { + let err = Error::NotAFile { path: PathBuf::from("/test") }; + assert_eq!(err.as_ref(), "Error::NotAFile"); + } + + #[test] + fn test_as_ref_not_a_directory() { + let err = Error::NotADirectory { path: PathBuf::from("/test") }; + assert_eq!(err.as_ref(), "Error::NotADirectory"); + } + + #[test] + fn test_as_ref_not_empty() { + let err = Error::NotEmpty { path: PathBuf::from("/test") }; + assert_eq!(err.as_ref(), "Error::NotEmpty"); + } + + #[test] + fn test_as_ref_invalid_utf8() { + let err = Error::InvalidUtf8 { path: PathBuf::from("/test") }; + assert_eq!(err.as_ref(), "Error::InvalidUtf8"); + } + + #[test] + fn test_as_ref_io() { + let io_error = io::Error::other("test"); + let err = Error::Io { + path: PathBuf::from("/test"), + operation: "read".to_string(), + source: io_error, + }; + assert_eq!(err.as_ref(), "Error::Io"); + } + + #[test] + fn test_as_ref_timeout() { + let err = Error::Timeout { + path: PathBuf::from("/test"), + operation: "read".to_string(), + duration: Duration::from_secs(30), + }; + assert_eq!(err.as_ref(), "Error::Timeout"); + } + + #[test] + fn test_as_ref_returns_static_str() { + // Verify that as_ref returns a &str that can be used for pattern matching + let err = Error::NotFound { path: PathBuf::from("/test") }; + let variant_name: &str = err.as_ref(); + assert!(variant_name.starts_with("Error::")); + } + + // ========================================================================= + // Debug Trait Tests + // ========================================================================= + + #[test] + fn test_error_debug_format() { + let err = Error::NotFound { path: PathBuf::from("/test/file.txt") }; + let debug = format!("{err:?}"); + assert!(debug.contains("NotFound")); + assert!(debug.contains("/test/file.txt")); + } + + // ========================================================================= + // Result Type Alias Tests + // ========================================================================= + + #[test] + fn test_result_type_alias_ok() { + let result: crate::error::Result = Ok(42); + assert!(result.is_ok()); + assert_eq!(result.ok(), Some(42)); + } + + #[test] + fn test_result_type_alias_err() { + let result: crate::error::Result = + Err(Error::NotFound { path: PathBuf::from("/test") }); + assert!(result.is_err()); + } + + // ========================================================================= + // Context Selector Tests (snafu integration) + // ========================================================================= + + #[test] + fn test_context_selectors_are_available() { + use crate::error::{ + AlreadyExistsSnafu, InvalidUtf8Snafu, IoSnafu, NotADirectorySnafu, NotAFileSnafu, + NotEmptySnafu, NotFoundSnafu, PermissionDeniedSnafu, TimeoutSnafu, + }; + use snafu::ResultExt; + + // Verify context selectors can be used + let io_result: Result<(), io::Error> = Err(io::Error::other("test")); + let _with_context: Result<(), Error> = + io_result.context(IoSnafu { path: PathBuf::from("/test"), operation: "test" }); + + // Verify simple selectors compile + let _: crate::error::NotFoundSnafu = + NotFoundSnafu { path: PathBuf::from("/test") }; + let _: crate::error::PermissionDeniedSnafu = + PermissionDeniedSnafu { path: PathBuf::from("/test") }; + let _: crate::error::AlreadyExistsSnafu = + AlreadyExistsSnafu { path: PathBuf::from("/test") }; + let _: crate::error::NotAFileSnafu = + NotAFileSnafu { path: PathBuf::from("/test") }; + let _: crate::error::NotADirectorySnafu = + NotADirectorySnafu { path: PathBuf::from("/test") }; + let _: crate::error::NotEmptySnafu = + NotEmptySnafu { path: PathBuf::from("/test") }; + let _: crate::error::InvalidUtf8Snafu = + InvalidUtf8Snafu { path: PathBuf::from("/test") }; + let _: crate::error::TimeoutSnafu = TimeoutSnafu { + path: PathBuf::from("/test"), + operation: "read", + duration: Duration::from_secs(1), + }; + } + + #[test] + fn test_io_context_with_result_ext() { + use crate::error::IoSnafu; + use snafu::ResultExt; + + fn fallible_io_operation() -> Result { + Err(io::Error::new(io::ErrorKind::NotFound, "file not found")) + } + + let result: crate::error::Result = fallible_io_operation() + .context(IoSnafu { path: PathBuf::from("/test/file.txt"), operation: "read" }); + + assert!(result.is_err()); + // Use match to extract error without unwrap_err + let Err(err) = result else { + // This branch is unreachable due to the assert above, + // but we use return to satisfy the compiler without panic + return; + }; + let display = format!("{err}"); + assert!(display.contains("read failed")); + assert!(display.contains("/test/file.txt")); + assert!(display.contains("file not found")); + } } #[cfg(test)]