diff --git a/src/csv/mod.rs b/src/csv/mod.rs index 33a67d3..55ac8c0 100644 --- a/src/csv/mod.rs +++ b/src/csv/mod.rs @@ -489,23 +489,21 @@ pub(crate) fn compare_paths( nominal: impl AsRef, actual: impl AsRef, config: &CSVCompareConfig, -) -> Result { +) -> Result { let nominal_file = fat_io_wrap_std(nominal.as_ref(), &File::open)?; let actual_file = fat_io_wrap_std(actual.as_ref(), &File::open)?; - let (nominal_table, actual_table, results) = - get_diffs_readers(&nominal_file, &actual_file, config)?; + let (_, _, results) = get_diffs_readers(&nominal_file, &actual_file, config)?; results.iter().for_each(|error| { error!("{}", &error); }); - - Ok(report::write_csv_detail( - nominal_table, - actual_table, - nominal.as_ref(), - actual.as_ref(), - results.as_slice(), - )?) + let is_error = !results.is_empty(); + let result = report::Difference { + file_path: nominal.as_ref().to_path_buf(), + is_error, + detail: results.into_iter().map(report::DiffDetail::CSV).collect(), + }; + Ok(result) } #[cfg(test)] diff --git a/src/external.rs b/src/external.rs index 79577b9..f3a2e05 100644 --- a/src/external.rs +++ b/src/external.rs @@ -1,5 +1,5 @@ -use crate::report::FileCompareResult; -use crate::{report, Error}; +use crate::report::{DiffDetail, Difference}; +use crate::Error; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::path::Path; @@ -17,46 +17,35 @@ pub(crate) fn compare_files>( nominal: P, actual: P, config: &ExternalConfig, -) -> Result { +) -> Result { + let mut diff = Difference::new_for_file(&nominal); let compared_file_name = nominal.as_ref().to_string_lossy().into_owned(); - let mut is_error = false; let output = std::process::Command::new(&config.executable) .args(&config.extra_params) .arg(nominal.as_ref()) .arg(actual.as_ref()) .output(); - let (stdout_string, stderr_string, error_message) = if let Ok(output) = output { + if let Ok(output) = output { let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); info!("External stdout: {}", stdout.as_str()); info!("External stderr: {}", stderr.as_str()); - let error_message = if !output.status.success() { + if !output.status.success() { let message = format!("External checker denied file {}", &compared_file_name); error!("{}", &message); - is_error = true; - message - } else { - "".to_owned() + diff.push_detail(DiffDetail::External { stdout, stderr }); + diff.error(); }; - - (stdout, stderr, error_message) } else { let error_message = format!( "External checker execution failed for file {}", &compared_file_name ); error!("{}", error_message); - is_error = true; - ("".to_owned(), "".to_owned(), error_message) + diff.push_detail(DiffDetail::Error(error_message)); + diff.error(); }; - Ok(report::write_external_detail( - nominal, - actual, - is_error, - &stdout_string, - &stderr_string, - &error_message, - )?) + Ok(diff) } #[cfg(test)] mod tests { diff --git a/src/hash.rs b/src/hash.rs index ef84eb5..8de94cf 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -1,6 +1,7 @@ use crate::{report, Deserialize, Serialize}; use data_encoding::HEXLOWER; +use crate::report::{DiffDetail, Difference}; use schemars_derive::JsonSchema; use std::fs::File; use std::io::Read; @@ -62,7 +63,7 @@ pub fn compare_files>( nominal_path: P, actual_path: P, config: &HashConfig, -) -> Result { +) -> Result { let act = config .function .hash_file(fat_io_wrap_std(actual_path.as_ref(), &File::open)?)?; @@ -70,21 +71,15 @@ pub fn compare_files>( .function .hash_file(fat_io_wrap_std(nominal_path.as_ref(), &File::open)?)?; - let diff = if act != nom { - vec![format!( - "Nominal file's hash is '{}' actual is '{}'", - HEXLOWER.encode(&nom), - HEXLOWER.encode(&act) - )] - } else { - vec![] - }; - - Ok(report::write_html_detail( - nominal_path, - actual_path, - diff.as_slice(), - )?) + let mut difference = Difference::new_for_file(nominal_path); + if act != nom { + difference.push_detail(DiffDetail::Hash { + actual: HEXLOWER.encode(&act), + nominal: HEXLOWER.encode(&nom), + }); + difference.error(); + } + Ok(difference) } #[cfg(test)] diff --git a/src/html.rs b/src/html.rs index b458742..5209edb 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,4 +1,5 @@ use crate::report; +use crate::report::{DiffDetail, Difference}; use regex::Regex; use schemars_derive::JsonSchema; use serde::{Deserialize, Serialize}; @@ -58,14 +59,12 @@ pub fn compare_files>( nominal_path: P, actual_path: P, config: &HTMLCompareConfig, -) -> Result { +) -> Result { let actual = BufReader::new(fat_io_wrap_std(actual_path.as_ref(), &File::open)?); let nominal = BufReader::new(fat_io_wrap_std(nominal_path.as_ref(), &File::open)?); - let mut diffs: Vec = Vec::new(); - let exclusion_list = config.get_ignore_list()?; - + let mut difference = Difference::new_for_file(nominal_path); actual .lines() .enumerate() @@ -84,16 +83,12 @@ pub fn compare_files>( ); error!("{}" , &error); - - diffs.push(error); + difference.push_detail(DiffDetail::Text {score: distance, line: l}); + difference.error(); } }); - Ok(report::write_html_detail( - nominal_path.as_ref(), - actual_path.as_ref(), - &diffs, - )?) + Ok(difference) } #[cfg(test)] diff --git a/src/image.rs b/src/image.rs index ba30969..929f7ad 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,3 +1,4 @@ +use crate::report::DiffDetail; use crate::{get_file_name, report}; use schemars_derive::JsonSchema; use serde::{Deserialize, Serialize}; @@ -41,8 +42,7 @@ pub fn compare_paths>( nominal_path: P, actual_path: P, config: &ImageCompareConfig, -) -> Result { - let mut diffs: Vec = Vec::new(); +) -> Result { let nominal = image::open(nominal_path.as_ref())?.into_rgba8(); let actual = image::open(actual_path.as_ref())?.into_rgba8(); @@ -53,6 +53,7 @@ pub fn compare_paths>( nominal_path.as_ref() )))?; let out_path = (nominal_file_name + "diff_image.png").to_string(); + let mut result_diff = report::Difference::new_for_file(&nominal_path); if result.score < config.threshold { let color_map = result.image.to_color_map(); @@ -64,18 +65,14 @@ pub fn compare_paths>( config.threshold, result.score ); - error!("{}", &error_message); - - diffs.push(error_message); - diffs.push(out_path); + result_diff.push_detail(DiffDetail::Image { + diff_image: out_path, + score: result.score, + }); + result_diff.error(); } - - Ok(report::write_image_detail( - nominal_path.as_ref(), - actual_path.as_ref(), - &diffs, - )?) + Ok(result_diff) } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index c7a5f35..787593b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ mod report; use crate::external::ExternalConfig; pub use crate::html::HTMLCompareConfig; use crate::properties::PropertiesConfig; -use crate::report::FileCompareResult; +use crate::report::{DiffDetail, Difference}; use schemars::schema_for; use schemars_derive::JsonSchema; use serde::{Deserialize, Serialize}; @@ -161,11 +161,7 @@ fn filter_exclude(paths: Vec, excludes: Vec) -> Vec { .collect() } -fn process_file( - nominal: impl AsRef, - actual: impl AsRef, - rule: &Rule, -) -> FileCompareResult { +fn process_file(nominal: impl AsRef, actual: impl AsRef, rule: &Rule) -> Difference { let file_name_nominal = nominal.as_ref().to_string_lossy(); let file_name_actual = actual.as_ref().to_string_lossy(); let _file_span = span!(tracing::Level::INFO, "Processing"); @@ -173,7 +169,7 @@ fn process_file( info!("File: {file_name_nominal} | {file_name_actual}"); - let compare_result: Result> = { + let compare_result: Result> = { match &rule.file_type { ComparisonMode::CSV(conf) => { csv::compare_paths(nominal.as_ref(), actual.as_ref(), conf).map_err(|e| e.into()) @@ -200,22 +196,25 @@ fn process_file( } } }; - - match compare_result { - Ok(result) => { - if result.is_error { - error!("Files didn't match"); - } else { - debug!("Files matched"); - } - - result - } + let compare_result = match compare_result { + Ok(r) => r, Err(e) => { - error!("Problem comparing the files"); - report::write_error_detail(nominal, actual, e) + let e = e.to_string(); + error!("Problem comparing the files {}", &e); + let mut d = Difference::new_for_file(nominal); + d.error(); + d.push_detail(DiffDetail::Error(e)); + d } + }; + + if compare_result.is_error { + error!("Files didn't match"); + } else { + debug!("Files matched"); } + + compare_result } fn get_files( @@ -232,7 +231,7 @@ fn process_rule( nominal: impl AsRef, actual: impl AsRef, rule: &Rule, - compare_results: &mut Vec, + compare_results: &mut Vec, ) -> Result { let _file_span = span!(tracing::Level::INFO, "Rule"); let _file_span = _file_span.enter(); @@ -276,9 +275,7 @@ fn process_rule( .zip(actual_cleaned_paths.into_iter()) .for_each(|(n, a)| { let compare_result = process_file(n, a, rule); - all_okay &= !compare_result.is_error; - compare_results.push(compare_result); }); @@ -292,13 +289,13 @@ pub fn compare_folders_cfg( config_struct: ConfigurationFile, report_path: impl AsRef, ) -> Result { - let mut rule_results: Vec = Vec::new(); + let mut rule_results: Vec = Vec::new(); let results: Vec = config_struct .rules .into_iter() .map(|rule| { - let mut compare_results: Vec = Vec::new(); + let mut compare_results: Vec = Vec::new(); let okay = process_rule( nominal.as_ref(), actual.as_ref(), @@ -315,9 +312,9 @@ pub fn compare_folders_cfg( false } }; - rule_results.push(report::RuleResult { + rule_results.push(report::RuleDifferences { rule, - compare_results, + diffs: compare_results, }); result @@ -325,7 +322,7 @@ pub fn compare_folders_cfg( .collect(); let all_okay = results.iter().all(|result| *result); - report::create(&rule_results, report_path)?; + report::create_json(&rule_results, report_path)?; Ok(all_okay) } diff --git a/src/pdf.rs b/src/pdf.rs index bdb620f..d5e840e 100644 --- a/src/pdf.rs +++ b/src/pdf.rs @@ -1,5 +1,6 @@ use crate::html::HTMLCompareConfig; use crate::report; +use crate::report::{DiffDetail, Difference}; use pdf_extract::extract_text; use std::path::Path; use strsim::normalized_damerau_levenshtein; @@ -24,17 +25,15 @@ pub fn compare_files>( nominal_path: P, actual_path: P, config: &HTMLCompareConfig, -) -> Result { +) -> Result { info!("Extracting text from actual pdf"); let actual = extract_text(actual_path.as_ref())?; info!("Extracting text from nominal pdf"); let nominal = extract_text(nominal_path.as_ref())?; - let mut diffs: Vec<(usize, String)> = Vec::new(); - let exclusion_list = config.get_ignore_list()?; - + let mut difference = Difference::new_for_file(&nominal); actual .lines() .enumerate() @@ -52,18 +51,12 @@ pub fn compare_files>( ); error!("{}" , &error); - - diffs.push((l, error)); + difference.push_detail(DiffDetail::Text {score: distance, line: l}); + difference.error(); } }); - Ok(report::write_pdf_detail( - nominal_path.as_ref(), - actual_path.as_ref(), - &nominal, - &actual, - &diffs, - )?) + Ok(difference) } #[cfg(test)] diff --git a/src/properties.rs b/src/properties.rs index 571f737..1ca0e57 100644 --- a/src/properties.rs +++ b/src/properties.rs @@ -1,4 +1,4 @@ -use crate::report::{get_relative_path, AdditionalOverviewColumn, FileCompareResult}; +use crate::report::{get_relative_path, DiffDetail, Difference}; use crate::Error; use chrono::offset::Utc; use chrono::DateTime; @@ -23,44 +23,51 @@ pub struct PropertiesConfig { forbid_name_regex: Option, } +#[derive(Serialize, Debug, Clone)] +pub enum MetaDataPropertyDiff { + Size { nominal: u64, actual: u64 }, + IllegalName, + CreationDate { nominal: String, actual: String }, +} + fn regex_matches_any_path( nominal_path: &str, actual_path: &str, regex: &str, -) -> Result { - let mut result: AdditionalOverviewColumn = Default::default(); +) -> Result, Error> { let regex = Regex::new(regex)?; if regex.is_match(nominal_path) || regex.is_match(actual_path) { error!("One of the files ({nominal_path}, {actual_path}) matched the regex {regex}"); + let mut result = Difference::new_for_file(nominal_path); + result.error(); + result.push_detail(DiffDetail::Properties(MetaDataPropertyDiff::IllegalName)); result.is_error = true; - return Ok(result); + return Ok(Some(result)); } - Ok(result) + Ok(None) } -fn file_size_out_of_tolerance( - nominal: &Path, - actual: &Path, - tolerance: u64, -) -> AdditionalOverviewColumn { - let mut result: AdditionalOverviewColumn = Default::default(); +fn file_size_out_of_tolerance(nominal: &Path, actual: &Path, tolerance: u64) -> Difference { + let mut result = Difference::new_for_file(nominal); if let (Ok(nominal_meta), Ok(actual_meta)) = (nominal.metadata(), actual.metadata()) { let size_diff = (nominal_meta.len() as i128 - actual_meta.len() as i128).unsigned_abs() as u64; if size_diff > tolerance { error!("File size tolerance exceeded, diff is {size_diff}, tolerance was {tolerance}"); - result.is_error = true; + result.error(); } - - result.nominal_value = nominal_meta.len().to_string(); - result.actual_value = actual_meta.len().to_string(); - result.diff_value = size_diff.to_string(); + result.push_detail(DiffDetail::Properties(MetaDataPropertyDiff::Size { + nominal: nominal_meta.len(), + actual: actual_meta.len(), + })); } else { - error!( + let msg = format!( "Could not get file metadata for either: {} or {}", &nominal.to_string_lossy(), &actual.to_string_lossy() ); + error!("{}", &msg); + result.push_detail(DiffDetail::Error(msg)); result.is_error = true; } result @@ -70,16 +77,18 @@ fn file_modification_time_out_of_tolerance( nominal: &Path, actual: &Path, tolerance: u64, -) -> AdditionalOverviewColumn { - let mut result: AdditionalOverviewColumn = Default::default(); +) -> Difference { + let mut result = Difference::new_for_file(nominal); if let (Ok(nominal_meta), Ok(actual_meta)) = (nominal.metadata(), actual.metadata()) { if let (Ok(mod_time_act), Ok(mod_time_nom)) = (nominal_meta.modified(), actual_meta.modified()) { let nominal_datetime: DateTime = mod_time_nom.into(); let actual_datetime: DateTime = mod_time_act.into(); - result.nominal_value = nominal_datetime.format("%Y-%m-%d %T").to_string(); - result.actual_value = actual_datetime.format("%Y-%m-%d %T").to_string(); + result.push_detail(DiffDetail::Properties(MetaDataPropertyDiff::CreationDate { + nominal: nominal_datetime.format("%Y-%m-%d %T").to_string(), + actual: actual_datetime.format("%Y-%m-%d %T").to_string(), + })); let now = SystemTime::now(); @@ -93,22 +102,27 @@ fn file_modification_time_out_of_tolerance( error!("Modification times too far off difference in timestamps {time_diff} s - tolerance {tolerance} s"); result.is_error = true; } - - result.diff_value = time_diff.to_string(); } else { - error!("Could not calculate duration between modification timestamps"); + let msg = format!("Could not calculate duration between modification timestamps"); + error!("{}", &msg); + result.push_detail(DiffDetail::Error(msg)); result.is_error = true; } } else { - error!("Could not read file modification timestamps"); + let msg = format!("Could not read file modification timestamps"); + error!("{}", &msg); + result.push_detail(DiffDetail::Error(msg)); result.is_error = true; } } else { - error!( + let msg = format!( "Could not get file metadata for either: {} or {}", &nominal.to_string_lossy(), &actual.to_string_lossy() ); + error!("{}", &msg); + result.push_detail(DiffDetail::Error(msg)); + result.is_error = true; } result @@ -118,51 +132,34 @@ pub(crate) fn compare_files>( nominal: P, actual: P, config: &PropertiesConfig, -) -> Result { +) -> Result { let nominal = nominal.as_ref(); let actual = actual.as_ref(); - let mut is_error = false; let compared_file_name_full = nominal.to_string_lossy(); let actual_file_name_full = actual.to_string_lossy(); - let compared_file_name = get_relative_path(actual, nominal) + get_relative_path(actual, nominal) .to_string_lossy() .to_string(); - let mut additional_columns: Vec = Vec::new(); - - let result: AdditionalOverviewColumn = - if let Some(name_regex) = config.forbid_name_regex.as_deref() { - regex_matches_any_path(&compared_file_name_full, &actual_file_name_full, name_regex)? - } else { - Default::default() - }; - is_error |= result.is_error; - additional_columns.push(result); - - let result: AdditionalOverviewColumn = if let Some(tolerance) = config.file_size_tolerance_bytes - { - file_size_out_of_tolerance(nominal, actual, tolerance) + let mut total_diff = Difference::new_for_file(nominal); + let result = if let Some(name_regex) = config.forbid_name_regex.as_deref() { + regex_matches_any_path(&compared_file_name_full, &actual_file_name_full, name_regex)? } else { - Default::default() + None }; - is_error |= result.is_error; - additional_columns.push(result); + result.map(|r| total_diff.join(r)); - let result: AdditionalOverviewColumn = - if let Some(tolerance) = config.modification_date_tolerance_secs { - file_modification_time_out_of_tolerance(nominal, actual, tolerance) - } else { - Default::default() - }; - is_error |= result.is_error; - additional_columns.push(result); - - Ok(FileCompareResult { - compared_file_name, - is_error, - detail_path: None, - additional_columns, - }) + let result = config + .file_size_tolerance_bytes + .map(|tolerance| file_size_out_of_tolerance(nominal, actual, tolerance)); + result.map(|r| total_diff.join(r)); + + let result = config + .modification_date_tolerance_secs + .map(|tolerance| file_modification_time_out_of_tolerance(nominal, actual, tolerance)); + result.map(|r| total_diff.join(r)); + + Ok(total_diff) } #[cfg(test)] diff --git a/src/report/mod.rs b/src/report/mod.rs index 17c10d8..af22890 100644 --- a/src/report/mod.rs +++ b/src/report/mod.rs @@ -1,6 +1,7 @@ mod template; use crate::csv::{DiffType, Position, Table}; +use crate::properties::MetaDataPropertyDiff; use crate::Rule; use serde::Serialize; use std::borrow::Cow; @@ -28,6 +29,8 @@ pub enum Error { IOIssue(#[from] std::io::Error), #[error("fs_extra crate error {0}")] FsExtraFailed(#[from] fs_extra::error::Error), + #[error("JSON serialization failed {0}")] + SerdeError(#[from] serde_json::Error), } #[derive(Serialize, Debug)] @@ -79,22 +82,60 @@ pub struct Report { } #[derive(Serialize, Debug, Clone)] +pub struct RuleDifferences { + pub rule: Rule, + pub diffs: Vec, +} + +#[derive(Serialize, Debug, Clone, Default)] pub struct Difference { - file_path: PathBuf, - rule_name: String, - is_error: bool, - detail: DiffDetail, + pub file_path: PathBuf, + pub is_error: bool, + pub detail: Vec, +} + +impl Difference { + pub fn new_for_file(f: impl AsRef) -> Self { + Self { + file_path: f.as_ref().to_path_buf(), + ..Default::default() + } + } + + pub fn error(&mut self) { + self.is_error = true; + } + + pub fn push_detail(&mut self, detail: DiffDetail) { + self.detail.push(detail); + } + + pub fn join(&mut self, other: Self) -> bool { + if self.file_path != other.file_path { + return false; + } + self.is_error |= other.is_error; + self.detail.extend(other.detail.into_iter()); + true + } } #[derive(Serialize, Debug, Clone)] #[allow(clippy::upper_case_acronyms)] pub enum DiffDetail { CSV(DiffType), - Image { score: f64, diff_image: PathBuf }, + Image { score: f64, diff_image: String }, Text { line: usize, score: f64 }, Hash { actual: String, nominal: String }, External { stdout: String, stderr: String }, - Properties, + Properties(MetaDataPropertyDiff), + Error(String), +} + +impl From for DiffDetail { + fn from(value: T) -> Self { + DiffDetail::Error(value.to_string()) + } } pub fn create_sub_folder() -> Result { @@ -544,6 +585,26 @@ pub fn write_error_detail( result } +pub(crate) fn create_json( + rule_results: &[RuleDifferences], + report_path: impl AsRef, +) -> Result<(), Error> { + let _reporting_span = span!(tracing::Level::INFO, "JSON Reporting"); + let _reporting_span = _reporting_span.enter(); + let report_dir = report_path.as_ref(); + if report_dir.is_dir() { + info!("Delete report folder"); + fat_io_wrap_std(&report_dir, &fs::remove_dir_all)?; + } + + info!("create report folder"); + fat_io_wrap_std(&report_dir, &fs::create_dir)?; + let writer = report_dir.join("report.json"); + let writer = fat_io_wrap_std(writer, &File::create)?; + serde_json::to_writer_pretty(writer, &rule_results)?; + Ok(()) +} + pub(crate) fn create( rule_results: &[RuleResult], report_path: impl AsRef, diff --git a/tests/integ.rs b/tests/integ.rs index 6e4f84c..fd02995 100644 --- a/tests/integ.rs +++ b/tests/integ.rs @@ -11,8 +11,8 @@ fn simple_test_identity() { #[test] fn display_of_status_message_in_cm_tables() { - let report_dir = - tempfile::tempdir().expect("Could not generate temporary directory for report"); + let report_dir = "/tmp/test_report/"; + // tempfile::tempdir().expect("Could not generate temporary directory for report"); assert!(compare_folders( "tests/integ/data/display_of_status_message_in_cm_tables/expected/",