use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Instant;
use crate::model::{new_run_id, status, TestResultRow};
use crate::runner::{detect_runner, run_matrix};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Aspect {
Build,
Unit,
Doctest,
Clippy,
Fmt,
Audit,
BenchSmoke,
Coverage,
FeaturePowerset,
Msrv,
Examples,
FunnelDecompose,
FunnelDemo,
Functional,
}
impl Aspect {
pub const ALL: &'static [Aspect] = &[
Aspect::Build,
Aspect::Unit,
Aspect::Doctest,
Aspect::Clippy,
Aspect::Fmt,
Aspect::Audit,
Aspect::BenchSmoke,
Aspect::Coverage,
Aspect::FeaturePowerset,
Aspect::Msrv,
Aspect::Examples,
Aspect::FunnelDecompose,
Aspect::FunnelDemo,
Aspect::Functional,
];
pub const DEFAULT: &'static [Aspect] = &[
Aspect::Build,
Aspect::Unit,
Aspect::Doctest,
Aspect::Clippy,
Aspect::Fmt,
Aspect::Audit,
Aspect::FunnelDecompose,
Aspect::FunnelDemo,
Aspect::Functional,
];
pub fn label(self) -> &'static str {
match self {
Aspect::Build => "build",
Aspect::Unit => "unit",
Aspect::Doctest => "doctest",
Aspect::Clippy => "clippy",
Aspect::Fmt => "fmt",
Aspect::Audit => "audit",
Aspect::BenchSmoke => "bench-smoke",
Aspect::Coverage => "coverage",
Aspect::FeaturePowerset => "feature-powerset",
Aspect::Msrv => "msrv",
Aspect::Examples => "examples",
Aspect::FunnelDecompose => "funnel",
Aspect::FunnelDemo => "funnel-demo",
Aspect::Functional => "functional",
}
}
}
pub fn aspect_label(a: Aspect) -> &'static str {
a.label()
}
pub fn parse_aspect(s: &str) -> Option<Aspect> {
match s.trim().to_ascii_lowercase().replace('_', "-").as_str() {
"build" => Some(Aspect::Build),
"unit" | "test" | "tests" => Some(Aspect::Unit),
"doctest" | "doc" | "doctests" => Some(Aspect::Doctest),
"clippy" | "lint" => Some(Aspect::Clippy),
"fmt" | "format" | "rustfmt" => Some(Aspect::Fmt),
"audit" => Some(Aspect::Audit),
"bench-smoke" | "bench" => Some(Aspect::BenchSmoke),
"coverage" | "cov" => Some(Aspect::Coverage),
"feature-powerset" | "powerset" | "hack" => Some(Aspect::FeaturePowerset),
"msrv" => Some(Aspect::Msrv),
"examples" | "example" => Some(Aspect::Examples),
"funnel" | "funnel-decompose" | "funnel_decompose" => Some(Aspect::FunnelDecompose),
"funnel-demo" | "funnel_demo" | "demo" => Some(Aspect::FunnelDemo),
"functional" | "func" | "self-test" => Some(Aspect::Functional),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AspectOutcome {
pub aspect: Aspect,
pub status: String,
pub metric: f64,
pub message: String,
pub duration_ms: f64,
pub cases: Vec<TestResultRow>,
}
impl AspectOutcome {
fn skip(aspect: Aspect, reason: impl Into<String>) -> Self {
Self {
aspect,
status: status::SKIP.into(),
metric: 0.0,
message: reason.into(),
duration_ms: 0.0,
cases: Vec::new(),
}
}
}
pub fn run_aspect(repo_root: &Path, aspect: Aspect) -> AspectOutcome {
let t0 = Instant::now();
let mut outcome = match aspect {
Aspect::Unit => run_unit(repo_root),
Aspect::Build => run_cargo_check(repo_root, aspect, &["build", "--all-features"]),
Aspect::Doctest => run_cargo_check(repo_root, aspect, &["test", "--doc"]),
Aspect::Examples => run_cargo_check(repo_root, aspect, &["build", "--examples"]),
Aspect::BenchSmoke => run_cargo_check(repo_root, aspect, &["bench", "--no-run"]),
Aspect::Clippy => run_clippy(repo_root),
Aspect::Fmt => run_fmt(repo_root),
Aspect::Audit => run_audit(repo_root),
Aspect::Coverage => run_coverage(repo_root),
Aspect::FeaturePowerset => run_powerset(repo_root),
Aspect::Msrv => run_msrv(repo_root),
Aspect::FunnelDecompose => run_funnel_decompose(repo_root),
Aspect::FunnelDemo => run_funnel_demo(repo_root),
Aspect::Functional => run_functional(),
};
if outcome.duration_ms == 0.0 {
outcome.duration_ms = t0.elapsed().as_secs_f64() * 1000.0;
}
outcome
}
pub fn run_full_matrix(repo_root: &Path, aspects: &[Aspect]) -> Vec<TestResultRow> {
let run_id = new_run_id();
let ts_micros = now_micros();
let repo = repo_name(repo_root);
let mut rows = Vec::new();
for &aspect in aspects {
let out = run_aspect(repo_root, aspect);
rows.extend(outcome_to_rows(&out, &run_id, &repo, ts_micros));
}
rows
}
pub fn outcome_to_rows(
out: &AspectOutcome,
run_id: &str,
repo: &str,
ts_micros: i64,
) -> Vec<TestResultRow> {
if (out.aspect == Aspect::Unit || out.aspect == Aspect::Functional) && !out.cases.is_empty() {
let label = out.aspect.label().to_string();
return out
.cases
.iter()
.map(|c| TestResultRow {
run_id: run_id.to_string(),
repo: repo.to_string(),
suite: if c.suite.is_empty() { repo.to_string() } else { c.suite.clone() },
test_name: c.test_name.clone(),
status: c.status.clone(),
duration_ms: c.duration_ms,
ts_micros,
message: c.message.clone(),
aspect: label.clone(),
metric: c.metric,
})
.collect();
}
vec![TestResultRow {
run_id: run_id.to_string(),
repo: repo.to_string(),
suite: repo.to_string(),
test_name: out.aspect.label().to_string(),
status: out.status.clone(),
duration_ms: out.duration_ms,
ts_micros,
message: out.message.clone(),
aspect: out.aspect.label().to_string(),
metric: out.metric,
}]
}
fn run_functional() -> AspectOutcome {
let cases = crate::functional::drain_functional_rows();
if cases.is_empty() {
return AspectOutcome::skip(
Aspect::Functional,
"no functional status recorded (build with --features testmatrix and run the self-tests)",
);
}
let red = cases.iter().filter(|c| status::is_red(&c.status)).count();
let status = if red > 0 { status::FAIL } else { status::PASS };
AspectOutcome {
aspect: Aspect::Functional,
status: status.into(),
metric: red as f64,
message: format!("{} functional check(s), {} red", cases.len(), red),
duration_ms: 0.0,
cases,
}
}
fn run_unit(repo_root: &Path) -> AspectOutcome {
let runner = detect_runner();
match run_matrix(repo_root, runner) {
Ok(run) => {
let run_id = new_run_id();
let ts = now_micros();
let repo = repo_name(repo_root);
let cases: Vec<TestResultRow> = run
.cases
.iter()
.map(|c| TestResultRow {
run_id: run_id.clone(),
repo: repo.clone(),
suite: if c.suite.is_empty() { repo.clone() } else { c.suite.clone() },
test_name: c.name.clone(),
status: c.status.clone(),
duration_ms: c.duration_ms,
ts_micros: ts,
message: c.message.clone(),
aspect: Aspect::Unit.label().to_string(),
metric: 0.0,
})
.collect();
let st = if run.green() { status::PASS } else { status::FAIL };
AspectOutcome {
aspect: Aspect::Unit,
status: st.into(),
metric: run.failed() as f64,
message: format!(
"{} passed · {} failed · {} ignored · {} stalled",
run.passed(),
run.failed(),
run.ignored(),
run.stalled_count(),
),
duration_ms: 0.0,
cases,
}
}
Err(e) => AspectOutcome {
aspect: Aspect::Unit,
status: status::FAIL.into(),
metric: 0.0,
message: format!("could not run tests: {e}"),
duration_ms: 0.0,
cases: Vec::new(),
},
}
}
fn run_cargo_check(repo_root: &Path, aspect: Aspect, args: &[&str]) -> AspectOutcome {
let out = match run_capture(repo_root, "cargo", args) {
Ok(o) => o,
Err(e) => {
return AspectOutcome {
aspect,
status: status::FAIL.into(),
metric: 0.0,
message: format!("cargo {} failed to launch: {e}", args.join(" ")),
duration_ms: 0.0,
cases: Vec::new(),
}
}
};
if out.ok {
AspectOutcome {
aspect,
status: status::PASS.into(),
metric: 0.0,
message: format!("cargo {} ok", args.join(" ")),
duration_ms: 0.0,
cases: Vec::new(),
}
} else {
AspectOutcome {
aspect,
status: status::FAIL.into(),
metric: 0.0,
message: first_error_line(&out.stderr),
duration_ms: 0.0,
cases: Vec::new(),
}
}
}
fn run_clippy(repo_root: &Path) -> AspectOutcome {
if !subcommand_present("clippy") {
return AspectOutcome::skip(Aspect::Clippy, "cargo-clippy not on PATH");
}
match run_capture(
repo_root,
"cargo",
&["clippy", "--all-targets", "--", "-D", "warnings"],
) {
Ok(o) => parse_clippy(o.ok, &o.stderr),
Err(e) => AspectOutcome {
aspect: Aspect::Clippy,
status: status::FAIL.into(),
metric: 0.0,
message: format!("clippy failed to launch: {e}"),
duration_ms: 0.0,
cases: Vec::new(),
},
}
}
fn fmt_files_needing_format(stdout: &str, stderr: &str) -> usize {
stdout
.lines()
.chain(stderr.lines())
.filter_map(|l| l.trim_start().strip_prefix("Diff in "))
.filter_map(|rest| rest.find(".rs").map(|i| &rest[..i + 3]))
.collect::<std::collections::BTreeSet<&str>>()
.len()
}
fn run_fmt(repo_root: &Path) -> AspectOutcome {
if !subcommand_present("fmt") {
return AspectOutcome::skip(Aspect::Fmt, "rustfmt / cargo-fmt not on PATH");
}
match run_capture(repo_root, "cargo", &["fmt", "--check"]) {
Ok(o) => {
if o.ok {
AspectOutcome {
aspect: Aspect::Fmt,
status: status::PASS.into(),
metric: 0.0,
message: "formatted".into(),
duration_ms: 0.0,
cases: Vec::new(),
}
} else {
let n = fmt_files_needing_format(&o.stdout, &o.stderr);
AspectOutcome {
aspect: Aspect::Fmt,
status: status::FAIL.into(),
metric: n as f64,
message: format!("{n} file(s) need formatting"),
duration_ms: 0.0,
cases: Vec::new(),
}
}
}
Err(e) => AspectOutcome {
aspect: Aspect::Fmt,
status: status::FAIL.into(),
metric: 0.0,
message: format!("fmt failed to launch: {e}"),
duration_ms: 0.0,
cases: Vec::new(),
},
}
}
fn run_audit(repo_root: &Path) -> AspectOutcome {
if !subcommand_present("audit") {
return AspectOutcome::skip(Aspect::Audit, "cargo-audit not on PATH");
}
match run_capture(repo_root, "cargo", &["audit"]) {
Ok(o) => parse_audit(o.ok, &format!("{}\n{}", o.stdout, o.stderr)),
Err(e) => AspectOutcome {
aspect: Aspect::Audit,
status: status::FAIL.into(),
metric: 0.0,
message: format!("audit failed to launch: {e}"),
duration_ms: 0.0,
cases: Vec::new(),
},
}
}
fn run_coverage(repo_root: &Path) -> AspectOutcome {
if !subcommand_present("llvm-cov") {
return AspectOutcome::skip(Aspect::Coverage, "cargo-llvm-cov not on PATH");
}
match run_capture(repo_root, "cargo", &["llvm-cov", "--summary-only"]) {
Ok(o) => parse_coverage(o.ok, &format!("{}\n{}", o.stdout, o.stderr)),
Err(e) => AspectOutcome {
aspect: Aspect::Coverage,
status: status::FAIL.into(),
metric: 0.0,
message: format!("llvm-cov failed to launch: {e}"),
duration_ms: 0.0,
cases: Vec::new(),
},
}
}
fn run_powerset(repo_root: &Path) -> AspectOutcome {
if !subcommand_present("hack") {
return AspectOutcome::skip(Aspect::FeaturePowerset, "cargo-hack not on PATH");
}
match run_capture(repo_root, "cargo", &["hack", "check", "--feature-powerset"]) {
Ok(o) => {
if o.ok {
AspectOutcome {
aspect: Aspect::FeaturePowerset,
status: status::PASS.into(),
metric: 0.0,
message: "every feature combo compiles".into(),
duration_ms: 0.0,
cases: Vec::new(),
}
} else {
AspectOutcome {
aspect: Aspect::FeaturePowerset,
status: status::FAIL.into(),
metric: 0.0,
message: first_error_line(&o.stderr),
duration_ms: 0.0,
cases: Vec::new(),
}
}
}
Err(e) => AspectOutcome {
aspect: Aspect::FeaturePowerset,
status: status::FAIL.into(),
metric: 0.0,
message: format!("hack failed to launch: {e}"),
duration_ms: 0.0,
cases: Vec::new(),
},
}
}
fn run_msrv(repo_root: &Path) -> AspectOutcome {
let msrv = match read_rust_version(repo_root) {
Some(v) => v,
None => return AspectOutcome::skip(Aspect::Msrv, "no rust-version in Cargo.toml"),
};
if !toolchain_present(&msrv) {
return AspectOutcome::skip(Aspect::Msrv, format!("toolchain {msrv} not installed"));
}
let plus = format!("+{msrv}");
match run_capture(repo_root, "cargo", &[&plus, "check"]) {
Ok(o) => {
if o.ok {
AspectOutcome {
aspect: Aspect::Msrv,
status: status::PASS.into(),
metric: 0.0,
message: format!("compiles on declared MSRV {msrv}"),
duration_ms: 0.0,
cases: Vec::new(),
}
} else {
AspectOutcome {
aspect: Aspect::Msrv,
status: status::FAIL.into(),
metric: 0.0,
message: format!("does NOT compile on declared MSRV {msrv}: {}", first_error_line(&o.stderr)),
duration_ms: 0.0,
cases: Vec::new(),
}
}
}
Err(e) => AspectOutcome {
aspect: Aspect::Msrv,
status: status::FAIL.into(),
metric: 0.0,
message: format!("msrv check failed to launch: {e}"),
duration_ms: 0.0,
cases: Vec::new(),
},
}
}
fn run_funnel_decompose(repo_root: &Path) -> AspectOutcome {
let out = match run_capture(
repo_root,
"cargo",
&["test", "--test", "funnel_decompose", "--", "--test-threads=1"],
) {
Ok(o) => o,
Err(e) => {
return AspectOutcome {
aspect: Aspect::FunnelDecompose,
status: status::FAIL.into(),
metric: 0.0,
message: format!("funnel_decompose test failed to launch: {e}"),
duration_ms: 0.0,
cases: Vec::new(),
}
}
};
let combined = format!("{}
{}", out.stdout, out.stderr);
parse_funnel_decompose(out.ok, &combined)
}
fn run_funnel_demo(repo_root: &Path) -> AspectOutcome {
let out = match run_capture(
repo_root,
"cargo",
&[
"test",
"--features",
"viz",
"--test",
"funnel_demo",
"--test",
"funnel_viz",
"--",
"--test-threads=1",
],
) {
Ok(o) => o,
Err(e) => {
return AspectOutcome {
aspect: Aspect::FunnelDemo,
status: status::FAIL.into(),
metric: 0.0,
message: format!("funnel_demo tests failed to launch: {e}"),
duration_ms: 0.0,
cases: Vec::new(),
}
}
};
let combined = format!("{}\n{}", out.stdout, out.stderr);
parse_funnel_demo(out.ok, &combined)
}
pub fn parse_funnel_decompose(exit_ok: bool, output: &str) -> AspectOutcome {
let mut pass = 0usize;
let mut fail = 0usize;
for line in output.lines() {
let l = line.trim();
if l.starts_with("test ") && l.ends_with(" ... ok") {
pass += 1;
} else if l.starts_with("test ") && (l.ends_with(" ... FAILED") || l.ends_with(" ... failed")) {
fail += 1;
}
}
if pass == 0 && fail == 0 {
for line in output.lines() {
let l = line.trim();
if let Some(rest) = l.strip_suffix(" passed;") {
let n: usize = rest.split_whitespace().last().and_then(|s| s.parse().ok()).unwrap_or(0);
pass += n;
}
if let Some(rest) = l.strip_suffix(" failed;") {
let n: usize = rest.split_whitespace().last().and_then(|s| s.parse().ok()).unwrap_or(0);
fail += n;
}
if l.starts_with("test result:") {
for part in l.split(';') {
let p = part.trim();
if let Some(n_str) = p.strip_suffix(" passed") {
let n: usize = n_str.trim().split_whitespace().last().and_then(|s| s.parse().ok()).unwrap_or(0);
if n > pass { pass = n; }
}
if let Some(n_str) = p.strip_suffix(" failed") {
let n: usize = n_str.trim().split_whitespace().last().and_then(|s| s.parse().ok()).unwrap_or(0);
if n > fail { fail = n; }
}
}
}
}
}
let total = pass + fail;
if exit_ok && fail == 0 {
AspectOutcome {
aspect: Aspect::FunnelDecompose,
status: status::PASS.into(),
metric: pass as f64,
message: format!("funnel decompose: {pass}/{total} pass"),
duration_ms: 0.0,
cases: Vec::new(),
}
} else {
AspectOutcome {
aspect: Aspect::FunnelDecompose,
status: status::FAIL.into(),
metric: pass as f64,
message: if fail > 0 {
format!("funnel decompose: {pass}/{total} pass, {fail} FAILED")
} else {
first_error_line(output)
},
duration_ms: 0.0,
cases: Vec::new(),
}
}
}
pub fn parse_funnel_demo(exit_ok: bool, output: &str) -> AspectOutcome {
let mut pass = 0usize;
let mut fail = 0usize;
for line in output.lines() {
let l = line.trim();
if l.starts_with("test ") && l.ends_with(" ... ok") {
pass += 1;
} else if l.starts_with("test ")
&& (l.ends_with(" ... FAILED") || l.ends_with(" ... failed"))
{
fail += 1;
}
}
if pass == 0 && fail == 0 {
for line in output.lines() {
let l = line.trim();
if l.starts_with("test result:") {
for part in l.split(';') {
let p = part.trim();
if let Some(n_str) = p.strip_suffix(" passed") {
let n: usize = n_str
.trim()
.split_whitespace()
.last()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
pass += n;
}
if let Some(n_str) = p.strip_suffix(" failed") {
let n: usize = n_str
.trim()
.split_whitespace()
.last()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
fail += n;
}
}
}
}
}
let total = pass + fail;
if exit_ok && fail == 0 {
AspectOutcome {
aspect: Aspect::FunnelDemo,
status: status::PASS.into(),
metric: pass as f64,
message: format!("funnel demo: {pass}/{total} injection tests pass"),
duration_ms: 0.0,
cases: Vec::new(),
}
} else {
AspectOutcome {
aspect: Aspect::FunnelDemo,
status: status::FAIL.into(),
metric: pass as f64,
message: if fail > 0 {
format!("funnel demo: {pass}/{total} pass, {fail} FAILED")
} else {
first_error_line(output)
},
duration_ms: 0.0,
cases: Vec::new(),
}
}
}
pub fn parse_clippy(exit_ok: bool, stderr: &str) -> AspectOutcome {
let warnings = count_clippy_warnings(stderr);
if exit_ok && warnings == 0 {
AspectOutcome {
aspect: Aspect::Clippy,
status: status::PASS.into(),
metric: 0.0,
message: "no clippy warnings".into(),
duration_ms: 0.0,
cases: Vec::new(),
}
} else {
AspectOutcome {
aspect: Aspect::Clippy,
status: status::FAIL.into(),
metric: warnings as f64,
message: format!("{warnings} clippy warning(s)"),
duration_ms: 0.0,
cases: Vec::new(),
}
}
}
fn count_clippy_warnings(stderr: &str) -> usize {
for line in stderr.lines() {
let l = line.trim();
if let Some(rest) = l.strip_prefix("warning: ") {
if let Some(n_str) = rest.strip_suffix(" warnings emitted").or_else(|| rest.strip_suffix(" warning emitted")) {
if let Ok(n) = n_str.trim().parse::<usize>() {
return n;
}
}
}
}
stderr
.lines()
.filter(|l| l.trim_start().starts_with("warning:") && !l.contains("emitted"))
.count()
}
pub fn parse_audit(exit_ok: bool, output: &str) -> AspectOutcome {
let count = count_audit_advisories(output);
if exit_ok && count == 0 {
AspectOutcome {
aspect: Aspect::Audit,
status: status::PASS.into(),
metric: 0.0,
message: "no advisories".into(),
duration_ms: 0.0,
cases: Vec::new(),
}
} else {
AspectOutcome {
aspect: Aspect::Audit,
status: status::FAIL.into(),
metric: count as f64,
message: format!("{count} advisory/ies found"),
duration_ms: 0.0,
cases: Vec::new(),
}
}
}
fn count_audit_advisories(output: &str) -> usize {
for line in output.lines() {
let l = line.trim().trim_start_matches("error: ");
for suffix in [" vulnerabilities found", " vulnerability found"] {
if let Some(idx) = l.find(suffix) {
let prefix = l[..idx].rsplit(|c: char| !c.is_ascii_digit()).next().unwrap_or("");
if let Ok(n) = prefix.parse::<usize>() {
return n;
}
}
}
}
0
}
pub fn parse_coverage(exit_ok: bool, output: &str) -> AspectOutcome {
let pct = parse_total_coverage_pct(output);
match pct {
Some(p) if exit_ok => AspectOutcome {
aspect: Aspect::Coverage,
status: status::PASS.into(),
metric: p,
message: format!("{p:.2}% line coverage"),
duration_ms: 0.0,
cases: Vec::new(),
},
Some(p) => AspectOutcome {
aspect: Aspect::Coverage,
status: status::FAIL.into(),
metric: p,
message: format!("coverage ran but cargo exited non-zero ({p:.2}%)"),
duration_ms: 0.0,
cases: Vec::new(),
},
None => AspectOutcome {
aspect: Aspect::Coverage,
status: status::FAIL.into(),
metric: 0.0,
message: "could not parse a TOTAL coverage % from llvm-cov output".into(),
duration_ms: 0.0,
cases: Vec::new(),
},
}
}
fn parse_total_coverage_pct(output: &str) -> Option<f64> {
let line = output.lines().find(|l| l.trim_start().starts_with("TOTAL"))?;
let mut last = None;
for tok in line.split_whitespace() {
if let Some(num) = tok.strip_suffix('%') {
if let Ok(p) = num.parse::<f64>() {
last = Some(p);
}
}
}
last
}
fn first_error_line(stderr: &str) -> String {
for line in stderr.lines() {
let l = line.trim();
if l.starts_with("error") || l.contains("error[") || l.starts_with("Error:") {
return l.chars().take(240).collect();
}
}
stderr
.lines()
.rev()
.map(|l| l.trim())
.find(|l| !l.is_empty())
.map(|l| l.chars().take(240).collect())
.unwrap_or_else(|| "failed (no stderr)".into())
}
struct Capture {
ok: bool,
stdout: String,
stderr: String,
}
fn run_capture(repo_root: &Path, prog: &str, args: &[&str]) -> std::io::Result<Capture> {
let out = Command::new(prog)
.args(args)
.current_dir(repo_root)
.stdin(Stdio::null())
.output()?;
Ok(Capture {
ok: out.status.success(),
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
})
}
pub fn subcommand_present(sub: &str) -> bool {
Command::new("cargo")
.args([sub, "--version"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.stdin(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn toolchain_present(tc: &str) -> bool {
let plus = format!("+{tc}");
Command::new("cargo")
.args([&plus, "--version"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.stdin(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub fn read_rust_version(repo_root: &Path) -> Option<String> {
let text = std::fs::read_to_string(repo_root.join("Cargo.toml")).ok()?;
parse_rust_version(&text)
}
pub fn parse_rust_version(manifest: &str) -> Option<String> {
for line in manifest.lines() {
let l = line.trim();
if let Some(rest) = l.strip_prefix("rust-version") {
let rest = rest.trim_start();
if let Some(rest) = rest.strip_prefix('=') {
let v = rest.trim().trim_matches('"').trim_matches('\'');
if !v.is_empty() {
return Some(v.to_string());
}
}
}
}
None
}
fn repo_name(repo_root: &Path) -> String {
repo_root
.file_name()
.and_then(|f| f.to_str())
.map(|s| s.to_string())
.unwrap_or_else(|| repo_root.display().to_string())
}
fn now_micros() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_micros() as i64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_aspect_round_trips_every_label() {
for &a in Aspect::ALL {
assert_eq!(parse_aspect(a.label()), Some(a), "{} round-trips", a.label());
}
assert_eq!(parse_aspect("tests"), Some(Aspect::Unit));
assert_eq!(parse_aspect("LINT"), Some(Aspect::Clippy));
assert_eq!(parse_aspect("bench_smoke"), Some(Aspect::BenchSmoke));
assert_eq!(parse_aspect("nope"), None);
}
#[test]
fn fmt_counts_unique_files_not_hunks() {
let stdout = "\
Diff in /repo/src/a.rs at line 10:
-old
+new
Diff in /repo/src/a.rs at line 42:
-foo
+bar
Diff in /repo/src/b.rs at line 3:
-baz
+qux
";
assert_eq!(
fmt_files_needing_format(stdout, ""),
2,
"a.rs (2 hunks) + b.rs (1 hunk) = 2 unique files"
);
assert_eq!(fmt_files_needing_format("", ""), 0, "clean run = 0 files");
}
#[test]
fn clippy_parser_counts_warnings_from_summary() {
let stderr = "\
warning: unused variable: `x`
--> src/a.rs:1:5
warning: this could be simpler
--> src/b.rs:2:1
warning: 7 warnings emitted
";
let o = parse_clippy(false, stderr);
assert_eq!(o.status, status::FAIL);
assert_eq!(o.metric, 7.0, "the summary line's count is authoritative");
assert!(o.message.contains("7 clippy warning"));
}
#[test]
fn clippy_parser_clean_run_passes() {
let o = parse_clippy(true, " Checking foo v0.1.0\n Finished\n");
assert_eq!(o.status, status::PASS);
assert_eq!(o.metric, 0.0);
}
#[test]
fn clippy_parser_counts_lead_lines_without_summary() {
let stderr = "warning: a\nwarning: b\n";
let o = parse_clippy(false, stderr);
assert_eq!(o.metric, 2.0, "fallback counts warning: lead lines");
}
#[test]
fn audit_parser_counts_vulnerabilities() {
let out = "\
Crate: openssl
error: 2 vulnerabilities found!
";
let o = parse_audit(false, out);
assert_eq!(o.status, status::FAIL);
assert_eq!(o.metric, 2.0);
assert!(o.message.contains("2 advisory"));
}
#[test]
fn audit_parser_singular_and_clean() {
let one = parse_audit(false, "1 vulnerability found!");
assert_eq!(one.metric, 1.0);
let clean = parse_audit(true, "Success No vulnerable packages found");
assert_eq!(clean.status, status::PASS);
assert_eq!(clean.metric, 0.0);
}
#[test]
fn coverage_parser_reads_total_line_pct() {
let out = "\
Filename Regions Missed Cover Lines Missed Cover
src/lib.rs 100 5 95.00% 200 8 96.00%
TOTAL 100 5 95.00% 200 8 87.50%
";
let o = parse_coverage(true, out);
assert_eq!(o.status, status::PASS);
assert!((o.metric - 87.50).abs() < 1e-9, "last % on TOTAL line = line coverage: {}", o.metric);
assert!(o.message.contains("87.50%"));
}
#[test]
fn coverage_parser_no_total_is_fail() {
let o = parse_coverage(true, "no summary here");
assert_eq!(o.status, status::FAIL);
assert_eq!(o.metric, 0.0);
}
#[test]
fn rust_version_parser_extracts_msrv() {
let manifest = "[package]\nname = \"x\"\nrust-version = \"1.85\"\nedition = \"2021\"\n";
assert_eq!(parse_rust_version(manifest), Some("1.85".into()));
assert_eq!(parse_rust_version("[package]\nname=\"x\"\n"), None);
}
#[test]
fn outcome_to_rows_synthetic_aspect_row_carries_metric() {
let out = AspectOutcome {
aspect: Aspect::Coverage,
status: status::PASS.into(),
metric: 91.3,
message: "91.30% line coverage".into(),
duration_ms: 12.0,
cases: Vec::new(),
};
let rows = outcome_to_rows(&out, "run1", "znippy", 100);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].aspect, "coverage");
assert_eq!(rows[0].test_name, "coverage");
assert_eq!(rows[0].metric, 91.3);
assert_eq!(rows[0].repo, "znippy");
assert_eq!(rows[0].run_id, "run1");
}
#[test]
fn outcome_to_rows_unit_expands_cases() {
let out = AspectOutcome {
aspect: Aspect::Unit,
status: status::FAIL.into(),
metric: 1.0,
message: "1 failed".into(),
duration_ms: 5.0,
cases: vec![
TestResultRow::unit("inner", "z", "", "a::ok", status::PASS, 1.0, 9, ""),
TestResultRow::unit("inner", "z", "suite", "a::bad", status::FAIL, 2.0, 9, "boom"),
],
};
let rows = outcome_to_rows(&out, "run1", "znippy", 100);
assert_eq!(rows.len(), 2, "unit aspect expands to per-test rows");
assert_eq!(rows[0].suite, "znippy");
assert_eq!(rows[1].suite, "suite");
assert!(rows.iter().all(|r| r.run_id == "run1" && r.ts_micros == 100));
assert!(rows.iter().all(|r| r.aspect == "unit"));
}
#[test]
fn run_aspect_fmt_on_this_worktree_returns_a_row() {
let dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let out = run_aspect(dir, Aspect::Fmt);
assert_eq!(out.aspect, Aspect::Fmt);
assert!(
matches!(out.status.as_str(), status::PASS | status::FAIL | status::SKIP),
"fmt outcome is one of pass/fail/skip: {}",
out.status
);
let rows = outcome_to_rows(&out, "r", "nornir-testmatrix", now_micros());
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].aspect, "fmt");
assert_eq!(rows[0].test_name, "fmt");
assert!(out.duration_ms >= 0.0);
}
#[test]
fn run_full_matrix_one_run_id_across_aspects() {
let dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let rows = run_full_matrix(dir, &[Aspect::Fmt, Aspect::Audit]);
assert!(!rows.is_empty(), "got rows back");
let run_ids: std::collections::HashSet<_> = rows.iter().map(|r| r.run_id.clone()).collect();
assert_eq!(run_ids.len(), 1, "all rows share ONE run id");
let aspects: std::collections::HashSet<_> = rows.iter().map(|r| r.aspect.clone()).collect();
assert!(aspects.contains("fmt"));
assert!(aspects.contains("audit"));
}
#[test]
fn funnel_demo_parser_all_pass_counts_both_binaries() {
let output = "\
running 2 tests
test inject_demo_is_idempotent ... ok
test inject_demo_roundtrips_to_exact_golden_dag ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
running 3 tests
test demo_button_injects_dag_into_state_json ... ok
test nuke_confirm_guard_is_two_click ... ok
test nuke_is_disk_only_funnel_persists ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
";
let o = parse_funnel_demo(true, output);
assert_eq!(o.aspect, Aspect::FunnelDemo);
assert_eq!(o.status, status::PASS);
assert_eq!(o.metric, 5.0, "5 passing demo injection tests");
assert!(o.message.contains("5/5"), "msg: {}", o.message);
}
#[test]
fn funnel_demo_parser_one_failure_is_fail_with_count() {
let output = "\
test inject_demo_roundtrips_to_exact_golden_dag ... ok
test demo_button_injects_dag_into_state_json ... FAILED
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
";
let o = parse_funnel_demo(false, output);
assert_eq!(o.status, status::FAIL);
assert_eq!(o.metric, 1.0);
assert!(o.message.contains("1 FAILED"), "msg: {}", o.message);
}
#[test]
fn funnel_demo_aspect_is_registered_and_parses() {
assert!(Aspect::ALL.contains(&Aspect::FunnelDemo));
assert!(Aspect::DEFAULT.contains(&Aspect::FunnelDemo));
assert_eq!(Aspect::FunnelDemo.label(), "funnel-demo");
assert_eq!(parse_aspect("funnel-demo"), Some(Aspect::FunnelDemo));
assert_eq!(parse_aspect("funnel_demo"), Some(Aspect::FunnelDemo));
assert_eq!(parse_aspect("demo"), Some(Aspect::FunnelDemo));
let out = parse_funnel_demo(true, "test x ... ok\ntest result: ok. 1 passed; 0 failed;");
let rows = outcome_to_rows(&out, "r", "nornir", now_micros());
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].aspect, "funnel-demo");
}
#[test]
fn functional_aspect_is_registered_and_parses() {
assert!(Aspect::ALL.contains(&Aspect::Functional));
assert!(Aspect::DEFAULT.contains(&Aspect::Functional));
assert_eq!(Aspect::Functional.label(), "functional");
assert_eq!(parse_aspect("functional"), Some(Aspect::Functional));
assert_eq!(parse_aspect("func"), Some(Aspect::Functional));
}
#[cfg(feature = "testmatrix")]
#[test]
fn functional_aspect_drains_emitted_red_status_into_red_matrix_row() {
use crate::functional::{drain_functional_rows, functional_status, test_lock};
use std::path::Path;
let _guard = test_lock();
let _ = drain_functional_rows();
functional_status("facett-map", "basemap_rendered", false, "0 ways, blank framebuffer");
let out = run_aspect(Path::new("."), Aspect::Functional);
assert_eq!(out.status, status::FAIL, "any red drained → aspect is FAIL");
assert_eq!(out.metric, 1.0, "one red check");
let rows = outcome_to_rows(&out, "runX", "nornir", 4242);
let red: Vec<_> = rows.iter().filter(|r| r.test_name == "basemap_rendered").collect();
assert_eq!(red.len(), 1, "exactly the emitted check as its own row");
assert_eq!(red[0].suite, "facett-map", "component → suite");
assert_eq!(red[0].aspect, "functional");
assert_eq!(red[0].status, status::FAIL);
assert!(status::is_red(&red[0].status), "matrix reads it as RED");
assert_eq!(red[0].message, "0 ways, blank framebuffer");
assert_eq!(red[0].run_id, "runX", "restamped onto the run");
assert_eq!(red[0].repo, "nornir");
assert_eq!(red[0].ts_micros, 4242);
}
#[cfg(feature = "testmatrix")]
#[test]
fn functional_aspect_green_when_all_checks_ok() {
use crate::functional::{drain_functional_rows, functional_status, test_lock};
use std::path::Path;
let _guard = test_lock();
let _ = drain_functional_rows();
functional_status("facett-map", "basemap_rendered", true, "1024 ways drawn");
let out = run_aspect(Path::new("."), Aspect::Functional);
assert_eq!(out.status, status::PASS, "no red → aspect PASS");
assert_eq!(out.metric, 0.0);
let rows = outcome_to_rows(&out, "runG", "nornir", 7);
let mine: Vec<_> = rows.iter().filter(|r| r.test_name == "basemap_rendered").collect();
assert_eq!(mine.len(), 1);
assert_eq!(mine[0].status, status::PASS);
assert!(status::is_green(&mine[0].status));
}
#[cfg(not(feature = "testmatrix"))]
#[test]
fn functional_aspect_is_skip_noop_in_release() {
use std::path::Path;
let out = run_aspect(Path::new("."), Aspect::Functional);
assert_eq!(out.status, status::SKIP, "release: nothing recorded → skip");
assert!(out.cases.is_empty(), "no rows compiled/written in release");
let rows = outcome_to_rows(&out, "r", "nornir", 1);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].status, status::SKIP);
assert!(status::is_neutral(&rows[0].status));
}
}