qubit-dcl 0.2.0

Reusable double-checked lock executor for Rust lock abstractions
Documentation
/*******************************************************************************
 *
 *    Copyright (c) 2025 - 2026.
 *    Haixing Hu, Qubit Co. Ltd.
 *
 *    All rights reserved.
 *
 ******************************************************************************/
#[cfg(test)]
mod tests {
    use std::{
        io,
        panic::{AssertUnwindSafe, catch_unwind},
        sync::{
            Arc,
            atomic::{AtomicBool, Ordering},
        },
    };

    use qubit_dcl::double_checked::ExecutorError;
    use qubit_dcl::{DoubleCheckedLock, double_checked::ExecutionResult};
    use qubit_lock::{ArcMutex, lock::Lock};

    mod test_double_checked_lock {
        use super::*;

        fn increment_unit_task(value: &mut i32) -> Result<(), io::Error> {
            *value += 1;
            Ok(())
        }

        #[test]
        fn test_call_with_runs_without_manual_executor_build() {
            let data = ArcMutex::new(10);
            let skip = Arc::new(AtomicBool::new(false));

            let updated = DoubleCheckedLock::on(data.clone())
                .when({
                    let skip = skip.clone();
                    move || !skip.load(Ordering::Acquire)
                })
                .call_with(|value: &mut i32| {
                    *value += 5;
                    Ok::<i32, io::Error>(*value)
                })
                .get_result();

            assert!(matches!(updated, ExecutionResult::Success(15)));
            assert_eq!(data.read(|value| *value), 15);
        }

        #[test]
        fn test_execute_with_returns_unmet_when_condition_fails() {
            let data = ArcMutex::new(10);

            let result = DoubleCheckedLock::on(data.clone())
                .when(|| false)
                .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_execute_with_runs_function_pointer_task() {
            let data = ArcMutex::new(10);

            let result = DoubleCheckedLock::on(data.clone())
                .when(|| true)
                .execute_with(increment_unit_task as fn(&mut i32) -> Result<(), io::Error>)
                .get_result();

            assert!(matches!(result, ExecutionResult::Success(())));
            assert_eq!(data.read(|value| *value), 11);
        }

        #[test]
        fn test_prepare_pipeline_works_in_convenience_mode() {
            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 result = DoubleCheckedLock::on(data.clone())
                .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>(())
                    }
                })
                .call_with(|value: &mut i32| {
                    *value += 3;
                    Ok::<i32, io::Error>(*value)
                })
                .get_result();

            assert!(matches!(result, ExecutionResult::Success(13)));
            assert!(prepared.load(Ordering::Acquire));
            assert!(committed.load(Ordering::Acquire));
            assert!(!rolled_back.load(Ordering::Acquire));
        }

        #[test]
        fn test_build_still_exposes_reusable_executor() {
            let data = ArcMutex::new(1);
            let executor = DoubleCheckedLock::on(data.clone()).when(|| true).build();

            let first = executor
                .call_with(|value: &mut i32| {
                    *value += 1;
                    Ok::<i32, io::Error>(*value)
                })
                .get_result();
            let second = executor
                .call_with(|value: &mut i32| {
                    *value += 1;
                    Ok::<i32, io::Error>(*value)
                })
                .get_result();

            assert!(matches!(first, ExecutionResult::Success(2)));
            assert!(matches!(second, ExecutionResult::Success(3)));
            assert_eq!(data.read(|value| *value), 3);
        }

        #[test]
        fn test_lock_builder_log_methods_are_chainable_before_call() {
            let data = ArcMutex::new(1);

            let result = DoubleCheckedLock::on(data)
                .log_unmet_condition(log::Level::Info, "condition not met")
                .log_prepare_failure(log::Level::Warn, "prepare failed")
                .log_prepare_commit_failure(log::Level::Error, "prepare commit failed")
                .log_prepare_rollback_failure(log::Level::Debug, "prepare rollback failed")
                .disable_unmet_condition_logging()
                .disable_prepare_failure_logging()
                .disable_prepare_commit_failure_logging()
                .disable_prepare_rollback_failure_logging()
                .when(|| true)
                .call(|| Ok::<i32, io::Error>(42))
                .get_result();

            assert!(matches!(result, ExecutionResult::Success(42)));
        }

        #[test]
        fn test_ready_builder_log_methods_are_chainable_before_execute() {
            let data = ArcMutex::new(1);
            let executed = Arc::new(AtomicBool::new(false));

            let result = DoubleCheckedLock::on(data)
                .when(|| true)
                .log_unmet_condition(log::Level::Info, "condition not met")
                .log_prepare_failure(log::Level::Warn, "prepare failed")
                .log_prepare_commit_failure(log::Level::Error, "prepare commit failed")
                .log_prepare_rollback_failure(log::Level::Debug, "prepare rollback failed")
                .disable_unmet_condition_logging()
                .disable_prepare_failure_logging()
                .disable_prepare_commit_failure_logging()
                .disable_prepare_rollback_failure_logging()
                .execute({
                    let executed = executed.clone();
                    move || {
                        executed.store(true, Ordering::Release);
                        Ok::<(), io::Error>(())
                    }
                })
                .get_result();

            assert!(matches!(result, ExecutionResult::Success(())));
            assert!(executed.load(Ordering::Acquire));
        }

        #[test]
        fn test_catch_panics_on_ready_builder_catches_task_panic() {
            let data = ArcMutex::new(10);
            let result = DoubleCheckedLock::on(data)
                .when(|| true)
                .catch_panics()
                .prepare(|| Ok::<(), io::Error>(()))
                .execute_with(|_value: &mut i32| -> Result<(), io::Error> {
                    panic!("panic in convenient API task");
                })
                .get_result();

            assert!(matches!(
                result,
                ExecutionResult::Failed(ExecutorError::Panic(_))
            ));
        }

        #[test]
        fn test_catch_panics_on_lock_builder_catches_task_panic() {
            let data = ArcMutex::new(10);
            let result = DoubleCheckedLock::on(data)
                .catch_panics()
                .when(|| true)
                .execute_with(|_value: &mut i32| -> Result<(), io::Error> {
                    panic!("panic in lock builder");
                })
                .get_result();

            assert!(matches!(
                result,
                ExecutionResult::Failed(ExecutorError::Panic(_))
            ));
        }

        #[test]
        fn test_set_catch_panics_on_lock_builder_catches_task_panic() {
            let data = ArcMutex::new(10);
            let result = DoubleCheckedLock::on(data)
                .set_catch_panics(true)
                .when(|| true)
                .execute_with(|_value: &mut i32| -> Result<(), io::Error> {
                    panic!("panic with set_catch_panics");
                })
                .get_result();

            assert!(matches!(
                result,
                ExecutionResult::Failed(ExecutorError::Panic(_))
            ));
        }

        #[test]
        fn test_disable_catch_panics_on_ready_builder_allows_panic() {
            let data = ArcMutex::new(10);
            let result = catch_unwind(AssertUnwindSafe(|| {
                DoubleCheckedLock::on(data)
                    .when(|| true)
                    .set_catch_panics(true)
                    .set_catch_panics(false)
                    .execute_with(|_value: &mut i32| -> Result<(), io::Error> {
                        panic!("panic should propagate");
                    })
                    .get_result()
            }));

            assert!(result.is_err());
        }

        #[test]
        fn test_disable_catch_panics_on_ready_builder_allows_panic_with_disable_method() {
            let data = ArcMutex::new(10);
            let caught = catch_unwind(AssertUnwindSafe(|| {
                DoubleCheckedLock::on(data)
                    .when(|| true)
                    .catch_panics()
                    .disable_catch_panics()
                    .execute_with(|_value: &mut i32| -> Result<(), io::Error> {
                        panic!("panic from ready builder disable_catch_panics");
                    })
                    .get_result();
            }));

            assert!(caught.is_err());
        }

        #[test]
        fn test_disable_catch_panics_on_lock_builder_allows_panic() {
            let data = ArcMutex::new(10);
            let caught = catch_unwind(AssertUnwindSafe(|| {
                DoubleCheckedLock::on(data)
                    .catch_panics()
                    .disable_catch_panics()
                    .when(|| true)
                    .execute_with(|_value: &mut i32| -> Result<(), io::Error> {
                        panic!("panic from lock builder disable_catch_panics");
                    })
                    .get_result();
            }));

            assert!(caught.is_err());
        }
    }
}