Skip to main content

moonlight_core/compare/
mod.rs

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