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}