1use std::sync::Arc;
7
8use tokio::sync::Mutex;
9
10use crate::mock_backend::{CheckpointCall, OperationRecord};
11
12pub async fn assert_checkpoint_count(calls: &Arc<Mutex<Vec<CheckpointCall>>>, expected: usize) {
36 let captured = calls.lock().await;
37 assert_eq!(
38 captured.len(),
39 expected,
40 "expected {expected} checkpoint calls, got {}",
41 captured.len()
42 );
43}
44
45pub async fn assert_no_checkpoints(calls: &Arc<Mutex<Vec<CheckpointCall>>>) {
70 assert_checkpoint_count(calls, 0).await;
71}
72
73pub async fn assert_operations(operations: &Arc<Mutex<Vec<OperationRecord>>>, expected: &[&str]) {
98 let recorded = operations.lock().await;
99 let actual: Vec<String> = recorded.iter().map(|r| r.to_type_name()).collect();
100 let expected: Vec<&str> = expected.to_vec();
101
102 if actual.len() != expected.len() {
103 panic!(
104 "Operation sequence length mismatch:\n Expected {} operations: {:?}\n Actual {} operations: {:?}",
105 expected.len(),
106 expected,
107 actual.len(),
108 actual,
109 );
110 }
111
112 for (i, (actual_op, expected_op)) in actual.iter().zip(expected.iter()).enumerate() {
113 if actual_op != expected_op {
114 panic!(
115 "Operation sequence mismatch at position {i}:\n Expected: {expected:?}\n Actual: {actual:?}\n First difference: expected \"{expected_op}\" but got \"{actual_op}\"",
116 );
117 }
118 }
119}
120
121pub async fn assert_operation_names(
146 operations: &Arc<Mutex<Vec<OperationRecord>>>,
147 expected: &[&str],
148) {
149 let recorded = operations.lock().await;
150 let actual: Vec<&str> = recorded.iter().map(|r| r.name.as_str()).collect();
151 let expected: Vec<&str> = expected.to_vec();
152
153 if actual.len() != expected.len() {
154 panic!(
155 "Operation name sequence length mismatch:\n Expected {} operations: {:?}\n Actual {} operations: {:?}",
156 expected.len(),
157 expected,
158 actual.len(),
159 actual,
160 );
161 }
162
163 for (i, (actual_name, expected_name)) in actual.iter().zip(expected.iter()).enumerate() {
164 if actual_name != expected_name {
165 panic!(
166 "Operation name mismatch at position {i}:\n Expected: {expected:?}\n Actual: {actual:?}\n First difference: expected \"{expected_name}\" but got \"{actual_name}\"",
167 );
168 }
169 }
170}
171
172pub async fn assert_operation_count(
194 operations: &Arc<Mutex<Vec<OperationRecord>>>,
195 expected: usize,
196) {
197 let recorded = operations.lock().await;
198 assert_eq!(
199 recorded.len(),
200 expected,
201 "expected {expected} operations, got {}",
202 recorded.len()
203 );
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use crate::mock_context::MockDurableContext;
210 use durable_lambda_core::context::DurableContext;
211
212 #[tokio::test]
215 async fn test_record_single_step_operation() {
216 let (mut ctx, _calls, ops) = MockDurableContext::new().build().await;
217 let _: Result<i32, String> = ctx.step("validate", || async { Ok(42) }).await.unwrap();
218
219 let recorded = ops.lock().await;
220 assert_eq!(recorded.len(), 1);
221 assert_eq!(recorded[0].name, "validate");
222 assert_eq!(recorded[0].operation_type, "step");
223 assert_eq!(recorded[0].to_type_name(), "step:validate");
224 }
225
226 #[tokio::test]
229 async fn test_record_multi_step_workflow_preserves_order() {
230 let (mut ctx, _calls, ops) = MockDurableContext::new().build().await;
231 let _: Result<i32, String> = ctx.step("validate", || async { Ok(1) }).await.unwrap();
232 let _: Result<i32, String> = ctx.step("charge", || async { Ok(2) }).await.unwrap();
233 let _: Result<i32, String> = ctx.step("confirm", || async { Ok(3) }).await.unwrap();
234
235 let recorded = ops.lock().await;
236 assert_eq!(recorded.len(), 3);
237 assert_eq!(recorded[0].to_type_name(), "step:validate");
238 assert_eq!(recorded[1].to_type_name(), "step:charge");
239 assert_eq!(recorded[2].to_type_name(), "step:confirm");
240 }
241
242 #[tokio::test]
245 async fn test_assert_operations_passes_for_matching_sequence() {
246 let (mut ctx, _calls, ops) = MockDurableContext::new().build().await;
247 let _: Result<i32, String> = ctx.step("validate", || async { Ok(1) }).await.unwrap();
248 let _: Result<i32, String> = ctx.step("charge", || async { Ok(2) }).await.unwrap();
249
250 assert_operations(&ops, &["step:validate", "step:charge"]).await;
251 }
252
253 #[tokio::test]
256 #[should_panic(expected = "Operation sequence mismatch")]
257 async fn test_assert_operations_panics_for_wrong_order() {
258 let (mut ctx, _calls, ops) = MockDurableContext::new().build().await;
259 let _: Result<i32, String> = ctx.step("validate", || async { Ok(1) }).await.unwrap();
260 let _: Result<i32, String> = ctx.step("charge", || async { Ok(2) }).await.unwrap();
261
262 assert_operations(&ops, &["step:charge", "step:validate"]).await;
264 }
265
266 #[tokio::test]
267 #[should_panic(expected = "Operation sequence length mismatch")]
268 async fn test_assert_operations_panics_for_wrong_count() {
269 let (mut ctx, _calls, ops) = MockDurableContext::new().build().await;
270 let _: Result<i32, String> = ctx.step("validate", || async { Ok(1) }).await.unwrap();
271
272 assert_operations(&ops, &["step:validate", "step:extra"]).await;
273 }
274
275 #[tokio::test]
278 async fn test_assert_operation_names_passes_for_matching() {
279 let (mut ctx, _calls, ops) = MockDurableContext::new().build().await;
280 let _: Result<i32, String> = ctx.step("validate", || async { Ok(1) }).await.unwrap();
281 let _: Result<i32, String> = ctx.step("charge", || async { Ok(2) }).await.unwrap();
282
283 assert_operation_names(&ops, &["validate", "charge"]).await;
284 }
285
286 #[tokio::test]
287 #[should_panic(expected = "Operation name mismatch")]
288 async fn test_assert_operation_names_panics_for_mismatch() {
289 let (mut ctx, _calls, ops) = MockDurableContext::new().build().await;
290 let _: Result<i32, String> = ctx.step("validate", || async { Ok(1) }).await.unwrap();
291
292 assert_operation_names(&ops, &["wrong_name"]).await;
293 }
294
295 #[tokio::test]
298 async fn test_assert_operation_count_passes() {
299 let (mut ctx, _calls, ops) = MockDurableContext::new().build().await;
300 let _: Result<i32, String> = ctx.step("s1", || async { Ok(1) }).await.unwrap();
301 let _: Result<i32, String> = ctx.step("s2", || async { Ok(2) }).await.unwrap();
302
303 assert_operation_count(&ops, 2).await;
304 }
305
306 #[tokio::test]
307 #[should_panic(expected = "expected 5 operations")]
308 async fn test_assert_operation_count_panics_for_mismatch() {
309 let (_ctx, _calls, ops) = MockDurableContext::new().build().await;
310 assert_operation_count(&ops, 5).await;
311 }
312
313 #[tokio::test]
316 async fn test_child_context_operations_recorded_in_sequence() {
317 let (mut ctx, _calls, ops) = MockDurableContext::new().build().await;
318
319 let _: Result<i32, String> = ctx.step("before", || async { Ok(1) }).await.unwrap();
320
321 let _: i32 = ctx
322 .child_context("sub", |mut child_ctx: DurableContext| async move {
323 let r: Result<i32, String> = child_ctx.step("inner", || async { Ok(42) }).await?;
324 Ok(r.unwrap())
325 })
326 .await
327 .unwrap();
328
329 let _: Result<i32, String> = ctx.step("after", || async { Ok(3) }).await.unwrap();
330
331 let recorded = ops.lock().await;
333 assert!(
336 recorded.len() >= 3,
337 "expected at least 3 operations, got {}",
338 recorded.len()
339 );
340 assert_eq!(recorded[0].to_type_name(), "step:before");
341 assert_eq!(recorded.last().unwrap().to_type_name(), "step:after");
343 }
344
345 #[tokio::test]
348 async fn test_replay_mode_produces_no_operation_records() {
349 let (mut ctx, _calls, ops) = MockDurableContext::new()
350 .with_step_result("validate", "42")
351 .build()
352 .await;
353
354 let _: Result<i32, String> = ctx
355 .step("validate", || async { panic!("not executed") })
356 .await
357 .unwrap();
358
359 assert_operation_count(&ops, 0).await;
361 }
362}