Skip to main content

durable_execution_sdk_testing/
types.rs

1//! Core types for the testing utilities crate.
2//!
3//! This module defines the fundamental types used throughout the testing framework,
4//! including execution status, error information, and operation waiting states.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9/// Status of a durable execution.
10///
11/// Represents the overall state of a durable execution test run.
12///
13/// # Examples
14///
15/// ```
16/// use durable_execution_sdk_testing::ExecutionStatus;
17///
18/// let status = ExecutionStatus::Succeeded;
19/// assert!(status.is_terminal());
20/// assert!(status.is_success());
21///
22/// let running = ExecutionStatus::Running;
23/// assert!(!running.is_terminal());
24/// ```
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26pub enum ExecutionStatus {
27    /// Execution is currently running
28    Running,
29    /// Execution completed successfully
30    Succeeded,
31    /// Execution failed with an error
32    Failed,
33    /// Execution was cancelled
34    Cancelled,
35    /// Execution timed out
36    TimedOut,
37}
38
39impl ExecutionStatus {
40    /// Returns true if this status represents a terminal state.
41    ///
42    /// Terminal states are those where the execution has completed
43    /// and will not change further.
44    pub fn is_terminal(&self) -> bool {
45        !matches!(self, Self::Running)
46    }
47
48    /// Returns true if this status represents a successful completion.
49    pub fn is_success(&self) -> bool {
50        matches!(self, Self::Succeeded)
51    }
52
53    /// Returns true if this status represents a failure.
54    pub fn is_failure(&self) -> bool {
55        matches!(self, Self::Failed | Self::Cancelled | Self::TimedOut)
56    }
57}
58
59impl std::fmt::Display for ExecutionStatus {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        match self {
62            Self::Running => write!(f, "Running"),
63            Self::Succeeded => write!(f, "Succeeded"),
64            Self::Failed => write!(f, "Failed"),
65            Self::Cancelled => write!(f, "Cancelled"),
66            Self::TimedOut => write!(f, "TimedOut"),
67        }
68    }
69}
70
71/// Error information from a failed execution.
72///
73/// Contains detailed information about an error that occurred during
74/// a durable execution, including type, message, optional data, and stack trace.
75///
76/// # Examples
77///
78/// ```
79/// use durable_execution_sdk_testing::TestResultError;
80///
81/// let error = TestResultError::new("ValidationError", "Invalid input");
82/// assert_eq!(error.error_type, Some("ValidationError".to_string()));
83/// assert_eq!(error.error_message, Some("Invalid input".to_string()));
84/// ```
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct TestResultError {
87    /// The type/category of the error
88    pub error_type: Option<String>,
89    /// Human-readable error message
90    pub error_message: Option<String>,
91    /// Additional error data (typically JSON)
92    pub error_data: Option<String>,
93    /// Stack trace if available
94    pub stack_trace: Option<Vec<String>>,
95}
96
97impl TestResultError {
98    /// Creates a new TestResultError with the given type and message.
99    pub fn new(error_type: impl Into<String>, error_message: impl Into<String>) -> Self {
100        Self {
101            error_type: Some(error_type.into()),
102            error_message: Some(error_message.into()),
103            error_data: None,
104            stack_trace: None,
105        }
106    }
107
108    /// Creates a new TestResultError with just a message.
109    pub fn from_message(message: impl Into<String>) -> Self {
110        Self {
111            error_type: None,
112            error_message: Some(message.into()),
113            error_data: None,
114            stack_trace: None,
115        }
116    }
117
118    /// Sets the error data.
119    pub fn with_data(mut self, data: impl Into<String>) -> Self {
120        self.error_data = Some(data.into());
121        self
122    }
123
124    /// Sets the stack trace.
125    pub fn with_stack_trace(mut self, stack_trace: Vec<String>) -> Self {
126        self.stack_trace = Some(stack_trace);
127        self
128    }
129}
130
131impl std::fmt::Display for TestResultError {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        match (&self.error_type, &self.error_message) {
134            (Some(t), Some(m)) => write!(f, "{}: {}", t, m),
135            (None, Some(m)) => write!(f, "{}", m),
136            (Some(t), None) => write!(f, "{}", t),
137            (None, None) => write!(f, "Unknown error"),
138        }
139    }
140}
141
142impl std::error::Error for TestResultError {}
143
144impl From<durable_execution_sdk::ErrorObject> for TestResultError {
145    fn from(error: durable_execution_sdk::ErrorObject) -> Self {
146        Self {
147            error_type: Some(error.error_type),
148            error_message: Some(error.error_message),
149            error_data: None,
150            stack_trace: error.stack_trace.map(|s| vec![s]),
151        }
152    }
153}
154
155/// Information about a single handler invocation.
156///
157/// Tracks timing and error information for each time the handler
158/// was invoked during a durable execution.
159///
160/// # Examples
161///
162/// ```
163/// use durable_execution_sdk_testing::Invocation;
164///
165/// let invocation = Invocation::new();
166/// assert!(invocation.start_timestamp.is_none());
167/// assert!(invocation.error.is_none());
168/// ```
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct Invocation {
171    /// Timestamp when the invocation started
172    pub start_timestamp: Option<DateTime<Utc>>,
173    /// Timestamp when the invocation ended
174    pub end_timestamp: Option<DateTime<Utc>>,
175    /// Request ID for this invocation (e.g., Lambda request ID)
176    pub request_id: Option<String>,
177    /// Error information if the invocation failed
178    pub error: Option<TestResultError>,
179}
180
181impl Invocation {
182    /// Creates a new empty Invocation.
183    pub fn new() -> Self {
184        Self {
185            start_timestamp: None,
186            end_timestamp: None,
187            request_id: None,
188            error: None,
189        }
190    }
191
192    /// Creates a new Invocation with the given start timestamp.
193    pub fn with_start(start: DateTime<Utc>) -> Self {
194        Self {
195            start_timestamp: Some(start),
196            end_timestamp: None,
197            request_id: None,
198            error: None,
199        }
200    }
201
202    /// Sets the end timestamp.
203    pub fn with_end(mut self, end: DateTime<Utc>) -> Self {
204        self.end_timestamp = Some(end);
205        self
206    }
207
208    /// Sets the request ID.
209    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
210        self.request_id = Some(request_id.into());
211        self
212    }
213
214    /// Sets the error.
215    pub fn with_error(mut self, error: TestResultError) -> Self {
216        self.error = Some(error);
217        self
218    }
219
220    /// Returns the duration of the invocation if both timestamps are available.
221    pub fn duration(&self) -> Option<chrono::Duration> {
222        match (&self.start_timestamp, &self.end_timestamp) {
223            (Some(start), Some(end)) => Some(*end - *start),
224            _ => None,
225        }
226    }
227}
228
229impl Default for Invocation {
230    fn default() -> Self {
231        Self::new()
232    }
233}
234
235/// Status to wait for in async operations.
236///
237/// Used with `wait_for_data()` to specify which state an operation
238/// should reach before the wait completes.
239///
240/// # Examples
241///
242/// ```
243/// use durable_execution_sdk_testing::WaitingOperationStatus;
244///
245/// let status = WaitingOperationStatus::Completed;
246/// assert_eq!(format!("{}", status), "Completed");
247/// ```
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
249pub enum WaitingOperationStatus {
250    /// Wait for operation to start
251    Started,
252    /// Wait for operation to be submitted (callbacks only)
253    Submitted,
254    /// Wait for operation to complete
255    Completed,
256}
257
258impl std::fmt::Display for WaitingOperationStatus {
259    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260        match self {
261            Self::Started => write!(f, "Started"),
262            Self::Submitted => write!(f, "Submitted"),
263            Self::Completed => write!(f, "Completed"),
264        }
265    }
266}
267
268/// Structured input wrapper for `run()`, matching the Node.js SDK's `InvokeRequest`.
269///
270/// Wraps an optional payload for handler invocation. Use [`InvokeRequest::with_payload`]
271/// to supply input, or [`InvokeRequest::new`] for a no-payload invocation.
272///
273/// `From<T>` is implemented so that raw payloads can be passed directly to `run()`
274/// without wrapping — backward compatibility is preserved.
275///
276/// # Examples
277///
278/// ```
279/// use durable_execution_sdk_testing::InvokeRequest;
280///
281/// // With a payload
282/// let req = InvokeRequest::with_payload("hello");
283/// assert_eq!(req.payload, Some("hello"));
284///
285/// // Without a payload
286/// let req: InvokeRequest<String> = InvokeRequest::new();
287/// assert!(req.payload.is_none());
288///
289/// // From a raw value (backward compat)
290/// let req: InvokeRequest<i32> = 42.into();
291/// assert_eq!(req.payload, Some(42));
292/// ```
293#[derive(Debug, Clone, Default, Serialize, Deserialize)]
294pub struct InvokeRequest<T = serde_json::Value> {
295    /// Optional payload to pass to the handler.
296    pub payload: Option<T>,
297}
298
299impl<T> InvokeRequest<T> {
300    /// Creates a new `InvokeRequest` with no payload.
301    pub fn new() -> Self {
302        Self { payload: None }
303    }
304
305    /// Creates a new `InvokeRequest` with the given payload.
306    pub fn with_payload(payload: T) -> Self {
307        Self {
308            payload: Some(payload),
309        }
310    }
311}
312
313impl<T> From<T> for InvokeRequest<T> {
314    fn from(value: T) -> Self {
315        Self::with_payload(value)
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn test_execution_status_terminal() {
325        assert!(!ExecutionStatus::Running.is_terminal());
326        assert!(ExecutionStatus::Succeeded.is_terminal());
327        assert!(ExecutionStatus::Failed.is_terminal());
328        assert!(ExecutionStatus::Cancelled.is_terminal());
329        assert!(ExecutionStatus::TimedOut.is_terminal());
330    }
331
332    #[test]
333    fn test_execution_status_success() {
334        assert!(ExecutionStatus::Succeeded.is_success());
335        assert!(!ExecutionStatus::Failed.is_success());
336        assert!(!ExecutionStatus::Running.is_success());
337    }
338
339    #[test]
340    fn test_execution_status_failure() {
341        assert!(!ExecutionStatus::Succeeded.is_failure());
342        assert!(ExecutionStatus::Failed.is_failure());
343        assert!(ExecutionStatus::Cancelled.is_failure());
344        assert!(ExecutionStatus::TimedOut.is_failure());
345        assert!(!ExecutionStatus::Running.is_failure());
346    }
347
348    #[test]
349    fn test_test_result_error_new() {
350        let error = TestResultError::new("TestError", "Test message");
351        assert_eq!(error.error_type, Some("TestError".to_string()));
352        assert_eq!(error.error_message, Some("Test message".to_string()));
353        assert!(error.error_data.is_none());
354        assert!(error.stack_trace.is_none());
355    }
356
357    #[test]
358    fn test_test_result_error_display() {
359        let error = TestResultError::new("TestError", "Test message");
360        assert_eq!(format!("{}", error), "TestError: Test message");
361
362        let error_no_type = TestResultError::from_message("Just a message");
363        assert_eq!(format!("{}", error_no_type), "Just a message");
364    }
365
366    #[test]
367    fn test_invocation_duration() {
368        use chrono::TimeZone;
369        let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
370        let end = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 5).unwrap();
371
372        let invocation = Invocation::with_start(start).with_end(end);
373        let duration = invocation.duration().unwrap();
374        assert_eq!(duration.num_seconds(), 5);
375    }
376
377    #[test]
378    fn test_waiting_operation_status_display() {
379        assert_eq!(format!("{}", WaitingOperationStatus::Started), "Started");
380        assert_eq!(
381            format!("{}", WaitingOperationStatus::Submitted),
382            "Submitted"
383        );
384        assert_eq!(
385            format!("{}", WaitingOperationStatus::Completed),
386            "Completed"
387        );
388    }
389
390    #[test]
391    fn test_invoke_request_new_has_no_payload() {
392        let req: InvokeRequest<String> = InvokeRequest::new();
393        assert!(req.payload.is_none());
394    }
395
396    #[test]
397    fn test_invoke_request_with_payload() {
398        let req = InvokeRequest::with_payload(42);
399        assert_eq!(req.payload, Some(42));
400    }
401
402    #[test]
403    fn test_invoke_request_default_has_no_payload() {
404        let req: InvokeRequest<String> = InvokeRequest::default();
405        assert!(req.payload.is_none());
406    }
407
408    #[test]
409    fn test_invoke_request_from_raw_value() {
410        let req: InvokeRequest<String> = "hello".to_string().into();
411        assert_eq!(req.payload, Some("hello".to_string()));
412    }
413
414    #[test]
415    fn test_invoke_request_serde_round_trip() {
416        let req = InvokeRequest::with_payload(serde_json::json!({"key": "value"}));
417        let serialized = serde_json::to_string(&req).unwrap();
418        let deserialized: InvokeRequest<serde_json::Value> =
419            serde_json::from_str(&serialized).unwrap();
420        assert_eq!(req.payload, deserialized.payload);
421    }
422
423    #[test]
424    fn test_invoke_request_serde_no_payload() {
425        let req: InvokeRequest<String> = InvokeRequest::new();
426        let serialized = serde_json::to_string(&req).unwrap();
427        let deserialized: InvokeRequest<String> = serde_json::from_str(&serialized).unwrap();
428        assert!(deserialized.payload.is_none());
429    }
430}