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}