Files
cadcraft/xtask/src/stats.rs
T

176 lines
6.8 KiB
Rust

//! `cargo xtask stats`: tests and lines of Rust per crate.
use std::path::Path;
#[derive(Default, Clone, Copy)]
struct Counts {
files: usize,
lines: usize,
tests: usize,
proptests: usize,
}
/// Upper bound on directory entries visited in one `walk`, so even a crafted tree of aliases can
/// never make the stats command run unbounded work.
const MAX_ENTRIES: usize = 2_000_000;
/// Accumulate Rust source counts under one directory with a fresh traversal-entry budget.
fn walk(dir: &Path, c: &mut Counts) -> Result<(), String> {
let mut budget = MAX_ENTRIES;
walk_inner(dir, c, &mut budget)
}
/// Traverse entries without following symlinks, sharing one budget across descendant directories.
/// Skip unreadable entries and excluded directories; report budget exhaustion to the caller.
fn walk_inner(dir: &Path, c: &mut Counts, budget: &mut usize) -> Result<(), String> {
let Ok(rd) = std::fs::read_dir(dir) else {
return Ok(());
};
for e in rd.flatten() {
if *budget == 0 {
return Err(format!("stats traversal budget exhausted at {}", dir.display()));
}
*budget -= 1;
// Use the entry's own type and never follow symlinks: a directory symlink would otherwise
// be re-scanned as a fresh directory, so a small graph of aliases could cause exponential
// repeated enumeration and reads (CWE-400).
let Ok(ft) = e.file_type() else { continue };
if ft.is_symlink() {
continue;
}
let p = e.path();
let name = e.file_name();
let name = name.to_string_lossy();
if ft.is_dir() {
// Nested fuzz workspaces and build output are not part of the crate.
if name != "target" && name != "fuzz" && !name.starts_with('.') {
walk_inner(&p, c, budget)?;
}
} else if ft.is_file()
&& name.ends_with(".rs")
&& let Ok(s) = std::fs::read_to_string(&p)
{
c.files += 1;
c.lines += s.lines().count();
let (t, pt) = count_tests(&s);
c.tests += t;
c.proptests += pt;
}
}
Ok(())
}
/// `#[test]` attributes and `proptest!` blocks (each block may hold many
/// property tests, and macro-generated tests are only counted once — use
/// `--exact` for harness-accurate numbers).
fn count_tests(src: &str) -> (usize, usize) {
let mut tests = 0;
let mut props = 0;
for line in src.lines() {
let t = line.trim_start();
if t.starts_with("//") {
continue;
}
tests += t.matches("#[test]").count();
if t.starts_with("proptest!") {
props += 1;
}
}
(tests, props)
}
fn exact_tests(pkg: &str) -> Option<usize> {
let out = crate::cargo().args(["test", "-q", "-p", pkg, "--", "--list"]).output().ok()?;
if !out.status.success() {
return None;
}
Some(String::from_utf8_lossy(&out.stdout).lines().filter(|l| l.ends_with(": test")).count())
}
/// Print per-package source statistics, optionally querying exact test counts, and propagate traversal errors.
pub fn run(_root: &Path, exact: bool) -> Result<(), String> {
let meta = crate::metadata()?;
let mut rows = Vec::new();
for p in meta["packages"].as_array().into_iter().flatten() {
let name = p["name"].as_str().unwrap_or("?").to_owned();
let Some(dir) = p["manifest_path"].as_str().and_then(|m| Path::new(m).parent().map(Path::to_path_buf)) else {
continue;
};
let mut c = Counts::default();
walk(&dir, &mut c)?;
let ex = if exact { exact_tests(&name) } else { None };
rows.push((name, c, ex));
}
rows.sort_by(|a, b| a.0.cmp(&b.0));
let exact_col = if exact { format!(" {:>8}", "harness") } else { String::new() };
println!("{:<26} {:>6} {:>8} {:>7} {:>10}{exact_col}", "crate", "files", "lines", "#[test]", "proptest!");
println!("{}", "-".repeat(62 + if exact { 9 } else { 0 }));
let mut total = Counts::default();
let mut total_exact = 0;
for (name, c, ex) in &rows {
let ex_s = match (exact, ex) {
(false, _) => String::new(),
(true, Some(n)) => {
total_exact += n;
format!(" {n:>8}")
}
(true, None) => format!(" {:>8}", "error"),
};
println!("{name:<26} {:>6} {:>8} {:>7} {:>10}{ex_s}", c.files, c.lines, c.tests, c.proptests);
total.files += c.files;
total.lines += c.lines;
total.tests += c.tests;
total.proptests += c.proptests;
}
println!("{}", "-".repeat(62 + if exact { 9 } else { 0 }));
let ex_s = if exact { format!(" {total_exact:>8}") } else { String::new() };
println!("{:<26} {:>6} {:>8} {:>7} {:>10}{ex_s}", "TOTAL", total.files, total.lines, total.tests, total.proptests);
if !exact {
println!("\n(#[test] counts source attributes; macro-generated tests count once. Use --exact for harness counts.)");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{Counts, count_tests, walk};
#[test]
fn counts_attributes_and_proptest_blocks() {
let src = "#[test]\nfn a() {}\n// #[test] commented\n #[test]\nfn b() {}\nproptest! {\n}\n";
assert_eq!(count_tests(src), (2, 1));
}
/// A directory alias does not cause its Rust source to be counted twice.
#[cfg(unix)]
#[test]
fn walk_does_not_follow_directory_symlinks() {
use std::os::unix::fs::symlink;
let base = std::env::temp_dir().join(format!("cadcraft-xtask-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let real = base.join("real");
std::fs::create_dir_all(&real).unwrap();
std::fs::write(real.join("a.rs"), "fn a() {}\n").unwrap();
// A directory symlink pointing back at `real` would be re-scanned if followed.
symlink(&real, base.join("alias")).unwrap();
let mut c = Counts::default();
walk(&base, &mut c).unwrap();
assert_eq!(c.files, 1, "the aliased directory was followed and counted twice");
std::fs::remove_dir_all(&base).ok();
}
/// Traversal reports an exhausted entry budget and accepts a tree that exactly fits it.
#[test]
fn walk_reports_budget_exhaustion() {
let base = std::env::temp_dir().join(format!("cadcraft-stats-budget-{}", std::process::id()));
std::fs::create_dir_all(&base).unwrap();
std::fs::write(base.join("a.rs"), "fn a() {}\n").unwrap();
let error = super::walk_inner(&base, &mut Counts::default(), &mut 0).unwrap_err();
assert!(error.contains("budget exhausted"));
let mut c = Counts::default();
super::walk_inner(&base, &mut c, &mut 1).unwrap();
assert_eq!(c.files, 1);
std::fs::remove_dir_all(base).unwrap();
}
}