Skip to main content

turnframe_test/executors/
in_memory.rs

1//! The reference [`ExecutorFactory`]: the kit's own in-memory executor, wired
2//! up for any [`PureWorkflow`].
3//!
4//! It is here for three reasons. It is what proves the suite is runnable
5//! against a real implementation rather than only compilable. It is the
6//! smallest complete example of what an adopter has to write to point the suite
7//! at their own executor. And it is what the kit's own falsification tests
8//! break on purpose, one rule at a time, to prove each check catches the defect
9//! it claims to catch and nothing else.
10
11use std::fmt;
12
13use turnframe_core::case::CaseRef;
14use turnframe_core::ids::{AccountId, UserId};
15use turnframe_core::turn::ActorContext;
16
17use super::{BatchOf, ExecutorFactory, SeedOf, SeededCase};
18use crate::workflows::{InMemoryExecutor, PureWorkflow};
19
20/// Account the reference factory seeds its case in.
21pub const CONFORMANCE_ACCOUNT: &str = "turnframe-executor-conformance";
22
23/// User the reference factory acts as.
24pub const CONFORMANCE_USER: &str = "conformance-user";
25
26/// An [`ExecutorFactory`] over [`InMemoryExecutor`], for any workflow whose
27/// transitions are a pure function.
28///
29/// ```
30/// use turnframe_core::case::CaseRef;
31/// use turnframe_core::ids::CaseRevision;
32/// use turnframe_test::executors::{self, InMemoryCase};
33/// use turnframe_test::workflows::trip::{
34///     TripCommand, TripWorkflow, complete_case, sample_new_extra,
35/// };
36///
37/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
38/// # tokio::runtime::Runtime::new()?.block_on(async {
39/// let case = InMemoryCase::<TripWorkflow>::new(
40///     CaseRef::new("trip", "trip-conformance", CaseRevision(4)),
41///     Some(complete_case()),
42///     TripCommand::AddExtra { extra: sample_new_extra(1) },
43///     TripCommand::SetName { value: "Another name".to_owned() },
44///     TripCommand::Rebook,
45/// );
46///
47/// let report = executors::run_all(&case).await;
48/// assert!(report.passed(), "{report}");
49/// # });
50/// # Ok(())
51/// # }
52/// ```
53pub struct InMemoryCase<W: PureWorkflow> {
54    account: AccountId,
55    user: UserId,
56    case_ref: CaseRef,
57    seed_state: Option<W::State>,
58    first: W::Command,
59    second: W::Command,
60    refused: W::Command,
61}
62
63impl<W: PureWorkflow> InMemoryCase<W> {
64    /// Describes a case and the three commands the suite drives it with.
65    ///
66    /// `seed_state` is the state the case starts from, installed at
67    /// `case_ref.expected_revision`; `None` means the case does not exist yet,
68    /// in which case the revision must be
69    /// [`CaseRevision::ZERO`](turnframe_core::ids::CaseRevision::ZERO) and
70    /// `first` must be the command that brings it into existence.
71    ///
72    /// See [`SeededCase`] for what the three commands have to satisfy — in
73    /// particular that `first` must not be naturally idempotent.
74    #[must_use]
75    pub fn new(
76        case_ref: CaseRef,
77        seed_state: Option<W::State>,
78        first: W::Command,
79        second: W::Command,
80        refused: W::Command,
81    ) -> Self {
82        Self {
83            account: AccountId::from(CONFORMANCE_ACCOUNT),
84            user: UserId::from(CONFORMANCE_USER),
85            case_ref,
86            seed_state,
87            first,
88            second,
89            refused,
90        }
91    }
92
93    /// Runs the suite as another tenant.
94    #[must_use]
95    pub fn with_actor(mut self, account: impl Into<AccountId>, user: impl Into<UserId>) -> Self {
96        self.account = account.into();
97        self.user = user.into();
98        self
99    }
100
101    /// The case the factory seeds.
102    #[must_use]
103    pub const fn case_ref(&self) -> &CaseRef {
104        &self.case_ref
105    }
106}
107
108impl<W: PureWorkflow> fmt::Debug for InMemoryCase<W> {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        f.debug_struct("InMemoryCase")
111            .field("account", &self.account)
112            .field("case_ref", &self.case_ref)
113            .field("seeded", &self.seed_state.is_some())
114            .finish_non_exhaustive()
115    }
116}
117
118#[async_trait::async_trait]
119impl<W> ExecutorFactory for InMemoryCase<W>
120where
121    W: PureWorkflow + Default,
122{
123    type Workflow = W;
124    type Executor = InMemoryExecutor<W>;
125
126    async fn seed(&self) -> Result<SeedOf<Self>, String> {
127        let executor = InMemoryExecutor::<W>::default();
128        if let Some(state) = self.seed_state.clone() {
129            executor.seed(
130                &self.account,
131                &self.case_ref.case_id,
132                state,
133                self.case_ref.expected_revision,
134            );
135        }
136        Ok(SeededCase::new(
137            executor,
138            ActorContext::new(self.account.clone(), self.user.clone()),
139            self.case_ref.clone(),
140            self.first.clone(),
141            self.second.clone(),
142            self.refused.clone(),
143        ))
144    }
145
146    async fn interrupt_after(
147        &self,
148        seeded: &SeedOf<Self>,
149        batch: &BatchOf<Self>,
150        applied: usize,
151    ) -> Result<(), String> {
152        seeded
153            .executor
154            .execute_prefix(batch, applied)
155            .map(|_| ())
156            .map_err(|error| format!("{error:?}"))
157    }
158}