1use crate::prelude::*;
2use crate::{DryContext, Op, OpMetadata, WetContext};
3use async_trait::async_trait;
4
5pub struct LoopOp<T> {
7 counter_var: String,
8 limit: usize,
9 ops: Vec<Arc<dyn Op<T>>>,
10 loop_id: String,
11 continue_var: String,
12 break_var: String,
13 continue_on_error: bool,
14}
15
16impl<T> LoopOp<T>
17where
18 T: Send + 'static,
19{
20 pub fn new(counter_var: String, limit: usize, ops: Vec<Arc<dyn Op<T>>>) -> Self {
27 let loop_id = ::uuid::Uuid::new_v4().to_string();
30 Self {
31 counter_var,
32 limit,
33 ops,
34 continue_var: format!("__continue_loop_{}", loop_id),
35 break_var: format!("__break_loop_{}", loop_id),
36 loop_id,
37 continue_on_error: false,
38 }
39 }
40
41 pub fn add_op(mut self, op: Arc<dyn Op<T>>) -> Self {
43 self.ops.push(op);
44 self
45 }
46
47 pub fn with_continue_on_error(mut self, continue_on_error: bool) -> Self {
49 self.continue_on_error = continue_on_error;
50 self
51 }
52
53 async fn rollback_iteration_ops(
55 &self,
56 succeeded_ops: &[Arc<dyn Op<T>>],
57 dry: &mut DryContext,
58 wet: &mut WetContext,
59 ) {
60 for op in succeeded_ops.iter().rev() {
61 if let Err(rollback_error) = op.rollback(dry, wet).await {
62 error!(
63 "Failed to rollback op {} in loop iteration: {}",
64 op.metadata().name,
65 rollback_error
66 );
67 } else {
68 debug!(
69 "Successfully rolled back op {} in loop iteration",
70 op.metadata().name
71 );
72 }
73 }
74 }
75
76 fn get_counter(&self, dry: &DryContext) -> usize {
78 dry.get::<usize>(&self.counter_var).unwrap_or(0)
79 }
80
81 fn set_counter(&self, dry: &mut DryContext, value: usize) {
83 dry.insert(&self.counter_var, value);
84 }
85}
86
87#[async_trait]
88impl<T> Op<Vec<T>> for LoopOp<T>
89where
90 T: Send + 'static,
91{
92 async fn perform(&self, dry: &mut DryContext, wet: &mut WetContext) -> OpResult<Vec<T>> {
93 let mut results = Vec::new();
94 let mut counter = self.get_counter(dry);
95
96 if !dry.contains(&self.counter_var) {
98 self.set_counter(dry, counter);
99 }
100
101 dry.insert("__current_loop_id", &self.loop_id);
103
104 while counter < self.limit {
105 if dry.is_aborted() {
107 let reason = dry
108 .abort_reason()
109 .cloned()
110 .unwrap_or_else(|| "Loop operation aborted".to_string());
111 return Err(OpError::Aborted(reason));
112 }
113
114 dry.insert(&self.continue_var, false);
116 dry.insert(&self.break_var, false);
117
118 let mut iteration_succeeded_ops = Vec::new();
120
121 for op in &self.ops {
123 if dry.is_aborted() {
125 self.rollback_iteration_ops(&iteration_succeeded_ops, dry, wet)
127 .await;
128 let reason = dry
129 .abort_reason()
130 .cloned()
131 .unwrap_or_else(|| "Loop operation aborted".to_string());
132 return Err(OpError::Aborted(reason));
133 }
134
135 match op.perform(dry, wet).await {
136 Ok(result) => {
137 results.push(result);
138 iteration_succeeded_ops.push(op.clone());
139
140 if dry.get::<bool>(&self.continue_var).unwrap_or(false) {
142 dry.insert(&self.continue_var, false); break; }
145
146 if dry.get::<bool>(&self.break_var).unwrap_or(false) {
148 dry.insert(&self.break_var, false); return Ok(results); }
151 }
152 Err(OpError::Aborted(reason)) => {
153 self.rollback_iteration_ops(&iteration_succeeded_ops, dry, wet)
155 .await;
156 return Err(OpError::Aborted(reason));
157 }
158 Err(error) => {
159 if self.continue_on_error {
160 warn!("Operation {} failed in loop iteration {}: {}. Continuing with next iteration.",
162 op.metadata().name, counter, error);
163 self.rollback_iteration_ops(&iteration_succeeded_ops, dry, wet)
165 .await;
166 break; } else {
168 self.rollback_iteration_ops(&iteration_succeeded_ops, dry, wet)
170 .await;
171 return Err(error);
172 }
173 }
174 }
175 }
176
177 counter += 1;
179 self.set_counter(dry, counter);
180 }
181
182 Ok(results)
183 }
184
185 fn metadata(&self) -> OpMetadata {
186 let description = if self.continue_on_error {
187 format!(
188 "Loop {} times over {} ops (continue on error)",
189 self.limit,
190 self.ops.len()
191 )
192 } else {
193 format!("Loop {} times over {} ops", self.limit, self.ops.len())
194 };
195 OpMetadata::builder("LoopOp")
196 .description(description)
197 .build()
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 struct TestOp {
206 value: i32,
207 }
208
209 #[async_trait]
210 impl Op<i32> for TestOp {
211 async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<i32> {
212 Ok(self.value)
213 }
214
215 fn metadata(&self) -> OpMetadata {
216 OpMetadata::builder("TestOp").build()
217 }
218 }
219
220 struct CounterOp;
221
222 #[async_trait]
223 impl Op<usize> for CounterOp {
224 async fn perform(&self, dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<usize> {
225 let counter: usize = dry.get("loop_counter").unwrap_or(0);
226 Ok(counter)
227 }
228
229 fn metadata(&self) -> OpMetadata {
230 OpMetadata::builder("CounterOp").build()
231 }
232 }
233
234 #[tokio::test]
236 async fn test0067_loop_op_basic() {
237 let mut dry = DryContext::new();
238 let mut wet = WetContext::new();
239
240 let ops: Vec<Arc<dyn Op<i32>>> = vec![
241 Arc::new(TestOp { value: 10 }),
242 Arc::new(TestOp { value: 20 }),
243 ];
244
245 let loop_op = LoopOp::new("loop_counter".to_string(), 3, ops);
246 let results = loop_op.perform(&mut dry, &mut wet).await.unwrap();
247
248 assert_eq!(results.len(), 6);
250 assert_eq!(results, vec![10, 20, 10, 20, 10, 20]);
251 }
252
253 #[tokio::test]
255 async fn test0068_loop_op_with_counter_access() {
256 let mut dry = DryContext::new();
257 let mut wet = WetContext::new();
258
259 let ops: Vec<Arc<dyn Op<usize>>> = vec![Arc::new(CounterOp)];
260
261 let loop_op = LoopOp::new("loop_counter".to_string(), 3, ops);
262 let results = loop_op.perform(&mut dry, &mut wet).await.unwrap();
263
264 assert_eq!(results, vec![0, 1, 2]);
266 }
267
268 #[tokio::test]
270 async fn test0069_loop_op_existing_counter() {
271 let mut dry = DryContext::new().with_value("my_counter", 2_usize);
272 let mut wet = WetContext::new();
273
274 let ops: Vec<Arc<dyn Op<i32>>> = vec![Arc::new(TestOp { value: 42 })];
275
276 let loop_op = LoopOp::new("my_counter".to_string(), 4, ops);
277 let results = loop_op.perform(&mut dry, &mut wet).await.unwrap();
278
279 assert_eq!(results.len(), 2);
281 assert_eq!(results, vec![42, 42]);
282 }
283
284 #[tokio::test]
286 async fn test0070_loop_op_zero_limit() {
287 let mut dry = DryContext::new();
288 let mut wet = WetContext::new();
289
290 let ops: Vec<Arc<dyn Op<i32>>> = vec![Arc::new(TestOp { value: 99 })];
291
292 let loop_op = LoopOp::new("counter".to_string(), 0, ops);
293 let results = loop_op.perform(&mut dry, &mut wet).await.unwrap();
294
295 assert_eq!(results.len(), 0);
297 }
298
299 #[tokio::test]
301 async fn test0071_loop_op_builder_pattern() {
302 let mut dry = DryContext::new();
303 let mut wet = WetContext::new();
304
305 let loop_op = LoopOp::new("builder_counter".to_string(), 2, vec![])
306 .add_op(Arc::new(TestOp { value: 1 }))
307 .add_op(Arc::new(TestOp { value: 2 }));
308
309 let results = loop_op.perform(&mut dry, &mut wet).await.unwrap();
310
311 assert_eq!(results.len(), 4);
312 assert_eq!(results, vec![1, 2, 1, 2]);
313 }
314
315 #[tokio::test]
317 async fn test0072_loop_op_rollback_on_iteration_failure() {
318 use std::sync::{Arc, Mutex};
319
320 struct RollbackTrackingOp {
321 id: u32,
322 should_fail: bool,
323 performed: Arc<Mutex<Vec<u32>>>, rolled_back: Arc<Mutex<Vec<u32>>>, }
326
327 #[async_trait]
328 impl Op<u32> for RollbackTrackingOp {
329 async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<u32> {
330 self.performed.lock().unwrap().push(self.id);
331 if self.should_fail {
332 Err(OpError::ExecutionFailed(format!("Op {} failed", self.id)))
333 } else {
334 Ok(self.id)
335 }
336 }
337
338 async fn rollback(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<()> {
339 self.rolled_back.lock().unwrap().push(self.id);
340 Ok(())
341 }
342
343 fn metadata(&self) -> OpMetadata {
344 OpMetadata::builder(&format!("RollbackTrackingOp{}", self.id)).build()
345 }
346 }
347
348 let performed = Arc::new(Mutex::new(Vec::new()));
350 let rolled_back = Arc::new(Mutex::new(Vec::new()));
351
352 let ops = vec![
353 Arc::new(RollbackTrackingOp {
354 id: 1,
355 should_fail: false,
356 performed: performed.clone(),
357 rolled_back: rolled_back.clone(),
358 }) as Arc<dyn Op<u32>>,
359 Arc::new(RollbackTrackingOp {
360 id: 2,
361 should_fail: false,
362 performed: performed.clone(),
363 rolled_back: rolled_back.clone(),
364 }) as Arc<dyn Op<u32>>,
365 Arc::new(RollbackTrackingOp {
366 id: 3,
367 should_fail: true, performed: performed.clone(),
369 rolled_back: rolled_back.clone(),
370 }) as Arc<dyn Op<u32>>,
371 ];
372
373 let loop_op = LoopOp::new("test_counter".to_string(), 2, ops);
374 let mut dry = DryContext::new();
375 let mut wet = WetContext::new();
376
377 let result = loop_op.perform(&mut dry, &mut wet).await;
379 assert!(result.is_err());
380
381 let performed_calls = performed.lock().unwrap();
383 assert_eq!(
384 *performed_calls,
385 vec![1, 2, 3],
386 "Should have performed ops 1, 2, 3 in first iteration"
387 );
388
389 let rolled_back_calls = rolled_back.lock().unwrap();
391 assert_eq!(
392 *rolled_back_calls,
393 vec![2, 1],
394 "Should have rolled back ops 2, 1 in reverse order (op 3 failed so no rollback)"
395 );
396 }
397
398 #[tokio::test]
400 async fn test0073_loop_op_rollback_order_within_iteration() {
401 use std::sync::{Arc, Mutex};
402
403 struct OrderTrackingOp {
404 id: u32,
405 rollback_order: Arc<Mutex<Vec<u32>>>,
406 }
407
408 #[async_trait]
409 impl Op<u32> for OrderTrackingOp {
410 async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<u32> {
411 Ok(self.id)
412 }
413
414 async fn rollback(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<()> {
415 self.rollback_order.lock().unwrap().push(self.id);
416 Ok(())
417 }
418
419 fn metadata(&self) -> OpMetadata {
420 OpMetadata::builder(&format!("OrderTrackingOp{}", self.id)).build()
421 }
422 }
423
424 struct FailingOp;
425
426 #[async_trait]
427 impl Op<u32> for FailingOp {
428 async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<u32> {
429 Err(OpError::ExecutionFailed("Intentional failure".to_string()))
430 }
431
432 fn metadata(&self) -> OpMetadata {
433 OpMetadata::builder("FailingOp").build()
434 }
435 }
436
437 let rollback_order = Arc::new(Mutex::new(Vec::new()));
438
439 let ops = vec![
440 Arc::new(OrderTrackingOp {
441 id: 1,
442 rollback_order: rollback_order.clone(),
443 }) as Arc<dyn Op<u32>>,
444 Arc::new(OrderTrackingOp {
445 id: 2,
446 rollback_order: rollback_order.clone(),
447 }) as Arc<dyn Op<u32>>,
448 Arc::new(OrderTrackingOp {
449 id: 3,
450 rollback_order: rollback_order.clone(),
451 }) as Arc<dyn Op<u32>>,
452 Arc::new(FailingOp) as Arc<dyn Op<u32>>, ];
454
455 let loop_op = LoopOp::new("test_counter".to_string(), 1, ops);
456 let mut dry = DryContext::new();
457 let mut wet = WetContext::new();
458
459 let result = loop_op.perform(&mut dry, &mut wet).await;
461 assert!(result.is_err());
462
463 let order = rollback_order.lock().unwrap();
465 assert_eq!(
466 *order,
467 vec![3, 2, 1],
468 "Rollback should happen in reverse order within iteration"
469 );
470 }
471
472 #[tokio::test]
474 async fn test0074_loop_op_successful_iterations_not_rolled_back() {
475 use std::sync::{Arc, Mutex};
476
477 struct IterationTrackingOp {
478 id: u32,
479 fail_on_iteration: Option<usize>, performed_iterations: Arc<Mutex<Vec<usize>>>,
481 rolled_back_iterations: Arc<Mutex<Vec<usize>>>,
482 }
483
484 #[async_trait]
485 impl Op<u32> for IterationTrackingOp {
486 async fn perform(&self, dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<u32> {
487 let counter: usize = dry.get("test_counter").unwrap_or(0);
488 self.performed_iterations.lock().unwrap().push(counter);
489
490 if let Some(fail_iteration) = self.fail_on_iteration {
491 if counter == fail_iteration {
492 return Err(OpError::ExecutionFailed(format!(
493 "Op {} failed on iteration {}",
494 self.id, counter
495 )));
496 }
497 }
498
499 Ok(self.id)
500 }
501
502 async fn rollback(&self, dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<()> {
503 let counter: usize = dry.get("test_counter").unwrap_or(0);
504 self.rolled_back_iterations.lock().unwrap().push(counter);
505 Ok(())
506 }
507
508 fn metadata(&self) -> OpMetadata {
509 OpMetadata::builder(&format!("IterationTrackingOp{}", self.id)).build()
510 }
511 }
512
513 let performed_iterations = Arc::new(Mutex::new(Vec::new()));
514 let rolled_back_iterations = Arc::new(Mutex::new(Vec::new()));
515
516 let ops = vec![Arc::new(IterationTrackingOp {
517 id: 1,
518 fail_on_iteration: Some(2), performed_iterations: performed_iterations.clone(),
520 rolled_back_iterations: rolled_back_iterations.clone(),
521 }) as Arc<dyn Op<u32>>];
522
523 let loop_op = LoopOp::new("test_counter".to_string(), 5, ops);
524 let mut dry = DryContext::new();
525 let mut wet = WetContext::new();
526
527 let result = loop_op.perform(&mut dry, &mut wet).await;
529 assert!(result.is_err());
530
531 let performed = performed_iterations.lock().unwrap();
533 assert_eq!(
534 *performed,
535 vec![0, 1, 2],
536 "Should have performed iterations 0, 1, 2"
537 );
538
539 let rolled_back = rolled_back_iterations.lock().unwrap();
542 assert_eq!(
543 *rolled_back,
544 Vec::<usize>::new(),
545 "No rollback should happen - the failing op wasn't successfully performed"
546 );
547 }
548
549 #[tokio::test]
551 async fn test0075_loop_op_mixed_iteration_with_rollback() {
552 use std::sync::{Arc, Mutex};
553
554 struct MixedIterationOp {
555 id: u32,
556 fail_on_iteration: Option<usize>,
557 performed_iterations: Arc<Mutex<Vec<(u32, usize)>>>, rolled_back_iterations: Arc<Mutex<Vec<(u32, usize)>>>, }
560
561 #[async_trait]
562 impl Op<u32> for MixedIterationOp {
563 async fn perform(&self, dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<u32> {
564 let counter: usize = dry.get("test_counter").unwrap_or(0);
565 self.performed_iterations
566 .lock()
567 .unwrap()
568 .push((self.id, counter));
569
570 if let Some(fail_iteration) = self.fail_on_iteration {
571 if counter == fail_iteration {
572 return Err(OpError::ExecutionFailed(format!(
573 "Op {} failed on iteration {}",
574 self.id, counter
575 )));
576 }
577 }
578
579 Ok(self.id)
580 }
581
582 async fn rollback(&self, dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<()> {
583 let counter: usize = dry.get("test_counter").unwrap_or(0);
584 self.rolled_back_iterations
585 .lock()
586 .unwrap()
587 .push((self.id, counter));
588 Ok(())
589 }
590
591 fn metadata(&self) -> OpMetadata {
592 OpMetadata::builder(&format!("MixedIterationOp{}", self.id)).build()
593 }
594 }
595
596 let performed_iterations = Arc::new(Mutex::new(Vec::new()));
597 let rolled_back_iterations = Arc::new(Mutex::new(Vec::new()));
598
599 let ops = vec![
601 Arc::new(MixedIterationOp {
602 id: 1,
603 fail_on_iteration: None, performed_iterations: performed_iterations.clone(),
605 rolled_back_iterations: rolled_back_iterations.clone(),
606 }) as Arc<dyn Op<u32>>,
607 Arc::new(MixedIterationOp {
608 id: 2,
609 fail_on_iteration: Some(1), performed_iterations: performed_iterations.clone(),
611 rolled_back_iterations: rolled_back_iterations.clone(),
612 }) as Arc<dyn Op<u32>>,
613 ];
614
615 let loop_op = LoopOp::new("test_counter".to_string(), 3, ops);
616 let mut dry = DryContext::new();
617 let mut wet = WetContext::new();
618
619 let result = loop_op.perform(&mut dry, &mut wet).await;
621 assert!(result.is_err());
622
623 let performed = performed_iterations.lock().unwrap();
627 assert_eq!(
628 *performed,
629 vec![(1, 0), (2, 0), (1, 1), (2, 1)],
630 "Should have performed all ops"
631 );
632
633 let rolled_back = rolled_back_iterations.lock().unwrap();
636 assert_eq!(
637 *rolled_back,
638 vec![(1, 1)],
639 "Should only rollback op1 from failed iteration 1"
640 );
641 }
642
643 struct BreakOp {
644 should_break: bool,
645 value: i32,
646 }
647
648 #[async_trait]
649 impl Op<i32> for BreakOp {
650 async fn perform(&self, dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<i32> {
651 if self.should_break {
652 let loop_id: String = dry.get("__current_loop_id").unwrap_or_default();
655 dry.insert(format!("__break_loop_{}", loop_id), true);
656 }
657 Ok(self.value)
658 }
659 fn metadata(&self) -> OpMetadata {
660 OpMetadata::builder("BreakOp").build()
661 }
662 }
663
664 #[tokio::test]
666 async fn test0113_loop_op_break_terminates_loop() {
667 let ops: Vec<Arc<dyn Op<i32>>> = vec![
668 Arc::new(TestOp { value: 10 }),
669 Arc::new(BreakOp {
670 should_break: true,
671 value: 99,
672 }),
673 Arc::new(TestOp { value: 20 }), ];
675
676 let loop_op = LoopOp::new("counter".to_string(), 5, ops);
677 let mut dry = DryContext::new();
678 let mut wet = WetContext::new();
679
680 let results = loop_op.perform(&mut dry, &mut wet).await.unwrap();
681 assert_eq!(results, vec![10, 99]);
683 }
684
685 #[tokio::test]
687 async fn test0114_loop_op_continue_on_error_skips_failed_iterations() {
688 use std::sync::{Arc as StdArc, Mutex};
689
690 let iterations_seen: StdArc<Mutex<Vec<usize>>> = StdArc::new(Mutex::new(vec![]));
691 let log = iterations_seen.clone();
692
693 struct IterationLogOp {
694 fail_on: Option<usize>,
695 log: StdArc<Mutex<Vec<usize>>>,
696 }
697
698 #[async_trait]
699 impl Op<i32> for IterationLogOp {
700 async fn perform(&self, dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<i32> {
701 let counter: usize = dry.get("it_counter").unwrap_or(0);
702 self.log.lock().unwrap().push(counter);
703 if Some(counter) == self.fail_on {
704 return Err(OpError::ExecutionFailed(format!("fail on {}", counter)));
705 }
706 Ok(counter as i32)
707 }
708 fn metadata(&self) -> OpMetadata {
709 OpMetadata::builder("IterationLogOp").build()
710 }
711 }
712
713 let loop_op = LoopOp::new(
714 "it_counter".to_string(),
715 4,
716 vec![Arc::new(IterationLogOp {
717 fail_on: Some(1),
718 log: log,
719 }) as Arc<dyn Op<i32>>],
720 )
721 .with_continue_on_error(true);
722
723 let mut dry = DryContext::new();
724 let mut wet = WetContext::new();
725 let results = loop_op.perform(&mut dry, &mut wet).await.unwrap();
726
727 let seen = iterations_seen.lock().unwrap().clone();
728 assert_eq!(seen, vec![0, 1, 2, 3]);
730 assert_eq!(results, vec![0, 2, 3]);
732 }
733
734 #[tokio::test]
736 async fn test0115_loop_op_with_no_ops_produces_no_results() {
737 let loop_op: LoopOp<i32> = LoopOp::new("counter".to_string(), 5, vec![]);
738 let mut dry = DryContext::new();
739 let mut wet = WetContext::new();
740 let results = loop_op.perform(&mut dry, &mut wet).await.unwrap();
741 assert!(results.is_empty());
742 assert_eq!(dry.get::<usize>("counter").unwrap(), 5);
744 }
745
746 #[tokio::test]
748 async fn test0076_loop_op_continue_on_error() {
749 use std::sync::{Arc, Mutex};
750
751 struct ContinueOnErrorOp {
752 id: u32,
753 fail_on_iteration: Option<usize>,
754 performed_iterations: Arc<Mutex<Vec<(u32, usize)>>>, rolled_back_iterations: Arc<Mutex<Vec<(u32, usize)>>>, }
757
758 #[async_trait]
759 impl Op<u32> for ContinueOnErrorOp {
760 async fn perform(&self, dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<u32> {
761 let counter: usize = dry.get("test_counter").unwrap_or(0);
762 self.performed_iterations
763 .lock()
764 .unwrap()
765 .push((self.id, counter));
766
767 if let Some(fail_iteration) = self.fail_on_iteration {
768 if counter == fail_iteration {
769 return Err(OpError::ExecutionFailed(format!(
770 "Op {} failed on iteration {}",
771 self.id, counter
772 )));
773 }
774 }
775
776 Ok(self.id)
777 }
778
779 async fn rollback(&self, dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<()> {
780 let counter: usize = dry.get("test_counter").unwrap_or(0);
781 self.rolled_back_iterations
782 .lock()
783 .unwrap()
784 .push((self.id, counter));
785 Ok(())
786 }
787
788 fn metadata(&self) -> OpMetadata {
789 OpMetadata::builder(&format!("ContinueOnErrorOp{}", self.id)).build()
790 }
791 }
792
793 let performed_iterations = Arc::new(Mutex::new(Vec::new()));
794 let rolled_back_iterations = Arc::new(Mutex::new(Vec::new()));
795
796 let ops = vec![
798 Arc::new(ContinueOnErrorOp {
799 id: 1,
800 fail_on_iteration: None, performed_iterations: performed_iterations.clone(),
802 rolled_back_iterations: rolled_back_iterations.clone(),
803 }) as Arc<dyn Op<u32>>,
804 Arc::new(ContinueOnErrorOp {
805 id: 2,
806 fail_on_iteration: Some(1), performed_iterations: performed_iterations.clone(),
808 rolled_back_iterations: rolled_back_iterations.clone(),
809 }) as Arc<dyn Op<u32>>,
810 ];
811
812 let loop_op = LoopOp::new("test_counter".to_string(), 3, ops).with_continue_on_error(true);
813 let mut dry = DryContext::new();
814 let mut wet = WetContext::new();
815
816 let result = loop_op.perform(&mut dry, &mut wet).await;
818 assert!(result.is_ok());
819 let results = result.unwrap();
820
821 let performed = performed_iterations.lock().unwrap();
826 assert_eq!(
827 *performed,
828 vec![(1, 0), (2, 0), (1, 1), (2, 1), (1, 2), (2, 2)],
829 "Should have performed all ops across all iterations"
830 );
831
832 assert_eq!(
834 results,
835 vec![1, 2, 1, 1, 2],
836 "Should have results from successful operations only"
837 );
838
839 let rolled_back = rolled_back_iterations.lock().unwrap();
841 assert_eq!(
842 *rolled_back,
843 vec![(1, 1)],
844 "Should only rollback op1 from failed iteration 1"
845 );
846 }
847}