havocompare 0.8.0

A flexible rule-based file and folder comparison tool and crate including nice html reporting. Compares CSVs, JSON, text files, pdf-texts and images.
Documentation
use crate::report::{DiffDetail, Difference};
use schemars_derive::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path;
use std::path::{Path, PathBuf};
use thiserror::Error;
use tracing::error;

#[derive(Debug, Error)]
/// Errors during html / plain text checking
pub enum Error {
    #[error("Failed to remove path's prefix")]
    StripPrefixError(#[from] path::StripPrefixError),
}

#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
pub struct DirectoryConfig {
    pub mode: Mode,
}

#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
pub enum Mode {
    /// check whether both paths are really the same: whether entry is missing in actual, and/or if entry exists in actual but not in nominal
    Identical,
    /// check only if entry is missing in actual, ignoring entries that exist in actual but not in nominal
    MissingOnly,
}

pub(crate) fn compare_paths<P: AsRef<Path>>(
    nominal: P,
    actual: P,
    nominal_entries: &[PathBuf],
    actual_entries: &[PathBuf],
    config: &DirectoryConfig,
) -> Result<Difference, Error> {
    let nominal_path = nominal.as_ref();
    let actual_path = actual.as_ref();

    let mut difference = Difference::new_for_file(nominal_path, actual_path);

    //remove root paths!
    let nominal_entries: Result<Vec<_>, path::StripPrefixError> = nominal_entries
        .iter()
        .map(|path| path.strip_prefix(nominal_path))
        .collect();
    let nominal_entries = nominal_entries?;

    let actual_entries: Result<Vec<_>, path::StripPrefixError> = actual_entries
        .iter()
        .map(|path| path.strip_prefix(actual_path))
        .collect();
    let actual_entries = actual_entries?;

    let mut is_the_same = true;
    if matches!(config.mode, Mode::Identical | Mode::MissingOnly) {
        nominal_entries.iter().for_each(|entry| {
            let detail = if let Some(f) = actual_entries.iter().find(|a| *a == entry) {
                (f.to_string_lossy().to_string(), false)
            } else {
                error!("{} doesn't exist in the actual folder", entry.display());
                is_the_same = false;
                ("".to_owned(), true)
            };

            difference.push_detail(DiffDetail::File {
                nominal: entry.to_string_lossy().to_string(),
                actual: detail.0,
                error: detail.1,
            });
        });
    }

    if matches!(config.mode, Mode::Identical) {
        actual_entries.iter().for_each(|entry| {
            if !nominal_entries.iter().any(|n| n == entry) {
                difference.push_detail(DiffDetail::File {
                    nominal: "".to_owned(),
                    actual: entry.to_string_lossy().to_string(),
                    error: true,
                });

                error!(
                    "Additional entry {} found in the actual folder",
                    entry.display()
                );
                is_the_same = false;
            }
        });
    }

    if !is_the_same {
        difference.error();
    }

    Ok(difference)
}

#[cfg(test)]

mod test {
    use super::*;

    #[test]
    fn test_compare_directories() {
        let nominal_dir = tempfile::tempdir().expect("Could not create nominal temp dir");

        std::fs::create_dir_all(nominal_dir.path().join("dir/a/aa"))
            .expect("Could not create directory");
        std::fs::create_dir_all(nominal_dir.path().join("dir/b"))
            .expect("Could not create directory");
        std::fs::create_dir_all(nominal_dir.path().join("dir/c"))
            .expect("Could not create directory");

        let actual_dir = tempfile::tempdir().expect("Could not create actual temp dir");

        std::fs::create_dir_all(actual_dir.path().join("dir/a/aa"))
            .expect("Could not create directory");
        std::fs::create_dir_all(actual_dir.path().join("dir/b"))
            .expect("Could not create directory");
        std::fs::create_dir_all(actual_dir.path().join("dir/c"))
            .expect("Could not create directory");

        let pattern_include = ["**/*/"];
        let pattern_exclude: Vec<String> = Vec::new();

        let nominal_entries = crate::get_files(&nominal_dir, &pattern_include, &pattern_exclude)
            .expect("Could not get files");
        let actual_entries = crate::get_files(&actual_dir, &pattern_include, &pattern_exclude)
            .expect("Could not get files");

        let result = compare_paths(
            nominal_dir.path(),
            actual_dir.path(),
            &nominal_entries,
            &actual_entries,
            &DirectoryConfig {
                mode: Mode::Identical,
            },
        )
        .expect("Could not compare paths");

        assert!(!result.is_error);

        std::fs::create_dir_all(actual_dir.path().join("dir/d"))
            .expect("Could not create directory");

        let nominal_entries = crate::get_files(&nominal_dir, &pattern_include, &pattern_exclude)
            .expect("Could not create directory");
        let actual_entries = crate::get_files(&actual_dir, &pattern_include, &pattern_exclude)
            .expect("Could not create directory");

        let result = compare_paths(
            nominal_dir.path(),
            actual_dir.path(),
            &nominal_entries,
            &actual_entries,
            &DirectoryConfig {
                mode: Mode::Identical,
            },
        )
        .expect("Could not compare paths");

        assert!(result.is_error);

        let result = compare_paths(
            nominal_dir.path(),
            actual_dir.path(),
            &nominal_entries,
            &actual_entries,
            &DirectoryConfig {
                mode: Mode::MissingOnly,
            },
        )
        .expect("Could not compare paths");

        assert!(!result.is_error);

        std::fs::create_dir_all(nominal_dir.path().join("dir/d"))
            .expect("Could not create directory");
        std::fs::create_dir_all(nominal_dir.path().join("dir/e"))
            .expect("Could not create directory");

        let nominal_entries = crate::get_files(&nominal_dir, &pattern_include, &pattern_exclude)
            .expect("Could not create directory");
        let actual_entries = crate::get_files(&actual_dir, &pattern_include, &pattern_exclude)
            .expect("Could not create directory");

        let result = compare_paths(
            nominal_dir.path(),
            actual_dir.path(),
            &nominal_entries,
            &actual_entries,
            &DirectoryConfig {
                mode: Mode::Identical,
            },
        )
        .expect("Could not compare paths");

        assert!(result.is_error);

        let result = compare_paths(
            nominal_dir.path(),
            actual_dir.path(),
            &nominal_entries,
            &actual_entries,
            &DirectoryConfig {
                mode: Mode::MissingOnly,
            },
        )
        .expect("Could not compare paths");

        assert!(result.is_error);
    }
}