liteboxfs 0.2.0

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

use std::{ffi::OsStr, path::Path, path::PathBuf};

use xpct::{be_empty, be_none, be_ok, consist_of, equal, expect, match_pattern, pattern};

use crate::{
    Connection, Error, FileOrigin,
    file_metadata::{Device, FileKind, Owner},
    settings::Settings,
};

fn open() -> crate::Result<Connection> {
    Connection::open_for_testing(&Settings::default())
}

#[test]
fn empty_directory_returns_no_entries() -> crate::Result<()> {
    let mut conn = open()?;

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

        expect!(fs.children("/dir"))
            .to(be_ok())
            .iter_map(|entry| entry.name().to_string_lossy().into_owned())
            .map(|iter| iter.collect::<Vec<_>>())
            .to(be_empty());

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

    Ok(())
}

#[test]
fn lists_immediate_children() -> crate::Result<()> {
    let mut conn = open()?;

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

        expect!(fs.children("/dir"))
            .to(be_ok())
            .iter_map(|entry| entry.name().to_string_lossy().into_owned())
            .map(|iter| iter.collect::<Vec<_>>())
            .to(consist_of([
                "a.txt".to_string(),
                "b.txt".to_string(),
                "sub".to_string(),
            ]));

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

    Ok(())
}

#[test]
fn does_not_include_nested_descendants() -> crate::Result<()> {
    let mut conn = open()?;

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

        expect!(fs.children("/dir"))
            .to(be_ok())
            .iter_map(|entry| entry.name().to_string_lossy().into_owned())
            .map(|iter| iter.collect::<Vec<_>>())
            .to(consist_of(["sub".to_string()]));

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

    Ok(())
}

#[test]
fn lists_children_of_root() -> crate::Result<()> {
    let mut conn = open()?;

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

        expect!(fs.children("/"))
            .to(be_ok())
            .iter_map(|entry| entry.name().to_string_lossy().into_owned())
            .map(|iter| iter.collect::<Vec<_>>())
            .to(consist_of(["a.txt".to_string(), "b.txt".to_string()]));

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

    Ok(())
}

#[test]
fn fails_when_path_does_not_exist() -> crate::Result<()> {
    let mut conn = open()?;

    conn.exec(|fs| {
        expect!(fs.children("/nonexistent"))
            .to(match_pattern(pattern!(Err(Error::FileNotFound { .. }))));
        crate::Result::Ok(())
    })?;

    Ok(())
}

#[test]
fn fails_when_path_is_not_a_directory() -> crate::Result<()> {
    let mut conn = open()?;

    conn.exec(|fs| {
        fs.create("/file.txt", FileKind::Regular, Owner::ROOT)?;
        expect!(fs.children("/file.txt")).to(match_pattern(pattern!(Err(Error::NotADirectory {
            file: FileOrigin::Litebox { .. },
        },))));
        crate::Result::Ok(())
    })?;

    Ok(())
}

#[test]
fn entry_path_is_absolute_path_under_parent() -> crate::Result<()> {
    let mut conn = open()?;

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

        let entry = expect!(fs.children("/dir"))
            .to(be_ok())
            .map(|mut iter| iter.next().expect("expected at least one entry"))
            .into_inner();
        expect!(entry.path()).to(equal(Path::new("/dir/a.txt")));
        expect!(entry.name()).to(equal(OsStr::new("a.txt")));

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

    Ok(())
}

#[test]
fn entry_reports_correct_kind() -> crate::Result<()> {
    let mut conn = open()?;

    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)?;

        expect!(fs.children("/dir"))
            .to(be_ok())
            .iter_map(|entry| {
                (
                    entry.name().to_string_lossy().into_owned(),
                    entry.kind().clone(),
                )
            })
            .map(|iter| iter.collect::<Vec<_>>())
            .to(consist_of([
                ("file.txt".to_string(), FileKind::Regular),
                ("sub".to_string(), FileKind::Dir),
            ]));

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

    Ok(())
}

#[test]
fn entry_file_id_matches_open_file_id() -> crate::Result<()> {
    let mut conn = open()?;

    conn.exec(|fs| {
        fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
        let file = fs.create("/dir/a.txt", FileKind::Regular, Owner::ROOT)?;
        let expected_id = file.file_id();
        drop(file);

        expect!(fs.children("/dir"))
            .to(be_ok())
            .iter_map(|entry| entry.file_id())
            .map(|iter| iter.collect::<Vec<_>>())
            .to(consist_of([expected_id]));

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

    Ok(())
}

#[test]
fn entry_metadata_matches_file_metadata() -> crate::Result<()> {
    let mut conn = open()?;

    conn.exec(|fs| {
        fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
        let mut file = fs.create("/dir/a.txt", FileKind::Regular, Owner::ROOT)?;
        let expected_mode = file.metadata()?.mode();
        let expected_user = file.metadata()?.user();
        let expected_group = file.metadata()?.group();
        drop(file);

        let entry = expect!(fs.children("/dir"))
            .to(be_ok())
            .map(|mut iter| iter.next().expect("expected at least one entry"))
            .into_inner();
        let metadata = entry.metadata();
        expect!(metadata.mode()).to(equal(expected_mode));
        expect!(metadata.user()).to(equal(expected_user));
        expect!(metadata.group()).to(equal(expected_group));

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

    Ok(())
}

#[test]
fn iterator_size_hint_matches_remaining() -> crate::Result<()> {
    let mut conn = open()?;

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

        let mut iter = expect!(fs.children("/dir")).to(be_ok()).into_inner();
        expect!(iter.size_hint()).to(equal((3usize, Some(3usize))));
        expect!(iter.len()).to(equal(3));
        iter.next();
        expect!(iter.size_hint()).to(equal((2usize, Some(2usize))));

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

    Ok(())
}

#[test]
fn fused_iterator_returns_none_after_exhaustion() -> crate::Result<()> {
    let mut conn = open()?;

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

        let mut iter = expect!(fs.children("/dir")).to(be_ok()).into_inner();
        expect!(iter.next()).to(be_none());
        expect!(iter.next()).to(be_none());

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

    Ok(())
}

#[test]
fn hard_links_appear_under_each_name() -> crate::Result<()> {
    let mut conn = open()?;

    conn.exec(|fs| {
        fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
        let mut file = fs.create("/dir/a.txt", FileKind::Regular, Owner::ROOT)?;
        file.link("/dir/b.txt")?;
        let expected_id = file.file_id();
        drop(file);

        expect!(fs.children("/dir"))
            .to(be_ok())
            .iter_map(|entry| entry.file_id())
            .map(|iter| iter.collect::<Vec<_>>())
            .to(consist_of([expected_id, expected_id]));

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

    Ok(())
}

#[test]
fn reports_all_special_file_kinds() -> crate::Result<()> {
    let mut conn = open()?;

    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.children("/dir"))
            .to(be_ok())
            .iter_map(|entry| {
                (
                    entry.name().to_string_lossy().into_owned(),
                    entry.kind().clone(),
                )
            })
            .map(|iter| iter.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),
            ]));

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

    Ok(())
}