1use crate::error::TestsError;
25use crate::result::{TestOutcome, TestResult};
26use chrono::Utc;
27use serde::{Deserialize, Serialize};
28use std::collections::HashMap;
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct TestList {
37 #[serde(rename = "test-count")]
39 pub test_count: usize,
40 #[serde(rename = "rust-suites")]
42 pub rust_suites: HashMap<String, TestSuite>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TestSuite {
48 #[serde(rename = "package-name")]
50 pub package_name: String,
51 #[serde(rename = "binary-id")]
53 pub binary_id: String,
54 #[serde(rename = "binary-name")]
56 pub binary_name: String,
57 pub kind: String,
59 pub testcases: HashMap<String, TestCase>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct TestCase {
66 pub kind: String,
68 pub ignored: bool,
70}
71
72impl TestList {
73 #[must_use]
75 pub fn all_test_names(&self) -> Vec<String> {
76 let mut names = Vec::new();
77 for (suite_id, suite) in &self.rust_suites {
78 for test_name in suite.testcases.keys() {
79 names.push(format!("{}::{}", suite_id, test_name));
80 }
81 }
82 names
83 }
84
85 #[must_use]
87 pub fn tests_in_suite(&self, suite_id: &str) -> Vec<&str> {
88 self.rust_suites
89 .get(suite_id)
90 .map(|s| s.testcases.keys().map(String::as_str).collect())
91 .unwrap_or_default()
92 }
93
94 #[must_use]
96 pub fn ignored_count(&self) -> usize {
97 self.rust_suites
98 .values()
99 .flat_map(|s| s.testcases.values())
100 .filter(|tc| tc.ignored)
101 .count()
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(tag = "type", rename_all = "lowercase")]
112pub enum LibtestEvent {
113 Suite(SuiteEvent),
115 Test(TestEvent),
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct SuiteEvent {
122 pub event: String,
124 pub test_count: Option<usize>,
126 pub passed: Option<usize>,
128 pub failed: Option<usize>,
130 pub ignored: Option<usize>,
132 pub exec_time: Option<f64>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct TestEvent {
139 pub event: String,
141 pub name: String,
143 pub exec_time: Option<f64>,
145 pub stdout: Option<String>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct TestRunSummary {
152 pub total: usize,
154 pub passed: usize,
156 pub failed: usize,
158 pub ignored: usize,
160 pub exec_time_secs: f64,
162 pub results: Vec<TestResult>,
164}
165
166impl TestRunSummary {
167 #[must_use]
169 pub fn empty() -> Self {
170 Self {
171 total: 0,
172 passed: 0,
173 failed: 0,
174 ignored: 0,
175 exec_time_secs: 0.0,
176 results: Vec::new(),
177 }
178 }
179
180 #[must_use]
182 pub fn all_passed(&self) -> bool {
183 self.failed == 0
184 }
185
186 #[must_use]
188 pub fn failing_tests(&self) -> Vec<&TestResult> {
189 self.results.iter().filter(|r| r.failed()).collect()
190 }
191}
192
193pub fn parse_list_output(json: &str) -> Result<TestList, TestsError> {
203 serde_json::from_str(json).map_err(TestsError::from)
204}
205
206pub fn parse_run_output(output: &str) -> Result<TestRunSummary, TestsError> {
214 let mut summary = TestRunSummary::empty();
215 let mut pending_tests: HashMap<String, chrono::DateTime<Utc>> = HashMap::new();
216 let now = Utc::now();
217
218 for line in output.lines() {
219 let line = line.trim();
220 if line.is_empty() {
221 continue;
222 }
223
224 let event: LibtestEvent = serde_json::from_str(line)?;
225
226 match event {
227 LibtestEvent::Suite(suite) => {
228 if suite.event == "started" {
229 if let Some(count) = suite.test_count {
230 summary.total = count;
231 }
232 } else {
233 if let Some(passed) = suite.passed {
235 summary.passed = passed;
236 }
237 if let Some(failed) = suite.failed {
238 summary.failed = failed;
239 }
240 if let Some(ignored) = suite.ignored {
241 summary.ignored = ignored;
242 }
243 if let Some(exec_time) = suite.exec_time {
244 summary.exec_time_secs = exec_time;
245 }
246 }
247 }
248 LibtestEvent::Test(test) => {
249 if test.event == "started" {
250 pending_tests.insert(test.name.clone(), now);
251 } else {
252 let outcome = match test.event.as_str() {
254 "ok" => TestOutcome::Passed,
255 "failed" => TestOutcome::Failed,
256 "ignored" => TestOutcome::Ignored,
257 _ => TestOutcome::Failed,
258 };
259
260 let duration_ms = test.exec_time.map(|t| (t * 1000.0) as u64).unwrap_or(0);
261
262 let test_name = normalize_test_name(&test.name);
264
265 let result = TestResult {
266 name: test_name,
267 outcome,
268 duration_ms,
269 timestamp: now,
270 output: test.stdout,
271 };
272
273 summary.results.push(result);
274 pending_tests.remove(&test.name);
275 }
276 }
277 }
278 }
279
280 Ok(summary)
281}
282
283fn normalize_test_name(name: &str) -> String {
288 if let Some(idx) = name.find('$') {
290 name[idx + 1..].to_string()
291 } else if let Some(idx) = name.find("::") {
292 name[idx + 2..].to_string()
294 } else {
295 name.to_string()
296 }
297}
298
299pub fn parse_event(json: &str) -> Result<LibtestEvent, TestsError> {
305 serde_json::from_str(json).map_err(TestsError::from)
306}
307
308pub struct StreamingParser {
314 pending_tests: HashMap<String, chrono::DateTime<Utc>>,
315 results: Vec<TestResult>,
316 total: usize,
317}
318
319impl StreamingParser {
320 #[must_use]
322 pub fn new() -> Self {
323 Self {
324 pending_tests: HashMap::new(),
325 results: Vec::new(),
326 total: 0,
327 }
328 }
329
330 pub fn process_line(&mut self, line: &str) -> Result<Option<TestResult>, TestsError> {
336 let line = line.trim();
337 if line.is_empty() {
338 return Ok(None);
339 }
340
341 let event: LibtestEvent = serde_json::from_str(line)?;
342 let now = Utc::now();
343
344 match event {
345 LibtestEvent::Suite(suite) => {
346 if suite.event == "started"
347 && let Some(count) = suite.test_count
348 {
349 self.total = count;
350 }
351 Ok(None)
352 }
353 LibtestEvent::Test(test) => {
354 if test.event == "started" {
355 self.pending_tests.insert(test.name.clone(), now);
356 Ok(None)
357 } else {
358 let outcome = match test.event.as_str() {
359 "ok" => TestOutcome::Passed,
360 "failed" => TestOutcome::Failed,
361 "ignored" => TestOutcome::Ignored,
362 _ => TestOutcome::Failed,
363 };
364
365 let duration_ms = test.exec_time.map(|t| (t * 1000.0) as u64).unwrap_or(0);
366
367 let test_name = normalize_test_name(&test.name);
368
369 let result = TestResult {
370 name: test_name,
371 outcome,
372 duration_ms,
373 timestamp: now,
374 output: test.stdout,
375 };
376
377 self.results.push(result.clone());
378 self.pending_tests.remove(&test.name);
379 Ok(Some(result))
380 }
381 }
382 }
383 }
384
385 #[must_use]
387 pub fn results(&self) -> &[TestResult] {
388 &self.results
389 }
390
391 #[must_use]
393 pub fn into_summary(self) -> TestRunSummary {
394 let passed = self.results.iter().filter(|r| r.passed()).count();
395 let failed = self.results.iter().filter(|r| r.failed()).count();
396 let ignored = self
397 .results
398 .iter()
399 .filter(|r| r.outcome == TestOutcome::Ignored)
400 .count();
401 let exec_time_secs =
402 self.results.iter().map(|r| r.duration_ms).sum::<u64>() as f64 / 1000.0;
403
404 TestRunSummary {
405 total: self.total,
406 passed,
407 failed,
408 ignored,
409 exec_time_secs,
410 results: self.results,
411 }
412 }
413}
414
415impl Default for StreamingParser {
416 fn default() -> Self {
417 Self::new()
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424 use similar_asserts::assert_eq;
425
426 #[test]
427 fn test_parse_list_output() {
428 let json = r#"{
429 "test-count": 2,
430 "rust-suites": {
431 "my-crate": {
432 "package-name": "my-crate",
433 "binary-id": "my-crate",
434 "binary-name": "my_crate",
435 "kind": "lib",
436 "testcases": {
437 "tests::test_one": {"kind": "test", "ignored": false},
438 "tests::test_two": {"kind": "test", "ignored": true}
439 }
440 }
441 }
442 }"#;
443
444 let list = parse_list_output(json).expect("Should parse");
445 assert_eq!(list.test_count, 2);
446 assert_eq!(list.rust_suites.len(), 1);
447 assert_eq!(list.ignored_count(), 1);
448 }
449
450 #[test]
451 fn test_parse_run_output_single_test() {
452 let output = r#"{"type":"suite","event":"started","test_count":1}
453{"type":"test","event":"started","name":"my-crate::my_crate$tests::test_one"}
454{"type":"test","event":"ok","name":"my-crate::my_crate$tests::test_one","exec_time":0.015}
455{"type":"suite","event":"ok","passed":1,"failed":0,"ignored":0,"exec_time":0.015}"#;
456
457 let summary = parse_run_output(output).expect("Should parse");
458 assert_eq!(summary.passed, 1);
459 assert_eq!(summary.failed, 0);
460 assert_eq!(summary.results.len(), 1);
461 assert_eq!(summary.results[0].name, "tests::test_one");
462 assert!(summary.results[0].passed());
463 }
464
465 #[test]
466 fn test_parse_run_output_failed_test() {
467 let output = r#"{"type":"suite","event":"started","test_count":1}
468{"type":"test","event":"started","name":"crate::bin$mod::test_fail"}
469{"type":"test","event":"failed","name":"crate::bin$mod::test_fail","exec_time":0.005,"stdout":"assertion failed"}
470{"type":"suite","event":"failed","passed":0,"failed":1,"ignored":0,"exec_time":0.005}"#;
471
472 let summary = parse_run_output(output).expect("Should parse");
473 assert_eq!(summary.failed, 1);
474 assert!(summary.results[0].failed());
475 assert_eq!(
476 summary.results[0].output,
477 Some("assertion failed".to_string())
478 );
479 }
480
481 #[test]
482 fn test_parse_run_output_multiple_tests() {
483 let output = r#"{"type":"suite","event":"started","test_count":3}
484{"type":"test","event":"started","name":"c::b$test_a"}
485{"type":"test","event":"ok","name":"c::b$test_a","exec_time":0.001}
486{"type":"test","event":"started","name":"c::b$test_b"}
487{"type":"test","event":"ignored","name":"c::b$test_b","exec_time":0.0}
488{"type":"test","event":"started","name":"c::b$test_c"}
489{"type":"test","event":"ok","name":"c::b$test_c","exec_time":0.002}
490{"type":"suite","event":"ok","passed":2,"failed":0,"ignored":1,"exec_time":0.003}"#;
491
492 let summary = parse_run_output(output).expect("Should parse");
493 assert_eq!(summary.total, 3);
494 assert_eq!(summary.passed, 2);
495 assert_eq!(summary.ignored, 1);
496 assert_eq!(summary.results.len(), 3);
497 }
498
499 #[test]
500 fn test_normalize_test_name() {
501 assert_eq!(
502 normalize_test_name("hindsight-tests::hindsight_tests$result::tests::test_passed"),
503 "result::tests::test_passed"
504 );
505 assert_eq!(
506 normalize_test_name("crate::binary$module::test"),
507 "module::test"
508 );
509 assert_eq!(normalize_test_name("simple_test"), "simple_test");
510 }
511
512 #[test]
513 fn test_streaming_parser() {
514 let mut parser = StreamingParser::new();
515
516 let result = parser
517 .process_line(r#"{"type":"suite","event":"started","test_count":1}"#)
518 .expect("Should parse");
519 assert!(result.is_none());
520
521 let result = parser
522 .process_line(r#"{"type":"test","event":"started","name":"c::b$t"}"#)
523 .expect("Should parse");
524 assert!(result.is_none());
525
526 let result = parser
527 .process_line(r#"{"type":"test","event":"ok","name":"c::b$t","exec_time":0.01}"#)
528 .expect("Should parse");
529 assert!(result.is_some());
530 assert_eq!(result.unwrap().name, "t");
531
532 let summary = parser.into_summary();
533 assert_eq!(summary.passed, 1);
534 }
535
536 #[test]
537 fn test_test_run_summary_helpers() {
538 let summary = TestRunSummary {
539 total: 3,
540 passed: 2,
541 failed: 1,
542 ignored: 0,
543 exec_time_secs: 0.1,
544 results: vec![
545 TestResult {
546 name: "test_pass".to_string(),
547 outcome: TestOutcome::Passed,
548 duration_ms: 50,
549 timestamp: Utc::now(),
550 output: None,
551 },
552 TestResult {
553 name: "test_fail".to_string(),
554 outcome: TestOutcome::Failed,
555 duration_ms: 50,
556 timestamp: Utc::now(),
557 output: Some("error".to_string()),
558 },
559 ],
560 };
561
562 assert!(!summary.all_passed());
563 assert_eq!(summary.failing_tests().len(), 1);
564 assert_eq!(summary.failing_tests()[0].name, "test_fail");
565 }
566
567 #[test]
568 fn test_test_list_helpers() {
569 let json = r#"{
570 "test-count": 3,
571 "rust-suites": {
572 "suite-a": {
573 "package-name": "a",
574 "binary-id": "suite-a",
575 "binary-name": "a",
576 "kind": "lib",
577 "testcases": {
578 "test_1": {"kind": "test", "ignored": false},
579 "test_2": {"kind": "test", "ignored": false}
580 }
581 },
582 "suite-b": {
583 "package-name": "b",
584 "binary-id": "suite-b",
585 "binary-name": "b",
586 "kind": "lib",
587 "testcases": {
588 "test_3": {"kind": "test", "ignored": true}
589 }
590 }
591 }
592 }"#;
593
594 let list = parse_list_output(json).expect("Should parse");
595 let all_names = list.all_test_names();
596 assert_eq!(all_names.len(), 3);
597
598 let suite_a_tests = list.tests_in_suite("suite-a");
599 assert_eq!(suite_a_tests.len(), 2);
600
601 assert_eq!(list.ignored_count(), 1);
602 }
603
604 #[test]
605 fn test_parse_empty_output() {
606 let summary = parse_run_output("").expect("Should parse empty");
607 assert_eq!(summary.total, 0);
608 assert_eq!(summary.results.len(), 0);
609 }
610
611 #[test]
612 fn test_parse_invalid_json() {
613 let result = parse_run_output("not json");
614 assert!(result.is_err());
615 }
616}