Skip to main content

arcsec_core/
error.rs

1//! The crate's error type.
2
3use core::fmt;
4
5/// Everything that can stop a solve.
6///
7/// The variants map onto the ASTAP-compatible exit codes the `arcsec` CLI returns
8/// (2 for too few stars, 1 for no solution, 32/33 for database problems), which is
9/// why "no match" is reported through [`ArcsecError::InsufficientQuads`] rather than
10/// a dedicated variant. New variants may be added, so match with a wildcard arm.
11#[derive(Debug)]
12#[non_exhaustive]
13pub enum ArcsecError {
14    /// The least-squares system was singular (degenerate or too few points).
15    Singular,
16    /// Fewer stars were detected than the solver needs.
17    InsufficientStars {
18        /// Stars actually detected.
19        found: usize,
20        /// Minimum the solver requires.
21        required: usize,
22    },
23    /// Pattern matching did not produce a verified solution.
24    ///
25    /// Returned both when too few patterns could be built and when the search
26    /// finished without any position verifying.
27    InsufficientQuads {
28        /// Patterns (or, for the blind solver, verification score) achieved.
29        found: usize,
30        /// Minimum required.
31        required: usize,
32    },
33    /// The plate fit's X and Y scales disagree by more than 10%.
34    BadSolution {
35        /// Ratio of the squared X scale to the squared Y scale.
36        ratio: f64,
37    },
38    /// The star database directory does not contain the named database.
39    CatalogNotFound(std::path::PathBuf),
40    /// A catalogue or index file could not be read or parsed.
41    CatalogIo(std::io::Error),
42    /// A caller-supplied parameter is out of range (e.g. a non-positive field of view).
43    InvalidParameter(String),
44}
45
46impl fmt::Display for ArcsecError {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            Self::Singular => write!(f, "singular matrix in LSQ solver"),
50            Self::InsufficientStars { found, required } => {
51                write!(f, "insufficient stars: found {found}, required {required}")
52            }
53            Self::InsufficientQuads { found, required } => {
54                write!(f, "insufficient quads: found {found}, required {required}")
55            }
56            Self::BadSolution { ratio } => {
57                write!(
58                    f,
59                    "bad solution: xy scale ratio {ratio:.4} not in [0.9, 1.1]"
60                )
61            }
62            Self::CatalogNotFound(p) => write!(f, "catalog not found: {}", p.display()),
63            Self::CatalogIo(e) => write!(f, "catalog I/O error: {e}"),
64            Self::InvalidParameter(msg) => write!(f, "invalid parameter: {msg}"),
65        }
66    }
67}
68
69impl core::error::Error for ArcsecError {
70    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
71        match self {
72            Self::CatalogIo(e) => Some(e),
73            _ => None,
74        }
75    }
76}
77
78impl From<std::io::Error> for ArcsecError {
79    fn from(e: std::io::Error) -> Self {
80        Self::CatalogIo(e)
81    }
82}
83
84/// `Result` specialised to [`ArcsecError`].
85pub type Result<T> = core::result::Result<T, ArcsecError>;
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use core::error::Error as _;
91
92    /// The messages are what the CLI prints, so pin them.
93    #[test]
94    fn display_messages() {
95        let cases = [
96            (ArcsecError::Singular, "singular matrix in LSQ solver"),
97            (
98                ArcsecError::InsufficientStars {
99                    found: 3,
100                    required: 5,
101                },
102                "insufficient stars: found 3, required 5",
103            ),
104            (
105                ArcsecError::InsufficientQuads {
106                    found: 0,
107                    required: 4,
108                },
109                "insufficient quads: found 0, required 4",
110            ),
111            (
112                ArcsecError::BadSolution { ratio: 1.23456 },
113                "bad solution: xy scale ratio 1.2346 not in [0.9, 1.1]",
114            ),
115            (
116                ArcsecError::CatalogNotFound(std::path::PathBuf::from("/db")),
117                "catalog not found: /db",
118            ),
119            (
120                ArcsecError::InvalidParameter("fov".into()),
121                "invalid parameter: fov",
122            ),
123        ];
124        for (err, want) in cases {
125            assert_eq!(err.to_string(), want);
126            assert!(err.source().is_none());
127        }
128    }
129
130    #[test]
131    fn io_errors_convert_and_keep_their_source() {
132        let err: ArcsecError = std::io::Error::new(std::io::ErrorKind::NotFound, "gone").into();
133        assert_eq!(err.to_string(), "catalog I/O error: gone");
134        let source = err.source().expect("source");
135        assert_eq!(source.to_string(), "gone");
136        assert!(
137            matches!(err, ArcsecError::CatalogIo(ref e) if e.kind() == std::io::ErrorKind::NotFound)
138        );
139    }
140}