Skip to main content

pg_embedded_setup_unpriv/
error.rs

1//! Domain error types for the embedded `PostgreSQL` bootstrapper.
2
3use color_eyre::Report;
4use thiserror::Error;
5
6/// Result alias for operations that may return a [`PgEmbeddedError`].
7pub type Result<T> = std::result::Result<T, PgEmbeddedError>;
8
9/// Result alias for bootstrap-specific fallible operations.
10pub type BootstrapResult<T> = std::result::Result<T, BootstrapError>;
11
12/// Result alias for privilege-management fallible operations.
13pub type PrivilegeResult<T> = std::result::Result<T, PrivilegeError>;
14
15/// Result alias for configuration fallible operations.
16pub type ConfigResult<T> = std::result::Result<T, ConfigError>;
17
18/// Top-level error exposed by the crate.
19#[derive(Debug, Error)]
20pub enum PgEmbeddedError {
21    /// Indicates bootstrap initialization failed.
22    #[error("bootstrap failed: {0}")]
23    Bootstrap(#[from] BootstrapError),
24    /// Indicates privilege management failed.
25    #[error("privilege management failed: {0}")]
26    Privilege(#[from] PrivilegeError),
27    /// Indicates configuration parsing failed.
28    #[error("configuration parsing failed: {0}")]
29    Config(#[from] ConfigError),
30}
31
32/// Categorizes bootstrap failures so callers can branch on structured errors.
33#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
34pub enum BootstrapErrorKind {
35    /// Represents errors without a more specific semantic meaning.
36    #[default]
37    Other,
38    /// Indicates the configured worker binary is missing from disk.
39    WorkerBinaryMissing,
40    /// Indicates a PATH entry used for worker discovery is not valid UTF-8.
41    WorkerBinaryPathNonUtf8,
42}
43
44/// Captures bootstrap-specific failures.
45#[derive(Debug, Error)]
46#[error("{report}")]
47pub struct BootstrapError {
48    kind: BootstrapErrorKind,
49    #[source]
50    report: Report,
51}
52
53impl BootstrapError {
54    /// Constructs a new bootstrap error with the provided kind and diagnostic
55    /// report.
56    #[must_use]
57    pub const fn new(kind: BootstrapErrorKind, report: Report) -> Self { Self { kind, report } }
58
59    /// Returns the semantic category for this bootstrap failure.
60    #[must_use]
61    pub const fn kind(&self) -> BootstrapErrorKind { self.kind }
62
63    /// Extracts the underlying diagnostic report.
64    pub fn into_report(self) -> Report { self.report }
65}
66
67impl From<Report> for BootstrapError {
68    fn from(report: Report) -> Self { Self::new(BootstrapErrorKind::Other, report) }
69}
70
71impl From<PrivilegeError> for BootstrapError {
72    fn from(err: PrivilegeError) -> Self {
73        let PrivilegeError(report) = err;
74        Self::new(BootstrapErrorKind::Other, report)
75    }
76}
77
78impl From<ConfigError> for BootstrapError {
79    fn from(err: ConfigError) -> Self {
80        let ConfigError(report) = err;
81        Self::new(BootstrapErrorKind::Other, report)
82    }
83}
84
85impl From<PgEmbeddedError> for BootstrapError {
86    fn from(err: PgEmbeddedError) -> Self {
87        match err {
88            PgEmbeddedError::Bootstrap(inner) => inner,
89            PgEmbeddedError::Privilege(inner) => inner.into(),
90            PgEmbeddedError::Config(inner) => inner.into(),
91        }
92    }
93}
94
95/// Captures privilege-management failures.
96#[derive(Debug, Error)]
97#[error(transparent)]
98pub struct PrivilegeError(#[from] Report);
99
100/// Captures configuration failures.
101#[derive(Debug, Error)]
102#[error(transparent)]
103pub struct ConfigError(#[from] Report);
104
105#[cfg(test)]
106mod tests {
107    //! Unit tests for error display formats.
108
109    use color_eyre::eyre::eyre;
110    use rstest::rstest;
111
112    use super::*;
113
114    #[rstest]
115    #[case::bootstrap(
116        "PG_EMBEDDED_WORKER must be set",
117        "bootstrap failed:",
118        |msg: &str| PgEmbeddedError::Bootstrap(BootstrapError::from(eyre!("{}", msg)))
119    )]
120    #[case::privilege(
121        "failed to drop privileges",
122        "privilege management failed:",
123        |msg: &str| PgEmbeddedError::Privilege(PrivilegeError::from(eyre!("{}", msg)))
124    )]
125    #[case::config(
126        "invalid port number",
127        "configuration parsing failed:",
128        |msg: &str| PgEmbeddedError::Config(ConfigError::from(eyre!("{}", msg)))
129    )]
130    fn pg_embedded_error_includes_inner_message(
131        #[case] inner_message: &str,
132        #[case] expected_prefix: &str,
133        #[case] constructor: fn(&str) -> PgEmbeddedError,
134    ) {
135        let pg_err = constructor(inner_message);
136
137        let display = pg_err.to_string();
138
139        assert!(
140            display.contains(expected_prefix),
141            "expected '{expected_prefix}' prefix, got: {display}"
142        );
143        assert!(
144            display.contains(inner_message),
145            "expected inner message '{inner_message}' in display, got: {display}"
146        );
147    }
148
149    #[test]
150    fn bootstrap_error_displays_report_message() {
151        let inner_message = "database connection failed";
152        let err = BootstrapError::from(eyre!(inner_message));
153
154        let display = err.to_string();
155
156        assert!(
157            display.contains(inner_message),
158            "expected '{inner_message}' in display, got: {display}"
159        );
160    }
161
162    #[test]
163    fn bootstrap_error_kind_defaults_to_other() {
164        assert_eq!(BootstrapErrorKind::default(), BootstrapErrorKind::Other);
165    }
166
167    #[test]
168    fn bootstrap_error_preserves_kind_and_into_report() {
169        let err = BootstrapError::new(
170            BootstrapErrorKind::WorkerBinaryMissing,
171            eyre!("worker gone"),
172        );
173        assert_eq!(err.kind(), BootstrapErrorKind::WorkerBinaryMissing);
174        assert!(err.into_report().to_string().contains("worker gone"));
175    }
176
177    #[test]
178    fn bootstrap_error_from_privilege_error_is_other() {
179        let privilege = PrivilegeError::from(eyre!("no privileges"));
180        let err = BootstrapError::from(privilege);
181        assert_eq!(err.kind(), BootstrapErrorKind::Other);
182        assert!(err.to_string().contains("no privileges"));
183    }
184
185    #[test]
186    fn bootstrap_error_from_config_error_is_other() {
187        let config = ConfigError::from(eyre!("bad config"));
188        let err = BootstrapError::from(config);
189        assert_eq!(err.kind(), BootstrapErrorKind::Other);
190        assert!(err.to_string().contains("bad config"));
191    }
192
193    #[rstest]
194    #[case::bootstrap(
195        |report| PgEmbeddedError::Bootstrap(BootstrapError::new(
196            BootstrapErrorKind::WorkerBinaryPathNonUtf8,
197            report,
198        )),
199        BootstrapErrorKind::WorkerBinaryPathNonUtf8,
200    )]
201    #[case::privilege(
202        |report| PgEmbeddedError::Privilege(PrivilegeError::from(report)),
203        BootstrapErrorKind::Other,
204    )]
205    #[case::config(
206        |report| PgEmbeddedError::Config(ConfigError::from(report)),
207        BootstrapErrorKind::Other,
208    )]
209    fn bootstrap_error_from_pg_embedded_error_maps_each_variant(
210        #[case] constructor: fn(Report) -> PgEmbeddedError,
211        #[case] expected_kind: BootstrapErrorKind,
212    ) {
213        let pg_err = constructor(eyre!("inner detail"));
214        let err = BootstrapError::from(pg_err);
215        assert_eq!(err.kind(), expected_kind);
216        assert!(err.to_string().contains("inner detail"));
217    }
218}