#[cfg(test)]
mod tests {
use std::{
cell::Cell,
io,
panic::{AssertUnwindSafe, catch_unwind, panic_any},
rc::Rc,
sync::{
Arc, Barrier,
atomic::{AtomicBool, AtomicUsize, Ordering},
},
thread,
};
use qubit_dcl::{
DoubleCheckedLockExecutor,
double_checked::{ExecutionContext, ExecutionResult, ExecutorError},
};
use qubit_lock::{ArcMutex, lock::Lock};
mod test_double_checked_lock_executor {
use super::*;
fn no_op_task() -> Result<(), io::Error> {
Ok(())
}
fn increment_unit_task(value: &mut i32) -> Result<(), io::Error> {
*value += 1;
Ok(())
}
fn failing_no_op_task() -> Result<(), io::Error> {
Err(io::Error::other("zero argument task failed"))
}
fn panicking_no_op_task() -> Result<(), io::Error> {
panic!("zero argument task panic");
}
fn failing_increment_unit_task(_value: &mut i32) -> Result<(), io::Error> {
Err(io::Error::other("mutable task failed"))
}
fn panicking_increment_unit_task(_value: &mut i32) -> Result<(), io::Error> {
panic!("mutable task panic");
}
fn increment_value_task(value: &mut i32) -> Result<i32, io::Error> {
*value += 1;
Ok(*value)
}
fn failing_increment_value_task(_value: &mut i32) -> Result<i32, io::Error> {
Err(io::Error::other("mutable value task failed"))
}
fn panicking_increment_value_task(_value: &mut i32) -> Result<i32, io::Error> {
panic!("mutable value task panic");
}
#[test]
fn test_call_with_executes_reusable_task_with_mutable_data() {
let data = ArcMutex::new(10);
let active = Arc::new(AtomicBool::new(false));
let executor = DoubleCheckedLockExecutor::builder()
.on(data.clone())
.when({
let active = active.clone();
move || !active.load(Ordering::Acquire)
})
.build();
let first = executor
.call_with(|value: &mut i32| {
*value += 5;
Ok::<i32, io::Error>(*value)
})
.get_result();
let second = executor
.call_with(|value: &mut i32| {
*value += 7;
Ok::<i32, io::Error>(*value)
})
.get_result();
assert!(matches!(first, ExecutionResult::Success(15)));
assert!(matches!(second, ExecutionResult::Success(22)));
assert_eq!(data.read(|value| *value), 22);
}
#[test]
fn test_execute_with_reports_condition_not_met_without_lock_mutation() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data.clone())
.when(|| false)
.build();
let result = executor
.execute_with(increment_unit_task as fn(&mut i32) -> Result<(), io::Error>)
.get_result();
assert!(matches!(result, ExecutionResult::ConditionNotMet));
assert_eq!(data.read(|value| *value), 10);
}
#[test]
fn test_call_and_execute_run_zero_argument_tasks_as_executor_api() {
let data = ArcMutex::new(0);
let counter = Arc::new(AtomicUsize::new(0));
let executor = DoubleCheckedLockExecutor::builder()
.on(data.clone())
.when(|| true)
.build();
let value = executor
.call(|| Ok::<i32, io::Error>(42))
.get_result()
.unwrap();
let executed = executor
.execute({
let counter = counter.clone();
move || {
counter.fetch_add(1, Ordering::AcqRel);
Ok::<(), io::Error>(())
}
})
.get_result();
assert_eq!(value, 42);
assert!(matches!(executed, ExecutionResult::Success(())));
assert_eq!(counter.load(Ordering::Acquire), 1);
}
#[test]
fn test_function_pointer_tasks_cover_error_and_panic_paths() {
let failed = DoubleCheckedLockExecutor::builder()
.on(ArcMutex::new(0))
.when(|| true)
.build()
.execute(failing_no_op_task as fn() -> Result<(), io::Error>)
.get_result();
match failed {
ExecutionResult::Failed(ExecutorError::TaskFailed(error)) => {
assert_eq!(error.to_string(), "zero argument task failed");
}
_ => panic!("expected zero argument task failure"),
}
let panicked = DoubleCheckedLockExecutor::builder()
.on(ArcMutex::new(0))
.when(|| true)
.catch_panics()
.build()
.execute(panicking_no_op_task as fn() -> Result<(), io::Error>)
.get_result();
assert!(matches!(
panicked,
ExecutionResult::Failed(ExecutorError::Panic(_))
));
let succeeded_with = DoubleCheckedLockExecutor::builder()
.on(ArcMutex::new(0))
.when(|| true)
.build()
.execute_with(increment_unit_task as fn(&mut i32) -> Result<(), io::Error>)
.get_result();
assert!(matches!(succeeded_with, ExecutionResult::Success(())));
let failed_with = DoubleCheckedLockExecutor::builder()
.on(ArcMutex::new(0))
.when(|| true)
.build()
.execute_with(failing_increment_unit_task as fn(&mut i32) -> Result<(), io::Error>)
.get_result();
match failed_with {
ExecutionResult::Failed(ExecutorError::TaskFailed(error)) => {
assert_eq!(error.to_string(), "mutable task failed");
}
_ => panic!("expected mutable task failure"),
}
let panicked_with = DoubleCheckedLockExecutor::builder()
.on(ArcMutex::new(0))
.when(|| true)
.catch_panics()
.build()
.execute_with(
panicking_increment_unit_task as fn(&mut i32) -> Result<(), io::Error>,
)
.get_result();
assert!(matches!(
panicked_with,
ExecutionResult::Failed(ExecutorError::Panic(_))
));
let called_with = DoubleCheckedLockExecutor::builder()
.on(ArcMutex::new(0))
.when(|| true)
.build()
.call_with(increment_value_task as fn(&mut i32) -> Result<i32, io::Error>)
.get_result();
assert!(matches!(called_with, ExecutionResult::Success(1)));
let call_failed_with = DoubleCheckedLockExecutor::builder()
.on(ArcMutex::new(0))
.when(|| true)
.build()
.call_with(failing_increment_value_task as fn(&mut i32) -> Result<i32, io::Error>)
.get_result();
match call_failed_with {
ExecutionResult::Failed(ExecutorError::TaskFailed(error)) => {
assert_eq!(error.to_string(), "mutable value task failed");
}
_ => panic!("expected mutable value task failure"),
}
let call_panicked_with = DoubleCheckedLockExecutor::builder()
.on(ArcMutex::new(0))
.when(|| true)
.catch_panics()
.build()
.call_with(panicking_increment_value_task as fn(&mut i32) -> Result<i32, io::Error>)
.get_result();
assert!(matches!(
call_panicked_with,
ExecutionResult::Failed(ExecutorError::Panic(_))
));
}
#[test]
fn test_inherent_call_returns_execution_context() {
let data = ArcMutex::new(0);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.build();
let context: ExecutionContext<i32, io::Error> =
executor.call(|| Ok::<i32, io::Error>(7));
assert!(matches!(context.get_result(), ExecutionResult::Success(7)));
}
#[test]
fn test_execution_context_peek_success_and_finish() {
let data = ArcMutex::new(0);
let executor = DoubleCheckedLockExecutor::builder()
.on(data.clone())
.when(|| true)
.build();
let context = executor.call(|| Ok::<i32, io::Error>(3));
assert!(context.is_success());
assert!(matches!(context.peek_result(), ExecutionResult::Success(3)));
assert!(matches!(context.get_result(), ExecutionResult::Success(3)));
let finished = executor
.execute(no_op_task as fn() -> Result<(), io::Error>)
.finish();
assert!(finished);
let skipped = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| false)
.build()
.execute(no_op_task as fn() -> Result<(), io::Error>)
.finish();
assert!(!skipped);
}
#[test]
fn test_configured_logger_logs_condition_not_met() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data.clone())
.when(|| false)
.log_unmet_condition(log::Level::Info, "condition not met")
.build();
let result = executor
.execute_with(increment_unit_task as fn(&mut i32) -> Result<(), io::Error>)
.get_result();
assert!(matches!(result, ExecutionResult::ConditionNotMet));
assert_eq!(data.read(|value| *value), 10);
}
#[test]
fn test_call_accepts_non_send_non_static_task() {
let data = ArcMutex::new(10);
let local_increment = 5;
let marker = Rc::new(Cell::new(0));
let executor = DoubleCheckedLockExecutor::builder()
.on(data.clone())
.when(|| true)
.build();
let result = executor
.call_with(|value: &mut i32| {
marker.set(local_increment);
*value += marker.get();
Ok::<i32, io::Error>(*value)
})
.get_result();
assert!(matches!(result, ExecutionResult::Success(15)));
assert_eq!(data.read(|value| *value), 15);
assert_eq!(marker.get(), 5);
}
#[test]
fn test_concurrent_calls_prepare_all_then_roll_back_losing_second_checks() {
const WORKERS: usize = 4;
let data = ArcMutex::new(0);
let should_run = Arc::new(AtomicBool::new(true));
let tester_calls = Arc::new(AtomicUsize::new(0));
let first_check_gate = Arc::new(Barrier::new(WORKERS));
let prepare_count = Arc::new(AtomicUsize::new(0));
let rollback_count = Arc::new(AtomicUsize::new(0));
let task_count = Arc::new(AtomicUsize::new(0));
let executor = Arc::new(
DoubleCheckedLockExecutor::builder()
.on(data.clone())
.when({
let should_run = should_run.clone();
let tester_calls = tester_calls.clone();
let first_check_gate = first_check_gate.clone();
move || {
let call_index = tester_calls.fetch_add(1, Ordering::AcqRel);
let current = should_run.load(Ordering::Acquire);
if call_index < WORKERS {
first_check_gate.wait();
return current;
}
should_run.load(Ordering::Acquire)
}
})
.prepare({
let prepare_count = prepare_count.clone();
move || {
prepare_count.fetch_add(1, Ordering::AcqRel);
Ok::<(), io::Error>(())
}
})
.rollback_prepare({
let rollback_count = rollback_count.clone();
move || {
rollback_count.fetch_add(1, Ordering::AcqRel);
Ok::<(), io::Error>(())
}
})
.build(),
);
let handles = (0..WORKERS)
.map(|_| {
let executor = executor.clone();
let should_run = should_run.clone();
let task_count = task_count.clone();
thread::spawn(move || {
executor
.call_with(move |value: &mut i32| {
task_count.fetch_add(1, Ordering::AcqRel);
*value += 1;
should_run.store(false, Ordering::Release);
Ok::<i32, io::Error>(*value)
})
.get_result()
})
})
.collect::<Vec<_>>();
let results = handles
.into_iter()
.map(|handle| handle.join().expect("worker thread should not panic"))
.collect::<Vec<_>>();
let success_count = results
.iter()
.filter(|result| matches!(result, ExecutionResult::Success(_)))
.count();
let unmet_count = results
.iter()
.filter(|result| matches!(result, ExecutionResult::ConditionNotMet))
.count();
assert_eq!(success_count, 1);
assert_eq!(unmet_count, WORKERS - 1);
assert_eq!(data.read(|value| *value), 1);
assert_eq!(task_count.load(Ordering::Acquire), 1);
assert_eq!(prepare_count.load(Ordering::Acquire), WORKERS);
assert_eq!(rollback_count.load(Ordering::Acquire), WORKERS - 1);
}
#[test]
fn test_task_panic_propagates_without_prepare_rollback() {
let data = ArcMutex::new(10);
let prepared = 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>(())
}
})
.build();
let caught = catch_unwind(AssertUnwindSafe(|| {
executor.execute_with(|_value: &mut i32| -> Result<(), io::Error> {
panic!("task panic");
});
}));
assert!(caught.is_err());
assert!(prepared.load(Ordering::Acquire));
assert!(!rolled_back.load(Ordering::Acquire));
}
#[test]
fn test_task_panic_returns_panic_error_when_catch_panics_enabled() {
let data = ArcMutex::new(10);
let prepared = Arc::new(AtomicBool::new(false));
let rolled_back = Arc::new(AtomicBool::new(false));
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.catch_panics()
.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>(())
}
})
.build();
let result = executor
.execute_with(|_value: &mut i32| -> Result<(), io::Error> {
panic!("task panic");
})
.get_result();
assert!(matches!(
result,
ExecutionResult::Failed(ExecutorError::Panic(_))
));
assert!(prepared.load(Ordering::Acquire));
assert!(rolled_back.load(Ordering::Acquire));
}
#[test]
fn test_set_catch_panics_on_executor_catches_task_panic() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.build()
.set_catch_panics(true);
let result = executor
.execute(|| -> Result<(), io::Error> {
panic!("executor set_catch_panics");
})
.get_result();
assert!(matches!(
result,
ExecutionResult::Failed(ExecutorError::Panic(_))
));
}
#[test]
fn test_disable_catch_panics_on_executor_allows_panic() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.catch_panics()
.build()
.set_catch_panics(false);
let caught = catch_unwind(AssertUnwindSafe(|| {
executor
.execute(|| -> Result<(), io::Error> {
panic!("executor panic should propagate");
})
.get_result();
}));
assert!(caught.is_err());
}
#[test]
#[allow(deprecated)]
fn test_with_catch_panics_alias_and_getter() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.build()
.with_catch_panics(true);
assert!(executor.catch_panics());
let result = executor
.execute(|| -> Result<(), io::Error> {
panic!("executor with_catch_panics");
})
.get_result();
assert!(matches!(
result,
ExecutionResult::Failed(ExecutorError::Panic(_))
));
}
#[test]
fn test_task_panic_with_string_payload_is_captured() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.catch_panics()
.build();
let result = executor
.execute_with(|_value: &mut i32| -> Result<(), io::Error> {
panic!("{}", String::from("string payload"));
})
.get_result();
let message = match result {
ExecutionResult::Failed(ExecutorError::Panic(error)) => error.message().to_string(),
_ => panic!("expected panic error"),
};
assert!(message.contains("string payload"));
}
#[test]
fn test_task_panic_with_non_string_payload_is_captured() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.catch_panics()
.build();
let result = executor
.execute_with(|_value: &mut i32| -> Result<(), io::Error> {
panic_any(vec![1, 2, 3]);
})
.get_result();
let message = match result {
ExecutionResult::Failed(ExecutorError::Panic(error)) => error.message().to_string(),
_ => panic!("expected panic error"),
};
assert!(message.contains("Any"));
}
#[test]
fn test_first_check_panic_with_catch_panics_returns_panic_error() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| panic!("first check panic"))
.catch_panics()
.build();
let result = executor
.execute_with(|_value: &mut i32| -> Result<(), io::Error> {
Ok::<(), io::Error>(())
})
.get_result();
let message = match result {
ExecutionResult::Failed(ExecutorError::Panic(error)) => error.message().to_string(),
_ => panic!("expected panic error"),
};
assert!(message.contains("first check panic"));
}
#[test]
fn test_second_check_panic_with_catch_panics_returns_panic_error() {
let checks = Arc::new(AtomicUsize::new(0));
let executor = DoubleCheckedLockExecutor::builder()
.on(ArcMutex::new(10))
.when({
let checks = checks.clone();
move || {
let called = checks.fetch_add(1, Ordering::AcqRel);
if called == 0 {
true
} else {
panic!("second check panic");
}
}
})
.catch_panics()
.build();
let result = executor
.execute_with(|_value: &mut i32| -> Result<(), io::Error> {
Ok::<(), io::Error>(())
})
.get_result();
let message = match result {
ExecutionResult::Failed(ExecutorError::Panic(error)) => error.message().to_string(),
_ => panic!("expected panic error"),
};
assert_eq!(checks.load(Ordering::Acquire), 2);
assert!(message.contains("second check panic"));
}
#[test]
fn test_prepare_action_panic_with_catch_panics_returns_prepare_failed() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.catch_panics()
.prepare(|| -> Result<(), io::Error> {
panic!("prepare action panic");
})
.build();
let result = executor
.execute_with(|_value: &mut i32| -> Result<(), io::Error> {
Ok::<(), io::Error>(())
})
.get_result();
assert!(matches!(
result,
ExecutionResult::Failed(ExecutorError::PrepareFailed(callback_error))
if callback_error.message() == "prepare action panic"
));
}
#[test]
fn test_prepare_commit_action_panic_with_catch_panics_replaces_success() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.catch_panics()
.prepare(|| Ok::<(), io::Error>(()))
.commit_prepare(|| -> Result<(), io::Error> {
panic!("commit panic");
})
.build();
let result = executor
.execute_with(|_value: &mut i32| -> Result<(), io::Error> {
Ok::<(), io::Error>(())
})
.get_result();
let message = match result {
ExecutionResult::Failed(ExecutorError::PrepareCommitFailed(error)) => {
error.message().to_string()
}
_ => panic!("expected commit failed error"),
};
assert!(message.contains("commit panic"));
}
#[test]
fn test_prepare_rollback_action_panic_with_catch_panics_replaces_result() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.catch_panics()
.prepare(|| Ok::<(), io::Error>(()))
.rollback_prepare(|| -> Result<(), io::Error> {
panic!("rollback panic");
})
.build();
let result = executor
.execute_with(|_value: &mut i32| -> Result<(), io::Error> {
Err(io::Error::other("task failed"))
})
.get_result();
let message = match result {
ExecutionResult::Failed(ExecutorError::PrepareRollbackFailed {
rollback, ..
}) => rollback.message().to_string(),
_ => panic!("expected rollback failed error"),
};
assert!(message.contains("rollback panic"));
}
#[test]
fn test_prepare_success_without_commit_keeps_success_result() {
let data = ArcMutex::new(1);
let prepare_called = Arc::new(AtomicUsize::new(0));
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.prepare({
let prepare_called = prepare_called.clone();
move || {
prepare_called.fetch_add(1, Ordering::AcqRel);
Ok::<(), io::Error>(())
}
})
.build();
let result = executor
.call_with(|value: &mut i32| {
*value += 1;
Ok::<i32, io::Error>(*value)
})
.get_result();
assert!(matches!(result, ExecutionResult::Success(2)));
assert_eq!(prepare_called.load(Ordering::Acquire), 1);
}
#[test]
fn test_execute_with_returning_prepare_commit_error_keeps_original_error_message() {
let data = ArcMutex::new(1);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.catch_panics()
.prepare(|| Ok::<(), io::Error>(()))
.commit_prepare(|| Err::<(), io::Error>(io::Error::other("commit callback error")))
.build();
let result = executor
.execute_with(|value: &mut i32| {
*value += 1;
Ok::<(), io::Error>(())
})
.get_result();
match result {
ExecutionResult::Failed(ExecutorError::PrepareCommitFailed(error)) => {
assert!(error.message().contains("commit callback error"));
}
_ => panic!("expected prepare commit failed result"),
}
}
#[test]
fn test_execute_with_returning_prepare_rollback_error_keeps_original_message() {
let data = ArcMutex::new(1);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.catch_panics()
.prepare(|| Ok::<(), io::Error>(()))
.rollback_prepare(|| {
Err::<(), io::Error>(io::Error::other("rollback callback error"))
})
.build();
let result = executor
.execute_with(|value: &mut i32| {
*value += 1;
Err::<(), io::Error>(io::Error::other("task failed"))
})
.get_result();
match result {
ExecutionResult::Failed(ExecutorError::PrepareRollbackFailed {
rollback, ..
}) => {
assert!(rollback.message().contains("rollback callback error"));
}
_ => panic!("expected prepare rollback failed result"),
}
}
#[test]
fn test_execute_with_prepare_failed_task_error_preserves_task_failure_if_no_rollback() {
let data = ArcMutex::new(1);
let prepare_calls = Arc::new(AtomicUsize::new(0));
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.catch_panics()
.prepare({
let prepare_calls = prepare_calls.clone();
move || {
prepare_calls.fetch_add(1, Ordering::AcqRel);
Ok::<(), io::Error>(())
}
})
.build();
let result = executor
.execute_with(|value: &mut i32| {
*value += 1;
Err::<(), io::Error>(io::Error::other("task failed"))
})
.get_result();
let task = match result {
ExecutionResult::Failed(ExecutorError::TaskFailed(error)) => error,
_ => panic!("expected task failed result"),
};
assert_eq!(prepare_calls.load(Ordering::Acquire), 1);
assert_eq!(task.to_string(), "task failed");
}
#[test]
fn test_execute_unit_with_returning_prepare_commit_error_keeps_original_error_message() {
let data = ArcMutex::new(1);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.catch_panics()
.prepare(|| Ok::<(), io::Error>(()))
.commit_prepare(|| {
Err::<(), io::Error>(io::Error::other("unit commit callback error"))
})
.build();
let result = executor
.execute_with(|value: &mut i32| {
*value += 1;
Ok::<(), io::Error>(())
})
.get_result();
match result {
ExecutionResult::Failed(ExecutorError::PrepareCommitFailed(error)) => {
assert!(error.message().contains("unit commit callback error"));
}
_ => panic!("expected prepare commit failed result"),
}
}
#[test]
fn test_execute_unit_with_returning_prepare_rollback_error_preserves_task_failure() {
let data = ArcMutex::new(1);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.catch_panics()
.prepare(|| Ok::<(), io::Error>(()))
.rollback_prepare(|| {
Err::<(), io::Error>(io::Error::other("unit rollback callback error"))
})
.build();
let result = executor
.execute_with(|value: &mut i32| {
*value += 1;
Err::<(), io::Error>(io::Error::other("task failed"))
})
.get_result();
match result {
ExecutionResult::Failed(ExecutorError::PrepareRollbackFailed {
rollback, ..
}) => {
assert!(rollback.message().contains("unit rollback callback error"));
}
_ => panic!("expected prepare rollback failed result"),
}
}
#[test]
fn test_prepare_commit_success_keeps_success_result() {
let data = ArcMutex::new(1);
let prepare_called = Arc::new(AtomicUsize::new(0));
let commit_called = Arc::new(AtomicUsize::new(0));
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.prepare({
let prepare_called = prepare_called.clone();
move || {
prepare_called.fetch_add(1, Ordering::AcqRel);
Ok::<(), io::Error>(())
}
})
.commit_prepare({
let commit_called = commit_called.clone();
move || {
commit_called.fetch_add(1, Ordering::AcqRel);
Ok::<(), io::Error>(())
}
})
.build();
let result = executor
.call_with(|value: &mut i32| {
*value += 1;
Ok::<i32, io::Error>(*value)
})
.get_result();
assert!(matches!(result, ExecutionResult::Success(2)));
assert_eq!(prepare_called.load(Ordering::Acquire), 1);
assert_eq!(commit_called.load(Ordering::Acquire), 1);
}
#[test]
fn test_execute_with_second_check_unmet_runs_prepare_rollback_success() {
let should_pass = Arc::new(AtomicBool::new(true));
let rollback_called = Arc::new(AtomicUsize::new(0));
let executor = DoubleCheckedLockExecutor::builder()
.on(ArcMutex::new(1))
.when({
let should_pass = should_pass.clone();
move || should_pass.fetch_and(false, Ordering::AcqRel)
})
.prepare(|| Ok::<(), io::Error>(()))
.rollback_prepare({
let rollback_called = rollback_called.clone();
move || {
rollback_called.fetch_add(1, Ordering::AcqRel);
Ok::<(), io::Error>(())
}
})
.build();
let result = executor
.execute_with(|_value: &mut i32| -> Result<(), io::Error> {
Ok::<(), io::Error>(())
})
.get_result();
assert!(matches!(result, ExecutionResult::ConditionNotMet));
assert_eq!(rollback_called.load(Ordering::Acquire), 1);
}
#[test]
fn test_task_panic_with_owned_string_payload_is_captured() {
let data = ArcMutex::new(10);
let executor = DoubleCheckedLockExecutor::builder()
.on(data)
.when(|| true)
.catch_panics()
.build();
let result = executor
.execute_with(|_value: &mut i32| -> Result<(), io::Error> {
panic_any(String::from("owned string payload"));
})
.get_result();
let message = match result {
ExecutionResult::Failed(ExecutorError::Panic(error)) => error.message().to_string(),
_ => panic!("expected panic error"),
};
assert!(message.contains("owned string payload"));
}
}
}