pkgs-cli 0.2.0

A simple cli tool to manage packages.
Documentation
use std::fmt::Debug;
use std::path::{Path, PathBuf};

use googletest::{
    description::Description,
    matcher::{Matcher, MatcherBase, MatcherResult},
};

pub fn is_symlink_for(src: impl AsRef<Path>) -> IsSymlinkForMatcher {
    IsSymlinkForMatcher {
        src: src.as_ref().to_path_buf(),
    }
}

#[derive(MatcherBase)]
pub struct IsSymlinkForMatcher {
    src: PathBuf,
}

impl<T> Matcher<T> for IsSymlinkForMatcher
where
    T: Debug + Copy + AsRef<Path>,
{
    fn matches(&self, actual: T) -> MatcherResult {
        let actual = actual.as_ref();
        if actual.is_symlink() && actual.read_link().is_ok_and(|link| link == self.src) {
            MatcherResult::Match
        } else {
            MatcherResult::NoMatch
        }
    }

    fn describe(&self, matcher_result: MatcherResult) -> Description {
        format!(
            "{} symlink for '{}'",
            match matcher_result {
                MatcherResult::Match => "is",
                MatcherResult::NoMatch => "isn't",
            },
            self.src.to_string_lossy()
        )
        .into()
    }

    fn explain_match(&self, actual: T) -> Description {
        let actual = actual.as_ref();
        if !actual.exists() {
            return "which doesn't exist".into();
        }
        if !actual.is_symlink() {
            return "which isn't a symlink".into();
        }

        let link = actual.read_link().unwrap();
        if link == self.src {
            "which is a symlink pointing to the targeted path".into()
        } else {
            format!(
                "which is a symlink but points to a different path '{}'",
                link.to_string_lossy()
            )
            .into()
        }
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use googletest::prelude::*;
    use indoc::formatdoc;

    use super::*;
    use crate::fs::create_symlink;

    #[gtest]
    fn it_works() -> Result<()> {
        let td = tempfile::tempdir()?;

        let src = td.path().join("src");
        fs::create_dir(td.path().join("src"))?;

        let dst = td.path().join("dst");
        create_symlink(&src, &dst)?;

        verify_that!(dst, is_symlink_for(src))
    }

    #[gtest]
    fn negative() -> Result<()> {
        let td = tempfile::tempdir()?;

        let src = td.path().join("src");
        fs::create_dir(&src)?;

        let dst = td.path().join("dst");
        create_symlink(&src, &dst)?;

        let result = verify_that!(dst, not(is_symlink_for(&src)));
        verify_that!(
            result,
            err(displays_as(contains_substring(formatdoc! {r#"
                Value of: dst
                Expected: isn't symlink for '{}'
                Actual: "{}",
                  which is a symlink pointing to the targeted path
            "#,
                src.to_string_lossy(),
                dst.to_string_lossy(),
            })))
        )
    }

    #[gtest]
    fn mismatch_the_same_path_with_different_name() -> Result<()> {
        let td = tempfile::tempdir()?;

        let src = td.path().join("src");
        fs::create_dir(td.path().join("src"))?;

        let dst = td.path().join("dst");
        create_symlink(&src, &dst)?;

        let result = verify_that!(dst, is_symlink_for(src.join("../src")));
        verify_that!(
            result,
            err(displays_as(contains_substring(formatdoc! {r#"
                Value of: dst
                Expected: is symlink for '{}'
                Actual: "{}",
                  which is a symlink but points to a different path '{}'
            "#,
                src.join("../src").to_string_lossy(),
                dst.to_string_lossy(),
                src.to_string_lossy(),
            })))
        )
    }

    #[gtest]
    fn mismatch_path_not_exists() -> Result<()> {
        let td = tempfile::tempdir()?;

        let src = td.path().join("src");
        fs::create_dir(&src)?;

        let dst = td.path().join("non_existent_dst");

        let result = verify_that!(dst, is_symlink_for(&src));
        verify_that!(
            result,
            err(displays_as(contains_substring(formatdoc! {r#"
                Value of: dst
                Expected: is symlink for '{}'
                Actual: "{}",
                  which doesn't exist
            "#,
                src.to_string_lossy(),
                dst.to_string_lossy(),
            })))
        )
    }

    #[gtest]
    fn mismatch_path_is_not_a_symlink() -> Result<()> {
        let td = tempfile::tempdir()?;

        let src = td.path().join("src");
        fs::create_dir(&src)?;

        let dst = td.path().join("dst");
        fs::create_dir(&dst)?;

        let result = verify_that!(dst, is_symlink_for(&src));
        verify_that!(
            result,
            err(displays_as(contains_substring(formatdoc! {r#"
                Value of: dst
                Expected: is symlink for '{}'
                Actual: "{}",
                  which isn't a symlink
            "#,
                src.to_string_lossy(),
                dst.to_string_lossy(),
            })))
        )
    }
}