liteboxfs 0.2.0

A modern POSIX filesystem in a SQLite database
Documentation
use std::io::{Read, Write};
use std::time::{Duration, SystemTime};

use xpct::{
    any, approx_eq_time, be_err, be_ok, equal, every, expect, fields, have_len, match_fields,
    match_pattern, pattern,
};

use liteboxfs::{
    Connection, CreateOptions, Error, FileBy, FileOrigin,
    metadata::{FileKind, Owner},
    root::Root,
};

mod basic {
    use super::*;

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

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

            let snapshot_id = expect!(fs.copy_root(Some("snapshot")))
                .to(be_ok())
                .map(|root| root.id)
                .to_not(equal(default_root_id))
                .into_inner();

            expect!(fs.find_root("snapshot"))
                .to(be_ok())
                .map(|root| root.id)
                .to(equal(snapshot_id));

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

        Ok(())
    }

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

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

            expect!(fs.copy_root(None))
                .to(be_ok())
                .map(|root| root.id)
                .to_not(equal(default_root_id));

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

        Ok(())
    }

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

        conn.exec(|fs| {
            let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
            file.write_all(b"content")?;
            drop(file);

            let snapshot = fs.copy_root(Some("snapshot"))?;

            fs.switch_root(snapshot.id)?;

            let mut file = fs.open("test.txt")?;
            let mut content = String::new();
            file.read_to_string(&mut content)?;
            expect!(content).to(equal("content"));

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

        Ok(())
    }

    // A previous bug caused this to fail with a foreign key constraint error.
    #[test]
    fn copy_root_after_deleting_previous_copy() -> liteboxfs::Result<()> {
        let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

        conn.exec(|fs| {
            let snapshot = fs.copy_root(Some("snapshot"))?;

            fs.delete_root(snapshot.id)?;

            expect!(fs.copy_root(Some("snapshot"))).to(be_ok());

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

        Ok(())
    }
}

mod listing {
    use super::*;

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

        conn.exec(|fs| {
            let default_root_id = fs.root_id();
            let snapshot = fs.copy_root(Some("my-snapshot"))?;

            expect!(fs.list_roots().collect::<liteboxfs::Result<Vec<_>>>())
                .to(be_ok())
                .to(have_len(2))
                .to(every(|| {
                    any(|ctx| {
                        ctx.cloned()
                            .to(match_fields(fields!(Root {
                                id: equal(default_root_id),
                                name: equal(Some(liteboxfs::DEFAULT_ROOT_NAME.to_string())),
                                created: approx_eq_time(SystemTime::now(), Duration::from_secs(1)),
                            })))
                            .to(match_fields(fields!(Root {
                                id: equal(snapshot.id),
                                name: equal(Some("my-snapshot".to_string())),
                                created: approx_eq_time(SystemTime::now(), Duration::from_secs(1)),
                            })))
                            .done()
                    })
                }));

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

        Ok(())
    }
}

mod directory_hierarchy {
    use super::*;

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

        conn.exec(|fs| {
            fs.create("dir1", FileKind::Dir, Owner::ROOT)?;
            fs.create("dir1/dir2", FileKind::Dir, Owner::ROOT)?;

            let mut file = fs.create("dir1/dir2/nested.txt", FileKind::Regular, Owner::ROOT)?;
            file.write_all(b"nested content")?;
            drop(file);

            let snapshot = fs.copy_root(Some("snapshot"))?;

            fs.switch_root(snapshot.id)?;

            let mut file = fs.open("dir1/dir2/nested.txt")?;
            let mut content = String::new();
            file.read_to_string(&mut content)?;
            expect!(content).to(equal("nested content".to_string()));

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

        Ok(())
    }
}

mod isolation {
    use super::*;

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

        let mut original_content = String::new();
        let mut snapshot_content = String::new();

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

            let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
            file.write_all(b"original")?;
            drop(file);

            let snapshot = fs.copy_root(Some("snapshot"))?;

            fs.switch_root(snapshot.id)?;
            let mut file = fs.open("test.txt")?;
            file.write_all(b"modified in snapshot")?;
            drop(file);

            let mut file = fs.open("test.txt")?;
            file.read_to_string(&mut snapshot_content)?;
            drop(file);

            fs.switch_root(default_root_id)?;
            let mut file = fs.open("test.txt")?;
            file.read_to_string(&mut original_content)?;

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

        expect!(original_content).to(equal("original"));
        expect!(snapshot_content).to(equal("modified in snapshot"));

        Ok(())
    }

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

        let mut original_content = String::new();
        let mut snapshot_content = String::new();

        conn.exec(|fs| {
            let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
            file.write_all(b"original")?;
            drop(file);

            let snapshot = fs.copy_root(Some("snapshot"))?;

            let mut file = fs.open("test.txt")?;
            file.write_all(b"modified in original")?;
            drop(file);

            let mut file = fs.open("test.txt")?;
            file.read_to_string(&mut original_content)?;
            drop(file);

            fs.switch_root(snapshot.id)?;
            let mut file = fs.open("test.txt")?;
            file.read_to_string(&mut snapshot_content)?;

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

        expect!(original_content).to(equal("modified in original"));
        expect!(snapshot_content).to(equal("original"));

        Ok(())
    }

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

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

            fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;

            let snapshot = fs.copy_root(Some("snapshot"))?;

            fs.switch_root(snapshot.id)?;
            fs.delete("test.txt")?;

            expect!(fs.open("test.txt")).to(be_err()).to(match_pattern(
                pattern!(Error::FileNotFound { file: FileOrigin::Litebox { locator, .. }, .. } if locator == &FileBy::from("/test.txt")),
            ));

            fs.switch_root(default_root_id)?;
            expect!(fs.open("test.txt")).to(be_ok());

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

        Ok(())
    }
}

mod errors {
    use super::*;

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

        conn.exec(|fs| {
            fs.copy_root(Some("snapshot"))?;

            expect!(fs.copy_root(Some("snapshot")))
                .to(be_err())
                .to(match_pattern(
                    pattern!(Error::RootAlreadyExists { name } if name == "snapshot"),
                ));

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

        Ok(())
    }
}