Skip to main content

smb_server_test_dashboard/
json.rs

1//! Hand-rolled JSON serialization for the REST responses (no dependencies).
2
3use crate::csv::{Summary, TestRow};
4use std::collections::BTreeMap;
5
6/// `/api/status`: overall counts plus per-suite and per-category breakdowns.
7pub fn status_body(rows: &[TestRow], summary: &Summary) -> String {
8    format!(
9        "{{\"total\":{},\"passed\":{},\"failed\":{},\"skipped\":{},\"unknown\":{},\
10         \"suites\":[{}],\"categories\":[{}]}}",
11        summary.total,
12        summary.passed,
13        summary.failed,
14        summary.skipped,
15        summary.unknown,
16        breakdown(rows, |r| &r.suite, "suite"),
17        breakdown(rows, |r| &r.category, "category"),
18    )
19}
20
21/// Group rows by a key and emit `[{"<label>":..,"passed":..,...}]`.
22#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // pass/fail/skip/error counter slots
23fn breakdown(rows: &[TestRow], key: impl Fn(&TestRow) -> &str, label: &str) -> String {
24    let mut groups: BTreeMap<&str, [usize; 4]> = BTreeMap::new();
25    for row in rows {
26        let slot = groups.entry(key(row)).or_insert([0, 0, 0, 0]);
27        match row.status.as_str() {
28            "pass" => slot[0] += 1,
29            "fail" => slot[1] += 1,
30            "skip" => slot[2] += 1,
31            _ => slot[3] += 1,
32        }
33    }
34    let items: Vec<String> = groups
35        .iter()
36        .map(|(name, [p, f, s, u])| {
37            format!(
38                "{{\"{label}\":{},\"passed\":{p},\"failed\":{f},\"skipped\":{s},\
39                 \"unknown\":{u},\"total\":{}}}",
40                quote(name),
41                p + f + s + u
42            )
43        })
44        .collect();
45    items.join(",")
46}
47
48/// `/api/tests`: the full row list.
49pub fn tests_body(rows: &[TestRow]) -> String {
50    let items: Vec<String> = rows.iter().map(row_to_json).collect();
51    format!("[{}]", items.join(","))
52}
53
54fn row_to_json(row: &TestRow) -> String {
55    format!(
56        "{{\"suite\":{},\"name\":{},\"category\":{},\"status\":{},\
57         \"duration_ms\":{},\"timestamp\":{},\"commit\":{},\"note\":{}}}",
58        quote(&row.suite),
59        quote(&row.name),
60        quote(&row.category),
61        quote(&row.status),
62        row.duration_ms,
63        quote(&row.timestamp),
64        quote(&row.commit),
65        quote(&row.note)
66    )
67}
68
69/// Quote and escape a string as a JSON value.
70#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // JSON string escaping ([RFC 8259])
71fn quote(s: &str) -> String {
72    let mut out = String::with_capacity(s.len() + 2);
73    out.push('"');
74    for c in s.chars() {
75        match c {
76            '"' => out.push_str("\\\""),
77            '\\' => out.push_str("\\\\"),
78            '\n' => out.push_str("\\n"),
79            '\r' => out.push_str("\\r"),
80            '\t' => out.push_str("\\t"),
81            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
82            c => out.push(c),
83        }
84    }
85    out.push('"');
86    out
87}