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