cranpose-services 0.1.161

Multiplatform system services for Cranpose (HTTP, URI, and OS integrations)
Documentation
use std::sync::PoisonError;

use super::*;

fn test_lock() -> std::sync::MutexGuard<'static, ()> {
    static LOCK: Mutex<()> = Mutex::new(());
    LOCK.lock().unwrap_or_else(PoisonError::into_inner)
}

#[test]
fn observer_is_removed_on_drop() {
    let _guard = test_lock();
    let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
    let seen = Arc::clone(&calls);
    let handle = observe_lifecycle(move |_| {
        seen.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
    });
    dispatch_lifecycle(LifecycleEvent {
        from: LifecycleState::Created,
        to: LifecycleState::Started,
    });
    assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 1);
    drop(handle);
    dispatch_lifecycle(LifecycleEvent {
        from: LifecycleState::Started,
        to: LifecycleState::Resumed,
    });
    assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 1);
}

#[test]
fn state_dispatch_derives_the_previous_state() {
    let _guard = test_lock();
    dispatch_lifecycle_state(LifecycleState::Paused);
    assert_eq!(current_lifecycle_state(), LifecycleState::Paused);
    dispatch_lifecycle_state(LifecycleState::Stopped);
    assert_eq!(current_lifecycle_state(), LifecycleState::Stopped);
}

#[test]
fn repeated_keep_screen_value_reaches_the_host_once() {
    let _guard = test_lock();
    struct RecordingHost(Arc<std::sync::atomic::AtomicUsize>);
    impl HostController for RecordingHost {
        fn set_keep_screen_on(&self, _enabled: bool) {
            self.0.fetch_add(1, Ordering::Relaxed);
        }
        fn platform_directories(&self) -> Option<PlatformDirectories> {
            Some(PlatformDirectories {
                data: PathBuf::from("data"),
                config: PathBuf::from("config"),
                cache: PathBuf::from("cache"),
                documents: Some(PathBuf::from("documents")),
                temporary: PathBuf::from("temporary"),
                shared: Some(PathBuf::from("shared")),
            })
        }
        fn exit(&self) {}
        fn background(&self) {}
    }
    let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
    set_host_controller(Arc::new(RecordingHost(Arc::clone(&calls))));
    set_keep_screen_on(true);
    set_keep_screen_on(true);
    assert_eq!(calls.load(Ordering::Relaxed), 1);
    set_application_id("sample").expect("a plain id is valid");
    assert_eq!(
        application_directories().unwrap().data,
        PathBuf::from("data/sample")
    );
    clear_application_id();
    clear_host_controller();
}

#[test]
fn durable_saves_run_and_report_completion() {
    let _services = crate::registry::test_service_guard();
    let _guard = test_lock();
    let ran = Arc::new(std::sync::atomic::AtomicUsize::new(0));
    let first = Arc::clone(&ran);
    let second = Arc::clone(&ran);
    let a = register_durable_save(move || {
        first.fetch_add(1, Ordering::Relaxed);
    });
    let b = register_durable_save(move || {
        second.fetch_add(1, Ordering::Relaxed);
    });
    assert_eq!(
        run_durable_saves(std::time::Duration::from_secs(5)),
        DurableSaveOutcome::Completed
    );
    assert_eq!(ran.load(Ordering::Relaxed), 2);
    drop((a, b));
    assert_eq!(
        run_durable_saves(std::time::Duration::from_secs(1)),
        DurableSaveOutcome::Nothing
    );
}

#[test]
fn a_save_that_overruns_the_deadline_reports_a_timeout() {
    let _services = crate::registry::test_service_guard();
    let _guard = test_lock();
    let registration = register_durable_save(|| {
        std::thread::sleep(std::time::Duration::from_millis(400));
    });
    assert_eq!(
        run_durable_saves(std::time::Duration::from_millis(30)),
        DurableSaveOutcome::TimedOut
    );
    drop(registration);
}

#[test]
fn a_dropped_registration_is_no_longer_saved() {
    let _services = crate::registry::test_service_guard();
    let _guard = test_lock();
    let ran = Arc::new(std::sync::atomic::AtomicUsize::new(0));
    let counted = Arc::clone(&ran);
    let registration = register_durable_save(move || {
        counted.fetch_add(1, Ordering::Relaxed);
    });
    drop(registration);
    assert_eq!(
        run_durable_saves(std::time::Duration::from_secs(1)),
        DurableSaveOutcome::Nothing
    );
    assert_eq!(ran.load(Ordering::Relaxed), 0);
}

#[test]
fn application_id_must_be_one_component() {
    let _guard = test_lock();
    assert_eq!(
        set_application_id("../sample"),
        Err(PlatformDirectoryError::InvalidApplicationId)
    );
    assert_eq!(
        set_application_id(""),
        Err(PlatformDirectoryError::InvalidApplicationId)
    );
    clear_application_id();
    assert_eq!(
        application_directories(),
        Err(PlatformDirectoryError::NoApplicationId)
    );
}

#[test]
fn a_surviving_durable_save_keeps_its_registration_when_a_leader_leaves() {
    let _guard = test_lock();
    durable_saves()
        .lock()
        .unwrap_or_else(PoisonError::into_inner)
        .clear();
    let ran: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
    let show_first = std::rc::Rc::new(std::cell::Cell::new(true));

    fn saves(show_first: bool, ran: &Arc<Mutex<Vec<&'static str>>>) {
        if show_first {
            let ran = Arc::clone(ran);
            DurableSaveEffect((), move || {
                ran.lock()
                    .unwrap_or_else(PoisonError::into_inner)
                    .push("first");
            });
        }
        let ran = Arc::clone(ran);
        DurableSaveEffect((), move || {
            ran.lock()
                .unwrap_or_else(PoisonError::into_inner)
                .push("tail");
        });
    }

    let mut composition = cranpose_core::Composition::new(cranpose_core::MemoryApplier::new());
    let root_key = cranpose_core::location_key(file!(), line!(), column!());
    let mut pass = {
        let ran = Arc::clone(&ran);
        let show_first = std::rc::Rc::clone(&show_first);
        move || saves(show_first.get(), &ran)
    };

    composition
        .render(root_key, &mut pass)
        .expect("initial composition");
    show_first.set(false);
    composition
        .render(root_key, &mut pass)
        .expect("drop the leading save");

    let outcome = run_durable_saves(std::time::Duration::from_secs(5));
    assert_eq!(outcome, DurableSaveOutcome::Completed);
    assert_eq!(
        ran.lock()
            .unwrap_or_else(PoisonError::into_inner)
            .as_slice(),
        ["tail"],
        "the surviving effect must keep its own registration; adopting the \
         departed leader's group keeps the wrong save alive"
    );
}