xchecker_runner/
ndjson.rs1#[derive(Debug, Clone)]
3pub enum NdjsonResult {
4 ValidJson(String),
6 NoValidJson { tail_excerpt: String },
8}
9
10pub(crate) fn parse_ndjson(stdout: &str) -> NdjsonResult {
11 let mut last_valid_json: Option<String> = None;
12
13 for line in stdout.lines() {
15 let trimmed = line.trim();
16
17 if trimmed.is_empty() {
19 continue;
20 }
21
22 if let Ok(value) = serde_json::from_str::<serde_json::Value>(trimmed) {
24 if let Ok(json_str) = serde_json::to_string(&value) {
27 last_valid_json = Some(json_str);
28 }
29 }
30 }
32
33 if let Some(json) = last_valid_json {
35 NdjsonResult::ValidJson(json)
36 } else {
37 let tail_excerpt = if stdout.len() <= 256 {
40 stdout.to_string()
41 } else {
42 let start = stdout.len() - 256;
44 stdout[start..].to_string()
45 };
46
47 NdjsonResult::NoValidJson { tail_excerpt }
48 }
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 #[test]
58 fn test_parse_ndjson_single_valid_json() {
59 let stdout = r#"{"status": "success", "result": "done"}"#;
60 let result = parse_ndjson(stdout);
61
62 match result {
63 NdjsonResult::ValidJson(json) => {
64 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
65 assert_eq!(parsed["status"], "success");
66 assert_eq!(parsed["result"], "done");
67 }
68 NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
69 }
70 }
71
72 #[test]
73 fn test_parse_ndjson_multiple_valid_json_returns_last() {
74 let stdout = r#"{"frame": 1}
75{"frame": 2}
76{"frame": 3}"#;
77 let result = parse_ndjson(stdout);
78
79 match result {
80 NdjsonResult::ValidJson(json) => {
81 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
82 assert_eq!(parsed["frame"], 3);
83 }
84 NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
85 }
86 }
87
88 #[test]
89 fn test_parse_ndjson_interleaved_noise_and_json() {
90 let stdout = r#"Starting process...
92{"frame": 1, "status": "initializing"}
93Some debug output
94Warning: something happened
95{"frame": 2, "status": "processing"}
96More noise here
97{"frame": 3, "status": "complete"}
98Done!"#;
99 let result = parse_ndjson(stdout);
100
101 match result {
102 NdjsonResult::ValidJson(json) => {
103 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
104 assert_eq!(parsed["frame"], 3);
105 assert_eq!(parsed["status"], "complete");
106 }
107 NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
108 }
109 }
110
111 #[test]
112 fn test_parse_ndjson_no_valid_json() {
113 let stdout = "This is just plain text\nNo JSON here\nJust noise";
114 let result = parse_ndjson(stdout);
115
116 match result {
117 NdjsonResult::ValidJson(_) => panic!("Expected NoValidJson"),
118 NdjsonResult::NoValidJson { tail_excerpt } => {
119 assert_eq!(tail_excerpt, stdout);
120 }
121 }
122 }
123
124 #[test]
125 fn test_parse_ndjson_partial_json() {
126 let stdout = r#"{"frame": 1, "status": "ok"}
128{"frame": 2, "incomplete": tru"#;
129 let result = parse_ndjson(stdout);
130
131 match result {
132 NdjsonResult::ValidJson(json) => {
133 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
135 assert_eq!(parsed["frame"], 1);
136 assert_eq!(parsed["status"], "ok");
137 }
138 NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson from first frame"),
139 }
140 }
141
142 #[test]
143 fn test_parse_ndjson_only_partial_json() {
144 let stdout = r#"{"incomplete": tru"#;
145 let result = parse_ndjson(stdout);
146
147 match result {
148 NdjsonResult::ValidJson(_) => panic!("Expected NoValidJson"),
149 NdjsonResult::NoValidJson { tail_excerpt } => {
150 assert_eq!(tail_excerpt, stdout);
151 }
152 }
153 }
154
155 #[test]
156 fn test_parse_ndjson_empty_string() {
157 let stdout = "";
158 let result = parse_ndjson(stdout);
159
160 match result {
161 NdjsonResult::ValidJson(_) => panic!("Expected NoValidJson"),
162 NdjsonResult::NoValidJson { tail_excerpt } => {
163 assert_eq!(tail_excerpt, "");
164 }
165 }
166 }
167
168 #[test]
169 fn test_parse_ndjson_only_whitespace() {
170 let stdout = " \n\n \t \n";
171 let result = parse_ndjson(stdout);
172
173 match result {
174 NdjsonResult::ValidJson(_) => panic!("Expected NoValidJson"),
175 NdjsonResult::NoValidJson { tail_excerpt } => {
176 assert_eq!(tail_excerpt, stdout);
177 }
178 }
179 }
180
181 #[test]
182 fn test_parse_ndjson_tail_excerpt_truncation() {
183 let long_text = "x".repeat(300);
185 let result = parse_ndjson(&long_text);
186
187 match result {
188 NdjsonResult::ValidJson(_) => panic!("Expected NoValidJson"),
189 NdjsonResult::NoValidJson { tail_excerpt } => {
190 assert_eq!(tail_excerpt.len(), 256);
191 assert_eq!(tail_excerpt, "x".repeat(256));
193 }
194 }
195 }
196
197 #[test]
198 fn test_parse_ndjson_tail_excerpt_no_truncation() {
199 let short_text = "Short text";
200 let result = parse_ndjson(short_text);
201
202 match result {
203 NdjsonResult::ValidJson(_) => panic!("Expected NoValidJson"),
204 NdjsonResult::NoValidJson { tail_excerpt } => {
205 assert_eq!(tail_excerpt, short_text);
206 }
207 }
208 }
209
210 #[test]
211 fn test_parse_ndjson_malformed_json_lines() {
212 let stdout = r#"{"valid": "json"}
213{malformed json}
214{"another": "valid"}
215[not an object]
216{"final": "valid"}"#;
217 let result = parse_ndjson(stdout);
218
219 match result {
220 NdjsonResult::ValidJson(json) => {
221 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
222 assert_eq!(parsed["final"], "valid");
223 }
224 NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
225 }
226 }
227
228 #[test]
229 fn test_parse_ndjson_json_array_is_valid() {
230 let stdout = r#"[1, 2, 3]
232{"object": "value"}
233[4, 5, 6]"#;
234 let result = parse_ndjson(stdout);
235
236 match result {
237 NdjsonResult::ValidJson(json) => {
238 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
239 assert!(parsed.is_array());
240 assert_eq!(parsed[0], 4);
241 }
242 NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
243 }
244 }
245
246 #[test]
247 fn test_parse_ndjson_json_primitives() {
248 let stdout = r#""string value"
25042
251true
252null
253{"final": "object"}"#;
254 let result = parse_ndjson(stdout);
255
256 match result {
257 NdjsonResult::ValidJson(json) => {
258 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
259 assert_eq!(parsed["final"], "object");
260 }
261 NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
262 }
263 }
264
265 #[test]
266 fn test_parse_ndjson_unicode_content() {
267 let stdout = r#"{"message": "Hello 世界"}
268{"emoji": "🎉🎊"}
269{"final": "完成"}"#;
270 let result = parse_ndjson(stdout);
271
272 match result {
273 NdjsonResult::ValidJson(json) => {
274 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
275 assert_eq!(parsed["final"], "完成");
276 }
277 NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
278 }
279 }
280
281 #[test]
282 fn test_parse_ndjson_escaped_characters() {
283 let stdout = r#"{"path": "C:\\Users\\test\\file.txt"}
284{"quote": "He said \\"hello\\""}
285{"final": "done"}"#;
286 let result = parse_ndjson(stdout);
287
288 match result {
289 NdjsonResult::ValidJson(json) => {
290 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
291 assert_eq!(parsed["final"], "done");
292 }
293 NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
294 }
295 }
296}