Skip to main content

qubit_fs/copy/
copy_failure.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8// facade tests.
9//! Recoverable facade copy failure.
10
11use std::error::Error;
12use std::fmt::Debug;
13use std::fmt::Display;
14use std::fmt::Formatter;
15use std::fmt::Result as FmtResult;
16
17use crate::copy::CopyFailureState;
18use crate::copy::CopyStats;
19use crate::copy::internal::CopyFailureParts;
20use crate::error::FsError;
21use crate::write::WriterRecovery;
22
23/// A copy error with publication state, partial statistics, and optional writer
24/// recovery.
25///
26/// # Examples
27///
28/// The example runs against an isolated in-memory fixture. Applications obtain
29/// their configured facade from a provider or registry integration.
30///
31/// ```rust
32/// # mod support { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/common/rustdoc_support.rs")); }
33/// # let filesystem = support::rustdoc_provider::filesystem();
34/// use qubit_fs::Path;
35/// use qubit_fs::copy::CopyOptions;
36/// use qubit_fs::copy::CopyFailureState;
37///
38/// let failure = filesystem.copy(
39///     &Path::parse("/missing")?, &Path::parse("/copy")?, CopyOptions::default(),
40/// ).expect_err("the fixture has no source");
41/// assert_eq!(CopyFailureState::Unchanged, failure.state());
42/// assert_eq!(0, failure.partial_stats().bytes);
43/// assert!(!failure.has_recovery());
44/// # Ok::<(), Box<dyn std::error::Error>>(())
45/// ```
46pub struct CopyFailure {
47    /// Heap-owned error, publication state, progress, and recovery writer.
48    parts: Box<CopyFailureParts>,
49}
50impl CopyFailure {
51    /// Creates a typed copy failure from validated facade facts.
52    #[must_use]
53    pub(crate) fn new(
54        error: FsError,
55        state: CopyFailureState,
56        partial_stats: CopyStats,
57        writer: Option<WriterRecovery>,
58    ) -> Self {
59        Self {
60            parts: Box::new(CopyFailureParts {
61                error,
62                state,
63                partial_stats,
64                writer,
65            }),
66        }
67    }
68    /// Returns the contextual filesystem error.
69    #[inline]
70    #[must_use]
71    pub const fn error(&self) -> &FsError {
72        &self.parts.error
73    }
74    /// Returns the publication state at failure.
75    #[inline]
76    #[must_use]
77    pub const fn state(&self) -> CopyFailureState {
78        self.parts.state
79    }
80    /// Returns statistics accumulated before failure.
81    #[inline]
82    #[must_use]
83    pub const fn partial_stats(&self) -> &CopyStats {
84        &self.parts.partial_stats
85    }
86    /// Returns whether a writer is available for recovery.
87    #[inline]
88    #[must_use]
89    pub const fn has_recovery(&self) -> bool {
90        self.parts.writer.is_some()
91    }
92
93    /// Returns the recovery writer if retained.
94    #[inline]
95    #[must_use]
96    pub fn recovery(&self) -> Option<&WriterRecovery> {
97        self.parts.writer.as_ref()
98    }
99
100    /// Returns a mutable recovery writer if retained.
101    #[inline]
102    #[must_use]
103    pub fn recovery_mut(&mut self) -> Option<&mut WriterRecovery> {
104        self.parts.writer.as_mut()
105    }
106
107    /// Takes ownership of the recovery writer when recovery responsibility
108    /// remains with the caller.
109    #[inline]
110    #[must_use]
111    pub fn take_recovery(&mut self) -> Option<WriterRecovery> {
112        self.parts.writer.take()
113    }
114
115    /// Splits the failure into error, state, statistics, and writer recovery.
116    ///
117    /// # Returns
118    /// The filesystem error, confirmed publication state, partial statistics,
119    /// and optional writer retained for recovery.
120    #[inline]
121    #[must_use]
122    pub fn into_parts(self) -> (FsError, CopyFailureState, CopyStats, Option<WriterRecovery>) {
123        let mut parts = self.parts;
124        (parts.error, parts.state, parts.partial_stats, parts.writer.take())
125    }
126}
127impl Debug for CopyFailure {
128    /// Formats safe failure facts without exposing a provider writer session.
129    #[inline]
130    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
131        formatter
132            .debug_struct("CopyFailure")
133            .field("error", &self.parts.error)
134            .field("state", &self.parts.state)
135            .field("partial_stats", &self.parts.partial_stats)
136            .field("has_recovery", &self.parts.writer.is_some())
137            .finish()
138    }
139}
140
141impl Display for CopyFailure {
142    /// Formats the wrapped file-system error while keeping the recovery state
143    /// intentionally separate.
144    #[inline]
145    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
146        Display::fmt(self.error(), formatter)
147    }
148}
149
150impl Error for CopyFailure {
151    /// Returns the underlying file-system error.
152    #[inline]
153    fn source(&self) -> Option<&(dyn Error + 'static)> {
154        Some(self.error())
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use std::hint::black_box;
161
162    use super::CopyFailure;
163    use crate::copy::CopyFailureState;
164    use crate::copy::CopyStats;
165    use crate::error::FsError;
166    use crate::error::FsErrorKind;
167    use crate::error::FsOperation;
168    use crate::write::WriterRecovery;
169
170    #[test]
171    fn recovery_accessors_are_executed_at_runtime() {
172        let has_recovery: fn(&CopyFailure) -> bool = black_box(CopyFailure::has_recovery);
173        let recovery: fn(&CopyFailure) -> Option<&WriterRecovery> = black_box(CopyFailure::recovery);
174        let recovery_mut: for<'a> fn(&'a mut CopyFailure) -> Option<&'a mut WriterRecovery> =
175            black_box(CopyFailure::recovery_mut);
176        let take_recovery: fn(&mut CopyFailure) -> Option<WriterRecovery> = black_box(CopyFailure::take_recovery);
177        let mut failure = CopyFailure::new(
178            FsError::new(FsErrorKind::NotFound, FsOperation::Copy, "missing source"),
179            CopyFailureState::Unchanged,
180            CopyStats::default(),
181            None,
182        );
183
184        assert!(!has_recovery(&failure));
185        assert!(recovery(&failure).is_none());
186        assert!(recovery_mut(&mut failure).is_none());
187        assert!(take_recovery(&mut failure).is_none());
188    }
189}