amethystate 0.9.1

Type-safe reactive persistence for Rust GUI apps
Documentation
use amethystate::DefaultStore;
use amethystate::migration::fields::FieldDescriptor;
use amethystate::migration::set::MigrationSet;
use amethystate::migration::{MigrationError, MigrationPlan};
use amethystate::store::config::StoreConfig;
use amethystate::store::{Store, SubscriptionKind};
use amethystate_core::test_utils::unique_path;
use parking_lot::Mutex;
use std::sync::Arc;
use std::time::Duration;

const EMPTY_FIELDS: &[FieldDescriptor] = &[];

#[test]
fn test_set_get_immediate() {
    let path = unique_path("immediate");
    let (store, _) = DefaultStore::open(StoreConfig::new(path), MigrationSet::default()).unwrap();

    store.set("user.name", &"Alice".to_string()).unwrap();

    let val: Option<String> = store.get("user.name").unwrap();
    assert_eq!(val, Some("Alice".to_string()));
}

#[test]
fn test_local_reactivity() {
    let path = unique_path("reactivity");
    let (store, _) = DefaultStore::open(StoreConfig::new(path), MigrationSet::default()).unwrap();

    let hit = Arc::new(Mutex::new(false));
    let hit_inner = hit.clone();

    store.subscribe(
        SubscriptionKind::ExactPath(Arc::from("ui.theme")),
        Arc::new(move |_| {
            let mut guard = hit_inner.lock();
            *guard = true;
        }),
    );

    store.set("ui.theme", &"dark".to_string()).unwrap();

    assert!(*hit.lock());
}

#[test]
fn test_delete_flow() {
    let path = unique_path("delete");
    {
        let (store, _) =
            DefaultStore::open(StoreConfig::new(&path), MigrationSet::default()).unwrap();
        store.set("temp.key", &1).unwrap();
        store.save_now().unwrap();
        store.delete("temp.key").unwrap();
        assert_eq!(store.get::<i32>("temp.key").unwrap(), None);
        store.save_now().unwrap();
    }

    let (store_reopened, _) =
        DefaultStore::open(StoreConfig::new(&path), MigrationSet::default()).unwrap();
    assert_eq!(store_reopened.get::<i32>("temp.key").unwrap(), None);
}

#[test]
fn test_smart_recovery_decode() {
    let path = unique_path("recovery");
    let (store, _) = DefaultStore::open(StoreConfig::new(path), MigrationSet::default()).unwrap();
    let garbage = vec![0x00, 0x01, 0x02];

    let result: String = store.decode(&garbage).unwrap();
    assert_eq!(result, String::default());
}

#[test]
fn test_deterministic_closure_and_reopen() {
    let path = unique_path("closure");
    {
        let (store, _) =
            DefaultStore::open(StoreConfig::new(&path), MigrationSet::default()).unwrap();
        store.set("test.key", &"hello".to_string()).unwrap();
    }

    let (store_reopened, _) = DefaultStore::open(StoreConfig::new(&path), MigrationSet::default())
        .expect("Database should be available immediately after close");

    let val: Option<String> = store_reopened.get("test.key").unwrap();
    assert_eq!(val, Some("hello".to_string()));
}

#[test]
fn test_drop_behavior_is_deterministic() {
    let path = unique_path("drop_logic");
    {
        let (store, _) =
            DefaultStore::open(StoreConfig::new(&path), MigrationSet::default()).unwrap();
        store.set("drop.test", &42u32).unwrap();
    }

    let (store_reopened, _) = DefaultStore::open(StoreConfig::new(&path), MigrationSet::default())
        .expect("Drop must release file lock deterministically");

    assert_eq!(store_reopened.get::<u32>("drop.test").unwrap(), Some(42));
}

#[test]
fn test_close_saves_pending_data() {
    let path = unique_path("save_on_close");
    let mut config = StoreConfig::new(&path);
    config.save_debounce = Duration::from_secs(3600);

    {
        let (store, _) = DefaultStore::open(config, MigrationSet::default()).unwrap();
        store.set("urgent.data", &true).unwrap();
    }

    let (store, _) = DefaultStore::open(StoreConfig::new(&path), MigrationSet::default()).unwrap();
    assert_eq!(store.get::<bool>("urgent.data").unwrap(), Some(true));
}

#[test]
fn test_is_initialized_false_on_fresh_store() {
    let path = unique_path("init_fresh");
    let (store, _) = DefaultStore::open(StoreConfig::new(path), MigrationSet::default()).unwrap();
    assert!(!store.is_initialized("settings").unwrap());
}

#[test]
fn test_mark_and_is_initialized() {
    let path = unique_path("init_mark");
    let (store, _) = DefaultStore::open(StoreConfig::new(path), MigrationSet::default()).unwrap();
    assert!(!store.is_initialized("settings").unwrap());
    store.mark_initialized("settings").unwrap();
    assert!(store.is_initialized("settings").unwrap());
}

#[test]
fn test_initialized_namespaces_are_independent() {
    let path = unique_path("init_namespaces");
    let (store, _) = DefaultStore::open(StoreConfig::new(path), MigrationSet::default()).unwrap();
    store.mark_initialized("settings").unwrap();
    assert!(store.is_initialized("settings").unwrap());
    assert!(!store.is_initialized("other").unwrap());
}

#[test]
fn test_component_atomic_rollback() {
    let path = unique_path("rollback");
    let mut cfg = StoreConfig::new(&path);
    cfg.save_debounce = Duration::from_millis(50);
    {
        let (store, _) = DefaultStore::open(cfg, MigrationSet::default()).unwrap();
        store.set("net.ip", &"1.1.1.1".to_string()).unwrap();
        store.save_now().unwrap();
    }

    let mset = MigrationSet::default()
        .add(
            "net",
            MigrationPlan::new().step(1, "ok", |ctx| ctx.set("ip", &"8.8.8.8".to_string())),
            0,
            EMPTY_FIELDS,
            &[],
        )
        .add(
            "ui",
            MigrationPlan::new().step(1, "fail", |_| {
                Err(MigrationError::Custom("crash".into()).into())
            }),
            0,
            EMPTY_FIELDS,
            &["net"],
        );

    let (store, report) = DefaultStore::open(StoreConfig::new(&path), mset).unwrap();
    assert!(report.has_failures());

    let val: String = store.get("net.ip").unwrap().unwrap();
    assert_eq!(val, "1.1.1.1");
}