M0.1/M0.9: xtask (layers, wasm, ci, stats), agent docs, NOTICE, README, control protocol doc

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-09-30 06:21:08 -04:00
1 parent bea0c16636
commit 8cd8f8f8a7
21 files changed
+2150 -6

No files matched your search

+489
View File
@@ -0,0 +1,489 @@
//! Dependency layering rules (plan/architecture.md §3).
//!
//! The rule engine works on a small, metadata-independent model so it can be
//! unit-tested; `from_metadata` builds that model from `cargo metadata`.
use serde_json::Value;
/// Where a workspace crate sits.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Class {
/// Regular layered crate.
Layer(u8),
/// Layer 0, and additionally may depend on no workspace crate at all.
Standalone,
/// Test tooling: may depend on anything up to L5 (it sits at L6 for rule
/// purposes); other crates may use it only as a dev-dependency.
Testkit,
/// Binaries and build tooling: exempt from the rules.
Exempt,
}
impl Class {
fn layer(self) -> Option<u8> {
match self {
Class::Layer(l) => Some(l),
Class::Standalone => Some(0),
Class::Testkit => Some(6),
Class::Exempt => None,
}
}
}
/// The layering table. Names are package names without the `drawcraft-`
/// prefix.
pub const TABLE: &[(&str, Class)] = &[
("geom", Class::Layer(0)),
("color", Class::Layer(0)),
("doc", Class::Layer(1)),
("pathops", Class::Layer(2)),
("text", Class::Layer(2)),
("effects", Class::Layer(2)),
("render", Class::Layer(3)),
("svg", Class::Layer(3)),
("pdf", Class::Layer(3)),
("format", Class::Layer(3)),
("tools", Class::Layer(4)),
("engine", Class::Layer(5)),
("ui-egui", Class::Layer(6)),
("mcp", Class::Layer(6)),
("testkit", Class::Testkit),
// L7 apps and tooling
("drawcraft", Class::Exempt),
("cli", Class::Exempt),
("web", Class::Exempt),
("xtask", Class::Exempt),
];
/// Explicit orderings *within* a layer (earlier may be used by later).
/// The L0 foundation is a small chain: `raster` builds on `color` and
/// `geom`, which the §3 diagram draws on one line. The GPU backend (`gpu`)
/// reuses the CPU reference (`compose`) for LUTs and parity tests.
pub const INTRA_LAYER_ORDER: &[&[&str]] = &[&["geom", "color"], &["pathops", "effects"]];
fn intra_layer_allowed(from: &str, to: &str) -> bool {
let (from, to) = (short_name(from), short_name(to));
INTRA_LAYER_ORDER.iter().any(|chain| {
match (
chain.iter().position(|n| *n == from),
chain.iter().position(|n| *n == to),
) {
(Some(f), Some(t)) => t < f,
_ => false,
}
})
}
/// External crates that constitute a UI toolkit / windowing dependency.
/// Entries ending in `*` are prefixes.
pub const UI_CRATES: &[&str] = &["egui", "eframe", "winit", "egui_kittest", "rfd", "bevy*"];
/// First layer allowed to use UI crates.
pub const UI_MIN_LAYER: u8 = 6;
pub fn short_name(pkg: &str) -> &str {
pkg.strip_prefix("drawcraft-").unwrap_or(pkg)
}
pub fn classify(pkg: &str) -> Option<Class> {
let s = short_name(pkg);
TABLE.iter().find(|(n, _)| *n == s).map(|(_, c)| *c)
}
fn is_ui_crate(name: &str) -> bool {
UI_CRATES.iter().any(|p| match p.strip_suffix('*') {
Some(prefix) => name.starts_with(prefix),
None => name == *p,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DepKind {
Normal,
Dev,
Build,
}
#[derive(Debug, Clone)]
pub struct Dep {
pub name: String,
pub kind: DepKind,
/// `true` if the dependency is a workspace member.
pub workspace: bool,
}
#[derive(Debug, Clone)]
pub struct Crate {
pub name: String,
pub deps: Vec<Dep>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Violation {
Unregistered {
krate: String,
},
Upward {
krate: String,
dep: String,
from: u8,
to: u8,
kind: DepKind,
},
StandaloneHasWorkspaceDep {
krate: String,
dep: String,
},
TestkitAsNormalDep {
krate: String,
},
UiBelowL6 {
krate: String,
dep: String,
layer: u8,
},
}
impl std::fmt::Display for Violation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Violation::Unregistered { krate } => write!(
f,
"{krate}: unknown workspace crate; register it in xtask/src/layers.rs TABLE (see plan/architecture.md §3)"
),
Violation::Upward {
krate,
dep,
from,
to,
kind,
} => write!(
f,
"{krate} (L{from}) -> {dep} (L{to}) [{kind:?}]: may only depend on strictly lower layers"
),
Violation::StandaloneHasWorkspaceDep { krate, dep } => {
write!(
f,
"{krate}: standalone crate must not depend on workspace crate {dep}"
)
}
Violation::TestkitAsNormalDep { krate } => {
write!(
f,
"{krate}: drawcraft-testkit may only be a dev-dependency"
)
}
Violation::UiBelowL6 { krate, dep, layer } => {
write!(
f,
"{krate} (L{layer}) depends on UI crate `{dep}`; UI toolkits are only allowed in L6+"
)
}
}
}
}
/// Check all rules. Returns violations sorted for stable output.
pub fn check(crates: &[Crate]) -> Vec<Violation> {
let mut out = Vec::new();
for c in crates {
let Some(class) = classify(&c.name) else {
out.push(Violation::Unregistered {
krate: c.name.clone(),
});
continue;
};
if class == Class::Exempt {
continue;
}
let layer = class.layer().unwrap_or(0);
for d in &c.deps {
// Self dev-dependencies (e.g. to enable features in tests) are fine.
if d.name == c.name {
continue;
}
if d.workspace {
if class == Class::Standalone {
out.push(Violation::StandaloneHasWorkspaceDep {
krate: c.name.clone(),
dep: d.name.clone(),
});
continue;
}
match classify(&d.name) {
// Unregistered deps are reported on their own entry.
None => {}
Some(Class::Testkit) => {
if d.kind != DepKind::Dev {
out.push(Violation::TestkitAsNormalDep {
krate: c.name.clone(),
});
}
}
Some(dc) => {
let to = dc.layer().unwrap_or(u8::MAX);
if to >= layer && !(to == layer && intra_layer_allowed(&c.name, &d.name)) {
out.push(Violation::Upward {
krate: c.name.clone(),
dep: d.name.clone(),
from: layer,
to: if to == u8::MAX { 7 } else { to },
kind: d.kind,
});
}
}
}
} else if layer < UI_MIN_LAYER && is_ui_crate(&d.name) {
out.push(Violation::UiBelowL6 {
krate: c.name.clone(),
dep: d.name.clone(),
layer,
});
}
}
}
out.sort_by_key(|v| v.to_string());
out.dedup();
out
}
/// Build the model from `cargo metadata --format-version 1 --no-deps`.
pub fn from_metadata(meta: &Value) -> Result<Vec<Crate>, String> {
let pkgs = meta["packages"]
.as_array()
.ok_or("metadata: no packages array")?;
let members: Vec<&str> = pkgs.iter().filter_map(|p| p["name"].as_str()).collect();
let mut out = Vec::new();
for p in pkgs {
let name = p["name"].as_str().ok_or("package without name")?.to_owned();
let mut deps = Vec::new();
for d in p["dependencies"].as_array().into_iter().flatten() {
let dname = d["name"].as_str().unwrap_or_default().to_owned();
let kind = match d["kind"].as_str() {
Some("dev") => DepKind::Dev,
Some("build") => DepKind::Build,
_ => DepKind::Normal,
};
let workspace = members.contains(&dname.as_str()) || d["path"].is_string();
deps.push(Dep {
name: dname,
kind,
workspace,
});
}
out.push(Crate { name, deps });
}
out.sort_by(|a, b| a.name.cmp(&b.name));
Ok(out)
}
pub fn describe(class: Option<Class>) -> String {
match class {
Some(Class::Layer(l)) => format!("L{l}"),
Some(Class::Standalone) => "L0 standalone".into(),
Some(Class::Testkit) => "testkit".into(),
Some(Class::Exempt) => "exempt".into(),
None => "UNREGISTERED".into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn c(name: &str, deps: &[(&str, DepKind, bool)]) -> Crate {
Crate {
name: name.into(),
deps: deps
.iter()
.map(|(n, k, w)| Dep {
name: (*n).into(),
kind: *k,
workspace: *w,
})
.collect(),
}
}
use DepKind::*;
#[test]
fn clean_downward_graph_passes() {
let g = [
c("drawcraft-geom", &[("kurbo", Normal, false)]),
c("drawcraft-doc", &[("drawcraft-geom", Normal, true)]),
c(
"drawcraft-engine",
&[
("drawcraft-doc", Normal, true),
("drawcraft-testkit", Dev, true),
],
),
c(
"drawcraft-ui-egui",
&[("drawcraft-engine", Normal, true), ("egui", Normal, false)],
),
c("drawcraft-cli", &[("drawcraft-ui-egui", Normal, true)]),
];
assert!(check(&g).is_empty(), "{:?}", check(&g));
}
#[test]
fn upward_dependency_flagged() {
let v = check(&[c("drawcraft-doc", &[("drawcraft-engine", Normal, true)])]);
assert!(matches!(v[..], [Violation::Upward { from: 1, to: 5, .. }]));
}
#[test]
fn sideways_dependency_flagged() {
let v = check(&[c("drawcraft-ops", &[("drawcraft-algo", Normal, true)])]);
assert!(matches!(v[..], [Violation::Upward { from: 2, to: 2, .. }]));
}
#[test]
fn l0_foundation_chain_allowed_one_way() {
assert!(
check(&[c(
"drawcraft-raster",
&[
("drawcraft-color", Normal, true),
("drawcraft-geom", Normal, true)
]
)])
.is_empty()
);
assert!(check(&[c("drawcraft-color", &[("drawcraft-geom", Normal, true)])]).is_empty());
let v = check(&[c("drawcraft-geom", &[("drawcraft-raster", Normal, true)])]);
assert!(matches!(v[..], [Violation::Upward { from: 0, to: 0, .. }]));
}
#[test]
fn self_dev_dependency_ignored() {
assert!(check(&[c("drawcraft-psd", &[("drawcraft-psd", Dev, true)])]).is_empty());
}
#[test]
fn upward_dev_dependency_flagged() {
let v = check(&[c("drawcraft-geom", &[("drawcraft-doc", Dev, true)])]);
assert!(matches!(v[..], [Violation::Upward { kind: Dev, .. }]));
}
#[test]
fn standalone_crates_have_no_workspace_deps() {
for s in [
"drawcraft-psd",
"drawcraft-codecs",
"drawcraft-adobe-assets",
] {
let v = check(&[c(s, &[("drawcraft-geom", Normal, true)])]);
assert!(
matches!(v[..], [Violation::StandaloneHasWorkspaceDep { .. }]),
"{s}"
);
assert!(check(&[c(s, &[("image", Normal, false)])]).is_empty());
}
}
#[test]
fn standalone_is_usable_from_higher_layers() {
assert!(check(&[c("drawcraft-io", &[("drawcraft-psd", Normal, true)])]).is_empty());
let v = check(&[c("drawcraft-geom", &[("drawcraft-codecs", Normal, true)])]);
assert!(matches!(v[..], [Violation::Upward { from: 0, to: 0, .. }]));
}
#[test]
fn ui_crates_below_l6_flagged() {
for dep in [
"egui",
"eframe",
"winit",
"egui_kittest",
"rfd",
"bevy_ecs",
"bevy",
] {
let v = check(&[c("drawcraft-engine", &[(dep, Normal, false)])]);
assert!(
matches!(v[..], [Violation::UiBelowL6 { layer: 5, .. }]),
"{dep}"
);
}
assert!(
check(&[c(
"drawcraft-engine",
&[("egui_extras_not", Normal, false)]
)])
.is_empty()
);
assert!(check(&[c("drawcraft-platform", &[("winit", Normal, false)])]).is_empty());
}
#[test]
fn unregistered_crate_is_error() {
let v = check(&[c("drawcraft-mystery", &[])]);
assert!(
matches!(&v[..], [Violation::Unregistered { krate }] if krate == "drawcraft-mystery")
);
assert!(v[0].to_string().contains("register"));
}
#[test]
fn testkit_only_as_dev_dependency() {
let v = check(&[c(
"drawcraft-raster",
&[("drawcraft-testkit", Normal, true)],
)]);
assert!(matches!(v[..], [Violation::TestkitAsNormalDep { .. }]));
assert!(check(&[c("drawcraft-raster", &[("drawcraft-testkit", Dev, true)])]).is_empty());
// testkit itself may use anything up to L5 but not L6 crates.
assert!(
check(&[c(
"drawcraft-testkit",
&[("drawcraft-engine", Normal, true)]
)])
.is_empty()
);
assert!(
!check(&[c(
"drawcraft-testkit",
&[("drawcraft-ui-egui", Normal, true)]
)])
.is_empty()
);
}
#[test]
fn apps_and_xtask_exempt() {
for app in ["drawcraft", "drawcraft-cli", "drawcraft-web", "xtask"] {
assert!(
check(&[c(
app,
&[
("egui", Normal, false),
("drawcraft-ui-egui", Normal, true)
]
)])
.is_empty()
);
}
}
#[test]
fn metadata_parsing() {
let meta: Value = serde_json::from_str(
r#"{"packages":[
{"name":"drawcraft-doc","dependencies":[
{"name":"drawcraft-geom","kind":null,"path":"/x/crates/geom"},
{"name":"serde","kind":null},
{"name":"proptest","kind":"dev"}]},
{"name":"drawcraft-geom","dependencies":[]}
]}"#,
)
.unwrap();
let g = from_metadata(&meta).unwrap();
assert_eq!(g.len(), 2);
let doc = g.iter().find(|c| c.name == "drawcraft-doc").unwrap();
assert!(doc.deps[0].workspace && !doc.deps[1].workspace);
assert_eq!(doc.deps[2].kind, Dev);
assert!(check(&g).is_empty());
}
}
+272 -1
View File
@@ -1 +1,272 @@
fn main() {}
//! Workspace tooling: `cargo xtask <command>`.
//!
//! Pure Rust (std + serde_json). External tools (`cargo`, `curl`, `tar`) are
//! invoked through `std::process::Command`.
mod layers;
mod stats;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
const USAGE: &str = "\
usage: cargo xtask <command>
commands:
layers enforce the crate dependency layering (plan/architecture.md §3)
wasm cargo check --target wasm32-unknown-unknown for the wasm-safe crates
ci fmt --check, clippy -D warnings, test, layers, wasm (stops at first failure)
corpus [--download]
show where test corpora live; --download fetches PngSuite into corpus/pngsuite
stats [--exact] count tests and lines per crate (--exact: ask the test harness via `-- --list`)
";
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
let rest: Vec<&str> = args.iter().skip(1).map(String::as_str).collect();
let result = match args.first().map(String::as_str) {
Some("layers") => cmd_layers(),
Some("wasm") => cmd_wasm(),
Some("ci") => cmd_ci(),
Some("corpus") => cmd_corpus(rest.contains(&"--download")),
Some("stats") => stats::run(&root(), rest.contains(&"--exact")),
Some("-h" | "--help" | "help") | None => {
print!("{USAGE}");
Ok(())
}
Some(other) => Err(format!("unknown command `{other}`\n\n{USAGE}")),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
/// Workspace root (parent of the xtask crate).
pub fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("xtask has a parent dir")
.to_path_buf()
}
pub fn cargo() -> Command {
let mut c = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()));
c.current_dir(root());
c
}
fn run(mut cmd: Command, what: &str) -> Result<(), String> {
eprintln!("$ {what}");
let status = cmd
.status()
.map_err(|e| format!("{what}: failed to spawn: {e}"))?;
if status.success() {
Ok(())
} else {
Err(format!("{what}: exited with {status}"))
}
}
pub fn metadata() -> Result<serde_json::Value, String> {
let out = cargo()
.args(["metadata", "--format-version", "1", "--no-deps"])
.output()
.map_err(|e| format!("cargo metadata: {e}"))?;
if !out.status.success() {
return Err(format!(
"cargo metadata failed:\n{}",
String::from_utf8_lossy(&out.stderr)
));
}
serde_json::from_slice(&out.stdout).map_err(|e| format!("cargo metadata: bad JSON: {e}"))
}
fn cmd_layers() -> Result<(), String> {
let crates = layers::from_metadata(&metadata()?)?;
println!("Dependency layering (plan/architecture.md §3)\n");
println!("{:<28} {:<14} workspace deps", "crate", "layer");
for c in &crates {
let ws: Vec<String> = c
.deps
.iter()
.filter(|d| d.workspace)
.map(|d| {
let k = match d.kind {
layers::DepKind::Normal => "",
layers::DepKind::Dev => " (dev)",
layers::DepKind::Build => " (build)",
};
format!("{}{k}", layers::short_name(&d.name))
})
.collect();
println!(
"{:<28} {:<14} {}",
c.name,
layers::describe(layers::classify(&c.name)),
ws.join(", ")
);
}
let violations = layers::check(&crates);
println!();
if violations.is_empty() {
println!("OK: {} crates, no layering violations.", crates.len());
Ok(())
} else {
println!("{} violation(s):", violations.len());
for v in &violations {
println!(" - {v}");
}
Err(format!("{} layering violation(s)", violations.len()))
}
}
/// Workspace packages that must build for wasm32: all L0–L5 crates plus
/// the egui shell.
fn wasm_set() -> Result<Vec<String>, String> {
let crates = layers::from_metadata(&metadata()?)?;
Ok(crates
.into_iter()
.filter(|c| match layers::classify(&c.name) {
Some(layers::Class::Layer(l)) => l <= 5 || layers::short_name(&c.name) == "ui-egui",
Some(layers::Class::Standalone) => true,
_ => false,
})
.map(|c| c.name)
.collect())
}
fn cmd_wasm() -> Result<(), String> {
let set = wasm_set()?;
let mut results = Vec::new();
for pkg in &set {
let mut c = cargo();
c.args(["check", "--target", "wasm32-unknown-unknown", "-p", pkg]);
let ok = run(
c,
&format!("cargo check --target wasm32-unknown-unknown -p {pkg}"),
)
.is_ok();
results.push((pkg.clone(), ok));
}
println!("\nwasm32-unknown-unknown check:");
for (p, ok) in &results {
println!(" {:<6} {p}", if *ok { "ok" } else { "FAIL" });
}
let failed = results.iter().filter(|r| !r.1).count();
if failed == 0 {
Ok(())
} else {
Err(format!("{failed} crate(s) failed the wasm check"))
}
}
fn cmd_ci() -> Result<(), String> {
type Step = (&'static str, Box<dyn Fn() -> Result<(), String>>);
let steps: Vec<Step> = vec![
(
"fmt",
Box::new(|| {
let mut c = cargo();
c.args(["fmt", "--all", "--", "--check"]);
run(c, "cargo fmt --all -- --check")
}),
),
(
"clippy",
Box::new(|| {
let mut c = cargo();
c.args([
"clippy",
"--workspace",
"--all-targets",
"--",
"-D",
"warnings",
]);
run(c, "cargo clippy --workspace --all-targets -- -D warnings")
}),
),
(
"test",
Box::new(|| {
let mut c = cargo();
c.args(["test", "--workspace"]);
run(c, "cargo test --workspace")
}),
),
("layers", Box::new(cmd_layers)),
("wasm", Box::new(cmd_wasm)),
];
let mut done = Vec::new();
for (name, f) in &steps {
eprintln!("\n=== ci: {name} ===");
if let Err(e) = f() {
println!("\nCI summary:");
for d in &done {
println!(" ok {d}");
}
println!(" FAIL {name}: {e}");
for (n, _) in steps.iter().skip(done.len() + 1) {
println!(" skip {n}");
}
return Err(format!("ci failed at `{name}`"));
}
done.push(*name);
}
println!(
"\nCI summary: all {} steps passed ({})",
done.len(),
done.join(", ")
);
Ok(())
}
const PNGSUITE_URL: &str = "http://www.schaik.com/pngsuite/PngSuite-2017jul19.tgz";
fn cmd_corpus(download: bool) -> Result<(), String> {
let corpus = root().join("corpus");
println!(
"Test corpora live under {} (git-ignored, never committed).
Tests that use a corpus skip cleanly when it is absent.
corpus/pngsuite/ PngSuite (public domain) — drawcraft-codecs compares every file
against the `image` crate. Fetch: cargo xtask corpus --download
corpus/psd/ PSD samples from MIT/BSD projects (ag-psd, psd-tools test data).
Copy files in manually; licences must be MIT/BSD/CC0.
corpus/tiff/ libtiff pics (optional)
corpus/exr/ OpenEXR sample images (optional)
corpus/raw/ raw.pixls.us samples, CC0 (optional)
",
corpus.display()
);
if !download {
return Ok(());
}
let dest = corpus.join("pngsuite");
std::fs::create_dir_all(&dest).map_err(|e| format!("create {}: {e}", dest.display()))?;
let tgz = corpus.join("PngSuite-2017jul19.tgz");
let mut curl = Command::new("curl");
curl.args(["-fsSL", "-o"]).arg(&tgz).arg(PNGSUITE_URL);
run(curl, &format!("curl {PNGSUITE_URL}"))?;
let mut tar = Command::new("tar");
tar.arg("-xzf").arg(&tgz).arg("-C").arg(&dest);
run(tar, "tar -xzf PngSuite-2017jul19.tgz")?;
let _ = std::fs::remove_file(&tgz);
let n = std::fs::read_dir(&dest)
.map(|d| {
d.flatten()
.filter(|e| e.path().extension().is_some_and(|x| x == "png"))
.count()
})
.unwrap_or(0);
println!("PngSuite: {n} PNG files in {}", dest.display());
if n == 0 {
Err("no PNG files extracted".into())
} else {
Ok(())
}
}
+149
View File
@@ -0,0 +1,149 @@
//! `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,
}
fn walk(dir: &Path, c: &mut Counts) {
let Ok(rd) = std::fs::read_dir(dir) else {
return;
};
for e in rd.flatten() {
let p = e.path();
let name = e.file_name();
let name = name.to_string_lossy();
if p.is_dir() {
// Nested fuzz workspaces and build output are not part of the crate.
if name != "target" && name != "fuzz" && !name.starts_with('.') {
walk(&p, c);
}
} else if 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;
}
}
}
/// `#[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(),
)
}
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::count_tests;
#[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));
}
}