durable_execution_sdk/macros.rs
1//! Ergonomic macros for promise combinators.
2//!
3//! This module provides declarative macros (`all!`, `any!`, `race!`, `all_settled!`)
4//! that enable combining heterogeneous futures without manual type erasure.
5//!
6//! # Problem
7//!
8//! The method-based API (`ctx.all()`, etc.) requires all futures in the vector to have
9//! the exact same concrete type. In Rust, each closure has a unique anonymous type,
10//! which means combining different step operations doesn't compile:
11//!
12//! ```rust,ignore
13//! // This doesn't compile - each closure has a unique type!
14//! let futures = vec![
15//! ctx.step(|_| Ok(1), None), // Type A
16//! ctx.step(|_| Ok(2), None), // Type B (different!)
17//! ];
18//! let results = ctx.all(futures).await?;
19//! ```
20//!
21//! # Solution
22//!
23//! These macros automatically box each future to erase their concrete types,
24//! enabling the common use case of combining different step operations:
25//!
26//! ```rust,ignore
27//! use durable_execution_sdk::all;
28//!
29//! // Clone contexts for each future
30//! let ctx1 = ctx.clone();
31//! let ctx2 = ctx.clone();
32//! let ctx3 = ctx.clone();
33//!
34//! // This works with macros!
35//! let results = all!(ctx,
36//! async move { ctx1.step(|_| Ok(1), None).await },
37//! async move { ctx2.step(|_| Ok(2), None).await },
38//! async move { ctx3.step(|_| Ok(3), None).await },
39//! ).await?;
40//! // results: Vec<i32> = [1, 2, 3]
41//! ```
42//!
43//! # When to Use Macros vs Methods
44//!
45//! ## Use Macros When:
46//!
47//! - **Combining different step operations** - Each closure has a unique type
48//! - **Mixing operation types** - Combining step + wait + callback operations
49//! - **Writing inline futures** - Ad-hoc combinations of different operations
50//! - **Small, fixed number of futures** - Known at compile time
51//!
52//! ```rust,ignore
53//! use durable_execution_sdk::{all, any};
54//!
55//! // Different closures = different types - use macros!
56//! let ctx1 = ctx.clone();
57//! let ctx2 = ctx.clone();
58//! let ctx3 = ctx.clone();
59//!
60//! let results = all!(ctx,
61//! async move { ctx1.step(|_| fetch_user_data(), None).await },
62//! async move { ctx2.step(|_| fetch_preferences(), None).await },
63//! async move { ctx3.step(|_| fetch_notifications(), None).await },
64//! ).await?;
65//!
66//! // Fallback pattern with any!
67//! let ctx1 = ctx.clone();
68//! let ctx2 = ctx.clone();
69//! let ctx3 = ctx.clone();
70//!
71//! let data = any!(ctx,
72//! async move { ctx1.step(|_| fetch_from_primary(), None).await },
73//! async move { ctx2.step(|_| fetch_from_secondary(), None).await },
74//! async move { ctx3.step(|_| fetch_from_cache(), None).await },
75//! ).await?;
76//! ```
77//!
78//! ## Use Methods When:
79//!
80//! - **Processing homogeneous futures from iterators/loops** - Same closure applied to different data
81//! - **Working with pre-boxed futures** - Already type-erased
82//! - **Programmatically generating futures** - Dynamic number of futures from a single source
83//! - **Using `ctx.map()`** - Preferred for iterating over collections
84//!
85//! ```rust,ignore
86//! // Same closure applied to different data - use methods!
87//! let user_ids = vec![1, 2, 3, 4, 5];
88//! let futures: Vec<_> = user_ids
89//! .into_iter()
90//! .map(|id| {
91//! let ctx = ctx.clone();
92//! async move { ctx.step(move |_| fetch_user(id), None).await }
93//! })
94//! .collect();
95//! let users = ctx.all(futures).await?;
96//!
97//! // Or even better, use ctx.map():
98//! let batch = ctx.map(user_ids, |child_ctx, id, _| {
99//! Box::pin(async move { child_ctx.step(|_| fetch_user(id), None).await })
100//! }, None).await?;
101//! ```
102//!
103//! # Quick Reference
104//!
105//! | Macro | Behavior | Returns | Use Case |
106//! |-------|----------|---------|----------|
107//! | `all!` | Wait for all to succeed | `Vec<T>` or first error | Parallel fetch, batch processing |
108//! | `any!` | First success wins | `T` or combined error | Fallback patterns, redundancy |
109//! | `race!` | First to settle wins | `T` (success or error) | Timeouts, competitive operations |
110//! | `all_settled!` | Wait for all to settle | `BatchResult<T>` | Collect all outcomes, partial success |
111//!
112//! # Available Macros
113//!
114//! - `all!` - Wait for all futures to succeed, return error on first failure
115//! - `any!` - Return first successful result, error only if all fail
116//! - `race!` - Return first result to settle (success or failure)
117//! - `all_settled!` - Wait for all futures to settle, return all outcomes
118
119/// Waits for all futures to complete successfully.
120///
121/// This macro boxes each future to enable combining heterogeneous future types,
122/// then delegates to `ctx.all()`. All futures run concurrently, and the macro
123/// returns when all have completed successfully or when the first error occurs.
124///
125/// # Arguments
126///
127/// * `$ctx` - The [`DurableContext`](crate::DurableContext) instance
128/// * `$fut` - One or more future expressions (comma-separated)
129///
130/// # Returns
131///
132/// * `Ok(Vec<T>)` - All results in the same order as the input futures
133/// * `Err(DurableError)` - The first error encountered (remaining futures are cancelled)
134///
135/// # Examples
136///
137/// ## Basic Usage
138///
139/// ```rust,ignore
140/// use durable_execution_sdk::all;
141///
142/// // Clone contexts for each future to satisfy lifetime requirements
143/// let ctx1 = ctx.clone();
144/// let ctx2 = ctx.clone();
145/// let ctx3 = ctx.clone();
146///
147/// let results = all!(ctx,
148/// async move { ctx1.step(|_| Ok(1), None).await },
149/// async move { ctx2.step(|_| Ok(2), None).await },
150/// async move { ctx3.step(|_| Ok(3), None).await },
151/// ).await?;
152/// assert_eq!(results, vec![1, 2, 3]);
153/// ```
154///
155/// ## Parallel Data Fetching
156///
157/// ```rust,ignore
158/// use durable_execution_sdk::all;
159///
160/// // Clone contexts for parallel operations
161/// let ctx1 = ctx.clone();
162/// let ctx2 = ctx.clone();
163/// let ctx3 = ctx.clone();
164///
165/// // Fetch multiple pieces of data in parallel
166/// let results = all!(ctx,
167/// async move { ctx1.step(|_| fetch_user(user_id), None).await },
168/// async move { ctx2.step(|_| fetch_preferences(user_id), None).await },
169/// async move { ctx3.step(|_| fetch_notifications(user_id), None).await },
170/// ).await?;
171/// let (user, preferences, notifications) = (
172/// results[0].clone(),
173/// results[1].clone(),
174/// results[2].clone()
175/// );
176/// ```
177///
178/// ## Error Handling
179///
180/// ```rust,ignore
181/// use durable_execution_sdk::all;
182///
183/// let ctx1 = ctx.clone();
184/// let ctx2 = ctx.clone();
185/// let ctx3 = ctx.clone();
186///
187/// // If any step fails, the entire operation fails
188/// let result = all!(ctx,
189/// async move { ctx1.step(|_| Ok(1), None).await },
190/// async move { ctx2.step(|_| Err::<i32, _>("failed".into()), None).await },
191/// async move { ctx3.step(|_| Ok(3), None).await }, // This may not execute
192/// ).await;
193///
194/// assert!(result.is_err());
195/// ```
196///
197/// # When to Use
198///
199/// Use `all!` when you need all operations to succeed and want to fail fast
200/// on the first error. For collecting all outcomes (including failures),
201/// use `all_settled!` instead.
202///
203/// # See Also
204///
205/// * `any!` - Return first success
206/// * `race!` - Return first to settle
207/// * `all_settled!` - Collect all outcomes
208#[macro_export]
209macro_rules! all {
210 ($ctx:expr, $($fut:expr),+ $(,)?) => {{
211 let futures: ::std::vec::Vec<
212 ::std::pin::Pin<
213 ::std::boxed::Box<
214 dyn ::std::future::Future<
215 Output = $crate::error::DurableResult<_>
216 > + ::std::marker::Send
217 >
218 >
219 > = ::std::vec![
220 $(::std::boxed::Box::pin($fut)),+
221 ];
222 $ctx.all(futures)
223 }};
224}
225
226/// Returns the first successful result from multiple futures.
227///
228/// This macro boxes each future to enable combining heterogeneous future types,
229/// then delegates to `ctx.any()`. All futures run concurrently, and the macro
230/// returns as soon as any future succeeds. If all futures fail, returns a
231/// combined error.
232///
233/// # Arguments
234///
235/// * `$ctx` - The [`DurableContext`](crate::DurableContext) instance
236/// * `$fut` - One or more future expressions (comma-separated)
237///
238/// # Returns
239///
240/// * `Ok(T)` - The first successful result (remaining futures are cancelled)
241/// * `Err(DurableError)` - Combined error if all futures fail
242///
243/// # Examples
244///
245/// ## Basic Fallback Pattern
246///
247/// ```rust,ignore
248/// use durable_execution_sdk::any;
249///
250/// // Clone contexts for each future
251/// let ctx1 = ctx.clone();
252/// let ctx2 = ctx.clone();
253/// let ctx3 = ctx.clone();
254///
255/// // Try multiple sources, return first success
256/// let data = any!(ctx,
257/// async move { ctx1.step(|_| fetch_from_primary(), None).await },
258/// async move { ctx2.step(|_| fetch_from_secondary(), None).await },
259/// async move { ctx3.step(|_| fetch_from_cache(), None).await },
260/// ).await?;
261/// ```
262///
263/// ## Redundant Service Calls
264///
265/// ```rust,ignore
266/// use durable_execution_sdk::any;
267///
268/// let ctx1 = ctx.clone();
269/// let ctx2 = ctx.clone();
270///
271/// // Call multiple redundant services, use first response
272/// let price = any!(ctx,
273/// async move { ctx1.step(|_| get_price_from_service_a(item_id), None).await },
274/// async move { ctx2.step(|_| get_price_from_service_b(item_id), None).await },
275/// ).await?;
276/// ```
277///
278/// ## Handling All Failures
279///
280/// ```rust,ignore
281/// use durable_execution_sdk::any;
282///
283/// let ctx1 = ctx.clone();
284/// let ctx2 = ctx.clone();
285///
286/// // If all sources fail, get combined error
287/// let result = any!(ctx,
288/// async move { ctx1.step(|_| Err::<String, _>("primary failed".into()), None).await },
289/// async move { ctx2.step(|_| Err::<String, _>("secondary failed".into()), None).await },
290/// ).await;
291///
292/// // Error contains information about all failures
293/// assert!(result.is_err());
294/// ```
295///
296/// # When to Use
297///
298/// Use `any!` for fallback patterns where you want the first successful result
299/// and don't care which source provides it. Unlike `race!`, `any!` ignores
300/// failures and only returns an error if ALL futures fail.
301///
302/// # See Also
303///
304/// * `all!` - Wait for all to succeed
305/// * `race!` - Return first to settle (success or failure)
306/// * `all_settled!` - Collect all outcomes
307#[macro_export]
308macro_rules! any {
309 ($ctx:expr, $($fut:expr),+ $(,)?) => {{
310 let futures: ::std::vec::Vec<
311 ::std::pin::Pin<
312 ::std::boxed::Box<
313 dyn ::std::future::Future<
314 Output = $crate::error::DurableResult<_>
315 > + ::std::marker::Send
316 >
317 >
318 > = ::std::vec![
319 $(::std::boxed::Box::pin($fut)),+
320 ];
321 $ctx.any(futures)
322 }};
323}
324
325/// Returns the result of the first future to settle (success or failure).
326///
327/// This macro boxes each future to enable combining heterogeneous future types,
328/// then delegates to `ctx.race()`. All futures run concurrently, and the macro
329/// returns as soon as any future completes, regardless of whether it succeeded
330/// or failed.
331///
332/// # Arguments
333///
334/// * `$ctx` - The [`DurableContext`](crate::DurableContext) instance
335/// * `$fut` - One or more future expressions (comma-separated)
336///
337/// # Returns
338///
339/// * `Ok(T)` - If the first future to settle succeeded
340/// * `Err(DurableError)` - If the first future to settle failed
341///
342/// # Examples
343///
344/// ## Timeout Pattern
345///
346/// ```rust,ignore
347/// use durable_execution_sdk::race;
348///
349/// let ctx1 = ctx.clone();
350/// let ctx2 = ctx.clone();
351///
352/// // Race between operation and timeout
353/// let result = race!(ctx,
354/// async move { ctx1.step(|_| slow_operation(), None).await },
355/// async move {
356/// ctx2.step(|_| {
357/// std::thread::sleep(std::time::Duration::from_secs(5));
358/// Err::<String, _>("timeout".into())
359/// }, None).await
360/// },
361/// ).await;
362///
363/// match result {
364/// Ok(data) => println!("Operation completed: {}", data),
365/// Err(e) => println!("Timed out or failed: {}", e),
366/// }
367/// ```
368///
369/// ## Competitive Operations
370///
371/// ```rust,ignore
372/// use durable_execution_sdk::race;
373///
374/// let ctx1 = ctx.clone();
375/// let ctx2 = ctx.clone();
376///
377/// // Use whichever service responds first
378/// let result = race!(ctx,
379/// async move { ctx1.step(|_| call_service_a(), None).await },
380/// async move { ctx2.step(|_| call_service_b(), None).await },
381/// ).await?;
382/// ```
383///
384/// ## First Failure Wins
385///
386/// ```rust,ignore
387/// use durable_execution_sdk::race;
388///
389/// let ctx1 = ctx.clone();
390/// let ctx2 = ctx.clone();
391///
392/// // If the first to settle is an error, that error is returned
393/// let result = race!(ctx,
394/// async move { ctx1.step(|_| Err::<i32, _>("fast error".into()), None).await },
395/// async move {
396/// ctx2.step(|_| {
397/// std::thread::sleep(std::time::Duration::from_millis(100));
398/// Ok(42)
399/// }, None).await
400/// },
401/// ).await;
402///
403/// assert!(result.is_err()); // Error settled first
404/// ```
405///
406/// # When to Use
407///
408/// Use `race!` when you want the first result regardless of success or failure.
409/// This is useful for timeout patterns or when competing operations should
410/// cancel each other. Unlike `any!`, `race!` returns immediately on the
411/// first settlement, even if it's a failure.
412///
413/// # See Also
414///
415/// * `all!` - Wait for all to succeed
416/// * `any!` - Return first success (ignores failures)
417/// * `all_settled!` - Collect all outcomes
418#[macro_export]
419macro_rules! race {
420 ($ctx:expr, $($fut:expr),+ $(,)?) => {{
421 let futures: ::std::vec::Vec<
422 ::std::pin::Pin<
423 ::std::boxed::Box<
424 dyn ::std::future::Future<
425 Output = $crate::error::DurableResult<_>
426 > + ::std::marker::Send
427 >
428 >
429 > = ::std::vec![
430 $(::std::boxed::Box::pin($fut)),+
431 ];
432 $ctx.race(futures)
433 }};
434}
435
436/// Waits for all futures to settle, returning all outcomes.
437///
438/// This macro boxes each future to enable combining heterogeneous future types,
439/// then delegates to `ctx.all_settled()`. Unlike `all!`, this macro does not
440/// short-circuit on failure - it waits for all futures to complete and returns
441/// a [`BatchResult`](crate::concurrency::BatchResult) containing all outcomes.
442///
443/// # Arguments
444///
445/// * `$ctx` - The [`DurableContext`](crate::DurableContext) instance
446/// * `$fut` - One or more future expressions (comma-separated)
447///
448/// # Returns
449///
450/// * `Ok(BatchResult<T>)` - Contains outcomes for all futures (never fails)
451///
452/// The [`BatchResult`](crate::concurrency::BatchResult) provides methods to:
453/// - `successes()` / `succeeded()` - Get successful results
454/// - `failures()` / `failed()` - Get failed results
455/// - `success_count()` / `failure_count()` - Count outcomes
456/// - `all_succeeded()` / `has_failures()` - Check overall status
457/// - `get_results()` - Get all successful results or error if any failed
458///
459/// # Examples
460///
461/// ## Collecting All Outcomes
462///
463/// ```rust,ignore
464/// use durable_execution_sdk::all_settled;
465///
466/// let ctx1 = ctx.clone();
467/// let ctx2 = ctx.clone();
468/// let ctx3 = ctx.clone();
469///
470/// let batch = all_settled!(ctx,
471/// async move { ctx1.step(|_| Ok(1), None).await },
472/// async move { ctx2.step(|_| Err::<i32, _>("failed".into()), None).await },
473/// async move { ctx3.step(|_| Ok(3), None).await },
474/// ).await?;
475///
476/// println!("Total: {}", batch.items.len()); // 3
477/// println!("Succeeded: {}", batch.success_count()); // 2
478/// println!("Failed: {}", batch.failure_count()); // 1
479/// ```
480///
481/// ## Processing Successes and Failures Separately
482///
483/// ```rust,ignore
484/// use durable_execution_sdk::all_settled;
485///
486/// let ctx1 = ctx.clone();
487/// let ctx2 = ctx.clone();
488/// let ctx3 = ctx.clone();
489///
490/// let batch = all_settled!(ctx,
491/// async move { ctx1.step(|_| process_item_a(), None).await },
492/// async move { ctx2.step(|_| process_item_b(), None).await },
493/// async move { ctx3.step(|_| process_item_c(), None).await },
494/// ).await?;
495///
496/// // Process successful results
497/// for item in batch.succeeded() {
498/// if let Some(result) = item.get_result() {
499/// println!("Success: {:?}", result);
500/// }
501/// }
502///
503/// // Log failures for retry or investigation
504/// for item in batch.failed() {
505/// if let Some(error) = item.get_error() {
506/// eprintln!("Failed at index {}: {:?}", item.index, error);
507/// }
508/// }
509/// ```
510///
511/// ## Partial Success Handling
512///
513/// ```rust,ignore
514/// use durable_execution_sdk::all_settled;
515///
516/// let ctx1 = ctx.clone();
517/// let ctx2 = ctx.clone();
518/// let ctx3 = ctx.clone();
519///
520/// let batch = all_settled!(ctx,
521/// async move { ctx1.step(|_| send_notification_email(), None).await },
522/// async move { ctx2.step(|_| send_notification_sms(), None).await },
523/// async move { ctx3.step(|_| send_notification_push(), None).await },
524/// ).await?;
525///
526/// if batch.all_succeeded() {
527/// println!("All notifications sent!");
528/// } else {
529/// println!("Sent {} of {} notifications",
530/// batch.success_count(),
531/// batch.items.len()
532/// );
533/// }
534/// ```
535///
536/// ## Order Preservation
537///
538/// Results are returned in the same order as input futures:
539///
540/// ```rust,ignore
541/// use durable_execution_sdk::all_settled;
542///
543/// let ctx1 = ctx.clone();
544/// let ctx2 = ctx.clone();
545/// let ctx3 = ctx.clone();
546///
547/// let batch = all_settled!(ctx,
548/// async move { ctx1.step(|_| Ok("first"), None).await },
549/// async move { ctx2.step(|_| Ok("second"), None).await },
550/// async move { ctx3.step(|_| Ok("third"), None).await },
551/// ).await?;
552///
553/// assert_eq!(batch.items[0].index, 0);
554/// assert_eq!(batch.items[1].index, 1);
555/// assert_eq!(batch.items[2].index, 2);
556/// ```
557///
558/// # When to Use
559///
560/// Use `all_settled!` when you need to:
561/// - Collect all outcomes regardless of individual success/failure
562/// - Implement partial success handling
563/// - Gather errors for logging or retry logic
564/// - Process results even when some operations fail
565///
566/// # See Also
567///
568/// * `all!` - Fail fast on first error
569/// * `any!` - Return first success
570/// * `race!` - Return first to settle
571#[macro_export]
572macro_rules! all_settled {
573 ($ctx:expr, $($fut:expr),+ $(,)?) => {{
574 let futures: ::std::vec::Vec<
575 ::std::pin::Pin<
576 ::std::boxed::Box<
577 dyn ::std::future::Future<
578 Output = $crate::error::DurableResult<_>
579 > + ::std::marker::Send
580 >
581 >
582 > = ::std::vec![
583 $(::std::boxed::Box::pin($fut)),+
584 ];
585 $ctx.all_settled(futures)
586 }};
587}
588
589#[cfg(test)]
590mod tests {
591 use std::future::Future;
592 use std::pin::Pin;
593 use std::sync::Arc;
594
595 use crate::client::{CheckpointResponse, MockDurableServiceClient, SharedDurableServiceClient};
596 use crate::context::TracingLogger;
597 use crate::error::DurableError;
598 use crate::lambda::InitialExecutionState;
599 use crate::state::ExecutionState;
600
601 fn create_mock_client() -> SharedDurableServiceClient {
602 Arc::new(
603 MockDurableServiceClient::new()
604 .with_checkpoint_response(Ok(CheckpointResponse::new("token-1")))
605 .with_checkpoint_response(Ok(CheckpointResponse::new("token-2")))
606 .with_checkpoint_response(Ok(CheckpointResponse::new("token-3")))
607 .with_checkpoint_response(Ok(CheckpointResponse::new("token-4")))
608 .with_checkpoint_response(Ok(CheckpointResponse::new("token-5"))),
609 )
610 }
611
612 fn create_test_state(client: SharedDurableServiceClient) -> Arc<ExecutionState> {
613 Arc::new(ExecutionState::new(
614 "arn:aws:lambda:us-east-1:123456789012:function:test:durable:abc123",
615 "initial-token",
616 InitialExecutionState::new(),
617 client,
618 ))
619 }
620
621 /// A mock context that provides the `all`, `any`, `race`, and `all_settled` methods for testing macros.
622 /// This simulates the DurableContext interface without requiring the full context.
623 struct MockContext {
624 state: Arc<ExecutionState>,
625 logger: Arc<dyn crate::context::Logger>,
626 id_counter: std::sync::atomic::AtomicU64,
627 }
628
629 impl MockContext {
630 fn new(state: Arc<ExecutionState>) -> Self {
631 Self {
632 state,
633 logger: Arc::new(TracingLogger),
634 id_counter: std::sync::atomic::AtomicU64::new(0),
635 }
636 }
637
638 fn next_op_id(&self) -> crate::context::OperationIdentifier {
639 let counter = self
640 .id_counter
641 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
642 crate::context::OperationIdentifier::new(format!("test-op-{}", counter), None, None)
643 }
644
645 /// Simulates the `all` method from DurableContext.
646 /// Returns all results if all futures succeed, or returns the first error.
647 pub async fn all<T, Fut>(&self, futures: Vec<Fut>) -> Result<Vec<T>, DurableError>
648 where
649 T: serde::Serialize + serde::de::DeserializeOwned + Send + Clone + 'static,
650 Fut: Future<Output = Result<T, DurableError>> + Send + 'static,
651 {
652 let op_id = self.next_op_id();
653 crate::handlers::promise::all_handler(futures, &self.state, &op_id, &self.logger).await
654 }
655
656 /// Simulates the `any` method from DurableContext.
657 /// Returns the first successful result, or an error if all fail.
658 pub async fn any<T, Fut>(&self, futures: Vec<Fut>) -> Result<T, DurableError>
659 where
660 T: serde::Serialize + serde::de::DeserializeOwned + Send + Clone + 'static,
661 Fut: Future<Output = Result<T, DurableError>> + Send + 'static,
662 {
663 let op_id = self.next_op_id();
664 crate::handlers::promise::any_handler(futures, &self.state, &op_id, &self.logger).await
665 }
666
667 /// Simulates the `race` method from DurableContext.
668 /// Returns the result of the first future to settle (success or failure).
669 pub async fn race<T, Fut>(&self, futures: Vec<Fut>) -> Result<T, DurableError>
670 where
671 T: serde::Serialize + serde::de::DeserializeOwned + Send + Clone + 'static,
672 Fut: Future<Output = Result<T, DurableError>> + Send + 'static,
673 {
674 let op_id = self.next_op_id();
675 crate::handlers::promise::race_handler(futures, &self.state, &op_id, &self.logger).await
676 }
677
678 /// Simulates the `all_settled` method from DurableContext.
679 /// Returns a BatchResult containing outcomes for all futures.
680 pub async fn all_settled<T, Fut>(
681 &self,
682 futures: Vec<Fut>,
683 ) -> Result<crate::concurrency::BatchResult<T>, DurableError>
684 where
685 T: serde::Serialize + serde::de::DeserializeOwned + Send + Clone + 'static,
686 Fut: Future<Output = Result<T, DurableError>> + Send + 'static,
687 {
688 let op_id = self.next_op_id();
689 crate::handlers::promise::all_settled_handler(
690 futures,
691 &self.state,
692 &op_id,
693 &self.logger,
694 )
695 .await
696 }
697 }
698
699 // =========================================================================
700 // all! macro tests
701 // Validates: Requirements 1.1, 1.2, 1.3, 1.4, 1.5
702 // =========================================================================
703
704 /// Test that all! macro returns Vec<T> with all results in order when all succeed.
705 /// Validates: Requirements 1.1, 1.2, 1.5
706 #[tokio::test]
707 async fn test_all_macro_success_returns_vec_in_order() {
708 let client = create_mock_client();
709 let state = create_test_state(client);
710 let ctx = MockContext::new(state);
711
712 let result = crate::all!(
713 ctx,
714 Box::pin(async { Ok::<_, DurableError>(1) })
715 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
716 Box::pin(async { Ok(2) }),
717 Box::pin(async { Ok(3) }),
718 )
719 .await;
720
721 assert!(result.is_ok());
722 let values = result.unwrap();
723 assert_eq!(values, vec![1, 2, 3]);
724 }
725
726 /// Test that all! macro works with a single future.
727 /// Validates: Requirements 1.4
728 #[tokio::test]
729 async fn test_all_macro_single_future() {
730 let client = create_mock_client();
731 let state = create_test_state(client);
732 let ctx = MockContext::new(state);
733
734 let result = crate::all!(
735 ctx,
736 Box::pin(async { Ok::<_, DurableError>(42) })
737 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
738 )
739 .await;
740
741 assert!(result.is_ok());
742 assert_eq!(result.unwrap(), vec![42]);
743 }
744
745 /// Test that all! macro works with two futures.
746 /// Validates: Requirements 1.4, 1.5
747 #[tokio::test]
748 async fn test_all_macro_two_futures() {
749 let client = create_mock_client();
750 let state = create_test_state(client);
751 let ctx = MockContext::new(state);
752
753 let result = crate::all!(
754 ctx,
755 Box::pin(async { Ok::<_, DurableError>("hello".to_string()) })
756 as Pin<Box<dyn Future<Output = Result<String, DurableError>> + Send>>,
757 Box::pin(async { Ok("world".to_string()) }),
758 )
759 .await;
760
761 assert!(result.is_ok());
762 assert_eq!(
763 result.unwrap(),
764 vec!["hello".to_string(), "world".to_string()]
765 );
766 }
767
768 /// Test that all! macro returns first error when any future fails.
769 /// Validates: Requirements 1.3
770 #[tokio::test]
771 async fn test_all_macro_failure_returns_first_error() {
772 let client = create_mock_client();
773 let state = create_test_state(client);
774 let ctx = MockContext::new(state);
775
776 let result = crate::all!(
777 ctx,
778 Box::pin(async { Ok::<_, DurableError>(1) })
779 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
780 Box::pin(async { Err(DurableError::execution("test error")) }),
781 Box::pin(async { Ok(3) }),
782 )
783 .await;
784
785 assert!(result.is_err());
786 let error = result.unwrap_err();
787 let error_msg = format!("{}", error);
788 assert!(error_msg.contains("failed") || error_msg.contains("error"));
789 }
790
791 /// Test that all! macro returns error when first future fails.
792 /// Validates: Requirements 1.3
793 #[tokio::test]
794 async fn test_all_macro_first_future_fails() {
795 let client = create_mock_client();
796 let state = create_test_state(client);
797 let ctx = MockContext::new(state);
798
799 let result = crate::all!(
800 ctx,
801 Box::pin(async { Err::<i32, _>(DurableError::execution("first error")) })
802 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
803 Box::pin(async { Ok(2) }),
804 )
805 .await;
806
807 assert!(result.is_err());
808 }
809
810 /// Test that all! macro supports trailing comma.
811 /// Validates: Requirements 1.1
812 #[tokio::test]
813 async fn test_all_macro_trailing_comma() {
814 let client = create_mock_client();
815 let state = create_test_state(client);
816 let ctx = MockContext::new(state);
817
818 // Note the trailing comma after the last future
819 let result = crate::all!(
820 ctx,
821 Box::pin(async { Ok::<_, DurableError>(1) })
822 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
823 Box::pin(async { Ok(2) }),
824 )
825 .await;
826
827 assert!(result.is_ok());
828 assert_eq!(result.unwrap(), vec![1, 2]);
829 }
830
831 /// Test that all! macro works with different output types (String).
832 /// Validates: Requirements 1.1, 1.6
833 #[tokio::test]
834 async fn test_all_macro_string_type() {
835 let client = create_mock_client();
836 let state = create_test_state(client);
837 let ctx = MockContext::new(state);
838
839 let result = crate::all!(
840 ctx,
841 Box::pin(async { Ok::<_, DurableError>("a".to_string()) })
842 as Pin<Box<dyn Future<Output = Result<String, DurableError>> + Send>>,
843 Box::pin(async { Ok("b".to_string()) }),
844 Box::pin(async { Ok("c".to_string()) }),
845 )
846 .await;
847
848 assert!(result.is_ok());
849 assert_eq!(
850 result.unwrap(),
851 vec!["a".to_string(), "b".to_string(), "c".to_string()]
852 );
853 }
854
855 /// Property 1: Macro expansion equivalence test for all! macro.
856 /// Verifies that all! macro produces the same result as manually boxing futures.
857 /// Validates: Requirements 1.1, 1.2, 1.3, 1.4, 1.5
858 #[tokio::test]
859 async fn test_all_macro_equivalence_with_manual_boxing() {
860 // Test with macro
861 let client1 = create_mock_client();
862 let state1 = create_test_state(client1);
863 let ctx1 = MockContext::new(state1);
864
865 let macro_result = crate::all!(
866 ctx1,
867 Box::pin(async { Ok::<_, DurableError>(10) })
868 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
869 Box::pin(async { Ok(20) }),
870 Box::pin(async { Ok(30) }),
871 )
872 .await;
873
874 // Test with manual boxing (same as what macro expands to)
875 let client2 = create_mock_client();
876 let state2 = create_test_state(client2);
877 let ctx2 = MockContext::new(state2);
878
879 let futures: Vec<Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>> = vec![
880 Box::pin(async { Ok::<_, DurableError>(10) }),
881 Box::pin(async { Ok(20) }),
882 Box::pin(async { Ok(30) }),
883 ];
884 let manual_result = ctx2.all(futures).await;
885
886 // Both should succeed with the same values
887 assert!(macro_result.is_ok());
888 assert!(manual_result.is_ok());
889 assert_eq!(macro_result.unwrap(), manual_result.unwrap());
890 }
891
892 /// Property 1: Macro expansion equivalence test for all! macro with failure.
893 /// Verifies that all! macro produces the same error behavior as manually boxing futures.
894 /// Validates: Requirements 1.1, 1.3
895 #[tokio::test]
896 async fn test_all_macro_equivalence_with_manual_boxing_failure() {
897 // Test with macro
898 let client1 = create_mock_client();
899 let state1 = create_test_state(client1);
900 let ctx1 = MockContext::new(state1);
901
902 let macro_result = crate::all!(
903 ctx1,
904 Box::pin(async { Ok::<_, DurableError>(1) })
905 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
906 Box::pin(async { Err(DurableError::execution("test error")) }),
907 )
908 .await;
909
910 // Test with manual boxing
911 let client2 = create_mock_client();
912 let state2 = create_test_state(client2);
913 let ctx2 = MockContext::new(state2);
914
915 let futures: Vec<Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>> = vec![
916 Box::pin(async { Ok::<_, DurableError>(1) }),
917 Box::pin(async { Err(DurableError::execution("test error")) }),
918 ];
919 let manual_result = ctx2.all(futures).await;
920
921 // Both should fail
922 assert!(macro_result.is_err());
923 assert!(manual_result.is_err());
924 }
925
926 // =========================================================================
927 // any! macro tests
928 // =========================================================================
929
930 /// Test that any! macro returns the first successful result.
931 /// Validates: Requirements 2.1, 2.2
932 #[tokio::test]
933 async fn test_any_macro_first_success_returned() {
934 let client = create_mock_client();
935 let state = create_test_state(client);
936 let ctx = MockContext::new(state);
937
938 // Use the any! macro with multiple futures where first succeeds
939 let result = crate::any!(
940 ctx,
941 Box::pin(async { Ok::<_, DurableError>(1) })
942 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
943 Box::pin(async { Ok(2) }),
944 Box::pin(async { Ok(3) }),
945 )
946 .await;
947
948 assert!(result.is_ok());
949 let value = result.unwrap();
950 // One of the successful values should be returned
951 assert!(value == 1 || value == 2 || value == 3);
952 }
953
954 /// Test that any! macro returns first success even when some fail.
955 /// Validates: Requirements 2.2, 2.4
956 #[tokio::test]
957 async fn test_any_macro_success_among_failures() {
958 let client = create_mock_client();
959 let state = create_test_state(client);
960 let ctx = MockContext::new(state);
961
962 let result = crate::any!(
963 ctx,
964 Box::pin(async { Err::<i32, _>(DurableError::execution("error 1")) })
965 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
966 Box::pin(async { Ok(42) }),
967 Box::pin(async { Err(DurableError::execution("error 2")) }),
968 )
969 .await;
970
971 assert!(result.is_ok());
972 assert_eq!(result.unwrap(), 42);
973 }
974
975 /// Test that any! macro returns combined error when all futures fail.
976 /// Validates: Requirements 2.3
977 #[tokio::test]
978 async fn test_any_macro_all_failures_returns_combined_error() {
979 let client = create_mock_client();
980 let state = create_test_state(client);
981 let ctx = MockContext::new(state);
982
983 let result = crate::any!(
984 ctx,
985 Box::pin(async { Err::<i32, _>(DurableError::execution("error 1")) })
986 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
987 Box::pin(async { Err(DurableError::execution("error 2")) }),
988 Box::pin(async { Err(DurableError::execution("error 3")) }),
989 )
990 .await;
991
992 assert!(result.is_err());
993 let error = result.unwrap_err();
994 // The error message should contain information about all failures
995 let error_msg = format!("{}", error);
996 assert!(error_msg.contains("All") || error_msg.contains("failed"));
997 }
998
999 /// Test that any! macro works with a single future.
1000 /// Validates: Requirements 2.4
1001 #[tokio::test]
1002 async fn test_any_macro_single_future_success() {
1003 let client = create_mock_client();
1004 let state = create_test_state(client);
1005 let ctx = MockContext::new(state);
1006
1007 let result = crate::any!(
1008 ctx,
1009 Box::pin(async { Ok::<_, DurableError>(99) })
1010 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1011 )
1012 .await;
1013
1014 assert!(result.is_ok());
1015 assert_eq!(result.unwrap(), 99);
1016 }
1017
1018 /// Test that any! macro works with a single failing future.
1019 /// Validates: Requirements 2.3, 2.4
1020 #[tokio::test]
1021 async fn test_any_macro_single_future_failure() {
1022 let client = create_mock_client();
1023 let state = create_test_state(client);
1024 let ctx = MockContext::new(state);
1025
1026 let result = crate::any!(
1027 ctx,
1028 Box::pin(async { Err::<i32, _>(DurableError::execution("single error")) })
1029 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1030 )
1031 .await;
1032
1033 assert!(result.is_err());
1034 }
1035
1036 /// Test that any! macro supports trailing comma.
1037 /// Validates: Requirements 2.1
1038 #[tokio::test]
1039 async fn test_any_macro_trailing_comma() {
1040 let client = create_mock_client();
1041 let state = create_test_state(client);
1042 let ctx = MockContext::new(state);
1043
1044 // Note the trailing comma after the last future
1045 let result = crate::any!(
1046 ctx,
1047 Box::pin(async { Ok::<_, DurableError>(1) })
1048 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1049 Box::pin(async { Ok(2) }),
1050 )
1051 .await;
1052
1053 assert!(result.is_ok());
1054 }
1055
1056 /// Test that any! macro works with different output types (String).
1057 /// Validates: Requirements 2.1, 2.4
1058 #[tokio::test]
1059 async fn test_any_macro_string_type() {
1060 let client = create_mock_client();
1061 let state = create_test_state(client);
1062 let ctx = MockContext::new(state);
1063
1064 let result = crate::any!(
1065 ctx,
1066 Box::pin(async { Ok::<_, DurableError>("hello".to_string()) })
1067 as Pin<Box<dyn Future<Output = Result<String, DurableError>> + Send>>,
1068 Box::pin(async { Ok("world".to_string()) }),
1069 )
1070 .await;
1071
1072 assert!(result.is_ok());
1073 let value = result.unwrap();
1074 assert!(value == "hello" || value == "world");
1075 }
1076
1077 // =========================================================================
1078 // Property 1: Macro expansion equivalence test
1079 // Validates: Requirements 1.1, 2.1, 3.1, 4.1
1080 // =========================================================================
1081
1082 /// Test that any! macro produces the same result as manually boxing futures.
1083 /// This validates that the macro expansion is equivalent to manual boxing.
1084 #[tokio::test]
1085 async fn test_any_macro_equivalence_with_manual_boxing() {
1086 // Test with macro
1087 let client1 = create_mock_client();
1088 let state1 = create_test_state(client1);
1089 let ctx1 = MockContext::new(state1);
1090
1091 let macro_result = crate::any!(
1092 ctx1,
1093 Box::pin(async { Err::<i32, _>(DurableError::execution("fail")) })
1094 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1095 Box::pin(async { Ok(42) }),
1096 )
1097 .await;
1098
1099 // Test with manual boxing (same as what macro expands to)
1100 let client2 = create_mock_client();
1101 let state2 = create_test_state(client2);
1102 let ctx2 = MockContext::new(state2);
1103
1104 let futures: Vec<Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>> = vec![
1105 Box::pin(async { Err::<i32, _>(DurableError::execution("fail")) }),
1106 Box::pin(async { Ok(42) }),
1107 ];
1108 let manual_result = ctx2.any(futures).await;
1109
1110 // Both should succeed with the same value
1111 assert!(macro_result.is_ok());
1112 assert!(manual_result.is_ok());
1113 assert_eq!(macro_result.unwrap(), manual_result.unwrap());
1114 }
1115
1116 // =========================================================================
1117 // race! macro tests
1118 // Validates: Requirements 3.1, 3.2, 3.3
1119 // =========================================================================
1120
1121 /// Test that race! macro returns the first result to settle (success case).
1122 /// Validates: Requirements 3.1, 3.2
1123 #[tokio::test]
1124 async fn test_race_macro_first_success_settles() {
1125 let client = create_mock_client();
1126 let state = create_test_state(client);
1127 let ctx = MockContext::new(state);
1128
1129 // First future completes immediately with success
1130 let result = crate::race!(
1131 ctx,
1132 Box::pin(async { Ok::<_, DurableError>(1) })
1133 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1134 Box::pin(async {
1135 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1136 Ok(2)
1137 }),
1138 )
1139 .await;
1140
1141 assert!(result.is_ok());
1142 // First one should win since it completes immediately
1143 assert_eq!(result.unwrap(), 1);
1144 }
1145
1146 /// Test that race! macro returns the first result to settle (failure case).
1147 /// Validates: Requirements 3.1, 3.2
1148 #[tokio::test]
1149 async fn test_race_macro_first_failure_settles() {
1150 let client = create_mock_client();
1151 let state = create_test_state(client);
1152 let ctx = MockContext::new(state);
1153
1154 // First future completes immediately with failure
1155 let result = crate::race!(
1156 ctx,
1157 Box::pin(async { Err::<i32, _>(DurableError::execution("fast error")) })
1158 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1159 Box::pin(async {
1160 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1161 Ok(2)
1162 }),
1163 )
1164 .await;
1165
1166 // The error should be returned since it settled first
1167 assert!(result.is_err());
1168 }
1169
1170 /// Test that race! macro works with a single future.
1171 /// Validates: Requirements 3.3
1172 #[tokio::test]
1173 async fn test_race_macro_single_future_success() {
1174 let client = create_mock_client();
1175 let state = create_test_state(client);
1176 let ctx = MockContext::new(state);
1177
1178 let result = crate::race!(
1179 ctx,
1180 Box::pin(async { Ok::<_, DurableError>(42) })
1181 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1182 )
1183 .await;
1184
1185 assert!(result.is_ok());
1186 assert_eq!(result.unwrap(), 42);
1187 }
1188
1189 /// Test that race! macro works with a single failing future.
1190 /// Validates: Requirements 3.2, 3.3
1191 #[tokio::test]
1192 async fn test_race_macro_single_future_failure() {
1193 let client = create_mock_client();
1194 let state = create_test_state(client);
1195 let ctx = MockContext::new(state);
1196
1197 let result = crate::race!(
1198 ctx,
1199 Box::pin(async { Err::<i32, _>(DurableError::execution("single error")) })
1200 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1201 )
1202 .await;
1203
1204 assert!(result.is_err());
1205 }
1206
1207 /// Test that race! macro supports trailing comma.
1208 /// Validates: Requirements 3.1
1209 #[tokio::test]
1210 async fn test_race_macro_trailing_comma() {
1211 let client = create_mock_client();
1212 let state = create_test_state(client);
1213 let ctx = MockContext::new(state);
1214
1215 // Note the trailing comma after the last future
1216 let result = crate::race!(
1217 ctx,
1218 Box::pin(async { Ok::<_, DurableError>(1) })
1219 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1220 Box::pin(async { Ok(2) }),
1221 )
1222 .await;
1223
1224 assert!(result.is_ok());
1225 }
1226
1227 /// Test that race! macro works with different output types (String).
1228 /// Validates: Requirements 3.1, 3.4
1229 #[tokio::test]
1230 async fn test_race_macro_string_type() {
1231 let client = create_mock_client();
1232 let state = create_test_state(client);
1233 let ctx = MockContext::new(state);
1234
1235 let result = crate::race!(
1236 ctx,
1237 Box::pin(async { Ok::<_, DurableError>("hello".to_string()) })
1238 as Pin<Box<dyn Future<Output = Result<String, DurableError>> + Send>>,
1239 Box::pin(async {
1240 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1241 Ok("world".to_string())
1242 }),
1243 )
1244 .await;
1245
1246 assert!(result.is_ok());
1247 assert_eq!(result.unwrap(), "hello");
1248 }
1249
1250 /// Test that race! macro works with multiple futures where success settles first.
1251 /// Validates: Requirements 3.1, 3.2, 3.3
1252 #[tokio::test]
1253 async fn test_race_macro_multiple_futures_success_first() {
1254 let client = create_mock_client();
1255 let state = create_test_state(client);
1256 let ctx = MockContext::new(state);
1257
1258 let result = crate::race!(
1259 ctx,
1260 Box::pin(async { Ok::<_, DurableError>(1) })
1261 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1262 Box::pin(async { Ok(2) }),
1263 Box::pin(async { Ok(3) }),
1264 )
1265 .await;
1266
1267 assert!(result.is_ok());
1268 // One of the values should be returned (first to settle)
1269 let value = result.unwrap();
1270 assert!(value == 1 || value == 2 || value == 3);
1271 }
1272
1273 /// Test that race! macro produces the same result as manually boxing futures.
1274 /// This validates that the macro expansion is equivalent to manual boxing.
1275 /// Validates: Requirements 3.1
1276 #[tokio::test]
1277 async fn test_race_macro_equivalence_with_manual_boxing() {
1278 // Test with macro
1279 let client1 = create_mock_client();
1280 let state1 = create_test_state(client1);
1281 let ctx1 = MockContext::new(state1);
1282
1283 let macro_result = crate::race!(
1284 ctx1,
1285 Box::pin(async { Ok::<_, DurableError>(10) })
1286 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1287 Box::pin(async {
1288 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1289 Ok(20)
1290 }),
1291 )
1292 .await;
1293
1294 // Test with manual boxing (same as what macro expands to)
1295 let client2 = create_mock_client();
1296 let state2 = create_test_state(client2);
1297 let ctx2 = MockContext::new(state2);
1298
1299 let futures: Vec<Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>> = vec![
1300 Box::pin(async { Ok::<_, DurableError>(10) }),
1301 Box::pin(async {
1302 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1303 Ok(20)
1304 }),
1305 ];
1306 let manual_result = ctx2.race(futures).await;
1307
1308 // Both should succeed with the same value (first to settle)
1309 assert!(macro_result.is_ok());
1310 assert!(manual_result.is_ok());
1311 assert_eq!(macro_result.unwrap(), manual_result.unwrap());
1312 }
1313
1314 /// Test that race! macro produces the same error behavior as manually boxing futures.
1315 /// Validates: Requirements 3.1, 3.2
1316 #[tokio::test]
1317 async fn test_race_macro_equivalence_with_manual_boxing_failure() {
1318 // Test with macro
1319 let client1 = create_mock_client();
1320 let state1 = create_test_state(client1);
1321 let ctx1 = MockContext::new(state1);
1322
1323 let macro_result = crate::race!(
1324 ctx1,
1325 Box::pin(async { Err::<i32, _>(DurableError::execution("fast error")) })
1326 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1327 Box::pin(async {
1328 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1329 Ok(20)
1330 }),
1331 )
1332 .await;
1333
1334 // Test with manual boxing
1335 let client2 = create_mock_client();
1336 let state2 = create_test_state(client2);
1337 let ctx2 = MockContext::new(state2);
1338
1339 let futures: Vec<Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>> = vec![
1340 Box::pin(async { Err::<i32, _>(DurableError::execution("fast error")) }),
1341 Box::pin(async {
1342 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1343 Ok(20)
1344 }),
1345 ];
1346 let manual_result = ctx2.race(futures).await;
1347
1348 // Both should fail (error settled first)
1349 assert!(macro_result.is_err());
1350 assert!(manual_result.is_err());
1351 }
1352
1353 // =========================================================================
1354 // all_settled! macro tests
1355 // Validates: Requirements 4.1, 4.2, 4.3, 4.4
1356 // =========================================================================
1357
1358 /// Test that all_settled! macro returns BatchResult<T> with all outcomes.
1359 /// Validates: Requirements 4.1, 4.2
1360 #[tokio::test]
1361 async fn test_all_settled_macro_returns_batch_result() {
1362 let client = create_mock_client();
1363 let state = create_test_state(client);
1364 let ctx = MockContext::new(state);
1365
1366 let result = crate::all_settled!(
1367 ctx,
1368 Box::pin(async { Ok::<_, DurableError>(1) })
1369 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1370 Box::pin(async { Ok(2) }),
1371 Box::pin(async { Ok(3) }),
1372 )
1373 .await;
1374
1375 assert!(result.is_ok());
1376 let batch = result.unwrap();
1377 assert_eq!(batch.items.len(), 3);
1378 assert_eq!(batch.success_count(), 3);
1379 assert_eq!(batch.failure_count(), 0);
1380 assert!(batch.all_succeeded());
1381 }
1382
1383 /// Test that all_settled! macro preserves order of results.
1384 /// Property 2: Result order preservation
1385 /// Validates: Requirements 4.4
1386 #[tokio::test]
1387 async fn test_all_settled_macro_preserves_order() {
1388 let client = create_mock_client();
1389 let state = create_test_state(client);
1390 let ctx = MockContext::new(state);
1391
1392 let result = crate::all_settled!(
1393 ctx,
1394 Box::pin(async { Ok::<_, DurableError>(10) })
1395 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1396 Box::pin(async { Ok(20) }),
1397 Box::pin(async { Ok(30) }),
1398 )
1399 .await;
1400
1401 assert!(result.is_ok());
1402 let batch = result.unwrap();
1403
1404 // Verify order is preserved by checking indices and values
1405 assert_eq!(batch.items.len(), 3);
1406 assert_eq!(batch.items[0].index, 0);
1407 assert_eq!(batch.items[0].get_result(), Some(&10));
1408 assert_eq!(batch.items[1].index, 1);
1409 assert_eq!(batch.items[1].get_result(), Some(&20));
1410 assert_eq!(batch.items[2].index, 2);
1411 assert_eq!(batch.items[2].get_result(), Some(&30));
1412 }
1413
1414 /// Test that all_settled! macro collects both successes and failures.
1415 /// Validates: Requirements 4.1, 4.2
1416 #[tokio::test]
1417 async fn test_all_settled_macro_mixed_results() {
1418 let client = create_mock_client();
1419 let state = create_test_state(client);
1420 let ctx = MockContext::new(state);
1421
1422 let result = crate::all_settled!(
1423 ctx,
1424 Box::pin(async { Ok::<_, DurableError>(1) })
1425 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1426 Box::pin(async { Err(DurableError::execution("test error")) }),
1427 Box::pin(async { Ok(3) }),
1428 )
1429 .await;
1430
1431 assert!(result.is_ok());
1432 let batch = result.unwrap();
1433 assert_eq!(batch.items.len(), 3);
1434 assert_eq!(batch.success_count(), 2);
1435 assert_eq!(batch.failure_count(), 1);
1436 assert!(!batch.all_succeeded());
1437 assert!(batch.has_failures());
1438 }
1439
1440 /// Test that all_settled! macro works with a single future.
1441 /// Validates: Requirements 4.3
1442 #[tokio::test]
1443 async fn test_all_settled_macro_single_future() {
1444 let client = create_mock_client();
1445 let state = create_test_state(client);
1446 let ctx = MockContext::new(state);
1447
1448 let result = crate::all_settled!(
1449 ctx,
1450 Box::pin(async { Ok::<_, DurableError>(42) })
1451 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1452 )
1453 .await;
1454
1455 assert!(result.is_ok());
1456 let batch = result.unwrap();
1457 assert_eq!(batch.items.len(), 1);
1458 assert_eq!(batch.success_count(), 1);
1459 assert_eq!(batch.items[0].get_result(), Some(&42));
1460 }
1461
1462 /// Test that all_settled! macro works with a single failing future.
1463 /// Validates: Requirements 4.2, 4.3
1464 #[tokio::test]
1465 async fn test_all_settled_macro_single_failure() {
1466 let client = create_mock_client();
1467 let state = create_test_state(client);
1468 let ctx = MockContext::new(state);
1469
1470 let result = crate::all_settled!(
1471 ctx,
1472 Box::pin(async { Err::<i32, _>(DurableError::execution("single error")) })
1473 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1474 )
1475 .await;
1476
1477 assert!(result.is_ok());
1478 let batch = result.unwrap();
1479 assert_eq!(batch.items.len(), 1);
1480 assert_eq!(batch.failure_count(), 1);
1481 assert!(batch.items[0].is_failed());
1482 }
1483
1484 /// Test that all_settled! macro supports trailing comma.
1485 /// Validates: Requirements 4.1
1486 #[tokio::test]
1487 async fn test_all_settled_macro_trailing_comma() {
1488 let client = create_mock_client();
1489 let state = create_test_state(client);
1490 let ctx = MockContext::new(state);
1491
1492 // Note the trailing comma after the last future
1493 let result = crate::all_settled!(
1494 ctx,
1495 Box::pin(async { Ok::<_, DurableError>(1) })
1496 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1497 Box::pin(async { Ok(2) }),
1498 )
1499 .await;
1500
1501 assert!(result.is_ok());
1502 assert_eq!(result.unwrap().items.len(), 2);
1503 }
1504
1505 /// Test that all_settled! macro works with different output types (String).
1506 /// Validates: Requirements 4.1, 4.5
1507 #[tokio::test]
1508 async fn test_all_settled_macro_string_type() {
1509 let client = create_mock_client();
1510 let state = create_test_state(client);
1511 let ctx = MockContext::new(state);
1512
1513 let result = crate::all_settled!(
1514 ctx,
1515 Box::pin(async { Ok::<_, DurableError>("a".to_string()) })
1516 as Pin<Box<dyn Future<Output = Result<String, DurableError>> + Send>>,
1517 Box::pin(async { Ok("b".to_string()) }),
1518 Box::pin(async { Ok("c".to_string()) }),
1519 )
1520 .await;
1521
1522 assert!(result.is_ok());
1523 let batch = result.unwrap();
1524 assert_eq!(batch.items.len(), 3);
1525 assert_eq!(batch.items[0].get_result(), Some(&"a".to_string()));
1526 assert_eq!(batch.items[1].get_result(), Some(&"b".to_string()));
1527 assert_eq!(batch.items[2].get_result(), Some(&"c".to_string()));
1528 }
1529
1530 /// Test that all_settled! macro produces the same result as manually boxing futures.
1531 /// Property 1: Macro expansion equivalence
1532 /// Validates: Requirements 4.1
1533 #[tokio::test]
1534 async fn test_all_settled_macro_equivalence_with_manual_boxing() {
1535 // Test with macro
1536 let client1 = create_mock_client();
1537 let state1 = create_test_state(client1);
1538 let ctx1 = MockContext::new(state1);
1539
1540 let macro_result = crate::all_settled!(
1541 ctx1,
1542 Box::pin(async { Ok::<_, DurableError>(10) })
1543 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1544 Box::pin(async { Err(DurableError::execution("test error")) }),
1545 Box::pin(async { Ok(30) }),
1546 )
1547 .await;
1548
1549 // Test with manual boxing (same as what macro expands to)
1550 let client2 = create_mock_client();
1551 let state2 = create_test_state(client2);
1552 let ctx2 = MockContext::new(state2);
1553
1554 let futures: Vec<Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>> = vec![
1555 Box::pin(async { Ok::<_, DurableError>(10) }),
1556 Box::pin(async { Err(DurableError::execution("test error")) }),
1557 Box::pin(async { Ok(30) }),
1558 ];
1559 let manual_result = ctx2.all_settled(futures).await;
1560
1561 // Both should succeed with BatchResult
1562 assert!(macro_result.is_ok());
1563 assert!(manual_result.is_ok());
1564
1565 let macro_batch = macro_result.unwrap();
1566 let manual_batch = manual_result.unwrap();
1567
1568 // Both should have same counts
1569 assert_eq!(macro_batch.items.len(), manual_batch.items.len());
1570 assert_eq!(macro_batch.success_count(), manual_batch.success_count());
1571 assert_eq!(macro_batch.failure_count(), manual_batch.failure_count());
1572 }
1573
1574 /// Test that all_settled! macro collects all failures without short-circuiting.
1575 /// Validates: Requirements 4.1, 4.2
1576 #[tokio::test]
1577 async fn test_all_settled_macro_all_failures() {
1578 let client = create_mock_client();
1579 let state = create_test_state(client);
1580 let ctx = MockContext::new(state);
1581
1582 let result = crate::all_settled!(
1583 ctx,
1584 Box::pin(async { Err::<i32, _>(DurableError::execution("error 1")) })
1585 as Pin<Box<dyn Future<Output = Result<i32, DurableError>> + Send>>,
1586 Box::pin(async { Err(DurableError::execution("error 2")) }),
1587 Box::pin(async { Err(DurableError::execution("error 3")) }),
1588 )
1589 .await;
1590
1591 // all_settled should still succeed (it collects all outcomes)
1592 assert!(result.is_ok());
1593 let batch = result.unwrap();
1594 assert_eq!(batch.items.len(), 3);
1595 assert_eq!(batch.success_count(), 0);
1596 assert_eq!(batch.failure_count(), 3);
1597 assert!(!batch.all_succeeded());
1598 }
1599}