Skip to main content

moonlight_core/compare/
mod.rs

1mod capture;
2mod diff;
3mod json_path;
4
5use crate::{target::CapturedTarget, Classification, ComparisonSummary, DiffEntry};
6use std::collections::HashSet;
7
8pub use capture::{
9    capture_body, capture_body_with_redaction_patterns, capture_body_with_redactions,
10    capture_headers, is_hop_by_hop_header,
11};
12
13#[derive(Debug, Clone)]
14pub struct CompareConfig {
15    pub ignore_json_paths: HashSet<String>,
16    pub ignore_json_path_patterns: Vec<String>,
17    pub redact_json_paths: HashSet<String>,
18    pub redact_json_path_patterns: Vec<String>,
19    pub ignore_headers: HashSet<String>,
20    pub ignore_stderr: bool,
21}
22
23impl CompareConfig {
24    pub fn new(
25        ignore_json_paths: &[String],
26        ignore_headers: &[String],
27        ignore_stderr: bool,
28    ) -> Self {
29        Self::new_with_redactions(ignore_json_paths, &[], ignore_headers, ignore_stderr)
30    }
31
32    pub fn new_with_redactions(
33        ignore_json_paths: &[String],
34        redact_json_paths: &[String],
35        ignore_headers: &[String],
36        ignore_stderr: bool,
37    ) -> Self {
38        Self::new_with_patterns(
39            ignore_json_paths,
40            &[],
41            redact_json_paths,
42            &[],
43            ignore_headers,
44            ignore_stderr,
45        )
46    }
47
48    pub fn new_with_patterns(
49        ignore_json_paths: &[String],
50        ignore_json_path_patterns: &[String],
51        redact_json_paths: &[String],
52        redact_json_path_patterns: &[String],
53        ignore_headers: &[String],
54        ignore_stderr: bool,
55    ) -> Self {
56        Self {
57            ignore_json_paths: ignore_json_paths.iter().cloned().collect(),
58            ignore_json_path_patterns: ignore_json_path_patterns.to_vec(),
59            redact_json_paths: redact_json_paths.iter().cloned().collect(),
60            redact_json_path_patterns: redact_json_path_patterns.to_vec(),
61            ignore_headers: ignore_headers
62                .iter()
63                .map(|value| value.to_ascii_lowercase())
64                .collect(),
65            ignore_stderr,
66        }
67    }
68}
69
70pub fn compare_targets(
71    primary: &CapturedTarget,
72    candidate: &CapturedTarget,
73    secondary: Option<&CapturedTarget>,
74    config: &CompareConfig,
75) -> ComparisonSummary {
76    let raw_candidate_diffs =
77        diff::diff_pair(primary, candidate, diff::TargetRole::Candidate, config);
78    let reference_noise = secondary
79        .map(|secondary| diff::diff_pair(primary, secondary, diff::TargetRole::Secondary, config))
80        .unwrap_or_default();
81    let noise_filtered_diffs = filter_candidate_diffs(&raw_candidate_diffs, &reference_noise);
82
83    let target_error = primary.observation.error.is_some()
84        || candidate.observation.error.is_some()
85        || secondary
86            .and_then(|target| target.observation.error.as_ref())
87            .is_some();
88
89    let classification = if target_error {
90        Classification::TargetError
91    } else if raw_candidate_diffs.is_empty() && reference_noise.is_empty() {
92        Classification::Match
93    } else if noise_filtered_diffs.is_empty() {
94        Classification::ReferenceNoise
95    } else if !reference_noise.is_empty() {
96        Classification::SuspiciousWithNoise
97    } else {
98        Classification::SuspiciousDifference
99    };
100
101    ComparisonSummary {
102        classification,
103        raw_diff_summary: summarize("candidate", &raw_candidate_diffs),
104        noise_summary: summarize("reference noise", &reference_noise),
105        raw_candidate_diffs,
106        reference_noise,
107        noise_filtered_diffs,
108    }
109}
110
111fn summarize(label: &str, diffs: &[DiffEntry]) -> String {
112    if diffs.is_empty() {
113        format!("no {label} diffs")
114    } else {
115        format!("{label}: {} diff(s)", diffs.len())
116    }
117}
118
119fn filter_candidate_diffs(
120    candidate_diffs: &[DiffEntry],
121    reference_noise: &[DiffEntry],
122) -> Vec<DiffEntry> {
123    candidate_diffs
124        .iter()
125        .filter(|candidate_diff| {
126            let Some(reference_diff) = reference_noise.iter().find(|reference_diff| {
127                reference_diff.kind == candidate_diff.kind
128                    && reference_diff.path == candidate_diff.path
129            }) else {
130                return true;
131            };
132
133            candidate_diff.candidate != candidate_diff.primary
134                && candidate_diff.candidate != reference_diff.secondary
135        })
136        .cloned()
137        .collect()
138}
139
140#[cfg(test)]
141mod tests;