liteboxfs 0.2.0

A modern POSIX filesystem in a SQLite database
Documentation
use xpct::{be_err, be_none, be_ok, be_some, consist_of, equal, expect};

use liteboxfs::{Connection, CreateOptions, Error, UserMetadata};

mod filesystem_metadata {
    use super::*;

    #[test]
    fn returns_empty_by_default() -> liteboxfs::Result<()> {
        let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

        conn.exec(|fs| {
            expect!(fs.filesystem_metadata())
                .to(be_ok())
                .map(|metadata| metadata.iter().count())
                .to(equal(0));

            liteboxfs::Result::Ok(())
        })?;

        Ok(())
    }

    #[test]
    fn set_and_get_roundtrip() -> liteboxfs::Result<()> {
        let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

        conn.exec(|fs| {
            let mut metadata = UserMetadata::new();
            metadata.set("key1".to_string(), b"value1".to_vec())?;
            metadata.set("key2".to_string(), b"value2".to_vec())?;

            fs.set_filesystem_metadata(&metadata)?;

            let retrieved = fs.filesystem_metadata()?;
            expect!(retrieved.get("key1"))
                .to(be_some())
                .to(equal(b"value1"));
            expect!(retrieved.get("key2"))
                .to(be_some())
                .to(equal(b"value2"));
            expect!(retrieved.iter().count()).to(equal(2));

            liteboxfs::Result::Ok(())
        })?;

        Ok(())
    }

    #[test]
    fn set_replaces_existing() -> liteboxfs::Result<()> {
        let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

        conn.exec(|fs| {
            // Set initial metadata.
            let mut metadata = UserMetadata::new();
            metadata.set("key1".to_string(), b"value1".to_vec())?;
            metadata.set("key2".to_string(), b"value2".to_vec())?;
            fs.set_filesystem_metadata(&metadata)?;

            // Replace with new metadata.
            let mut new_metadata = UserMetadata::new();
            new_metadata.set("key3".to_string(), b"value3".to_vec())?;
            fs.set_filesystem_metadata(&new_metadata)?;

            // Verify old keys are gone and new key is present.
            let retrieved = fs.filesystem_metadata()?;
            expect!(retrieved.get("key1")).to(be_none());
            expect!(retrieved.get("key2")).to(be_none());
            expect!(retrieved.get("key3"))
                .to(be_some())
                .to(equal(b"value3"));
            expect!(retrieved.iter().count()).to(equal(1));

            liteboxfs::Result::Ok(())
        })?;

        Ok(())
    }

    #[test]
    fn setting_empty_metadata_clears_existing() -> liteboxfs::Result<()> {
        let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

        conn.exec(|fs| {
            // Set initial metadata.
            let mut metadata = UserMetadata::new();
            metadata.set("key1".to_string(), b"value1".to_vec())?;
            fs.set_filesystem_metadata(&metadata)?;

            // Clear by setting empty metadata.
            let empty_metadata = UserMetadata::new();
            fs.set_filesystem_metadata(&empty_metadata)?;

            // Verify metadata is now empty.
            let retrieved = fs.filesystem_metadata()?;
            expect!(retrieved.iter().count()).to(equal(0));

            liteboxfs::Result::Ok(())
        })?;

        Ok(())
    }
}

mod root_metadata {
    use super::*;

    #[test]
    fn returns_empty_by_default() -> liteboxfs::Result<()> {
        let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

        conn.exec(|fs| {
            expect!(fs.root_metadata())
                .to(be_ok())
                .map(|metadata| metadata.iter().count())
                .to(equal(0));

            liteboxfs::Result::Ok(())
        })?;

        Ok(())
    }

    #[test]
    fn set_and_get_roundtrip() -> liteboxfs::Result<()> {
        let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

        conn.exec(|fs| {
            let mut metadata = UserMetadata::new();
            metadata.set("root_key".to_string(), b"root_value".to_vec())?;

            fs.set_root_metadata(&metadata)?;

            let retrieved = fs.root_metadata()?;
            expect!(retrieved.get("root_key"))
                .to(be_some())
                .to(equal(b"root_value"));
            expect!(retrieved.iter().count()).to(equal(1));

            liteboxfs::Result::Ok(())
        })?;

        Ok(())
    }

    #[test]
    fn set_replaces_existing() -> liteboxfs::Result<()> {
        let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

        conn.exec(|fs| {
            // Set initial metadata.
            let mut metadata = UserMetadata::new();
            metadata.set("key1".to_string(), b"value1".to_vec())?;
            fs.set_root_metadata(&metadata)?;

            // Replace with new metadata.
            let mut new_metadata = UserMetadata::new();
            new_metadata.set("key2".to_string(), b"value2".to_vec())?;
            fs.set_root_metadata(&new_metadata)?;

            // Verify old key is gone and new key is present.
            let retrieved = fs.root_metadata()?;
            expect!(retrieved.get("key1")).to(be_none());
            expect!(retrieved.get("key2"))
                .to(be_some())
                .to(equal(b"value2"));
            expect!(retrieved.iter().count()).to(equal(1));

            liteboxfs::Result::Ok(())
        })?;

        Ok(())
    }

    #[test]
    fn setting_empty_metadata_clears_existing() -> liteboxfs::Result<()> {
        let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

        conn.exec(|fs| {
            // Set initial metadata.
            let mut metadata = UserMetadata::new();
            metadata.set("key1".to_string(), b"value1".to_vec())?;
            fs.set_root_metadata(&metadata)?;

            // Clear by setting empty metadata.
            let empty_metadata = UserMetadata::new();
            fs.set_root_metadata(&empty_metadata)?;

            // Verify metadata is now empty.
            expect!(fs.root_metadata())
                .to(be_ok())
                .map(|metadata| metadata.iter().count())
                .to(equal(0));

            liteboxfs::Result::Ok(())
        })?;

        Ok(())
    }

    #[test]
    fn metadata_is_isolated_per_root() -> liteboxfs::Result<()> {
        let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

        conn.exec(|fs| {
            let default_root_id = fs.root_id();

            // Set metadata on the default root.
            let mut default_metadata = UserMetadata::new();
            default_metadata.set("key".to_string(), b"default_value".to_vec())?;
            fs.set_root_metadata(&default_metadata)?;

            // Create a new root and switch to it.
            let new_root = fs.create_root(Some("other"))?;
            fs.switch_root(new_root.id)?;

            // New root should have no metadata.
            let retrieved = fs.root_metadata()?;
            expect!(retrieved.iter().count()).to(equal(0));

            // Set different metadata on the new root.
            let mut other_metadata = UserMetadata::new();
            other_metadata.set("key".to_string(), b"other_value".to_vec())?;
            fs.set_root_metadata(&other_metadata)?;

            // Switch back to default root and verify its metadata is unchanged.
            fs.switch_root(default_root_id)?;
            let default_retrieved = fs.root_metadata()?;
            expect!(default_retrieved.get("key"))
                .to(be_some())
                .to(equal(b"default_value"));

            liteboxfs::Result::Ok(())
        })?;

        Ok(())
    }
}

mod isolation {
    use super::*;

    #[test]
    fn filesystem_metadata_is_distinct_from_root_metadata() -> liteboxfs::Result<()> {
        let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

        conn.exec(|fs| {
            // Set filesystem metadata.
            let mut fs_metadata = UserMetadata::new();
            fs_metadata.set("key".to_string(), b"filesystem_value".to_vec())?;
            fs.set_filesystem_metadata(&fs_metadata)?;

            // Set root metadata with the same key but different value.
            let mut root_metadata = UserMetadata::new();
            root_metadata.set("key".to_string(), b"root_value".to_vec())?;
            fs.set_root_metadata(&root_metadata)?;

            // Verify they are separate.
            let fs_retrieved = fs.filesystem_metadata()?;
            expect!(fs_retrieved.get("key"))
                .to(be_some())
                .to(equal(b"filesystem_value"));

            let root_retrieved = fs.root_metadata()?;
            expect!(root_retrieved.get("key"))
                .to(be_some())
                .to(equal(b"root_value"));

            liteboxfs::Result::Ok(())
        })?;

        Ok(())
    }
}

mod limits {
    use super::*;

    #[test]
    fn exceeds_key_length_limit() -> liteboxfs::Result<()> {
        let mut metadata = UserMetadata::new();
        let key = "k".repeat(UserMetadata::MAX_KEY_LEN + 1);

        expect!(metadata.set(key, b"value".to_vec()))
            .to(be_err())
            .to(equal(Error::MetadataLimitExceeded));

        Ok(())
    }

    #[test]
    fn exceeds_value_length_limit() -> liteboxfs::Result<()> {
        let mut metadata = UserMetadata::new();
        let long_value = vec![0u8; UserMetadata::MAX_VALUE_LEN + 1];

        expect!(metadata.set("key".to_string(), long_value))
            .to(be_err())
            .to(equal(Error::MetadataLimitExceeded));

        Ok(())
    }
}

mod types {
    use super::*;

    #[test]
    fn remove_key() -> liteboxfs::Result<()> {
        let mut metadata = UserMetadata::new();
        metadata.set("key".to_string(), b"value".to_vec())?;

        metadata.remove("key");

        expect!(metadata.get("key")).to(be_none());

        Ok(())
    }

    #[test]
    fn clear_keys() -> liteboxfs::Result<()> {
        let mut metadata = UserMetadata::new();
        metadata.set("key1".to_string(), b"value1".to_vec())?;
        metadata.set("key2".to_string(), b"value2".to_vec())?;

        metadata.clear();

        expect!(metadata.get("key1")).to(be_none());
        expect!(metadata.get("key2")).to(be_none());

        Ok(())
    }

    #[test]
    fn iter_values() -> liteboxfs::Result<()> {
        let mut metadata = UserMetadata::new();
        metadata.set("key1".to_string(), b"value1".to_vec())?;
        metadata.set("key2".to_string(), b"value2".to_vec())?;

        expect!(metadata.iter().len()).to(equal(2));
        expect!(metadata.iter().collect::<Vec<_>>()).to(consist_of([
            ("key1", b"value1".as_slice()),
            ("key2", b"value2".as_slice()),
        ]));

        Ok(())
    }

    #[test]
    fn into_iter_values() -> liteboxfs::Result<()> {
        let mut metadata = UserMetadata::new();
        metadata.set("key1".to_string(), b"value1".to_vec())?;
        metadata.set("key2".to_string(), b"value2".to_vec())?;

        expect!(metadata.clone().into_iter().len()).to(equal(2));
        expect!(metadata.into_iter().collect::<Vec<_>>()).to(consist_of([
            ("key1".to_string(), b"value1".to_vec()),
            ("key2".to_string(), b"value2".to_vec()),
        ]));

        Ok(())
    }
}