#[cfg(test)]
mod tests {
use std::{
io,
panic::{AssertUnwindSafe, catch_unwind},
sync::{
Arc,
atomic::{AtomicBool, AtomicUsize, Ordering},
},
};
use qubit_dcl::{
DoubleCheckedLockExecutor,
double_checked::{ExecutionResult, ExecutorError},
};
use qubit_lock::{ArcMutex, lock::Lock};
mod test_executor_ready_builder {
use super::*;
fn increment_and_return_task(value: &mut i32) -> Result<i32, io::Error> {
*value += 1;
Ok(*value)
}
fn increment_unit_task(value: &mut i32) -> Result<(), io::Error> {
*value += 1;
Ok(())
}
#[test]
fn test_prepare_commit_runs_after_success() {
let data = ArcMutex::new(10);
let prepared = Arc::new(AtomicBool::new(false));
let committed = Arc::new(AtomicBool::new(false));
let rolled_back = Arc::new(AtomicBool::new(false));
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.prepare({
let prepared = prepared.clone();
move || {
prepared.store(true, Ordering::Release);
Ok::<(), io::Error>(())
}
})
.rollback_prepare({
let rolled_back = rolled_back.clone();
move || {
rolled_back.store(true, Ordering::Release);
Ok::<(), io::Error>(())
}
})
.commit_prepare({
let committed = committed.clone();
move || {
committed.store(true, Ordering::Release);
Ok::<(), io::Error>(())
}
})
.build();
let result = executor
.call_with(increment_and_return_task as fn(&mut i32) -> Result<i32, io::Error>)
.get_result();
assert!(matches!(result, ExecutionResult::Success(11)));
assert!(prepared.load(Ordering::Acquire));
assert!(committed.load(Ordering::Acquire));
assert!(!rolled_back.load(Ordering::Acquire));
}
#[test]
fn test_execute_with_prepare_commit_finalizes_unit_result() {
let data = ArcMutex::new(10);
let committed = Arc::new(AtomicBool::new(false));
let executor = DoubleCheckedLockExecutor::builder()
.on(data.clone())
.when(|| true)
.prepare(|| Ok::<(), io::Error>(()))
.commit_prepare({
let committed = committed.clone();
move || {
committed.store(true, Ordering::Release);
Ok::<(), io::Error>(())
}
})
.build();
let result = executor
.execute_with(increment_unit_task as fn(&mut i32) -> Result<(), io::Error>)
.get_result();
assert!(matches!(result, ExecutionResult::Success(())));
assert!(committed.load(Ordering::Acquire));
assert_eq!(data.read(|value| *value), 11);
}
#[test]
fn test_prepare_commit_failure_without_logger_replaces_success() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.prepare(|| Ok::<(), io::Error>(()))
.commit_prepare(|| Err::<(), _>(io::Error::other("commit failed")))
.build();
let result = executor
.call_with(|value: &mut i32| Ok::<i32, io::Error>(*value))
.get_result();
assert!(matches!(
result,
ExecutionResult::Failed(ExecutorError::PrepareCommitFailed(callback_error))
if callback_error.message() == "commit failed"
));
}
#[test]
fn test_prepare_commit_failure_with_logger_replaces_success() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.log_unmet_condition(log::Level::Error, "condition not met")
.prepare(|| Ok::<(), io::Error>(()))
.commit_prepare(|| Err::<(), _>(io::Error::other("commit failed")))
.build();
let result = executor
.call_with(|value: &mut i32| Ok::<i32, io::Error>(*value))
.get_result();
assert!(matches!(
result,
ExecutionResult::Failed(ExecutorError::PrepareCommitFailed(callback_error))
if callback_error.message() == "commit failed"
));
}
#[test]
fn test_prepare_rollback_runs_after_task_failure() {
let data = ArcMutex::new(10);
let rolled_back = Arc::new(AtomicBool::new(false));
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.prepare(|| Ok::<(), io::Error>(()))
.rollback_prepare({
let rolled_back = rolled_back.clone();
move || {
rolled_back.store(true, Ordering::Release);
Ok::<(), io::Error>(())
}
})
.build();
let result = executor
.call_with(|_value: &mut i32| Err::<i32, _>(io::Error::other("task failed")))
.get_result();
assert!(matches!(
result,
ExecutionResult::Failed(ExecutorError::TaskFailed(_))
));
assert!(rolled_back.load(Ordering::Acquire));
}
#[test]
fn test_prepare_failure_skips_task() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data.clone())
.when(|| true)
.prepare(|| Err::<(), _>(io::Error::other("prepare failed")))
.build();
let result = executor
.call_with(increment_and_return_task as fn(&mut i32) -> Result<i32, io::Error>)
.get_result();
assert!(matches!(
result,
ExecutionResult::Failed(ExecutorError::PrepareFailed(callback_error))
if callback_error.message() == "prepare failed"
));
assert_eq!(data.read(|value| *value), 10);
}
#[test]
fn test_prepare_failure_uses_configured_logger() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data.clone())
.when(|| true)
.log_unmet_condition(log::Level::Error, "condition not met")
.prepare(|| Err::<(), _>(io::Error::other("prepare failed")))
.build();
let result = executor
.call_with(increment_and_return_task as fn(&mut i32) -> Result<i32, io::Error>)
.get_result();
assert!(matches!(
result,
ExecutionResult::Failed(ExecutorError::PrepareFailed(callback_error))
if callback_error.message() == "prepare failed"
));
assert_eq!(data.read(|value| *value), 10);
}
#[test]
fn test_prepare_rollback_failure_replaces_result() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.prepare(|| Ok::<(), io::Error>(()))
.rollback_prepare(|| Err::<(), _>(io::Error::other("rollback failed")))
.build();
let result = executor
.call_with(|_value: &mut i32| Err::<i32, _>(io::Error::other("task failed")))
.get_result();
assert!(matches!(
result,
ExecutionResult::Failed(ExecutorError::PrepareRollbackFailed {
rollback,
..
}) if rollback.message() == "rollback failed"
));
}
#[test]
fn test_prepare_logger_methods_on_ready_builder_are_chainable() {
let data = ArcMutex::new(1);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.log_prepare_failure(log::Level::Warn, "prepare failed")
.log_prepare_commit_failure(log::Level::Error, "prepare commit failed")
.log_prepare_rollback_failure(log::Level::Info, "prepare rollback failed")
.disable_unmet_condition_logging()
.disable_prepare_failure_logging()
.disable_prepare_commit_failure_logging()
.disable_prepare_rollback_failure_logging()
.build();
let result = executor
.call_with(|value: &mut i32| Ok::<i32, io::Error>(*value))
.get_result();
assert!(matches!(result, ExecutionResult::Success(1)));
}
#[test]
fn test_ready_builder_set_catch_panics_enables_panic_capture() {
let data = ArcMutex::new(1);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.set_catch_panics(true)
.build();
let result = executor
.call_with(|_value: &mut i32| -> Result<i32, io::Error> {
panic!("panic from ready builder");
})
.get_result();
assert!(matches!(
result,
ExecutionResult::Failed(ExecutorError::Panic(_))
));
}
#[test]
fn test_ready_builder_disable_catch_panics_allows_panic() {
let data = ArcMutex::new(1);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.catch_panics()
.disable_catch_panics()
.build();
let caught = catch_unwind(AssertUnwindSafe(|| {
executor
.call_with(|_value: &mut i32| -> Result<i32, io::Error> {
panic!("panic should propagate");
})
.get_result();
}));
assert!(caught.is_err());
}
#[test]
fn test_ready_builder_disable_catch_panics_without_prior_set_still_disables_catch() {
let data = ArcMutex::new(1);
let caught = catch_unwind(AssertUnwindSafe(|| {
DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.catch_panics()
.disable_catch_panics()
.build()
.call_with(|_value: &mut i32| -> Result<i32, io::Error> {
panic!("panic should still propagate");
})
.get_result();
}));
assert!(caught.is_err());
}
#[test]
fn test_prepare_success_without_commit_preserves_success() {
let data = ArcMutex::new(1);
let executor = DoubleCheckedLockExecutor::builder()
.on(data.clone())
.when(|| true)
.prepare(|| Ok::<(), io::Error>(()))
.build();
let result = executor
.call_with(increment_and_return_task as fn(&mut i32) -> Result<i32, io::Error>)
.get_result();
assert!(matches!(result, ExecutionResult::Success(2)));
assert_eq!(data.read(|value| *value), 2);
}
#[test]
fn test_prepare_success_without_rollback_preserves_task_failure() {
let data = ArcMutex::new(1);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.prepare(|| Ok::<(), io::Error>(()))
.build();
let result = executor
.call_with(|_value: &mut i32| Err::<i32, _>(io::Error::other("task failed")))
.get_result();
assert!(matches!(
result,
ExecutionResult::Failed(ExecutorError::TaskFailed(_))
));
}
#[test]
fn test_prepare_success_without_rollback_preserves_condition_failure() {
let data = ArcMutex::new(1);
let checks = Arc::new(AtomicUsize::new(0));
let executor = DoubleCheckedLockExecutor::builder()
.on(data.clone())
.when({
let checks = checks.clone();
move || checks.fetch_add(1, Ordering::AcqRel) == 0
})
.prepare(|| Ok::<(), io::Error>(()))
.build();
let result = executor
.call_with(increment_and_return_task as fn(&mut i32) -> Result<i32, io::Error>)
.get_result();
assert!(matches!(result, ExecutionResult::ConditionNotMet));
assert_eq!(data.read(|value| *value), 1);
assert_eq!(checks.load(Ordering::Acquire), 2);
}
}
}