liteboxfs 0.2.0

A modern POSIX filesystem in a SQLite database
Documentation
use std::path::PathBuf;

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

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

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

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

        expect!(fs.descendants("dir"))
            .to(be_ok())
            .map(|entries| entries.collect::<Vec<_>>())
            .to(be_empty());

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

    Ok(())
}

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

    conn.exec(|fs| {
        fs.create("dir", FileKind::Dir, Owner::ROOT)?;
        fs.create("dir/file.txt", FileKind::Regular, Owner::ROOT)?;
        fs.create("dir/sub", FileKind::Dir, Owner::ROOT)?;
        fs.create(
            "dir/link",
            FileKind::Symlink {
                target: PathBuf::from("/target"),
            },
            Owner::ROOT,
        )?;

        let entries: Vec<(PathBuf, FileKind)> = fs
            .descendants("dir")?
            .map(|e| (e.path().to_owned(), e.kind().clone()))
            .collect();

        expect!(entries).to(consist_of([
            (PathBuf::from("/dir/file.txt"), FileKind::Regular),
            (PathBuf::from("/dir/sub"), FileKind::Dir),
            (
                PathBuf::from("/dir/link"),
                FileKind::Symlink {
                    target: PathBuf::from("/target"),
                },
            ),
        ]));

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

    Ok(())
}

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

    conn.exec(|fs| {
        fs.create("dir", FileKind::Dir, Owner::ROOT)?;
        fs.create("dir/a.txt", FileKind::Regular, Owner::ROOT)?;
        fs.create("dir/sub", FileKind::Dir, Owner::ROOT)?;
        fs.create("dir/sub/b.txt", FileKind::Regular, Owner::ROOT)?;
        fs.create("dir/sub/deep", FileKind::Dir, Owner::ROOT)?;
        fs.create("dir/sub/deep/c.txt", FileKind::Regular, Owner::ROOT)?;

        let paths: Vec<PathBuf> = fs
            .descendants("dir")?
            .map(|e| e.path().to_owned())
            .collect();

        expect!(paths).to(consist_of([
            PathBuf::from("/dir/a.txt"),
            PathBuf::from("/dir/sub"),
            PathBuf::from("/dir/sub/b.txt"),
            PathBuf::from("/dir/sub/deep"),
            PathBuf::from("/dir/sub/deep/c.txt"),
        ]));

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

    Ok(())
}

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

    conn.exec(|fs| {
        fs.create("dir", FileKind::Dir, Owner::ROOT)?;
        fs.create("dir/sub_a", FileKind::Dir, Owner::ROOT)?;
        fs.create("dir/sub_a/file.txt", FileKind::Regular, Owner::ROOT)?;
        fs.create("dir/sub_b", FileKind::Dir, Owner::ROOT)?;
        fs.create("dir/sub_b/nested", FileKind::Dir, Owner::ROOT)?;
        fs.create("dir/sub_b/nested/file.txt", FileKind::Regular, Owner::ROOT)?;

        let paths: Vec<PathBuf> = fs
            .descendants("dir")?
            .map(|e| e.path().to_owned())
            .collect();

        let index_of = |path: &str| {
            paths
                .iter()
                .position(|p| p == &PathBuf::from(path))
                .unwrap()
        };

        expect!(index_of("/dir/sub_a")).to(be_lt(index_of("/dir/sub_a/file.txt")));
        expect!(index_of("/dir/sub_b")).to(be_lt(index_of("/dir/sub_b/nested")));
        expect!(index_of("/dir/sub_b/nested")).to(be_lt(index_of("/dir/sub_b/nested/file.txt")));

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

    Ok(())
}

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

    conn.exec(|fs| {
        fs.create("dir", FileKind::Dir, Owner::ROOT)?;
        fs.create("dir/regular", FileKind::Regular, Owner::ROOT)?;
        fs.create("dir/subdir", FileKind::Dir, Owner::ROOT)?;
        fs.create(
            "dir/symlink",
            FileKind::Symlink {
                target: PathBuf::from("/target"),
            },
            Owner::ROOT,
        )?;
        fs.create(
            "dir/blockdev",
            FileKind::Block {
                dev: Device::new(8, 1),
            },
            Owner::ROOT,
        )?;
        fs.create(
            "dir/chardev",
            FileKind::Char {
                dev: Device::new(1, 3),
            },
            Owner::ROOT,
        )?;
        fs.create("dir/fifo", FileKind::Pipe, Owner::ROOT)?;

        expect!(fs.descendants("dir"))
            .to(be_ok())
            .map(|entries| {
                entries
                    .map(|e| (e.name().to_string_lossy().into_owned(), e.kind().clone()))
                    .collect::<Vec<_>>()
            })
            .to(consist_of([
                ("regular".to_string(), FileKind::Regular),
                ("subdir".to_string(), FileKind::Dir),
                (
                    "symlink".to_string(),
                    FileKind::Symlink {
                        target: PathBuf::from("/target"),
                    },
                ),
                (
                    "blockdev".to_string(),
                    FileKind::Block {
                        dev: Device::new(8, 1),
                    },
                ),
                (
                    "chardev".to_string(),
                    FileKind::Char {
                        dev: Device::new(1, 3),
                    },
                ),
                ("fifo".to_string(), FileKind::Pipe),
            ]));

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

    Ok(())
}

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

    conn.exec(|fs| {
        fs.create("root", FileKind::Dir, Owner::ROOT)?;
        fs.create("root/other.txt", FileKind::Regular, Owner::ROOT)?;
        fs.create("root/sub", FileKind::Dir, Owner::ROOT)?;
        fs.create("root/sub/file.txt", FileKind::Regular, Owner::ROOT)?;

        // Descendants of a subdirectory should not include entries outside it.
        let paths: Vec<PathBuf> = fs
            .descendants("root/sub")?
            .map(|e| e.path().to_owned())
            .collect();

        expect!(paths).to(consist_of([PathBuf::from("/root/sub/file.txt")]));

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

    Ok(())
}

mod errors {
    use super::*;

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

        conn.exec(|fs| {
            expect!(fs.descendants("nonexistent")).to(be_err()).to(match_pattern(
                pattern!(Error::FileNotFound { file: FileOrigin::Litebox { locator, .. }, .. } if locator == &FileBy::from("/nonexistent")),
            ));

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

        Ok(())
    }

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

        conn.exec(|fs| {
            fs.create("file.txt", FileKind::Regular, Owner::ROOT)?;

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

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

        Ok(())
    }
}