1use crate::{DiffEntry, DiffKind, TargetObservation};
2use bytes::Bytes;
3use serde_json::Value;
4use std::collections::BTreeSet;
5
6use super::CompareConfig;
7
8#[derive(Debug, Clone)]
9pub struct CapturedTarget {
10 pub observation: TargetObservation,
11 pub body_bytes: Bytes,
12 pub stderr_bytes: Bytes,
13}
14
15#[derive(Debug, Clone, Copy)]
16pub(super) enum TargetRole {
17 Candidate,
18 Secondary,
19}
20
21impl TargetRole {
22 fn label(self) -> &'static str {
23 match self {
24 Self::Candidate => "candidate",
25 Self::Secondary => "secondary",
26 }
27 }
28
29 fn values(self, value: Option<String>) -> (Option<String>, Option<String>) {
30 match self {
31 Self::Candidate => (value, None),
32 Self::Secondary => (None, value),
33 }
34 }
35}
36
37pub(super) fn diff_pair(
38 primary: &CapturedTarget,
39 other: &CapturedTarget,
40 role: TargetRole,
41 config: &CompareConfig,
42) -> Vec<DiffEntry> {
43 let mut diffs = Vec::new();
44 diff_target_errors(primary, other, role, &mut diffs);
45 diff_status(primary, other, role, &mut diffs);
46 diff_headers(primary, other, role, config, &mut diffs);
47 diff_bodies(primary, other, role, config, &mut diffs);
48 diff_stderr(primary, other, role, config, &mut diffs);
49 diffs
50}
51
52fn diff_target_errors(
53 primary: &CapturedTarget,
54 other: &CapturedTarget,
55 role: TargetRole,
56 diffs: &mut Vec<DiffEntry>,
57) {
58 if primary.observation.error != other.observation.error {
59 let (candidate, secondary) = role.values(other.observation.error.clone());
60 diffs.push(DiffEntry {
61 kind: DiffKind::TargetError,
62 path: "$target_error".to_string(),
63 primary: primary.observation.error.clone(),
64 candidate,
65 secondary,
66 message: format!("primary target error differs from {}", role.label()),
67 });
68 }
69}
70
71fn diff_status(
72 primary: &CapturedTarget,
73 other: &CapturedTarget,
74 role: TargetRole,
75 diffs: &mut Vec<DiffEntry>,
76) {
77 if primary.observation.status != other.observation.status {
78 let (candidate, secondary) =
79 role.values(other.observation.status.map(|value| value.to_string()));
80 diffs.push(DiffEntry {
81 kind: DiffKind::Status,
82 path: "$status".to_string(),
83 primary: primary.observation.status.map(|value| value.to_string()),
84 candidate,
85 secondary,
86 message: format!("primary status differs from {}", role.label()),
87 });
88 }
89}
90
91fn diff_headers(
92 primary: &CapturedTarget,
93 other: &CapturedTarget,
94 role: TargetRole,
95 config: &CompareConfig,
96 diffs: &mut Vec<DiffEntry>,
97) {
98 let keys: BTreeSet<String> = primary
99 .observation
100 .headers
101 .keys()
102 .chain(other.observation.headers.keys())
103 .filter(|name| !config.ignored_headers.contains(*name))
104 .cloned()
105 .collect();
106
107 for key in keys {
108 let primary_value = primary.observation.headers.get(&key).cloned();
109 let other_value = other.observation.headers.get(&key).cloned();
110 if primary_value != other_value {
111 let (candidate, secondary) = role.values(other_value);
112 diffs.push(DiffEntry {
113 kind: DiffKind::Header,
114 path: format!("$.headers.{key}"),
115 primary: primary_value,
116 candidate,
117 secondary,
118 message: format!("primary header {key} differs from {}", role.label()),
119 });
120 }
121 }
122}
123
124fn diff_bodies(
125 primary: &CapturedTarget,
126 other: &CapturedTarget,
127 role: TargetRole,
128 config: &CompareConfig,
129 diffs: &mut Vec<DiffEntry>,
130) {
131 if primary.body_bytes == other.body_bytes {
132 return;
133 }
134
135 let primary_json = serde_json::from_slice::<Value>(&primary.body_bytes);
136 let other_json = serde_json::from_slice::<Value>(&other.body_bytes);
137
138 match (primary_json, other_json) {
139 (Ok(primary_json), Ok(other_json)) => {
140 diff_json("$", &primary_json, &other_json, role, config, diffs);
141 }
142 _ => {
143 let primary_text = normalize_text(&primary.body_bytes);
144 let other_text = normalize_text(&other.body_bytes);
145 if primary_text != other_text {
146 let (candidate, secondary) = role.values(Some(other_text));
147 diffs.push(DiffEntry {
148 kind: DiffKind::Body,
149 path: "$body".to_string(),
150 primary: Some(primary_text),
151 candidate,
152 secondary,
153 message: format!("primary body differs from {}", role.label()),
154 });
155 }
156 }
157 }
158}
159
160fn diff_stderr(
161 primary: &CapturedTarget,
162 other: &CapturedTarget,
163 role: TargetRole,
164 config: &CompareConfig,
165 diffs: &mut Vec<DiffEntry>,
166) {
167 if config.ignore_stderr || primary.stderr_bytes == other.stderr_bytes {
168 return;
169 }
170
171 let primary_text = normalize_text(&primary.stderr_bytes);
172 let other_text = normalize_text(&other.stderr_bytes);
173 if primary_text != other_text {
174 let (candidate, secondary) = role.values(Some(other_text));
175 diffs.push(DiffEntry {
176 kind: DiffKind::Stderr,
177 path: "$stderr".to_string(),
178 primary: Some(primary_text),
179 candidate,
180 secondary,
181 message: format!("primary stderr differs from {}", role.label()),
182 });
183 }
184}
185
186fn diff_json(
187 path: &str,
188 primary: &Value,
189 other: &Value,
190 role: TargetRole,
191 config: &CompareConfig,
192 diffs: &mut Vec<DiffEntry>,
193) {
194 if config.ignored_json_paths.contains(path) {
195 return;
196 }
197
198 match (primary, other) {
199 (Value::Object(primary_map), Value::Object(other_map)) => {
200 let keys: BTreeSet<String> = primary_map
201 .keys()
202 .chain(other_map.keys())
203 .cloned()
204 .collect();
205 for key in keys {
206 let child_path = if path == "$" {
207 format!("$.{key}")
208 } else {
209 format!("{path}.{key}")
210 };
211 match (primary_map.get(&key), other_map.get(&key)) {
212 (Some(primary_value), Some(other_value)) => {
213 diff_json(&child_path, primary_value, other_value, role, config, diffs);
214 }
215 (primary_value, other_value) => {
216 push_json_diff(child_path, primary_value, other_value, role, diffs);
217 }
218 }
219 }
220 }
221 (Value::Array(primary_items), Value::Array(other_items)) => {
222 let max_len = primary_items.len().max(other_items.len());
223 for index in 0..max_len {
224 let child_path = format!("{path}[{index}]");
225 match (primary_items.get(index), other_items.get(index)) {
226 (Some(primary_value), Some(other_value)) => {
227 diff_json(&child_path, primary_value, other_value, role, config, diffs);
228 }
229 (primary_value, other_value) => {
230 push_json_diff(child_path, primary_value, other_value, role, diffs);
231 }
232 }
233 }
234 }
235 _ if primary == other => {}
236 _ => push_json_diff(path.to_string(), Some(primary), Some(other), role, diffs),
237 }
238}
239
240fn push_json_diff(
241 path: String,
242 primary: Option<&Value>,
243 other: Option<&Value>,
244 role: TargetRole,
245 diffs: &mut Vec<DiffEntry>,
246) {
247 let other_value = other.map(json_preview);
248 let (candidate, secondary) = role.values(other_value);
249 diffs.push(DiffEntry {
250 kind: DiffKind::Body,
251 path: path.clone(),
252 primary: primary.map(json_preview),
253 candidate,
254 secondary,
255 message: format!("primary body value {path} differs from {}", role.label()),
256 });
257}
258
259fn json_preview(value: &Value) -> String {
260 serde_json::to_string(value).unwrap_or_else(|_| "<json>".to_string())
261}
262
263fn normalize_text(body: &[u8]) -> String {
264 String::from_utf8_lossy(body)
265 .lines()
266 .map(str::trim_end)
267 .collect::<Vec<_>>()
268 .join("\n")
269 .trim()
270 .to_string()
271}