mod capture;
mod diff;
use crate::{Classification, ComparisonSummary, DiffEntry};
use std::collections::HashSet;
pub use capture::{capture_body, capture_headers, is_hop_by_hop_header};
pub use diff::CapturedTarget;
#[derive(Debug, Clone)]
pub struct CompareConfig {
pub ignored_json_paths: HashSet<String>,
pub ignored_headers: HashSet<String>,
pub ignore_stderr: bool,
}
impl CompareConfig {
pub fn new(
ignored_json_paths: &[String],
ignored_headers: &[String],
ignore_stderr: bool,
) -> Self {
Self {
ignored_json_paths: ignored_json_paths.iter().cloned().collect(),
ignored_headers: ignored_headers
.iter()
.map(|value| value.to_ascii_lowercase())
.collect(),
ignore_stderr,
}
}
}
pub fn compare_targets(
primary: &CapturedTarget,
candidate: &CapturedTarget,
secondary: Option<&CapturedTarget>,
config: &CompareConfig,
) -> ComparisonSummary {
let raw_candidate_diffs =
diff::diff_pair(primary, candidate, diff::TargetRole::Candidate, config);
let reference_noise = secondary
.map(|secondary| diff::diff_pair(primary, secondary, diff::TargetRole::Secondary, config))
.unwrap_or_default();
let noise_filtered_diffs = filter_candidate_diffs(&raw_candidate_diffs, &reference_noise);
let target_error = primary.observation.error.is_some()
|| candidate.observation.error.is_some()
|| secondary
.and_then(|target| target.observation.error.as_ref())
.is_some();
let classification = if target_error {
Classification::TargetError
} else if raw_candidate_diffs.is_empty() && reference_noise.is_empty() {
Classification::Match
} else if noise_filtered_diffs.is_empty() {
Classification::ReferenceNoise
} else if !reference_noise.is_empty() {
Classification::SuspiciousWithNoise
} else {
Classification::SuspiciousDifference
};
ComparisonSummary {
classification,
raw_diff_summary: summarize("candidate", &raw_candidate_diffs),
noise_summary: summarize("reference noise", &reference_noise),
raw_candidate_diffs,
reference_noise,
noise_filtered_diffs,
}
}
fn summarize(label: &str, diffs: &[DiffEntry]) -> String {
if diffs.is_empty() {
format!("no {label} diffs")
} else {
format!("{label}: {} diff(s)", diffs.len())
}
}
fn filter_candidate_diffs(
candidate_diffs: &[DiffEntry],
reference_noise: &[DiffEntry],
) -> Vec<DiffEntry> {
candidate_diffs
.iter()
.filter(|candidate_diff| {
let Some(reference_diff) = reference_noise.iter().find(|reference_diff| {
reference_diff.kind == candidate_diff.kind
&& reference_diff.path == candidate_diff.path
}) else {
return true;
};
candidate_diff.candidate != candidate_diff.primary
&& candidate_diff.candidate != reference_diff.secondary
})
.cloned()
.collect()
}
#[cfg(test)]
mod tests;