use serde::{Deserialize, Serialize};
use thiserror::Error;
pub type DurableResult<T> = Result<T, DurableError>;
pub type StepResult<T> = Result<T, DurableError>;
pub type CheckpointResult<T> = Result<T, DurableError>;
#[derive(Debug, Error)]
pub enum DurableError {
#[error("Execution error: {message}")]
Execution {
message: String,
termination_reason: TerminationReason,
},
#[error("Invocation error: {message}")]
Invocation {
message: String,
termination_reason: TerminationReason,
},
#[error("Checkpoint error: {message}")]
Checkpoint {
message: String,
is_retriable: bool,
aws_error: Option<AwsError>,
},
#[error("Callback error: {message}")]
Callback {
message: String,
callback_id: Option<String>,
},
#[error("Non-deterministic execution: {message}")]
NonDeterministic {
message: String,
operation_id: Option<String>,
},
#[error("Validation error: {message}")]
Validation {
message: String,
},
#[error("Serialization error: {message}")]
SerDes {
message: String,
},
#[error("Suspend execution")]
Suspend {
scheduled_timestamp: Option<f64>,
},
#[error("Orphaned child: {message}")]
OrphanedChild {
message: String,
operation_id: String,
},
#[error("User code error: {message}")]
UserCode {
message: String,
error_type: String,
stack_trace: Option<String>,
},
#[error("Size limit exceeded: {message}")]
SizeLimit {
message: String,
actual_size: Option<usize>,
max_size: Option<usize>,
},
#[error("Throttling: {message}")]
Throttling {
message: String,
retry_after_ms: Option<u64>,
},
#[error("Resource not found: {message}")]
ResourceNotFound {
message: String,
resource_id: Option<String>,
},
#[error("Configuration error: {message}")]
Configuration {
message: String,
},
}
impl DurableError {
pub fn execution(message: impl Into<String>) -> Self {
Self::Execution {
message: message.into(),
termination_reason: TerminationReason::ExecutionError,
}
}
pub fn invocation(message: impl Into<String>) -> Self {
Self::Invocation {
message: message.into(),
termination_reason: TerminationReason::InvocationError,
}
}
pub fn checkpoint_retriable(message: impl Into<String>) -> Self {
Self::Checkpoint {
message: message.into(),
is_retriable: true,
aws_error: None,
}
}
pub fn checkpoint_non_retriable(message: impl Into<String>) -> Self {
Self::Checkpoint {
message: message.into(),
is_retriable: false,
aws_error: None,
}
}
pub fn validation(message: impl Into<String>) -> Self {
Self::Validation {
message: message.into(),
}
}
pub fn serdes(message: impl Into<String>) -> Self {
Self::SerDes {
message: message.into(),
}
}
pub fn suspend() -> Self {
Self::Suspend {
scheduled_timestamp: None,
}
}
pub fn suspend_until(timestamp: f64) -> Self {
Self::Suspend {
scheduled_timestamp: Some(timestamp),
}
}
pub fn size_limit(message: impl Into<String>) -> Self {
Self::SizeLimit {
message: message.into(),
actual_size: None,
max_size: None,
}
}
pub fn size_limit_with_details(
message: impl Into<String>,
actual_size: usize,
max_size: usize,
) -> Self {
Self::SizeLimit {
message: message.into(),
actual_size: Some(actual_size),
max_size: Some(max_size),
}
}
pub fn throttling(message: impl Into<String>) -> Self {
Self::Throttling {
message: message.into(),
retry_after_ms: None,
}
}
pub fn throttling_with_retry_delay(message: impl Into<String>, retry_after_ms: u64) -> Self {
Self::Throttling {
message: message.into(),
retry_after_ms: Some(retry_after_ms),
}
}
pub fn resource_not_found(message: impl Into<String>) -> Self {
Self::ResourceNotFound {
message: message.into(),
resource_id: None,
}
}
pub fn resource_not_found_with_id(
message: impl Into<String>,
resource_id: impl Into<String>,
) -> Self {
Self::ResourceNotFound {
message: message.into(),
resource_id: Some(resource_id.into()),
}
}
pub fn is_retriable(&self) -> bool {
matches!(
self,
Self::Checkpoint {
is_retriable: true,
..
}
)
}
pub fn is_suspend(&self) -> bool {
matches!(self, Self::Suspend { .. })
}
pub fn is_invalid_checkpoint_token(&self) -> bool {
match self {
Self::Checkpoint {
aws_error: Some(aws_error),
..
} => {
aws_error.code == "InvalidParameterValueException"
&& aws_error.message.contains("Invalid checkpoint token")
}
Self::Checkpoint { message, .. } => message.contains("Invalid checkpoint token"),
_ => false,
}
}
pub fn is_size_limit(&self) -> bool {
matches!(self, Self::SizeLimit { .. })
}
pub fn is_throttling(&self) -> bool {
matches!(self, Self::Throttling { .. })
}
pub fn is_resource_not_found(&self) -> bool {
matches!(self, Self::ResourceNotFound { .. })
}
pub fn get_retry_after_ms(&self) -> Option<u64> {
match self {
Self::Throttling { retry_after_ms, .. } => *retry_after_ms,
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[repr(u8)]
pub enum TerminationReason {
#[default]
UnhandledError = 0,
InvocationError = 1,
ExecutionError = 2,
CheckpointFailed = 3,
NonDeterministicExecution = 4,
StepInterrupted = 5,
CallbackError = 6,
SerializationError = 7,
SizeLimitExceeded = 8,
OperationTerminated = 9,
RetryScheduled = 10,
WaitScheduled = 11,
CallbackPending = 12,
ContextValidationError = 13,
LambdaTimeoutApproaching = 14,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AwsError {
pub code: String,
pub message: String,
pub request_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorObject {
#[serde(rename = "ErrorType")]
pub error_type: String,
#[serde(rename = "ErrorMessage")]
pub error_message: String,
#[serde(rename = "StackTrace", skip_serializing_if = "Option::is_none")]
pub stack_trace: Option<String>,
}
impl ErrorObject {
pub fn new(error_type: impl Into<String>, error_message: impl Into<String>) -> Self {
Self {
error_type: error_type.into(),
error_message: error_message.into(),
stack_trace: None,
}
}
pub fn with_stack_trace(
error_type: impl Into<String>,
error_message: impl Into<String>,
stack_trace: impl Into<String>,
) -> Self {
Self {
error_type: error_type.into(),
error_message: error_message.into(),
stack_trace: Some(stack_trace.into()),
}
}
}
impl From<&DurableError> for ErrorObject {
fn from(error: &DurableError) -> Self {
match error {
DurableError::Execution { message, .. } => ErrorObject::new("ExecutionError", message),
DurableError::Invocation { message, .. } => {
ErrorObject::new("InvocationError", message)
}
DurableError::Checkpoint { message, .. } => {
ErrorObject::new("CheckpointError", message)
}
DurableError::Callback { message, .. } => ErrorObject::new("CallbackError", message),
DurableError::NonDeterministic { message, .. } => {
ErrorObject::new("NonDeterministicExecutionError", message)
}
DurableError::Validation { message } => ErrorObject::new("ValidationError", message),
DurableError::SerDes { message } => ErrorObject::new("SerDesError", message),
DurableError::Suspend { .. } => {
ErrorObject::new("SuspendExecution", "Execution suspended")
}
DurableError::OrphanedChild { message, .. } => {
ErrorObject::new("OrphanedChildError", message)
}
DurableError::UserCode {
message,
error_type,
stack_trace,
} => {
let mut obj = ErrorObject::new(error_type, message);
obj.stack_trace = stack_trace.clone();
obj
}
DurableError::SizeLimit {
message,
actual_size,
max_size,
} => {
let detailed_message = match (actual_size, max_size) {
(Some(actual), Some(max)) => {
format!("{} (actual: {} bytes, max: {} bytes)", message, actual, max)
}
_ => message.clone(),
};
ErrorObject::new("SizeLimitExceededError", detailed_message)
}
DurableError::Throttling {
message,
retry_after_ms,
} => {
let detailed_message = match retry_after_ms {
Some(ms) => format!("{} (retry after: {}ms)", message, ms),
None => message.clone(),
};
ErrorObject::new("ThrottlingError", detailed_message)
}
DurableError::ResourceNotFound {
message,
resource_id,
} => {
let detailed_message = match resource_id {
Some(id) => format!("{} (resource: {})", message, id),
None => message.clone(),
};
ErrorObject::new("ResourceNotFoundError", detailed_message)
}
DurableError::Configuration { message } => {
ErrorObject::new("ConfigurationError", message)
}
}
}
}
impl From<serde_json::Error> for DurableError {
fn from(error: serde_json::Error) -> Self {
Self::SerDes {
message: error.to_string(),
}
}
}
impl From<std::io::Error> for DurableError {
fn from(error: std::io::Error) -> Self {
Self::Execution {
message: error.to_string(),
termination_reason: TerminationReason::UnhandledError,
}
}
}
impl From<Box<dyn std::error::Error + Send + Sync>> for DurableError {
fn from(error: Box<dyn std::error::Error + Send + Sync>) -> Self {
Self::UserCode {
message: error.to_string(),
error_type: "UserCodeError".to_string(),
stack_trace: None,
}
}
}
impl From<Box<dyn std::error::Error>> for DurableError {
fn from(error: Box<dyn std::error::Error>) -> Self {
Self::UserCode {
message: error.to_string(),
error_type: "UserCodeError".to_string(),
stack_trace: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
fn non_empty_string_strategy() -> impl Strategy<Value = String> {
"[a-zA-Z0-9_ ]{1,64}".prop_map(|s| s)
}
fn optional_string_strategy() -> impl Strategy<Value = Option<String>> {
prop_oneof![Just(None), non_empty_string_strategy().prop_map(Some),]
}
fn optional_usize_strategy() -> impl Strategy<Value = Option<usize>> {
prop_oneof![Just(None), (1usize..10_000_000usize).prop_map(Some),]
}
fn optional_u64_strategy() -> impl Strategy<Value = Option<u64>> {
prop_oneof![Just(None), (1u64..100_000u64).prop_map(Some),]
}
fn execution_error_strategy() -> impl Strategy<Value = DurableError> {
non_empty_string_strategy().prop_map(|message| DurableError::Execution {
message,
termination_reason: TerminationReason::ExecutionError,
})
}
fn invocation_error_strategy() -> impl Strategy<Value = DurableError> {
non_empty_string_strategy().prop_map(|message| DurableError::Invocation {
message,
termination_reason: TerminationReason::InvocationError,
})
}
fn checkpoint_error_strategy() -> impl Strategy<Value = DurableError> {
(non_empty_string_strategy(), any::<bool>()).prop_map(|(message, is_retriable)| {
DurableError::Checkpoint {
message,
is_retriable,
aws_error: None,
}
})
}
fn callback_error_strategy() -> impl Strategy<Value = DurableError> {
(non_empty_string_strategy(), optional_string_strategy()).prop_map(
|(message, callback_id)| DurableError::Callback {
message,
callback_id,
},
)
}
fn non_deterministic_error_strategy() -> impl Strategy<Value = DurableError> {
(non_empty_string_strategy(), optional_string_strategy()).prop_map(
|(message, operation_id)| DurableError::NonDeterministic {
message,
operation_id,
},
)
}
fn validation_error_strategy() -> impl Strategy<Value = DurableError> {
non_empty_string_strategy().prop_map(|message| DurableError::Validation { message })
}
fn serdes_error_strategy() -> impl Strategy<Value = DurableError> {
non_empty_string_strategy().prop_map(|message| DurableError::SerDes { message })
}
fn suspend_error_strategy() -> impl Strategy<Value = DurableError> {
prop_oneof![
Just(()).prop_map(|_| DurableError::Suspend {
scheduled_timestamp: None
}),
(0.0f64..1e15f64).prop_map(|ts| DurableError::Suspend {
scheduled_timestamp: Some(ts)
}),
]
}
fn orphaned_child_error_strategy() -> impl Strategy<Value = DurableError> {
(non_empty_string_strategy(), non_empty_string_strategy()).prop_map(
|(message, operation_id)| DurableError::OrphanedChild {
message,
operation_id,
},
)
}
fn user_code_error_strategy() -> impl Strategy<Value = DurableError> {
(
non_empty_string_strategy(),
non_empty_string_strategy(),
optional_string_strategy(),
)
.prop_map(
|(message, error_type, stack_trace)| DurableError::UserCode {
message,
error_type,
stack_trace,
},
)
}
fn size_limit_error_strategy() -> impl Strategy<Value = DurableError> {
(
non_empty_string_strategy(),
optional_usize_strategy(),
optional_usize_strategy(),
)
.prop_map(|(message, actual_size, max_size)| DurableError::SizeLimit {
message,
actual_size,
max_size,
})
}
fn throttling_error_strategy() -> impl Strategy<Value = DurableError> {
(non_empty_string_strategy(), optional_u64_strategy()).prop_map(
|(message, retry_after_ms)| DurableError::Throttling {
message,
retry_after_ms,
},
)
}
fn resource_not_found_error_strategy() -> impl Strategy<Value = DurableError> {
(non_empty_string_strategy(), optional_string_strategy()).prop_map(
|(message, resource_id)| DurableError::ResourceNotFound {
message,
resource_id,
},
)
}
fn durable_error_strategy() -> impl Strategy<Value = DurableError> {
prop_oneof![
execution_error_strategy(),
invocation_error_strategy(),
checkpoint_error_strategy(),
callback_error_strategy(),
non_deterministic_error_strategy(),
validation_error_strategy(),
serdes_error_strategy(),
suspend_error_strategy(),
orphaned_child_error_strategy(),
user_code_error_strategy(),
size_limit_error_strategy(),
throttling_error_strategy(),
resource_not_found_error_strategy(),
]
}
proptest! {
#[test]
fn prop_durable_error_to_error_object_produces_valid_fields(error in durable_error_strategy()) {
let error_object: ErrorObject = (&error).into();
prop_assert!(
!error_object.error_type.is_empty(),
"ErrorObject.error_type should be non-empty for {:?}",
error
);
prop_assert!(
!error_object.error_message.is_empty(),
"ErrorObject.error_message should be non-empty for {:?}",
error
);
}
#[test]
fn prop_size_limit_error_classification(error in size_limit_error_strategy()) {
prop_assert!(
error.is_size_limit(),
"SizeLimit error should return true for is_size_limit()"
);
prop_assert!(
!error.is_retriable(),
"SizeLimit error should return false for is_retriable()"
);
prop_assert!(
!error.is_throttling(),
"SizeLimit error should return false for is_throttling()"
);
prop_assert!(
!error.is_resource_not_found(),
"SizeLimit error should return false for is_resource_not_found()"
);
}
#[test]
fn prop_throttling_error_classification(error in throttling_error_strategy()) {
prop_assert!(
error.is_throttling(),
"Throttling error should return true for is_throttling()"
);
prop_assert!(
!error.is_size_limit(),
"Throttling error should return false for is_size_limit()"
);
prop_assert!(
!error.is_resource_not_found(),
"Throttling error should return false for is_resource_not_found()"
);
}
#[test]
fn prop_resource_not_found_error_classification(error in resource_not_found_error_strategy()) {
prop_assert!(
error.is_resource_not_found(),
"ResourceNotFound error should return true for is_resource_not_found()"
);
prop_assert!(
!error.is_retriable(),
"ResourceNotFound error should return false for is_retriable()"
);
prop_assert!(
!error.is_size_limit(),
"ResourceNotFound error should return false for is_size_limit()"
);
prop_assert!(
!error.is_throttling(),
"ResourceNotFound error should return false for is_throttling()"
);
}
#[test]
fn prop_retriable_checkpoint_error_classification(message in non_empty_string_strategy()) {
let error = DurableError::Checkpoint {
message,
is_retriable: true,
aws_error: None,
};
prop_assert!(
error.is_retriable(),
"Checkpoint error with is_retriable=true should return true for is_retriable()"
);
}
#[test]
fn prop_non_retriable_checkpoint_error_classification(message in non_empty_string_strategy()) {
let error = DurableError::Checkpoint {
message,
is_retriable: false,
aws_error: None,
};
prop_assert!(
!error.is_retriable(),
"Checkpoint error with is_retriable=false should return false for is_retriable()"
);
}
#[test]
fn prop_error_object_type_matches_variant(error in durable_error_strategy()) {
let error_object: ErrorObject = (&error).into();
let expected_type = match &error {
DurableError::Execution { .. } => "ExecutionError",
DurableError::Invocation { .. } => "InvocationError",
DurableError::Checkpoint { .. } => "CheckpointError",
DurableError::Callback { .. } => "CallbackError",
DurableError::NonDeterministic { .. } => "NonDeterministicExecutionError",
DurableError::Validation { .. } => "ValidationError",
DurableError::SerDes { .. } => "SerDesError",
DurableError::Suspend { .. } => "SuspendExecution",
DurableError::OrphanedChild { .. } => "OrphanedChildError",
DurableError::UserCode { error_type, .. } => error_type.as_str(),
DurableError::SizeLimit { .. } => "SizeLimitExceededError",
DurableError::Throttling { .. } => "ThrottlingError",
DurableError::ResourceNotFound { .. } => "ResourceNotFoundError",
DurableError::Configuration { .. } => "ConfigurationError",
};
prop_assert_eq!(
error_object.error_type,
expected_type,
"ErrorObject.error_type should match expected type for {:?}",
error
);
}
}
#[test]
fn test_execution_error() {
let error = DurableError::execution("test error");
assert!(matches!(error, DurableError::Execution { .. }));
assert!(!error.is_retriable());
assert!(!error.is_suspend());
}
#[test]
fn test_checkpoint_retriable() {
let error = DurableError::checkpoint_retriable("test error");
assert!(error.is_retriable());
}
#[test]
fn test_checkpoint_non_retriable() {
let error = DurableError::checkpoint_non_retriable("test error");
assert!(!error.is_retriable());
}
#[test]
fn test_suspend() {
let error = DurableError::suspend();
assert!(error.is_suspend());
}
#[test]
fn test_suspend_until() {
let error = DurableError::suspend_until(1234567890.0);
assert!(error.is_suspend());
if let DurableError::Suspend {
scheduled_timestamp,
} = error
{
assert_eq!(scheduled_timestamp, Some(1234567890.0));
}
}
#[test]
fn test_error_object_from_durable_error() {
let error = DurableError::validation("invalid input");
let obj: ErrorObject = (&error).into();
assert_eq!(obj.error_type, "ValidationError");
assert_eq!(obj.error_message, "invalid input");
}
#[test]
fn test_from_serde_json_error() {
let json_error = serde_json::from_str::<String>("invalid").unwrap_err();
let error: DurableError = json_error.into();
assert!(matches!(error, DurableError::SerDes { .. }));
}
#[test]
fn test_is_invalid_checkpoint_token_with_aws_error() {
let error = DurableError::Checkpoint {
message: "Checkpoint API returned 400: Invalid checkpoint token".to_string(),
is_retriable: true,
aws_error: Some(AwsError {
code: "InvalidParameterValueException".to_string(),
message: "Invalid checkpoint token: token has been consumed".to_string(),
request_id: None,
}),
};
assert!(error.is_invalid_checkpoint_token());
assert!(error.is_retriable());
}
#[test]
fn test_is_invalid_checkpoint_token_without_aws_error() {
let error = DurableError::Checkpoint {
message: "Invalid checkpoint token: token expired".to_string(),
is_retriable: true,
aws_error: None,
};
assert!(error.is_invalid_checkpoint_token());
}
#[test]
fn test_is_not_invalid_checkpoint_token() {
let error = DurableError::Checkpoint {
message: "Network error".to_string(),
is_retriable: true,
aws_error: None,
};
assert!(!error.is_invalid_checkpoint_token());
}
#[test]
fn test_is_invalid_checkpoint_token_wrong_error_type() {
let error = DurableError::Validation {
message: "Invalid checkpoint token".to_string(),
};
assert!(!error.is_invalid_checkpoint_token());
}
#[test]
fn test_is_invalid_checkpoint_token_wrong_aws_error_code() {
let error = DurableError::Checkpoint {
message: "Some error".to_string(),
is_retriable: false,
aws_error: Some(AwsError {
code: "ServiceException".to_string(),
message: "Invalid checkpoint token".to_string(),
request_id: None,
}),
};
assert!(!error.is_invalid_checkpoint_token());
}
#[test]
fn test_size_limit_error() {
let error = DurableError::size_limit("Payload too large");
assert!(error.is_size_limit());
assert!(!error.is_retriable());
assert!(!error.is_throttling());
assert!(!error.is_resource_not_found());
}
#[test]
fn test_size_limit_error_with_details() {
let error =
DurableError::size_limit_with_details("Payload too large", 7_000_000, 6_000_000);
assert!(error.is_size_limit());
if let DurableError::SizeLimit {
actual_size,
max_size,
..
} = error
{
assert_eq!(actual_size, Some(7_000_000));
assert_eq!(max_size, Some(6_000_000));
} else {
panic!("Expected SizeLimit error");
}
}
#[test]
fn test_throttling_error() {
let error = DurableError::throttling("Rate limit exceeded");
assert!(error.is_throttling());
assert!(!error.is_retriable());
assert!(!error.is_size_limit());
assert!(!error.is_resource_not_found());
assert_eq!(error.get_retry_after_ms(), None);
}
#[test]
fn test_throttling_error_with_retry_delay() {
let error = DurableError::throttling_with_retry_delay("Rate limit exceeded", 5000);
assert!(error.is_throttling());
assert_eq!(error.get_retry_after_ms(), Some(5000));
}
#[test]
fn test_resource_not_found_error() {
let error = DurableError::resource_not_found("Execution not found");
assert!(error.is_resource_not_found());
assert!(!error.is_retriable());
assert!(!error.is_size_limit());
assert!(!error.is_throttling());
}
#[test]
fn test_resource_not_found_error_with_id() {
let error = DurableError::resource_not_found_with_id(
"Execution not found",
"arn:aws:lambda:us-east-1:123456789012:function:test",
);
assert!(error.is_resource_not_found());
if let DurableError::ResourceNotFound { resource_id, .. } = error {
assert_eq!(
resource_id,
Some("arn:aws:lambda:us-east-1:123456789012:function:test".to_string())
);
} else {
panic!("Expected ResourceNotFound error");
}
}
#[test]
fn test_error_object_from_size_limit_error() {
let error =
DurableError::size_limit_with_details("Payload too large", 7_000_000, 6_000_000);
let obj: ErrorObject = (&error).into();
assert_eq!(obj.error_type, "SizeLimitExceededError");
assert!(obj.error_message.contains("Payload too large"));
assert!(obj.error_message.contains("7000000"));
assert!(obj.error_message.contains("6000000"));
}
#[test]
fn test_error_object_from_throttling_error() {
let error = DurableError::throttling_with_retry_delay("Rate limit exceeded", 5000);
let obj: ErrorObject = (&error).into();
assert_eq!(obj.error_type, "ThrottlingError");
assert!(obj.error_message.contains("Rate limit exceeded"));
assert!(obj.error_message.contains("5000ms"));
}
#[test]
fn test_error_object_from_resource_not_found_error() {
let error = DurableError::resource_not_found_with_id("Execution not found", "test-arn");
let obj: ErrorObject = (&error).into();
assert_eq!(obj.error_type, "ResourceNotFoundError");
assert!(obj.error_message.contains("Execution not found"));
assert!(obj.error_message.contains("test-arn"));
}
#[test]
fn test_get_retry_after_ms_non_throttling() {
let error = DurableError::validation("test");
assert_eq!(error.get_retry_after_ms(), None);
}
#[test]
fn test_termination_reason_size_is_one_byte() {
assert_eq!(
std::mem::size_of::<TerminationReason>(),
1,
"TerminationReason should be 1 byte with #[repr(u8)]"
);
}
#[test]
fn test_termination_reason_discriminant_values() {
assert_eq!(TerminationReason::UnhandledError as u8, 0);
assert_eq!(TerminationReason::InvocationError as u8, 1);
assert_eq!(TerminationReason::ExecutionError as u8, 2);
assert_eq!(TerminationReason::CheckpointFailed as u8, 3);
assert_eq!(TerminationReason::NonDeterministicExecution as u8, 4);
assert_eq!(TerminationReason::StepInterrupted as u8, 5);
assert_eq!(TerminationReason::CallbackError as u8, 6);
assert_eq!(TerminationReason::SerializationError as u8, 7);
assert_eq!(TerminationReason::SizeLimitExceeded as u8, 8);
assert_eq!(TerminationReason::OperationTerminated as u8, 9);
assert_eq!(TerminationReason::RetryScheduled as u8, 10);
assert_eq!(TerminationReason::WaitScheduled as u8, 11);
assert_eq!(TerminationReason::CallbackPending as u8, 12);
assert_eq!(TerminationReason::ContextValidationError as u8, 13);
assert_eq!(TerminationReason::LambdaTimeoutApproaching as u8, 14);
}
#[test]
fn test_termination_reason_serde_uses_string_representation() {
let reason = TerminationReason::UnhandledError;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"UnhandledError\"");
let reason = TerminationReason::InvocationError;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"InvocationError\"");
let reason = TerminationReason::ExecutionError;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"ExecutionError\"");
let reason = TerminationReason::CheckpointFailed;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"CheckpointFailed\"");
let reason = TerminationReason::NonDeterministicExecution;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"NonDeterministicExecution\"");
let reason = TerminationReason::StepInterrupted;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"StepInterrupted\"");
let reason = TerminationReason::CallbackError;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"CallbackError\"");
let reason = TerminationReason::SerializationError;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"SerializationError\"");
let reason = TerminationReason::SizeLimitExceeded;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"SizeLimitExceeded\"");
let reason = TerminationReason::OperationTerminated;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"OperationTerminated\"");
let reason = TerminationReason::RetryScheduled;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"RetryScheduled\"");
let reason = TerminationReason::WaitScheduled;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"WaitScheduled\"");
let reason = TerminationReason::CallbackPending;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"CallbackPending\"");
let reason = TerminationReason::ContextValidationError;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"ContextValidationError\"");
let reason = TerminationReason::LambdaTimeoutApproaching;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"LambdaTimeoutApproaching\"");
}
#[test]
fn test_termination_reason_serde_round_trip() {
let reasons = [
TerminationReason::UnhandledError,
TerminationReason::InvocationError,
TerminationReason::ExecutionError,
TerminationReason::CheckpointFailed,
TerminationReason::NonDeterministicExecution,
TerminationReason::StepInterrupted,
TerminationReason::CallbackError,
TerminationReason::SerializationError,
TerminationReason::SizeLimitExceeded,
TerminationReason::OperationTerminated,
TerminationReason::RetryScheduled,
TerminationReason::WaitScheduled,
TerminationReason::CallbackPending,
TerminationReason::ContextValidationError,
TerminationReason::LambdaTimeoutApproaching,
];
for reason in reasons {
let json = serde_json::to_string(&reason).unwrap();
let deserialized: TerminationReason = serde_json::from_str(&json).unwrap();
assert_eq!(reason, deserialized, "Round-trip failed for {:?}", reason);
}
}
}