Skip to main content

faucet_cli/pipeline_test/
report.rs

1//! Rendering for `faucet test` results — the human checklist and the
2//! machine-readable `--json` document.
3
4use serde::Serialize;
5
6/// One test case's final result.
7#[derive(Debug, Serialize)]
8pub struct CaseOutcome {
9    /// Case name from the spec file.
10    pub name: String,
11    /// Spec file the case came from.
12    pub spec: String,
13    /// `"pass"` or `"fail"`.
14    pub status: &'static str,
15    /// One message per failed assertion (empty on pass).
16    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/// The full report across every spec file.
35#[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    /// Render the human checklist, grouped by spec file in declared order.
56    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    /// Render the `--json` document.
88    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}