tests/law_common/mod.rs
//! **The workspace member scan the three law guards share.**
//!
//! ## Why this file exists
//!
//! `tests/rayon_free_law.rs`, `tests/bench_target_law.rs` and
//! `tests/required_features_law.rs` each carried their OWN copy of the same twelve
//! lines, and each copy said, in a doc comment, that a newly added crate is covered
//! "automatically (no carve-outs)". All four copies read the member list like this:
//!
//! ```ignore
//! manifest.lines().find(|l| l.trim_start().starts_with("members"))
//! ```
//!
//! One LINE. `members` is a TOML array, and an array may wrap — `cargo add`, a
//! `taplo fmt`, or a human adding an eleventh crate past the 100-column mark all
//! produce the wrapped form, and znippy's own root manifest is already written that
//! way. The moment this one does, `find` returns the line holding `members = [`, the
//! `line.find(']')` on it returns `None`, the `if let` falls through, and every guard
//! quietly narrows to the ROOT crate while still reporting green over a workspace it
//! has stopped looking at. Nothing fails. Nothing warns. Ten crates leave the law.
//!
//! That is the same silent-narrowing defect the guards themselves were written to
//! catch, sitting in the guards' own discovery, in triplicate — and it had already
//! been found and fixed once on the `master` branch (`c4c5084`, "rayon-law members
//! parse was single-line — the scan silently shrank") without ever reaching `main`,
//! where all the work is.
//!
//! ## The fix is by construction, not another guard
//!
//! The parse now spans lines, and there is ONE copy of it (LAW 5). It also refuses
//! to return a short list quietly: a member directory with no `Cargo.toml` is a
//! panic, not a skip, because "the path was wrong" and "the crate is clean" must not
//! look the same to a law.
use std::path::PathBuf;
/// The workspace root, as a directory.
pub fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
/// Every `members = [...]` entry from the root manifest, in declaration order.
///
/// Spans lines: from the `members` key to the matching `]`, with `#` comments
/// stripped first so a commented-out crate is not counted and a `]` inside a comment
/// cannot end the array early.
pub fn member_names(manifest: &str) -> Vec<String> {
// Strip `#` comments. No TOML string in a members array legitimately contains
// one, and leaving them in lets a comment close the array.
let stripped: String = manifest
.lines()
.map(|l| match l.find('#') {
Some(i) => &l[..i],
None => l,
})
.collect::<Vec<_>>()
.join("\n");
let Some(key) = stripped.find("members") else {
return Vec::new();
};
let Some(open) = stripped[key..].find('[') else {
return Vec::new();
};
let open = key + open;
let Some(close) = stripped[open..].find(']') else {
return Vec::new();
};
let close = open + close;
stripped[open + 1..close]
.split(',')
.map(|s| s.trim().trim_matches('"').trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
/// The workspace root plus every member, as crate directories.
///
/// Panics if a declared member has no `Cargo.toml` — a law that scanned a path that
/// is not there would report the same green as one that scanned a clean crate.
pub fn crate_dirs() -> Vec<PathBuf> {
let root = workspace_root();
let manifest = std::fs::read_to_string(root.join("Cargo.toml")).expect("read root Cargo.toml");
let names = member_names(&manifest);
assert!(
!names.is_empty(),
"the root manifest declares NO workspace members — the member scan is empty, so \
every law using it would pass over the root crate alone"
);
let mut dirs = vec![root.clone()];
for name in &names {
let dir = root.join(name);
assert!(
dir.join("Cargo.toml").is_file(),
"member {name:?} is declared in the root manifest but {dir:?} has no Cargo.toml — \
a law cannot tell a bad path from a clean crate"
);
dirs.push(dir);
}
dirs
}
/// Every member's `src/` tree, plus the root crate's.
pub fn workspace_src_roots() -> Vec<PathBuf> {
crate_dirs().into_iter().map(|d| d.join("src")).collect()
}
// ---------------------------------------------------------------------------
// The scan's own guard. A discovery helper that silently returns a short list is
// exactly what this module exists to remove, so it is proved here rather than
// assumed.
// ---------------------------------------------------------------------------
#[cfg(test)]
mod scan_guard {
use super::*;
/// The old single-line reader, kept verbatim so the red is a MEASUREMENT of the
/// defect rather than a story about it.
fn single_line_reader(manifest: &str) -> Vec<String> {
let mut out = Vec::new();
if let Some(line) = manifest
.lines()
.find(|l| l.trim_start().starts_with("members"))
{
if let (Some(a), Some(b)) = (line.find('['), line.find(']')) {
for raw in line[a + 1..b].split(',') {
let name = raw.trim().trim_matches('"');
if !name.is_empty() {
out.push(name.to_string());
}
}
}
}
out
}
const WRAPPED: &str = r#"
[workspace]
members = [
"gatling",
"lbzip2",
"osm-katana",
]
resolver = "2"
"#;
#[test]
fn a_wrapped_members_array_is_read_whole_and_the_old_reader_read_none_of_it() {
assert_eq!(
member_names(WRAPPED),
vec!["gatling", "lbzip2", "osm-katana"],
"the wrapped form must parse to all three members"
);
// THE RED: the reader every law shipped saw ZERO of them, and said nothing.
assert!(
single_line_reader(WRAPPED).is_empty(),
"the old single-line reader is supposed to MISS the wrapped form — if it \
does not, this fixture no longer reproduces the defect and the guard above \
is measuring nothing"
);
}
#[test]
fn the_inline_form_still_parses_and_both_readers_agree_on_it() {
let inline = "[workspace]\nmembers = [\"a\", \"b\"]\nresolver = \"2\"\n";
assert_eq!(member_names(inline), vec!["a", "b"]);
assert_eq!(single_line_reader(inline), member_names(inline));
}
#[test]
fn a_commented_out_member_is_not_a_member_and_a_hash_cannot_close_the_array() {
let m = "[workspace]\nmembers = [\n \"a\",\n # \"gone\"],\n \"b\",\n]\n";
assert_eq!(
member_names(m),
vec!["a", "b"],
"a commented member must not count, and its `]` must not end the array"
);
}
#[test]
fn this_workspace_resolves_every_declared_member_to_a_real_crate() {
let dirs = crate_dirs();
// Root + members. Pinning the floor, not the exact count, so adding a crate
// does not edit a test — but a scan that collapsed to the root cannot pass.
assert!(
dirs.len() >= 8,
"the workspace scan found only {} director(ies) — it has collapsed: {dirs:?}",
dirs.len()
);
}
}