Skip to main content

lenso_test/
durable.rs

1//! Faults around a delegated durable operation, without a storage implementation.
2
3use std::future::Future;
4
5use crate::{FaultInjector, FaultPointError, ScenarioBoundary, SimulatorFault};
6
7/// Evidence available at a durable operation's completion boundary.
8///
9/// A timeout alone supplies no evidence of rollback. `Committed` on an injected
10/// lost acknowledgement is test-oracle evidence: a real disconnected caller
11/// usually has only `Unknown` and must reconcile through its own Store contract.
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum CommitKnowledge {
14    /// The delegate was never invoked, or the backend positively rejected dispatch.
15    NotExecuted,
16    /// The backend positively confirmed that this operation rolled back.
17    RolledBack,
18    /// The delegate confirmed commit before a test-owned acknowledgement fault.
19    Committed,
20    /// The available response cannot establish whether the operation committed.
21    Unknown,
22}
23
24/// The source of a durable operation failure.
25#[derive(Clone, Debug, Eq, PartialEq)]
26pub enum DurableFailureCause<E> {
27    /// An error from the real backend or a recorded external completion.
28    Backend(E),
29    /// An explicit test-owned boundary fault.
30    Injected(SimulatorFault),
31}
32
33/// A failure with explicit commit evidence, independent of its transport error.
34#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct DurableFailure<E> {
36    /// Evidence, never inferred merely from the error's name.
37    pub knowledge: CommitKnowledge,
38    /// The backend error or injected boundary condition.
39    pub cause: DurableFailureCause<E>,
40}
41
42impl<E> DurableFailure<E> {
43    /// Records backend evidence. Use `Unknown` unless the backend proves more.
44    pub fn backend(knowledge: CommitKnowledge, error: E) -> Self {
45        Self {
46            knowledge,
47            cause: DurableFailureCause::Backend(error),
48        }
49    }
50
51    fn injected(knowledge: CommitKnowledge, fault: SimulatorFault) -> Self {
52        Self {
53            knowledge,
54            cause: DurableFailureCause::Injected(fault),
55        }
56    }
57}
58
59/// A test-only facade around a finite operation owned by a Plugin's private Store.
60///
61/// It neither parses SQL nor supplies transactions, retries, deduplication, or
62/// results. The delegate executes the real operation. A simulator may instead
63/// replay *recorded typed external completions*, which never qualify a database.
64/// `AfterDurableCommit` is visited only after a successful delegate completion;
65/// `BeforeResponse` deliberately hides its evidence to model an uncertain reply.
66#[derive(Clone, Debug)]
67pub struct DurableFaultFacade {
68    operation: String,
69    faults: FaultInjector,
70}
71
72impl DurableFaultFacade {
73    /// Validates all boundary labels before any delegate can be invoked.
74    pub fn new(operation: &str, faults: FaultInjector) -> Result<Self, FaultPointError> {
75        for suffix in [
76            ".before-operation",
77            ".after-durable-commit",
78            ".before-response",
79        ] {
80            crate::faults::validate_point(&format!("{operation}{suffix}"))?;
81        }
82        // Empty operation names are invalid even though the suffixed label is valid.
83        crate::faults::validate_point(operation)?;
84        Ok(Self {
85            operation: operation.to_owned(),
86            faults,
87        })
88    }
89
90    /// Delegates exactly once, preserving backend evidence unless a fault hides it.
91    pub async fn execute<T, E, F, Fut>(&self, delegate: F) -> Result<T, DurableFailure<E>>
92    where
93        F: FnOnce() -> Fut,
94        Fut: Future<Output = Result<T, DurableFailure<E>>>,
95    {
96        if let Err(fault) = self.check(ScenarioBoundary::BeforeOperation) {
97            return Err(DurableFailure::injected(
98                CommitKnowledge::NotExecuted,
99                fault,
100            ));
101        }
102        let result = delegate().await;
103        if result.is_ok()
104            && let Err(fault) = self.check(ScenarioBoundary::AfterDurableCommit)
105        {
106            return Err(DurableFailure::injected(CommitKnowledge::Committed, fault));
107        }
108        if let Err(fault) = self.check(ScenarioBoundary::BeforeResponse) {
109            return Err(DurableFailure::injected(CommitKnowledge::Unknown, fault));
110        }
111        result
112    }
113
114    fn check(&self, boundary: ScenarioBoundary) -> Result<(), SimulatorFault> {
115        self.faults
116            .check_at(&self.operation, boundary)
117            .expect("facade constructor validated every boundary")
118    }
119}