use serde::{Deserialize, Serialize};
pub mod status {
pub const PASS: &str = "pass";
pub const FAIL: &str = "fail";
pub const IGNORED: &str = "ignored";
pub const STALLED: &str = "stalled";
pub const SKIP: &str = "skip";
pub const LISTED: &str = "listed";
pub fn is_red(s: &str) -> bool {
s == FAIL || s == STALLED
}
pub fn is_listed(s: &str) -> bool {
s == LISTED
}
pub fn is_green(s: &str) -> bool {
s == PASS
}
pub fn is_neutral(s: &str) -> bool {
s == IGNORED || s == SKIP
}
}
pub const ASPECT_UNIT: &str = "unit";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TestResultRow {
pub run_id: String,
pub repo: String,
pub suite: String,
pub test_name: String,
pub status: String,
pub duration_ms: f64,
pub ts_micros: i64,
pub message: String,
#[serde(default = "default_aspect")]
pub aspect: String,
#[serde(default)]
pub metric: f64,
}
fn default_aspect() -> String {
ASPECT_UNIT.to_string()
}
impl TestResultRow {
pub fn key(&self) -> (i64, String, String, String) {
(self.ts_micros, self.run_id.clone(), self.suite.clone(), self.test_name.clone())
}
#[allow(clippy::too_many_arguments)]
pub fn unit(
run_id: impl Into<String>,
repo: impl Into<String>,
suite: impl Into<String>,
test_name: impl Into<String>,
status: impl Into<String>,
duration_ms: f64,
ts_micros: i64,
message: impl Into<String>,
) -> Self {
Self {
run_id: run_id.into(),
repo: repo.into(),
suite: suite.into(),
test_name: test_name.into(),
status: status.into(),
duration_ms,
ts_micros,
message: message.into(),
aspect: ASPECT_UNIT.to_string(),
metric: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub enum TestSelector {
Run(String),
Repo(String),
All,
}
pub fn parse_nextest_list(json: &str) -> Vec<String> {
let Ok(v) = serde_json::from_str::<serde_json::Value>(json) else {
return Vec::new();
};
let mut out = Vec::new();
let suites = v.get("rust-suites").and_then(|s| s.as_object());
if let Some(suites) = suites {
for (suite_name, suite) in suites {
let suite_label = suite
.get("binary-id")
.and_then(|b| b.as_str())
.unwrap_or(suite_name.as_str());
if let Some(cases) = suite.get("testcases").and_then(|c| c.as_object()) {
for case_name in cases.keys() {
out.push(format!("{suite_label}::{case_name}"));
}
}
}
}
out.sort();
out.dedup();
out
}
pub fn parse_cargo_test_list(output: &str) -> Vec<String> {
let mut out = Vec::new();
for line in output.lines() {
let l = line.trim();
if let Some(name) = l.strip_suffix(": test") {
let name = name.trim();
if !name.is_empty() {
out.push(name.to_string());
}
}
}
out.sort();
out.dedup();
out
}
pub fn listed_rows(
names: &[String],
run_id: &str,
repo: &str,
ts_micros: i64,
) -> Vec<TestResultRow> {
names
.iter()
.map(|full| {
let (suite, test_name) = match full.rsplit_once("::") {
Some((s, n)) if !s.is_empty() => (s.to_string(), n.to_string()),
_ => (repo.to_string(), full.clone()),
};
TestResultRow {
run_id: run_id.to_string(),
repo: repo.to_string(),
suite,
test_name,
status: status::LISTED.to_string(),
duration_ms: 0.0,
ts_micros,
message: String::new(),
aspect: ASPECT_UNIT.to_string(),
metric: 0.0,
}
})
.collect()
}
pub fn rows_to_json(rows: &[TestResultRow]) -> String {
fn esc(s: &str) -> String {
let mut o = String::with_capacity(s.len() + 2);
for c in s.chars() {
match c {
'"' => o.push_str("\\\""),
'\\' => o.push_str("\\\\"),
'\n' => o.push_str("\\n"),
'\t' => o.push_str("\\t"),
'\r' => o.push_str("\\r"),
c => o.push(c),
}
}
o
}
let mut s = String::from("[\n");
for (i, r) in rows.iter().enumerate() {
let ts_rfc = rfc3339_from_micros(r.ts_micros);
s.push_str(&format!(
" {{\"run_id\": \"{}\", \"repo\": \"{}\", \"suite\": \"{}\", \
\"test_name\": \"{}\", \"status\": \"{}\", \"duration_ms\": {:.3}, \
\"ts\": \"{}\", \"message\": \"{}\", \"aspect\": \"{}\", \"metric\": {:.3}}}{}\n",
esc(&r.run_id),
esc(&r.repo),
esc(&r.suite),
esc(&r.test_name),
esc(&r.status),
r.duration_ms,
esc(&ts_rfc),
esc(&r.message),
esc(&r.aspect),
r.metric,
if i + 1 < rows.len() { "," } else { "" },
));
}
s.push(']');
s
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunSummary {
pub run_id: String,
pub repo: String,
pub ts_micros: i64,
pub passed: usize,
pub failed: usize,
pub ignored: usize,
pub stalled: usize,
pub skipped: usize,
pub listed: usize,
}
impl RunSummary {
pub fn total(&self) -> usize {
self.passed + self.failed + self.ignored + self.stalled + self.skipped + self.listed
}
pub fn green(&self) -> bool {
self.failed == 0 && self.stalled == 0
}
}
pub fn summarize_runs(rows: &[TestResultRow]) -> Vec<RunSummary> {
use std::collections::BTreeMap;
let mut by_run: BTreeMap<String, RunSummary> = BTreeMap::new();
for r in rows {
let s = by_run.entry(r.run_id.clone()).or_insert_with(|| RunSummary {
run_id: r.run_id.clone(),
repo: r.repo.clone(),
ts_micros: r.ts_micros,
..Default::default()
});
s.ts_micros = s.ts_micros.max(r.ts_micros);
match r.status.as_str() {
status::PASS => s.passed += 1,
status::FAIL => s.failed += 1,
status::IGNORED => s.ignored += 1,
status::STALLED => s.stalled += 1,
status::SKIP => s.skipped += 1,
status::LISTED => s.listed += 1,
_ => s.failed += 1, }
}
let mut out: Vec<RunSummary> = by_run.into_values().collect();
out.sort_by(|a, b| b.ts_micros.cmp(&a.ts_micros));
out
}
pub fn runs_to_json(summaries: &[RunSummary]) -> String {
let arr: Vec<serde_json::Value> = summaries
.iter()
.map(|s| {
serde_json::json!({
"run_id": s.run_id,
"repo": s.repo,
"ts_micros": s.ts_micros,
"passed": s.passed,
"failed": s.failed,
"ignored": s.ignored,
"stalled": s.stalled,
"skipped": s.skipped,
"listed": s.listed,
"total": s.total(),
"green": s.green(),
})
})
.collect();
serde_json::to_string_pretty(&serde_json::Value::Array(arr))
.unwrap_or_else(|_| "[]".to_string())
}
pub fn render_matrix(rows: &[TestResultRow]) -> String {
use std::collections::BTreeMap;
if rows.is_empty() {
return "(no test runs recorded)\n".to_string();
}
let summaries = summarize_runs(rows);
let mut by_run: BTreeMap<String, Vec<&TestResultRow>> = BTreeMap::new();
for r in rows {
by_run.entry(r.run_id.clone()).or_default().push(r);
}
let mut out = String::new();
for s in &summaries {
let mark = if s.green() { "✓" } else { "✗" };
let ts = rfc3339_from_micros(s.ts_micros);
out.push_str(&format!(
"{mark} {repo} run {run} [{ts}]\n {p} passed · {f} failed · {ig} ignored · {st} stalled · {sk} skipped ({tot} total)\n",
repo = s.repo,
run = short_run(&s.run_id),
p = s.passed,
f = s.failed,
ig = s.ignored,
st = s.stalled,
sk = s.skipped,
tot = s.total(),
));
if let Some(cases) = by_run.get(&s.run_id) {
for c in cases.iter().filter(|c| status::is_red(&c.status)) {
let detail = if c.message.is_empty() { String::new() } else { format!(" — {}", c.message) };
out.push_str(&format!(
" ✗ [{}] {}::{} {}{}\n",
c.aspect, c.suite, c.test_name, c.status, detail
));
}
}
out.push('\n');
}
out
}
pub fn short_run(run_id: &str) -> String {
if run_id.len() > 12 {
format!("{}…", &run_id[..8])
} else {
run_id.to_string()
}
}
pub fn new_run_id() -> String {
let mut b = seed_16();
b[6] = (b[6] & 0x0f) | 0x40;
b[8] = (b[8] & 0x3f) | 0x80;
format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15],
)
}
fn seed_16() -> [u8; 16] {
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static CTR: AtomicU64 = AtomicU64::new(0);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let ctr = CTR.fetch_add(1, Ordering::Relaxed);
let stack = &nanos as *const _ as u64;
let mut state = nanos ^ ctr.rotate_left(17) ^ stack.rotate_left(31);
let mut out = [0u8; 16];
for chunk in out.chunks_mut(8) {
state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
let bytes = z.to_le_bytes();
chunk.copy_from_slice(&bytes[..chunk.len()]);
}
out
}
pub fn rfc3339_from_micros(ts_micros: i64) -> String {
let secs = ts_micros.div_euclid(1_000_000);
let micros = ts_micros.rem_euclid(1_000_000);
let days = secs.div_euclid(86_400);
let secs_of_day = secs.rem_euclid(86_400);
let (h, m, s) = (
secs_of_day / 3600,
(secs_of_day % 3600) / 60,
secs_of_day % 60,
);
let (year, month, day) = civil_from_days(days);
format!(
"{year:04}-{month:02}-{day:02}T{h:02}:{m:02}:{s:02}.{micros:06}+00:00"
)
}
fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = (doy - (153 * mp + 2) / 5 + 1) as u32; let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; (if m <= 2 { y + 1 } else { y }, m, d)
}
#[cfg(test)]
mod tests {
use super::*;
fn row(run: &str, repo: &str, name: &str, st: &str, aspect: &str, metric: f64, ts: i64) -> TestResultRow {
TestResultRow {
run_id: run.into(),
repo: repo.into(),
suite: repo.into(),
test_name: name.into(),
status: st.into(),
duration_ms: 1.0,
ts_micros: ts,
message: String::new(),
aspect: aspect.into(),
metric,
}
}
#[test]
fn summarize_counts_every_status_including_skip() {
let rows = vec![
row("r", "z", "a", status::PASS, "unit", 0.0, 100),
row("r", "z", "b", status::FAIL, "unit", 0.0, 100),
row("r", "z", "c", status::IGNORED, "unit", 0.0, 100),
row("r", "z", "d", status::STALLED, "unit", 0.0, 100),
row("r", "z", "clippy", status::SKIP, "clippy", 0.0, 100),
];
let s = summarize_runs(&rows);
assert_eq!(s.len(), 1);
assert_eq!(
(s[0].passed, s[0].failed, s[0].ignored, s[0].stalled, s[0].skipped),
(1, 1, 1, 1, 1)
);
assert_eq!(s[0].total(), 5);
assert!(!s[0].green(), "a fail makes the run red");
}
#[test]
fn skip_is_neither_red_nor_green() {
assert!(!status::is_red(status::SKIP));
assert!(!status::is_green(status::SKIP));
assert!(status::is_neutral(status::SKIP));
assert!(status::is_red(status::FAIL));
assert!(status::is_red(status::STALLED));
assert!(status::is_green(status::PASS));
}
#[test]
fn summarize_orders_newest_first() {
let rows = vec![
row("old", "z", "a", status::PASS, "unit", 0.0, 100),
row("new", "z", "b", status::PASS, "unit", 0.0, 500),
];
let s = summarize_runs(&rows);
assert_eq!(s[0].run_id, "new", "newest run id first");
assert_eq!(s[1].run_id, "old");
}
#[test]
fn runs_to_json_newest_first_with_green_and_total() {
let rows = vec![
row("old", "z", "a", status::PASS, "unit", 0.0, 100),
row("old", "z", "b", status::PASS, "unit", 0.0, 100),
row("new", "z", "a", status::PASS, "unit", 0.0, 500),
row("new", "z", "c", status::FAIL, "unit", 0.0, 500),
];
let summaries = summarize_runs(&rows);
let json = runs_to_json(&summaries);
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
let arr = v.as_array().unwrap();
assert_eq!(arr.len(), 2, "one summary per run");
assert_eq!(arr[0]["run_id"], "new");
assert_eq!(arr[0]["failed"], 1);
assert_eq!(arr[0]["total"], 2);
assert_eq!(arr[0]["green"], false, "newest run has a failure");
assert_eq!(arr[1]["run_id"], "old");
assert_eq!(arr[1]["passed"], 2);
assert_eq!(arr[1]["green"], true);
let back: RunSummary = serde_json::from_str(
&serde_json::to_string(&summaries[0]).unwrap(),
)
.unwrap();
assert_eq!(back, summaries[0]);
}
#[test]
fn json_carries_aspect_and_metric() {
let rows = vec![row("r", "z", "coverage", status::PASS, "coverage", 87.5, 100)];
let json = rows_to_json(&rows);
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
let o = &v.as_array().unwrap()[0];
assert_eq!(o["aspect"], "coverage");
assert_eq!(o["metric"], 87.5);
assert_eq!(o["status"], "pass");
}
#[test]
fn render_matrix_tags_red_case_with_aspect() {
let rows = vec![
row("r", "z", "build", status::FAIL, "build", 0.0, 100),
row("r", "z", "ok", status::PASS, "unit", 0.0, 100),
];
let m = render_matrix(&rows);
assert!(m.contains("[build]"), "red case is tagged with its aspect: {m}");
assert!(m.contains("1 failed"), "summary line counts the failure");
assert!(m.contains("skipped"), "summary line names the skip bucket");
}
#[test]
fn rfc3339_is_correct_for_known_epoch() {
assert_eq!(rfc3339_from_micros(1_609_459_200_000_000), "2021-01-01T00:00:00.000000+00:00");
assert_eq!(rfc3339_from_micros(0), "1970-01-01T00:00:00.000000+00:00");
assert_eq!(rfc3339_from_micros(1_609_459_200_123_456), "2021-01-01T00:00:00.123456+00:00");
}
#[test]
fn new_run_id_is_uuid_v4_shaped_and_unique() {
let a = new_run_id();
let b = new_run_id();
assert_ne!(a, b, "two ids differ");
assert_eq!(a.len(), 36, "uuid string length");
assert_eq!(a.as_bytes()[14], b'4', "version nibble is 4");
let variant = a.as_bytes()[19];
assert!(matches!(variant, b'8' | b'9' | b'a' | b'b'), "RFC4122 variant nibble: {variant}");
}
#[test]
fn unit_constructor_defaults_aspect_and_metric() {
let r = TestResultRow::unit("r", "z", "z", "t", status::PASS, 5.0, 100, "");
assert_eq!(r.aspect, ASPECT_UNIT);
assert_eq!(r.metric, 0.0);
}
#[test]
fn parse_nextest_list_extracts_exact_test_names() {
let json = r#"{
"rust-suites": {
"nornir": {
"binary-id": "nornir",
"testcases": {
"warehouse::tests::round_trip": {"ignored": false},
"viz::tests::renders": {"ignored": false}
}
},
"roundtrip": {
"binary-id": "nornir::roundtrip",
"testcases": {
"end_to_end": {"ignored": false}
}
}
}
}"#;
let names = parse_nextest_list(json);
assert_eq!(
names,
vec![
"nornir::roundtrip::end_to_end".to_string(),
"nornir::viz::tests::renders".to_string(),
"nornir::warehouse::tests::round_trip".to_string(),
],
"exact discovered names, sorted: {names:?}"
);
}
#[test]
fn parse_nextest_list_handles_empty_and_garbage() {
assert!(parse_nextest_list("not json").is_empty());
assert!(parse_nextest_list(r#"{"rust-suites":{}}"#).is_empty());
assert!(
parse_nextest_list(r#"{"rust-suites":{"s":{"binary-id":"s","testcases":{}}}}"#)
.is_empty(),
"a suite with no testcases yields nothing"
);
}
#[test]
fn parse_cargo_test_list_keeps_tests_drops_benches() {
let out = "\
warehouse::tests::round_trip: test
viz::tests::renders: test
bench_throughput: benchmark
2 tests, 1 benchmark
";
let names = parse_cargo_test_list(out);
assert_eq!(
names,
vec![
"viz::tests::renders".to_string(),
"warehouse::tests::round_trip".to_string(),
],
"only `: test` lines, sorted; benchmark + summary dropped: {names:?}"
);
}
#[test]
fn listed_rows_builds_listed_status_rows_with_split_suite() {
let names = vec![
"nornir::warehouse::tests::round_trip".to_string(),
"bare_name".to_string(),
];
let rows = listed_rows(&names, "run1", "nornir", 4242);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].suite, "nornir::warehouse::tests");
assert_eq!(rows[0].test_name, "round_trip");
assert_eq!(rows[0].status, status::LISTED);
assert_eq!(rows[0].run_id, "run1");
assert_eq!(rows[0].repo, "nornir");
assert_eq!(rows[0].ts_micros, 4242);
assert_eq!(rows[0].aspect, ASPECT_UNIT);
assert_eq!(rows[1].suite, "nornir");
assert_eq!(rows[1].test_name, "bare_name");
assert_eq!(rows[1].status, status::LISTED);
}
#[test]
fn listed_status_is_not_red_green_or_neutral() {
assert!(!status::is_red(status::LISTED));
assert!(!status::is_green(status::LISTED));
assert!(!status::is_neutral(status::LISTED), "listed is its own bucket, not skip");
assert!(status::is_listed(status::LISTED));
}
#[test]
fn summarize_counts_listed_separately_not_as_failed() {
let rows = vec![
row("r", "z", "a", status::PASS, "unit", 0.0, 100),
row("r", "z", "b", status::LISTED, "unit", 0.0, 100),
row("r", "z", "c", status::LISTED, "unit", 0.0, 100),
];
let s = summarize_runs(&rows);
assert_eq!(s[0].listed, 2, "listed rows counted in their own bucket");
assert_eq!(s[0].failed, 0, "listed must NEVER count as a failure");
assert!(s[0].green(), "a run of pass + listed is still green (no red)");
assert_eq!(s[0].total(), 3);
}
#[test]
fn deserialize_legacy_row_without_aspect_defaults() {
let json = r#"{"run_id":"r","repo":"z","suite":"z","test_name":"t","status":"pass","duration_ms":1.0,"ts_micros":100,"message":""}"#;
let r: TestResultRow = serde_json::from_str(json).unwrap();
assert_eq!(r.aspect, ASPECT_UNIT);
assert_eq!(r.metric, 0.0);
assert_eq!(r.status, status::PASS);
}
}