Skip to main content

wsi_rs/
error.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3
4#[derive(Debug, thiserror::Error)]
5#[non_exhaustive]
6pub enum WsiError {
7    #[error("unsupported format: {0}")]
8    UnsupportedFormat(String),
9    #[error("TIFF error in {path}: {message}")]
10    Tiff { path: PathBuf, message: String },
11    #[error("JPEG decode error: {0}")]
12    Jpeg(String),
13    #[error("JPEG2000 decode error: {0}")]
14    Jp2k(String),
15    #[error("XML parse error: {0}")]
16    Xml(String),
17    #[error("invalid slide {path}: {message}")]
18    InvalidSlide { path: PathBuf, message: String },
19    #[error("tile read failed at ({col}, {row}) level {level}: {reason}")]
20    TileRead {
21        col: i64,
22        row: i64,
23        level: u32,
24        reason: String,
25    },
26    #[error("I/O error: {0}")]
27    Io(#[from] std::io::Error),
28    #[error("I/O error at {path}: {source}")]
29    IoWithPath {
30        #[source]
31        source: Arc<std::io::Error>,
32        path: PathBuf,
33    },
34
35    // --- New variants for multi-dimensional engine ---
36    #[error("scene index {index} out of range (dataset has {count} scenes)")]
37    SceneOutOfRange { index: usize, count: usize },
38
39    #[error("series index {index} out of range (scene has {count} series)")]
40    SeriesOutOfRange { index: usize, count: usize },
41
42    #[error("level {level} out of range (series has {count} levels)")]
43    LevelOutOfRange { level: u32, count: u32 },
44
45    #[error("plane axis {axis} value {value} exceeds max {max}")]
46    PlaneOutOfRange { axis: String, value: u32, max: u32 },
47
48    #[error("associated image not found: {0}")]
49    AssociatedImageNotFound(String),
50
51    #[error("display conversion error: {0}")]
52    DisplayConversion(String),
53
54    /// Codec-layer error from j2k or the transitional facade.
55    #[error("codec error in {codec}: {source}")]
56    Codec {
57        codec: &'static str,
58        #[source]
59        source: Box<dyn std::error::Error + Send + Sync>,
60    },
61
62    /// Operation is intentionally unsupported on this path.
63    #[error("unsupported: {reason}")]
64    Unsupported { reason: String },
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn error_display_formats() {
73        let err = WsiError::Tiff {
74            path: "/tmp/test.svs".into(),
75            message: "bad IFD".into(),
76        };
77        assert!(err.to_string().contains("test.svs"));
78        assert!(err.to_string().contains("bad IFD"));
79    }
80
81    #[test]
82    fn io_error_converts() {
83        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
84        let wsi_err: WsiError = io_err.into();
85        assert!(matches!(wsi_err, WsiError::Io(_)));
86    }
87
88    #[test]
89    fn scene_out_of_range_display() {
90        let err = WsiError::SceneOutOfRange { index: 2, count: 1 };
91        assert!(err.to_string().contains("2"));
92        assert!(err.to_string().contains("1"));
93    }
94
95    #[test]
96    fn series_out_of_range_display() {
97        let err = WsiError::SeriesOutOfRange { index: 3, count: 2 };
98        assert!(err.to_string().contains("3"));
99    }
100
101    #[test]
102    fn plane_out_of_range_display() {
103        let err = WsiError::PlaneOutOfRange {
104            axis: "z".into(),
105            value: 5,
106            max: 3,
107        };
108        assert!(err.to_string().contains("z"));
109        assert!(err.to_string().contains("5"));
110    }
111
112    #[test]
113    fn level_out_of_range_display() {
114        let err = WsiError::LevelOutOfRange {
115            level: 10,
116            count: 5,
117        };
118        assert!(err.to_string().contains("10"));
119    }
120
121    #[test]
122    fn associated_image_not_found_display() {
123        let err = WsiError::AssociatedImageNotFound("label".into());
124        assert!(err.to_string().contains("label"));
125    }
126
127    #[test]
128    fn display_conversion_display() {
129        let err = WsiError::DisplayConversion("non-uint8 requires windowing".into());
130        assert!(err.to_string().contains("windowing"));
131    }
132
133    #[test]
134    fn io_with_path_display() {
135        let err = WsiError::IoWithPath {
136            source: Arc::new(std::io::Error::new(
137                std::io::ErrorKind::NotFound,
138                "file not found",
139            )),
140            path: "/tmp/slide.svs".into(),
141        };
142        let msg = err.to_string();
143        assert!(msg.contains("/tmp/slide.svs"), "got: {msg}");
144        assert!(msg.contains("file not found"), "got: {msg}");
145    }
146
147    #[test]
148    fn codec_display_includes_codec_and_source() {
149        let inner: Box<dyn std::error::Error + Send + Sync> = "boom".into();
150        let err = WsiError::Codec {
151            codec: "jpeg",
152            source: inner,
153        };
154        let msg = err.to_string();
155        assert!(msg.contains("jpeg"), "got: {msg}");
156        assert!(msg.contains("boom"), "got: {msg}");
157    }
158
159    #[test]
160    fn codec_pattern_match_round_trips() {
161        let err = WsiError::Codec {
162            codec: "j2k",
163            source: "decode failed".into(),
164        };
165        match err {
166            WsiError::Codec { codec, source: _ } => assert_eq!(codec, "j2k"),
167            other => panic!("expected Codec, got {other:?}"),
168        }
169    }
170
171    #[test]
172    fn unsupported_display() {
173        let err = WsiError::Unsupported {
174            reason: "device backend unavailable".into(),
175        };
176        assert!(err.to_string().contains("device backend unavailable"));
177    }
178}