Skip to main content

smb_server_test_dashboard/
csv.rs

1//! CSV storage for test results. The schema is a header line followed by rows:
2//! `suite,name,category,status,duration_ms,timestamp,commit,note`. `suite` is one
3//! of `protocol`/`unit`/`system`; `status` is `pass`/`fail`/`skip`/`unknown`;
4//! `commit` is the changeset the test last passed at (empty when not passing);
5//! `note` explains a skip/fail reason (empty otherwise).
6
7use std::path::Path;
8
9/// A single test result row.
10pub struct TestRow {
11    pub suite: String,
12    pub name: String,
13    pub category: String,
14    pub status: String,
15    pub duration_ms: u64,
16    pub timestamp: String,
17    pub commit: String,
18    pub note: String,
19}
20
21/// Aggregate counts over a set of rows.
22pub struct Summary {
23    pub total: usize,
24    pub passed: usize,
25    pub failed: usize,
26    pub skipped: usize,
27    pub unknown: usize,
28}
29
30/// Read every result row from the CSV. A missing file yields an empty set so the
31/// dashboard renders cleanly before the first test run.
32pub fn load(path: &Path) -> Vec<TestRow> {
33    let Ok(text) = std::fs::read_to_string(path) else {
34        return Vec::new();
35    };
36    text.lines()
37        .skip(1) // header
38        .filter(|line| !line.trim().is_empty())
39        .filter_map(parse_row)
40        .collect()
41}
42
43#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // CSV column layout
44fn parse_row(line: &str) -> Option<TestRow> {
45    let fields = split_csv_line(line);
46    if fields.len() < 4 {
47        return None;
48    }
49    Some(TestRow {
50        suite: fields[0].clone(),
51        name: fields[1].clone(),
52        category: fields[2].clone(),
53        status: normalize_status(&fields[3]),
54        duration_ms: fields.get(4).and_then(|f| f.parse().ok()).unwrap_or(0),
55        timestamp: fields.get(5).cloned().unwrap_or_default(),
56        commit: fields.get(6).cloned().unwrap_or_default(),
57        note: fields.get(7).cloned().unwrap_or_default(),
58    })
59}
60
61fn normalize_status(raw: &str) -> String {
62    match raw.trim().to_ascii_lowercase().as_str() {
63        "pass" | "passed" | "ok" => "pass",
64        "fail" | "failed" | "error" => "fail",
65        "skip" | "skipped" | "notexecuted" | "ignored" => "skip",
66        _ => "unknown",
67    }
68    .to_string()
69}
70
71/// Split one CSV line, honouring double-quoted fields with `""` escapes.
72fn split_csv_line(line: &str) -> Vec<String> {
73    let mut fields = Vec::new();
74    let mut field = String::new();
75    let mut in_quotes = false;
76    let mut chars = line.chars().peekable();
77    while let Some(c) = chars.next() {
78        match c {
79            '"' if in_quotes && chars.peek() == Some(&'"') => {
80                field.push('"');
81                chars.next();
82            }
83            '"' => in_quotes = !in_quotes,
84            ',' if !in_quotes => fields.push(std::mem::take(&mut field)),
85            _ => field.push(c),
86        }
87    }
88    fields.push(field);
89    fields
90}
91
92/// Count pass/fail/skip/unknown across the rows.
93pub fn summarize(rows: &[TestRow]) -> Summary {
94    let mut summary =
95        Summary { total: rows.len(), passed: 0, failed: 0, skipped: 0, unknown: 0 };
96    for row in rows {
97        match row.status.as_str() {
98            "pass" => summary.passed += 1,
99            "fail" => summary.failed += 1,
100            "skip" => summary.skipped += 1,
101            _ => summary.unknown += 1,
102        }
103    }
104    summary
105}