faucet_cli/pipeline_test/
report.rs1use serde::Serialize;
5
6#[derive(Debug, Serialize)]
8pub struct CaseOutcome {
9 pub name: String,
11 pub spec: String,
13 pub status: &'static str,
15 pub failures: Vec<String>,
17}
18
19impl CaseOutcome {
20 pub fn new(name: String, spec: String, failures: Vec<String>) -> Self {
21 Self {
22 name,
23 spec,
24 status: if failures.is_empty() { "pass" } else { "fail" },
25 failures,
26 }
27 }
28
29 pub fn passed(&self) -> bool {
30 self.failures.is_empty()
31 }
32}
33
34#[derive(Debug, Serialize)]
36pub struct TestReport {
37 pub total: usize,
38 pub passed: usize,
39 pub failed: usize,
40 pub tests: Vec<CaseOutcome>,
41}
42
43impl TestReport {
44 pub fn new(tests: Vec<CaseOutcome>) -> Self {
45 let total = tests.len();
46 let passed = tests.iter().filter(|t| t.passed()).count();
47 Self {
48 total,
49 passed,
50 failed: total - passed,
51 tests,
52 }
53 }
54
55 pub fn render_human(&self) -> String {
57 let mut out = String::new();
58 let mut current_spec: Option<&str> = None;
59 for case in &self.tests {
60 if current_spec != Some(case.spec.as_str()) {
61 if current_spec.is_some() {
62 out.push('\n');
63 }
64 out.push_str(&case.spec);
65 out.push('\n');
66 current_spec = Some(case.spec.as_str());
67 }
68 if case.passed() {
69 out.push_str(&format!(" ✓ {}\n", case.name));
70 } else {
71 out.push_str(&format!(" ✗ {}\n", case.name));
72 for failure in &case.failures {
73 out.push_str(&format!(" - {failure}\n"));
74 }
75 }
76 }
77 out.push_str(&format!(
78 "\n{} test{}, {} passed, {} failed\n",
79 self.total,
80 if self.total == 1 { "" } else { "s" },
81 self.passed,
82 self.failed
83 ));
84 out
85 }
86
87 pub fn render_json(&self) -> String {
89 serde_json::to_string_pretty(self).expect("report serialization is infallible")
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 fn report() -> TestReport {
98 TestReport::new(vec![
99 CaseOutcome::new("ok case".into(), "a.yaml".into(), vec![]),
100 CaseOutcome::new(
101 "bad case".into(),
102 "a.yaml".into(),
103 vec!["records[0].x: expected 1, got 2".into()],
104 ),
105 CaseOutcome::new("other".into(), "b.yaml".into(), vec![]),
106 ])
107 }
108
109 #[test]
110 fn counts_and_status() {
111 let r = report();
112 assert_eq!((r.total, r.passed, r.failed), (3, 2, 1));
113 assert_eq!(r.tests[0].status, "pass");
114 assert_eq!(r.tests[1].status, "fail");
115 }
116
117 #[test]
118 fn human_rendering_groups_by_spec() {
119 let text = report().render_human();
120 assert!(
121 text.contains("a.yaml\n ✓ ok case\n ✗ bad case\n"),
122 "{text}"
123 );
124 assert!(
125 text.contains(" - records[0].x: expected 1, got 2"),
126 "{text}"
127 );
128 assert!(text.contains("b.yaml\n ✓ other"), "{text}");
129 assert!(text.contains("3 tests, 2 passed, 1 failed"), "{text}");
130 }
131
132 #[test]
133 fn singular_summary_line() {
134 let r = TestReport::new(vec![CaseOutcome::new(
135 "one".into(),
136 "s.yaml".into(),
137 vec![],
138 )]);
139 assert!(r.render_human().contains("1 test, 1 passed, 0 failed"));
140 }
141
142 #[test]
143 fn json_rendering_is_machine_readable() {
144 let v: serde_json::Value = serde_json::from_str(&report().render_json()).unwrap();
145 assert_eq!(v["total"], 3);
146 assert_eq!(v["tests"][1]["status"], "fail");
147 assert_eq!(
148 v["tests"][1]["failures"][0],
149 "records[0].x: expected 1, got 2"
150 );
151 }
152}