sqlarfs 0.1.1

A file archive format and virtual filesystem backed by a SQLite database
Documentation
use std::fs;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::time::SystemTime;

use sqlarfs::{FileMetadata, FileMode};
use xpct::core::{DispatchFormat, MatchOutcome, Matcher, TransformMatch};
use xpct::format::diff::DiffStyle;
use xpct::format::{DiffFormat, MessageFormat, MismatchFormat};
use xpct::matchers::diff::{DiffSegment, Diffable};
use xpct::matchers::Mismatch;
use xpct::{all, be_some, why};

#[derive(Debug)]
pub struct RegularFileMetadata {
    pub mode: Option<FileMode>,
    pub mtime: Option<SystemTime>,
    pub size: u64,
}

#[derive(Debug)]
pub struct DirMetadata {
    pub mode: Option<FileMode>,
    pub mtime: Option<SystemTime>,
}

#[derive(Debug)]
pub struct SymlinkMetadata {
    pub mtime: Option<SystemTime>,
    pub target: PathBuf,
}

pub fn have_file_metadata<'a>() -> Matcher<'a, FileMetadata, RegularFileMetadata, ()> {
    all(|ctx| {
        ctx.map(|metadata| match metadata {
            FileMetadata::File { mode, mtime, size } => {
                Some(RegularFileMetadata { mode, mtime, size })
            }
            _ => None,
        })
        .to(why(
            be_some(),
            "this is not the metadata for a regular file",
        ))
    })
}

pub fn have_dir_metadata<'a>() -> Matcher<'a, FileMetadata, DirMetadata, ()> {
    all(|ctx| {
        ctx.map(|metadata| match metadata {
            FileMetadata::Dir { mode, mtime } => Some(DirMetadata { mode, mtime }),
            _ => None,
        })
        .to(why(be_some(), "this is not the metadata for a directory"))
    })
}

pub fn have_symlink_metadata<'a>() -> Matcher<'a, FileMetadata, SymlinkMetadata, ()> {
    all(|ctx| {
        ctx.map(|metadata| match metadata {
            FileMetadata::Symlink { mtime, target } => Some(SymlinkMetadata { mtime, target }),
            _ => None,
        })
        .to(why(
            be_some(),
            "this is not the metadata for a symbolic link",
        ))
    })
}

struct HaveSameContentsMatcher<Expected, Actual> {
    expected: Expected,
    marker: PhantomData<Actual>,
}

impl<Expected, Actual> TransformMatch for HaveSameContentsMatcher<Expected, Actual>
where
    Actual: AsRef<Path> + std::fmt::Debug,
    Expected: AsRef<Path> + std::fmt::Debug,
{
    type In = Actual;
    type PosOut = Actual;
    type NegOut = Actual;
    type PosFail = Vec<DiffSegment>;
    type NegFail = ();

    fn match_pos(
        self,
        actual: Self::In,
    ) -> xpct::Result<MatchOutcome<Self::PosOut, Self::PosFail>> {
        let actual_contents = fs::read_to_string(actual.as_ref())?;
        let expected_contents = fs::read_to_string(self.expected.as_ref())?;

        if actual_contents == expected_contents {
            Ok(MatchOutcome::Success(actual))
        } else {
            let diff = actual_contents.diff(expected_contents);
            Ok(MatchOutcome::Fail(diff))
        }
    }

    fn match_neg(
        self,
        actual: Self::In,
    ) -> xpct::Result<MatchOutcome<Self::NegOut, Self::NegFail>> {
        let actual_contents = fs::read_to_string(actual.as_ref())?;
        let expected_contents = fs::read_to_string(self.expected.as_ref())?;

        if actual_contents != expected_contents {
            Ok(MatchOutcome::Success(actual))
        } else {
            Ok(MatchOutcome::Fail(()))
        }
    }
}

pub fn have_same_contents<'a, Actual, Expected>(expected: Expected) -> Matcher<'a, Actual, Actual>
where
    Actual: std::fmt::Debug + AsRef<Path> + 'a,
    Expected: std::fmt::Debug + AsRef<Path> + 'a,
{
    let matcher = HaveSameContentsMatcher {
        expected,
        marker: PhantomData,
    };

    Matcher::transform(
        matcher,
        DispatchFormat::new(
            DiffFormat::<String, String>::new(DiffStyle::provided()),
            MessageFormat::new("", "Expected these to have different contents"),
        ),
    )
}

struct HaveSamePermissionsMatcher<Expected, Actual> {
    expected: Expected,
    marker: PhantomData<Actual>,
}

impl<Expected, Actual> TransformMatch for HaveSamePermissionsMatcher<Expected, Actual>
where
    Actual: AsRef<Path> + std::fmt::Debug,
    Expected: AsRef<Path> + std::fmt::Debug,
{
    type In = Actual;
    type PosOut = Actual;
    type NegOut = Actual;
    type PosFail = Mismatch<fs::Permissions, fs::Permissions>;
    type NegFail = ();

    fn match_pos(
        self,
        actual: Self::In,
    ) -> xpct::Result<MatchOutcome<Self::PosOut, Self::PosFail>> {
        let actual_permissions = fs::symlink_metadata(actual.as_ref())?.permissions();
        let expected_permissions = fs::symlink_metadata(self.expected.as_ref())?.permissions();

        if actual_permissions == expected_permissions {
            Ok(MatchOutcome::Success(actual))
        } else {
            Ok(MatchOutcome::Fail(Mismatch {
                actual: actual_permissions,
                expected: expected_permissions,
            }))
        }
    }

    fn match_neg(
        self,
        actual: Self::In,
    ) -> xpct::Result<MatchOutcome<Self::NegOut, Self::NegFail>> {
        let actual_permissions = fs::symlink_metadata(actual.as_ref())?.permissions();
        let expected_permissions = fs::symlink_metadata(self.expected.as_ref())?.permissions();

        if actual_permissions != expected_permissions {
            Ok(MatchOutcome::Success(actual))
        } else {
            Ok(MatchOutcome::Fail(()))
        }
    }
}

pub fn have_same_permissions<'a, Actual, Expected>(
    expected: Expected,
) -> Matcher<'a, Actual, Actual>
where
    Actual: std::fmt::Debug + AsRef<Path> + 'a,
    Expected: std::fmt::Debug + AsRef<Path> + 'a,
{
    let matcher = HaveSamePermissionsMatcher {
        expected,
        marker: PhantomData,
    };

    Matcher::transform(
        matcher,
        DispatchFormat::new(
            MismatchFormat::new("to be the same permissions as", ""),
            MessageFormat::new("", "Expected these to have different permissions"),
        ),
    )
}

struct HaveSameMtimeMatcher<Expected, Actual> {
    expected: Expected,
    marker: PhantomData<Actual>,
}

impl<Expected, Actual> TransformMatch for HaveSameMtimeMatcher<Expected, Actual>
where
    Actual: AsRef<Path> + std::fmt::Debug,
    Expected: AsRef<Path> + std::fmt::Debug,
{
    type In = Actual;
    type PosOut = Actual;
    type NegOut = Actual;
    type PosFail = Mismatch<SystemTime, SystemTime>;
    type NegFail = ();

    fn match_pos(
        self,
        actual: Self::In,
    ) -> xpct::Result<MatchOutcome<Self::PosOut, Self::PosFail>> {
        let actual_mtime = fs::symlink_metadata(actual.as_ref())?.modified()?;
        let expected_mtime = fs::symlink_metadata(self.expected.as_ref())?.modified()?;

        if actual_mtime == expected_mtime {
            Ok(MatchOutcome::Success(actual))
        } else {
            Ok(MatchOutcome::Fail(Mismatch {
                actual: actual_mtime,
                expected: expected_mtime,
            }))
        }
    }

    fn match_neg(
        self,
        actual: Self::In,
    ) -> xpct::Result<MatchOutcome<Self::NegOut, Self::NegFail>> {
        let actual_mtime = fs::symlink_metadata(actual.as_ref())?.modified()?;
        let expected_mtime = fs::symlink_metadata(self.expected.as_ref())?.modified()?;

        if actual_mtime != expected_mtime {
            Ok(MatchOutcome::Success(actual))
        } else {
            Ok(MatchOutcome::Fail(()))
        }
    }
}

pub fn have_same_mtime<'a, Actual, Expected>(expected: Expected) -> Matcher<'a, Actual, Actual>
where
    Actual: std::fmt::Debug + AsRef<Path> + 'a,
    Expected: std::fmt::Debug + AsRef<Path> + 'a,
{
    let matcher = HaveSamePermissionsMatcher {
        expected,
        marker: PhantomData,
    };

    Matcher::transform(
        matcher,
        DispatchFormat::new(
            MismatchFormat::new("to be the same mtime as", ""),
            MessageFormat::new("", "Expected these to have different mtimes"),
        ),
    )
}

struct HaveSameSymlinkTargetMatcher<Expected, Actual> {
    expected: Expected,
    marker: PhantomData<Actual>,
}

impl<Expected, Actual> TransformMatch for HaveSameSymlinkTargetMatcher<Expected, Actual>
where
    Actual: AsRef<Path> + std::fmt::Debug,
    Expected: AsRef<Path> + std::fmt::Debug,
{
    type In = Actual;
    type PosOut = Actual;
    type NegOut = Actual;
    type PosFail = Mismatch<PathBuf, PathBuf>;
    type NegFail = ();

    fn match_pos(
        self,
        actual: Self::In,
    ) -> xpct::Result<MatchOutcome<Self::PosOut, Self::PosFail>> {
        let actual_target = fs::read_link(actual.as_ref())?;
        let expected_target = fs::read_link(self.expected.as_ref())?;

        if actual_target == expected_target {
            Ok(MatchOutcome::Success(actual))
        } else {
            Ok(MatchOutcome::Fail(Mismatch {
                actual: actual_target,
                expected: expected_target,
            }))
        }
    }

    fn match_neg(
        self,
        actual: Self::In,
    ) -> xpct::Result<MatchOutcome<Self::NegOut, Self::NegFail>> {
        let actual_target = fs::read_link(actual.as_ref())?;
        let expected_target = fs::read_link(self.expected.as_ref())?;

        if actual_target != expected_target {
            Ok(MatchOutcome::Success(actual))
        } else {
            Ok(MatchOutcome::Fail(()))
        }
    }
}

pub fn have_same_symlink_target<'a, Actual, Expected>(
    expected: Expected,
) -> Matcher<'a, Actual, Actual>
where
    Actual: std::fmt::Debug + AsRef<Path> + 'a,
    Expected: std::fmt::Debug + AsRef<Path> + 'a,
{
    let matcher = HaveSameSymlinkTargetMatcher {
        expected,
        marker: PhantomData,
    };

    Matcher::transform(
        matcher,
        DispatchFormat::new(
            MismatchFormat::new("to be the same symlink target as", ""),
            MessageFormat::new("", "Expected these to have different symlink targets"),
        ),
    )
}