pg_embedded_setup_unpriv/
error.rs1use color_eyre::Report;
4use thiserror::Error;
5
6pub type Result<T> = std::result::Result<T, PgEmbeddedError>;
8
9pub type BootstrapResult<T> = std::result::Result<T, BootstrapError>;
11
12pub type PrivilegeResult<T> = std::result::Result<T, PrivilegeError>;
14
15pub type ConfigResult<T> = std::result::Result<T, ConfigError>;
17
18#[derive(Debug, Error)]
20pub enum PgEmbeddedError {
21 #[error("bootstrap failed: {0}")]
23 Bootstrap(#[from] BootstrapError),
24 #[error("privilege management failed: {0}")]
26 Privilege(#[from] PrivilegeError),
27 #[error("configuration parsing failed: {0}")]
29 Config(#[from] ConfigError),
30}
31
32#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
34pub enum BootstrapErrorKind {
35 #[default]
37 Other,
38 WorkerBinaryMissing,
40 WorkerBinaryPathNonUtf8,
42 ClusterPasswordMissing,
44 ClusterPasswordUnreadable,
47 ClusterPasswordEmpty,
49 ExtensionConfigInvalid,
55 ExtensionManifestUnavailable,
57 ExtensionManifestDigestMismatch,
59 ExtensionManifestInvalid,
62 ExtensionUnavailable,
65 ExtensionArchiveUnavailable,
67 ExtensionArchiveDigestMismatch,
69 ExtensionArchiveInvalid,
72 ExtensionInstallFailed,
74}
75
76impl BootstrapErrorKind {
77 pub(crate) const fn is_extension(self) -> bool {
80 matches!(
81 self,
82 Self::ExtensionConfigInvalid
83 | Self::ExtensionManifestUnavailable
84 | Self::ExtensionManifestDigestMismatch
85 | Self::ExtensionManifestInvalid
86 | Self::ExtensionUnavailable
87 | Self::ExtensionArchiveUnavailable
88 | Self::ExtensionArchiveDigestMismatch
89 | Self::ExtensionArchiveInvalid
90 | Self::ExtensionInstallFailed
91 )
92 }
93}
94
95#[derive(Debug, Error)]
97#[error("{report}")]
98pub struct BootstrapError {
99 kind: BootstrapErrorKind,
100 #[source]
101 report: Report,
102}
103
104impl BootstrapError {
105 #[must_use]
108 pub const fn new(kind: BootstrapErrorKind, report: Report) -> Self { Self { kind, report } }
109
110 #[must_use]
112 pub const fn kind(&self) -> BootstrapErrorKind { self.kind }
113
114 pub fn into_report(self) -> Report { self.report }
116
117 pub(crate) const fn report(&self) -> &Report { &self.report }
120}
121
122#[derive(Debug, Error)]
127#[error("{context}: operation timed out after {seconds:.1}s")]
128pub(crate) struct LifecycleTimeout {
129 pub(crate) context: &'static str,
131 pub(crate) seconds: f64,
133}
134
135#[derive(Debug, Error)]
142#[error(
143 "the PostgreSQL binaries came from the shared binary cache ({version}); remove that cache \
144 entry to download them afresh"
145)]
146pub(crate) struct CachedBinariesUsed {
147 pub(crate) version: String,
149}
150
151impl From<Report> for BootstrapError {
152 fn from(report: Report) -> Self { Self::new(BootstrapErrorKind::Other, report) }
153}
154
155impl From<PrivilegeError> for BootstrapError {
156 fn from(err: PrivilegeError) -> Self {
157 let PrivilegeError(report) = err;
158 Self::new(BootstrapErrorKind::Other, report)
159 }
160}
161
162impl From<ConfigError> for BootstrapError {
163 fn from(err: ConfigError) -> Self {
164 let ConfigError(report) = err;
165 Self::new(BootstrapErrorKind::Other, report)
166 }
167}
168
169impl From<PgEmbeddedError> for BootstrapError {
170 fn from(err: PgEmbeddedError) -> Self {
171 match err {
172 PgEmbeddedError::Bootstrap(inner) => inner,
173 PgEmbeddedError::Privilege(inner) => inner.into(),
174 PgEmbeddedError::Config(inner) => inner.into(),
175 }
176 }
177}
178
179#[derive(Debug, Error)]
181#[error(transparent)]
182pub struct PrivilegeError(#[from] Report);
183
184#[derive(Debug, Error)]
186#[error(transparent)]
187pub struct ConfigError(#[from] Report);
188
189#[cfg(test)]
190mod tests {
191 use color_eyre::eyre::eyre;
194 use rstest::rstest;
195
196 use super::*;
197
198 #[rstest]
199 #[case::bootstrap(
200 "PG_EMBEDDED_WORKER must be set",
201 "bootstrap failed:",
202 |msg: &str| PgEmbeddedError::Bootstrap(BootstrapError::from(eyre!("{}", msg)))
203 )]
204 #[case::privilege(
205 "failed to drop privileges",
206 "privilege management failed:",
207 |msg: &str| PgEmbeddedError::Privilege(PrivilegeError::from(eyre!("{}", msg)))
208 )]
209 #[case::config(
210 "invalid port number",
211 "configuration parsing failed:",
212 |msg: &str| PgEmbeddedError::Config(ConfigError::from(eyre!("{}", msg)))
213 )]
214 fn pg_embedded_error_includes_inner_message(
215 #[case] inner_message: &str,
216 #[case] expected_prefix: &str,
217 #[case] constructor: fn(&str) -> PgEmbeddedError,
218 ) {
219 let pg_err = constructor(inner_message);
220
221 let display = pg_err.to_string();
222
223 assert!(
224 display.contains(expected_prefix),
225 "expected '{expected_prefix}' prefix, got: {display}"
226 );
227 assert!(
228 display.contains(inner_message),
229 "expected inner message '{inner_message}' in display, got: {display}"
230 );
231 }
232
233 #[test]
234 fn bootstrap_error_displays_report_message() {
235 let inner_message = "database connection failed";
236 let err = BootstrapError::from(eyre!(inner_message));
237
238 let display = err.to_string();
239
240 assert!(
241 display.contains(inner_message),
242 "expected '{inner_message}' in display, got: {display}"
243 );
244 }
245
246 #[test]
247 fn bootstrap_error_kind_defaults_to_other() {
248 assert_eq!(BootstrapErrorKind::default(), BootstrapErrorKind::Other);
249 }
250
251 #[test]
252 fn bootstrap_error_preserves_kind_and_into_report() {
253 let err = BootstrapError::new(
254 BootstrapErrorKind::WorkerBinaryMissing,
255 eyre!("worker gone"),
256 );
257 assert_eq!(err.kind(), BootstrapErrorKind::WorkerBinaryMissing);
258 assert!(err.into_report().to_string().contains("worker gone"));
259 }
260
261 #[rstest]
262 #[case::config(BootstrapErrorKind::ExtensionConfigInvalid)]
263 #[case::manifest_unavailable(BootstrapErrorKind::ExtensionManifestUnavailable)]
264 #[case::manifest_digest(BootstrapErrorKind::ExtensionManifestDigestMismatch)]
265 #[case::manifest_invalid(BootstrapErrorKind::ExtensionManifestInvalid)]
266 #[case::unavailable(BootstrapErrorKind::ExtensionUnavailable)]
267 #[case::archive_unavailable(BootstrapErrorKind::ExtensionArchiveUnavailable)]
268 #[case::archive_digest(BootstrapErrorKind::ExtensionArchiveDigestMismatch)]
269 #[case::archive_invalid(BootstrapErrorKind::ExtensionArchiveInvalid)]
270 #[case::install_failed(BootstrapErrorKind::ExtensionInstallFailed)]
271 fn extension_error_kinds_survive_wrapping(#[case] kind: BootstrapErrorKind) {
272 let err = BootstrapError::new(kind, eyre!("extension detail"));
273 assert_eq!(err.kind(), kind);
274 let wrapped = BootstrapError::from(PgEmbeddedError::Bootstrap(err));
275 assert_eq!(wrapped.kind(), kind);
276 assert!(wrapped.to_string().contains("extension detail"));
277 }
278
279 #[test]
280 fn bootstrap_error_from_privilege_error_is_other() {
281 let privilege = PrivilegeError::from(eyre!("no privileges"));
282 let err = BootstrapError::from(privilege);
283 assert_eq!(err.kind(), BootstrapErrorKind::Other);
284 assert!(err.to_string().contains("no privileges"));
285 }
286
287 #[test]
288 fn bootstrap_error_from_config_error_is_other() {
289 let config = ConfigError::from(eyre!("bad config"));
290 let err = BootstrapError::from(config);
291 assert_eq!(err.kind(), BootstrapErrorKind::Other);
292 assert!(err.to_string().contains("bad config"));
293 }
294
295 #[rstest]
296 #[case::bootstrap(
297 |report| PgEmbeddedError::Bootstrap(BootstrapError::new(
298 BootstrapErrorKind::WorkerBinaryPathNonUtf8,
299 report,
300 )),
301 BootstrapErrorKind::WorkerBinaryPathNonUtf8,
302 )]
303 #[case::privilege(
304 |report| PgEmbeddedError::Privilege(PrivilegeError::from(report)),
305 BootstrapErrorKind::Other,
306 )]
307 #[case::config(
308 |report| PgEmbeddedError::Config(ConfigError::from(report)),
309 BootstrapErrorKind::Other,
310 )]
311 fn bootstrap_error_from_pg_embedded_error_maps_each_variant(
312 #[case] constructor: fn(Report) -> PgEmbeddedError,
313 #[case] expected_kind: BootstrapErrorKind,
314 ) {
315 let pg_err = constructor(eyre!("inner detail"));
316 let err = BootstrapError::from(pg_err);
317 assert_eq!(err.kind(), expected_kind);
318 assert!(err.to_string().contains("inner detail"));
319 }
320}