liteboxfs 0.2.0

A modern POSIX filesystem in a SQLite database
Documentation
#![cfg(all(feature = "fs", target_os = "linux"))]

use xpct::{be_err, be_ok, expect, match_pattern, pattern};

use crate::{
    Connection, CreateOptions, Error, FilesystemId,
    lock::{LockHandler, LockResult, LockType},
};

// Each test uses a freshly-generated `FilesystemId` so that the lock file path under
// `$XDG_RUNTIME_DIR/liteboxfs/fs/{uuid}/litebox.lock` is unique per test invocation, avoiding
// contention between concurrent tests (and stale lock files from previous runs).
fn fresh_handler() -> LockHandler {
    LockHandler::new(FilesystemId::new())
}

#[test]
fn shared_lock_blocks_exclusive() -> crate::Result<()> {
    let handler = fresh_handler();

    let shared = expect!(handler.acquire_litebox_lock(LockType::Read)?)
        .to(match_pattern(pattern!(LockResult::Acquired(_))))
        .into_inner();

    // The shared lock held above must prevent acquisition of an exclusive lock from any other
    // handle on the same litebox.
    expect!(handler.acquire_litebox_lock(LockType::Write)?)
        .to(match_pattern(pattern!(LockResult::Locked)));

    drop(shared);

    // Once the shared lock is released, an exclusive lock can be acquired.
    expect!(handler.acquire_litebox_lock(LockType::Write)?)
        .to(match_pattern(pattern!(LockResult::Acquired(_))));

    Ok(())
}

#[test]
fn exclusive_lock_blocks_shared() -> crate::Result<()> {
    let handler = fresh_handler();

    let exclusive = expect!(handler.acquire_litebox_lock(LockType::Write)?)
        .to(match_pattern(pattern!(LockResult::Acquired(_))))
        .into_inner();

    expect!(handler.acquire_litebox_lock(LockType::Read)?)
        .to(match_pattern(pattern!(LockResult::Locked)));

    drop(exclusive);

    expect!(handler.acquire_litebox_lock(LockType::Read)?)
        .to(match_pattern(pattern!(LockResult::Acquired(_))));

    Ok(())
}

#[test]
fn shared_locks_compose() -> crate::Result<()> {
    let handler = fresh_handler();

    let first = expect!(handler.acquire_litebox_lock(LockType::Read)?)
        .to(match_pattern(pattern!(LockResult::Acquired(_))))
        .into_inner();

    // Multiple shared holders should coexist.
    let second = expect!(handler.acquire_litebox_lock(LockType::Read)?)
        .to(match_pattern(pattern!(LockResult::Acquired(_))))
        .into_inner();

    drop(first);
    drop(second);

    Ok(())
}

#[test]
fn try_upgrade_succeeds_with_no_other_holder() -> crate::Result<()> {
    let handler = fresh_handler();

    let shared = match handler.acquire_litebox_lock(LockType::Read)? {
        LockResult::Acquired(handle) => handle,
        LockResult::Locked => panic!("expected to acquire shared lock"),
    };

    // With no other holder, the upgrade should succeed and return the now-exclusive handle.
    let upgraded = expect!(shared.try_upgrade()?).to(be_ok()).into_inner();

    // While the exclusive lock is held, no shared lock can be acquired.
    expect!(handler.acquire_litebox_lock(LockType::Read)?)
        .to(match_pattern(pattern!(LockResult::Locked)));

    drop(upgraded);

    Ok(())
}

#[test]
fn try_upgrade_fails_when_contended() -> crate::Result<()> {
    let handler = fresh_handler();

    let shared_a = match handler.acquire_litebox_lock(LockType::Read)? {
        LockResult::Acquired(handle) => handle,
        LockResult::Locked => panic!("expected to acquire shared lock A"),
    };

    let shared_b = match handler.acquire_litebox_lock(LockType::Read)? {
        LockResult::Acquired(handle) => handle,
        LockResult::Locked => panic!("expected to acquire shared lock B"),
    };

    // Upgrade should fail because `shared_b` is still a conflicting holder; the original handle
    // must be returned and continue to hold its shared lock.
    let still_shared = expect!(shared_a.try_upgrade()?).to(be_err()).into_inner();

    // Confirm that `still_shared` is still really shared by trying to acquire an exclusive lock,
    // which should still be blocked.
    expect!(handler.acquire_litebox_lock(LockType::Write)?)
        .to(match_pattern(pattern!(LockResult::Locked)));

    drop(still_shared);
    drop(shared_b);

    Ok(())
}

#[test]
fn connection_open_fails_when_exclusive_lock_held() -> crate::Result<()> {
    let temp_dir = tempfile::tempdir().unwrap();
    let path = temp_dir.path().join("test.litebox");

    // Create the litebox normally first, then drop the connection (and its shared lock) and
    // read the filesystem UUID back out via raw SQLite so we can synthesize the exclusive lock
    // below.
    drop(Connection::create_new(&path, &CreateOptions::new())?);
    let uuid: FilesystemId = {
        let raw = rusqlite::Connection::open(&path)?;
        let uuid_str: String = raw.query_row(
            "SELECT value FROM liteboxfs_settings WHERE key = 'uuid'",
            [],
            |row| row.get(0),
        )?;
        uuid_str.parse().expect("expected a valid UUID")
    };

    // Take the exclusive lock directly, simulating what a mount would hold.
    let handler = LockHandler::new(uuid);
    let _exclusive = match handler.acquire_litebox_lock(LockType::Write)? {
        LockResult::Acquired(handle) => handle,
        LockResult::Locked => panic!("expected to acquire exclusive lock"),
    };

    // With the exclusive lock held, opening the same litebox must fail with `LiteboxLocked`.
    expect!(Connection::open(&path, &Default::default()))
        .to(be_err())
        .to(match_pattern(pattern!(Error::LiteboxLocked)));

    Ok(())
}

#[test]
fn in_memory_connection_does_not_take_litebox_lock() -> crate::Result<()> {
    // In-memory connections share no on-disk state, so the lock is irrelevant. Two of them must
    // be openable back-to-back regardless of any lock state.
    let _a = Connection::open_in_memory(&CreateOptions::new())?;
    let _b = Connection::open_in_memory(&CreateOptions::new())?;

    // Equally, holding an exclusive litebox lock for some unrelated UUID must not prevent an
    // in-memory connection from being opened.
    let handler = fresh_handler();
    let _exclusive = match handler.acquire_litebox_lock(LockType::Write)? {
        LockResult::Acquired(handle) => handle,
        LockResult::Locked => panic!("expected to acquire exclusive lock"),
    };
    let _c = Connection::open_in_memory(&CreateOptions::new())?;

    Ok(())
}