moonlight_core/compare/
mod.rs1mod 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;