1#[derive(Debug, Clone)]
9pub struct DiffResult<T: Clone + PartialEq> {
10 pub input_label: String,
11 pub implementation_a: T,
12 pub implementation_b: T,
13 pub matches: bool,
14}
15
16pub fn differential_test<A, B, T>(
19 inputs: &[(String, Vec<u8>)],
20 impl_a: A,
21 impl_b: B,
22) -> Vec<DiffResult<T>>
23where
24 A: Fn(&[u8]) -> T,
25 B: Fn(&[u8]) -> T,
26 T: Clone + PartialEq,
27{
28 inputs
29 .iter()
30 .map(|(label, input)| {
31 let result_a = impl_a(input);
32 let result_b = impl_b(input);
33 DiffResult {
34 input_label: label.clone(),
35 matches: result_a == result_b,
36 implementation_a: result_a,
37 implementation_b: result_b,
38 }
39 })
40 .collect()
41}
42
43#[derive(Debug, Clone)]
45pub struct DiffSummary {
46 pub total: usize,
47 pub matching: usize,
48 pub mismatching: usize,
49 pub mismatches: Vec<String>,
50}
51
52impl DiffSummary {
53 pub fn from_results<T: Clone + PartialEq>(results: &[DiffResult<T>]) -> Self {
54 let total = results.len();
55 let matching = results.iter().filter(|r| r.matches).count();
56 let mismatches: Vec<String> = results
57 .iter()
58 .filter(|r| !r.matches)
59 .map(|r| r.input_label.clone())
60 .collect();
61 Self {
62 total,
63 matching,
64 mismatching: total - matching,
65 mismatches,
66 }
67 }
68
69 pub fn all_match(&self) -> bool {
70 self.mismatching == 0
71 }
72
73 pub fn match_rate(&self) -> f64 {
74 if self.total == 0 {
75 1.0
76 } else {
77 self.matching as f64 / self.total as f64
78 }
79 }
80}
81
82pub fn compare_hashes(
84 inputs: &[Vec<u8>],
85 hash_a: impl Fn(&[u8]) -> Vec<u8>,
86 hash_b: impl Fn(&[u8]) -> Vec<u8>,
87) -> DiffSummary {
88 let results: Vec<DiffResult<Vec<u8>>> = inputs
89 .iter()
90 .enumerate()
91 .map(|(i, input)| {
92 let ra = hash_a(input);
93 let rb = hash_b(input);
94 DiffResult {
95 input_label: format!("input-{i}"),
96 matches: ra == rb,
97 implementation_a: ra,
98 implementation_b: rb,
99 }
100 })
101 .collect();
102 DiffSummary::from_results(&results)
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn matching_implementations() {
111 let inputs: Vec<(String, Vec<u8>)> =
112 vec![("a".into(), vec![1, 2, 3]), ("b".into(), vec![4, 5, 6])];
113 let results = differential_test(
114 &inputs,
115 |input: &[u8]| input.len(),
116 |input: &[u8]| input.len(),
117 );
118 let summary = DiffSummary::from_results(&results);
119 assert!(summary.all_match());
120 assert_eq!(summary.match_rate(), 1.0);
121 }
122
123 #[test]
124 fn mismatching_implementations() {
125 let inputs: Vec<(String, Vec<u8>)> = vec![("a".into(), vec![1]), ("b".into(), vec![2])];
126 let results = differential_test(
127 &inputs,
128 |input: &[u8]| input[0] as u32 * 2,
129 |input: &[u8]| input[0] as u32 * 3,
130 );
131 let summary = DiffSummary::from_results(&results);
132 assert!(!summary.all_match());
133 assert_eq!(summary.mismatching, 2);
134 }
135
136 #[test]
137 fn empty_inputs() {
138 let inputs: Vec<(String, Vec<u8>)> = vec![];
139 let results = differential_test(&inputs, |_: &[u8]| 0u32, |_: &[u8]| 0u32);
140 let summary = DiffSummary::from_results(&results);
141 assert_eq!(summary.total, 0);
142 assert_eq!(summary.match_rate(), 1.0);
143 }
144
145 #[test]
146 fn partial_match() {
147 let inputs: Vec<(String, Vec<u8>)> = vec![("a".into(), vec![0]), ("b".into(), vec![1])];
148 let results = differential_test(
149 &inputs,
150 |input: &[u8]| input[0],
151 |input: &[u8]| if input[0] == 0 { 0 } else { 99 },
152 );
153 let summary = DiffSummary::from_results(&results);
154 assert_eq!(summary.matching, 1);
155 assert_eq!(summary.mismatching, 1);
156 }
157
158 #[test]
159 fn compare_hashes_identical() {
160 let inputs = vec![vec![1, 2, 3], vec![4, 5, 6]];
161 let summary = compare_hashes(&inputs, |d| vec![d.len() as u8], |d| vec![d.len() as u8]);
162 assert!(summary.all_match());
163 }
164
165 #[test]
166 fn compare_hashes_different() {
167 let inputs = vec![vec![1, 2, 3]];
168 let summary = compare_hashes(&inputs, |_| vec![1u8], |_| vec![2u8]);
169 assert!(!summary.all_match());
170 }
171
172 #[test]
173 fn mismatch_labels_recorded() {
174 let inputs: Vec<(String, Vec<u8>)> =
175 vec![("first".into(), vec![1]), ("second".into(), vec![2])];
176 let results = differential_test(&inputs, |input: &[u8]| input[0], |_: &[u8]| 99u8);
177 let summary = DiffSummary::from_results(&results);
178 assert!(summary.mismatches.contains(&"first".to_string()));
179 assert!(summary.mismatches.contains(&"second".to_string()));
180 }
181}