use std::path::{Path, PathBuf};
pub fn rust_sources(dir: &Path) -> Vec<PathBuf> {
let (production, tests) = partition_sources(dir);
for path in &tests {
declared_under_test_cfg(path).unwrap_or_else(|why| panic!("{why}"));
}
production
}
pub fn test_sources(dir: &Path) -> Vec<PathBuf> {
partition_sources(dir).1
}
fn partition_sources(dir: &Path) -> (Vec<PathBuf>, Vec<PathBuf>) {
let mut production = Vec::new();
let mut tests = Vec::new();
for entry in std::fs::read_dir(dir).expect("read source dir") {
let path = entry.expect("dir entry").path();
if path.is_dir() {
if path.file_name().is_some_and(|n| n == "tests") {
tests.push(path);
continue;
}
let (nested_production, nested_tests) = partition_sources(&path);
production.extend(nested_production);
tests.extend(nested_tests);
} else if path.extension().is_some_and(|e| e == "rs") {
if is_test_source_path(&path) {
tests.push(path);
} else {
production.push(path);
}
}
}
(production, tests)
}
fn opens_fn_item(trimmed: &str) -> bool {
let mut rest = trimmed;
if let Some(after_pub) = rest.strip_prefix("pub") {
rest = match after_pub.strip_prefix('(') {
Some(scoped) => match scoped.split_once(')') {
Some((_, tail)) => tail,
None => return false,
},
None => after_pub,
};
if !rest.starts_with(' ') {
return false;
}
}
loop {
rest = rest.trim_start();
if rest.starts_with("fn ") {
return true;
}
let Some((word, tail)) = rest.split_once(' ') else {
return false;
};
if !matches!(word, "default" | "const" | "async" | "unsafe" | "extern")
&& !word.starts_with('"')
{
return false;
}
rest = tail;
}
}
pub fn function_bodies(src: &str) -> Vec<String> {
let lines: Vec<&str> = src.lines().collect();
let mut bodies = Vec::new();
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim_start();
if !opens_fn_item(trimmed) {
continue;
}
let indent = line.len() - trimmed.len();
let closing = format!("{}}}", " ".repeat(indent));
let end = lines[i + 1..]
.iter()
.position(|l| *l == closing)
.map(|p| i + 1 + p)
.unwrap_or(lines.len() - 1);
bodies.push(lines[i..=end].join("\n"));
}
bodies
}
pub fn is_test_source_path(path: &Path) -> bool {
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default();
if let Some(prefix) = name.strip_suffix("tests.rs")
&& (prefix.is_empty()
|| (prefix.ends_with('_')
&& prefix
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')))
{
return true;
}
let parts: Vec<&str> = path
.components()
.filter_map(|c| c.as_os_str().to_str())
.collect();
parts
.windows(3)
.enumerate()
.any(|(i, w)| w[0] == "crates" && w[2] == "tests" && i + 3 < parts.len())
}
pub fn declared_under_test_cfg(module_file: &Path) -> Result<(), String> {
let stem = module_file
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| format!("{} has no module name", module_file.display()))?;
let dir = module_file
.parent()
.ok_or_else(|| format!("{} has no parent directory", module_file.display()))?;
let sibling: Option<PathBuf> = dir
.file_name()
.map(|name| dir.with_file_name(format!("{}.rs", name.to_string_lossy())));
let parent = ["mod.rs", "lib.rs", "main.rs"]
.iter()
.map(|name| dir.join(name))
.chain(sibling)
.find(|candidate| candidate.is_file())
.ok_or_else(|| format!("no parent module file for {}", module_file.display()))?;
let text =
std::fs::read_to_string(&parent).map_err(|e| format!("read {}: {e}", parent.display()))?;
let lines: Vec<&str> = text.lines().collect();
let item = lines
.iter()
.position(|line| is_mod_item(line, stem))
.ok_or_else(|| {
format!(
"{} declares no `mod {stem};` for {}",
parent.display(),
module_file.display()
)
})?;
let gated = (0..=item)
.rev()
.take_while(|&i| {
let trimmed = lines[i].trim_start();
i == item || trimmed.starts_with("#[") || trimmed.starts_with("//")
})
.any(|i| is_test_only_cfg(lines[i]));
if gated {
return Ok(());
}
Err(format!(
"{} must be declared under a test-only `cfg` in {}",
module_file.display(),
parent.display()
))
}
fn is_mod_item(line: &str, stem: &str) -> bool {
let mut code = line.split("//").next().unwrap_or("").trim();
while let Some(rest) = code.strip_prefix("#[") {
let Some(close) = rest.find(']') else {
return false;
};
code = rest[close + 1..].trim_start();
}
if let Some(rest) = code.strip_prefix("pub") {
let rest = match rest.strip_prefix('(') {
Some(vis) => match vis.find(')') {
Some(close) => &vis[close + 1..],
None => return false,
},
None => rest,
};
if !rest.starts_with(char::is_whitespace) {
return false;
}
code = rest.trim_start();
}
code.strip_prefix("mod")
.filter(|rest| rest.starts_with(char::is_whitespace))
.map(|rest| rest.trim_start())
.and_then(|rest| rest.strip_prefix(stem))
.is_some_and(|rest| rest.trim_start() == ";")
}
pub fn is_test_only_cfg(line: &str) -> bool {
line.trim_start()
.strip_prefix("#[cfg(")
.and_then(split_group)
.is_some_and(|(predicate, _)| predicate_is_test_only(predicate))
}
pub fn production_half(text: &str) -> &str {
let mut offset = 0usize;
let mut cfg_at: Option<usize> = None;
for line in text.split_inclusive('\n') {
if line.starts_with("#[cfg(") && is_test_only_cfg(line) {
cfg_at.get_or_insert(offset);
} else if let Some(start) = cfg_at
&& !line.starts_with("#[")
&& !line.starts_with("//")
&& !line.trim().is_empty()
{
if line.trim_end().ends_with('{') {
return &text[..start];
}
cfg_at = None;
}
offset += line.len();
}
text
}
fn split_group(rest: &str) -> Option<(&str, &str)> {
let mut depth = 1usize;
for (index, ch) in rest.char_indices() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some((&rest[..index], &rest[index + 1..]));
}
}
_ => {}
}
}
None
}
fn predicate_is_test_only(predicate: &str) -> bool {
let predicate = predicate.trim();
if predicate == "test" {
return true;
}
let Some((terms, tail)) = predicate.strip_prefix("all(").and_then(split_group) else {
return false;
};
tail.is_empty() && top_level_terms(terms).any(predicate_is_test_only)
}
fn top_level_terms(terms: &str) -> impl Iterator<Item = &str> {
let mut depth = 0usize;
let mut start = 0usize;
let mut out = Vec::new();
for (index, ch) in terms.char_indices() {
match ch {
'(' => depth += 1,
')' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
out.push(&terms[start..index]);
start = index + 1;
}
_ => {}
}
}
out.push(&terms[start..]);
out.into_iter()
}
pub fn workspace_crate_dirs() -> Vec<PathBuf> {
let crates = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("crates/ above crates/core");
let mut dirs: Vec<PathBuf> = std::fs::read_dir(crates)
.expect("crates dir")
.map(|entry| entry.expect("crate entry").path())
.collect();
dirs.sort();
dirs
}
pub fn workspace_production_sources() -> Vec<PathBuf> {
let mut sources: Vec<PathBuf> = Vec::new();
for krate in workspace_crate_dirs() {
let src = krate.join("src");
if src.is_dir() {
sources.extend(rust_sources(&src));
}
}
assert!(
sources.len() > 100,
"the walk must cover every crate's production sources, found {}",
sources.len()
);
sources
}
#[cfg(test)]
mod tests {
use super::*;
fn workspace_sources() -> Vec<std::path::PathBuf> {
fn expand(dir: &Path, out: &mut Vec<PathBuf>) {
for path in test_sources(dir) {
if path.is_dir() {
expand(&path, out);
} else {
out.push(path);
}
}
}
let mut out = Vec::new();
for krate in workspace_crate_dirs() {
let src = krate.join("src");
if src.is_dir() {
out.extend(rust_sources(&src));
expand(&src, &mut out);
}
let tests = krate.join("tests");
if tests.is_dir() {
out.extend(partition_sources(&tests).0);
expand(&tests, &mut out);
}
}
assert!(!out.is_empty(), "the workspace ships no source");
out
}
fn is_rust_source_walk(body: &str) -> bool {
body.contains("read_dir(")
&& (body.contains("\"rs\"")
|| body.contains("\".rs\"")
|| body.contains("rust_sources(")
|| body.contains("test_sources("))
}
#[test]
fn the_renderer_internals_are_re_exported_only_under_the_test_feature() {
let mod_rs = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/log/mod.rs");
let text = std::fs::read_to_string(&mod_rs).expect("readable log module");
for (idx, _) in text.match_indices("pub use render::{") {
let end = text[idx..].find("};").expect("a terminated re-export") + idx;
let statement = &text[idx..end];
let gated = text[..idx]
.lines()
.rev()
.take_while(|l| {
let t = l.trim_start();
t.starts_with("#[") || t.starts_with("//")
})
.any(|l| l.contains("cfg(feature = \"test-helpers\")"));
for name in ["render_kv_row", "render_stage_header_line", "strip_ansi"] {
assert!(
!statement.contains(name) || gated,
"`{name}` is re-exported without the test-helpers gate:\n{statement}"
);
}
}
}
#[test]
fn the_ansi_stripper_is_defined_once_in_the_workspace() {
let needle = format!("fn {}(", "strip_ansi");
let mut defs = Vec::new();
for source in workspace_sources() {
let text = std::fs::read_to_string(&source).expect("readable source");
for _ in 0..text.matches(&needle).count() {
defs.push(crate::path_util::slash_display(&source));
}
}
assert_eq!(
defs.len(),
1,
"the stripper belongs to the renderer that writes the escapes: {defs:?}"
);
assert!(
defs[0].contains("core/src/log/render.rs"),
"the one definition is the renderer's: {defs:?}"
);
}
#[test]
fn the_walk_detector_reads_both_extension_spellings() {
assert!(is_rust_source_walk(
"fn a(d: &Path) { for e in std::fs::read_dir(d)? { if e.path().extension() == Some(\"rs\".as_ref()) {} } }"
));
assert!(is_rust_source_walk(
"fn b(d: &Path) { for e in std::fs::read_dir(d)? { if e.path().to_string_lossy().ends_with(\".rs\") {} } }"
));
assert!(is_rust_source_walk(
"fn c(d: &Path) { for e in std::fs::read_dir(d)? { out.extend(rust_sources(&e)); } }"
));
assert!(!is_rust_source_walk(
"fn d(d: &Path) { std::fs::read_dir(d) }"
));
}
#[test]
fn function_bodies_opens_every_fn_spelling() {
let src = "\
fn plain() {
}
pub fn public() {
}
pub(crate) fn crate_scoped() {
}
pub(super) fn super_scoped() {
}
pub(in crate::a) fn path_scoped() {
}
async fn asynchronous() {
}
const fn constant() {
}
unsafe fn unsafely() {
}
pub async unsafe fn qualified() {
}
unsafe extern \"C\" fn abi() {
}
const NOT_A_FN: usize = 1;
unsafe impl Send for NotAFn {
}
";
let names: Vec<String> = function_bodies(src)
.iter()
.map(|b| {
b.split_once("fn ")
.and_then(|(_, rest)| rest.split('(').next())
.unwrap_or_default()
.to_string()
})
.collect();
assert_eq!(
names,
vec![
"plain",
"public",
"crate_scoped",
"super_scoped",
"path_scoped",
"asynchronous",
"constant",
"unsafely",
"qualified",
"abi",
],
);
}
#[test]
fn every_rust_source_walk_comes_from_the_shared_scanner() {
let mut walks = Vec::new();
for source in workspace_sources() {
let text = std::fs::read_to_string(&source).expect("readable source");
for body in function_bodies(&text) {
if !is_rust_source_walk(&body) {
continue;
}
let name = body
.split_once("fn ")
.and_then(|(_, rest)| rest.split(['(', '<']).next())
.unwrap_or_default()
.to_string();
walks.push(format!(
"{}: {name}",
crate::path_util::slash_display(&source)
));
}
}
const ALLOWED: [&str; 4] = [
"core/src/test_helpers/test_sources.rs: is_rust_source_walk",
"core/src/test_helpers/test_sources.rs: partition_sources",
"core/src/test_helpers/test_sources.rs: the_walk_detector_reads_both_extension_spellings",
"stage-build/src/command.rs: crate_has_binary_target",
];
let mut found: Vec<String> = walks
.iter()
.map(|w| {
ALLOWED
.iter()
.find(|a| w.ends_with(*a))
.map_or_else(|| w.clone(), |a| (*a).to_string())
})
.collect();
found.sort();
let mut expected: Vec<String> = ALLOWED.iter().map(|a| (*a).to_string()).collect();
expected.sort();
assert_eq!(
found, expected,
"a Rust-source walk outside the shared scanner: it must take its \
files from `rust_sources` / `test_sources`"
);
}
#[test]
fn walk_skips_only_a_gated_tests_directory() {
let tmp = tempfile::TempDir::new().unwrap();
let module = tmp.path().join("m");
std::fs::create_dir_all(module.join("tests")).unwrap();
std::fs::write(module.join("tests").join("mod.rs"), "").unwrap();
std::fs::write(module.join("mod.rs"), "#[cfg(test)]\nmod tests;\n").unwrap();
assert_eq!(rust_sources(&module), vec![module.join("mod.rs")]);
std::fs::write(module.join("mod.rs"), "mod tests;\n").unwrap();
assert!(
std::panic::catch_unwind(|| rust_sources(&module)).is_err(),
"an ungated tests/ directory must fail the walk"
);
}
fn synthetic_module(
tmp: &Path,
parent_file: &str,
parent_text: &str,
tests_dir: bool,
) -> PathBuf {
let module = tmp.join("m");
std::fs::create_dir_all(&module).unwrap();
std::fs::write(tmp.join(parent_file), parent_text).unwrap();
if tests_dir {
let dir = module.join("tests");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("mod.rs"), "").unwrap();
dir
} else {
let file = module.join("tests.rs");
std::fs::write(&file, "").unwrap();
file
}
}
#[test]
fn cfg_test_on_the_same_line_is_accepted() {
let tmp = tempfile::TempDir::new().unwrap();
let tests = synthetic_module(tmp.path(), "m/mod.rs", "#[cfg(test)] mod tests;\n", false);
declared_under_test_cfg(&tests).unwrap();
}
#[test]
fn cfg_test_above_another_attribute_is_accepted() {
let tmp = tempfile::TempDir::new().unwrap();
let tests = synthetic_module(
tmp.path(),
"m/mod.rs",
"#[cfg(test)]\n#[allow(clippy::unwrap_used)]\nmod tests;\n",
false,
);
declared_under_test_cfg(&tests).unwrap();
}
#[test]
fn cfg_test_above_a_doc_comment_is_accepted() {
let tmp = tempfile::TempDir::new().unwrap();
let tests = synthetic_module(
tmp.path(),
"m/mod.rs",
"#[cfg(test)]\n/// Unit tests.\npub(crate) mod tests;\n",
false,
);
declared_under_test_cfg(&tests).unwrap();
}
#[test]
fn cfg_all_test_and_unix_is_accepted() {
let tmp = tempfile::TempDir::new().unwrap();
let tests = synthetic_module(
tmp.path(),
"m/mod.rs",
"#[cfg(all(test, unix))]\nmod tests;\n",
false,
);
declared_under_test_cfg(&tests).unwrap();
}
#[test]
fn cfg_any_test_or_feature_is_rejected() {
let tmp = tempfile::TempDir::new().unwrap();
let tests = synthetic_module(
tmp.path(),
"m/mod.rs",
"#[cfg(any(test, feature = \"x\"))]\nmod tests;\n",
false,
);
let msg = declared_under_test_cfg(&tests).expect_err("`any(…)` is not a test-only gate");
let parent = tmp.path().join("m").join("mod.rs");
assert!(
msg.contains(&tests.display().to_string())
&& msg.contains(&parent.display().to_string()),
"message must name the module file and its parent: {msg}"
);
}
#[test]
fn cfg_all_test_and_not_windows_is_accepted() {
let tmp = tempfile::TempDir::new().unwrap();
let tests = synthetic_module(
tmp.path(),
"m/mod.rs",
"#[cfg(all(test, not(windows)))]\nmod tests;\n",
false,
);
declared_under_test_cfg(&tests).unwrap();
}
#[test]
fn cfg_all_feature_then_test_is_accepted() {
let tmp = tempfile::TempDir::new().unwrap();
let tests = synthetic_module(
tmp.path(),
"m/mod.rs",
"#[cfg(all(feature = \"x\", test))]\nmod tests;\n",
false,
);
declared_under_test_cfg(&tests).unwrap();
}
#[test]
fn cfg_not_test_is_rejected() {
let tmp = tempfile::TempDir::new().unwrap();
let tests = synthetic_module(
tmp.path(),
"m/mod.rs",
"#[cfg(not(test))]\nmod tests;\n",
false,
);
let msg = declared_under_test_cfg(&tests).expect_err("`not(test)` is not a test-only gate");
let parent = tmp.path().join("m").join("mod.rs");
assert!(
msg.contains(&tests.display().to_string())
&& msg.contains(&parent.display().to_string()),
"message must name the module file and its parent: {msg}"
);
}
#[test]
fn only_a_test_only_predicate_gates() {
for line in [
"#[cfg(test)]",
" #[cfg(test)] mod tests;",
"#[cfg(all(test, unix))]",
"#[cfg(all(test, not(windows)))]",
"#[cfg(all(feature = \"x\", test))]",
"#[cfg(all(all(test), unix))]",
] {
assert!(is_test_only_cfg(line), "{line}");
}
for line in [
"#[cfg(any(test, feature = \"x\"))]",
"#[cfg(all(any(test, unix), windows))]",
"#[cfg(not(test))]",
"#[cfg(not(all(test, unix)))]",
"#[cfg(feature = \"testing\")]",
"#[cfg(unix)]",
"// gated by #[cfg(test)] somewhere else",
"mod tests;",
] {
assert!(!is_test_only_cfg(line), "{line}");
}
}
#[test]
fn sibling_parent_file_is_found() {
let tmp = tempfile::TempDir::new().unwrap();
let tests = synthetic_module(tmp.path(), "m.rs", "#[cfg(test)]\nmod tests;\n", false);
declared_under_test_cfg(&tests).unwrap();
}
#[test]
fn tests_directory_needs_the_same_declaration() {
let tmp = tempfile::TempDir::new().unwrap();
let tests = synthetic_module(tmp.path(), "m/mod.rs", "#[cfg(test)]\nmod tests;\n", true);
declared_under_test_cfg(&tests).unwrap();
std::fs::write(tests.parent().unwrap().join("mod.rs"), "mod tests;\n").unwrap();
declared_under_test_cfg(&tests).expect_err("an ungated tests/ directory is not declared");
}
#[test]
fn comment_ending_in_mod_tests_is_not_the_item() {
let tmp = tempfile::TempDir::new().unwrap();
let tests = synthetic_module(
tmp.path(),
"m/mod.rs",
"// helpers shared with mod tests\nfn helper() {}\n#[cfg(test)]\nmod tests;\n",
false,
);
declared_under_test_cfg(&tests).unwrap();
}
#[test]
fn ungated_mod_tests_fails_naming_both_files() {
let tmp = tempfile::TempDir::new().unwrap();
let tests = synthetic_module(tmp.path(), "m/mod.rs", "mod tests;\n", false);
let msg = declared_under_test_cfg(&tests).expect_err("an ungated declaration is rejected");
let parent = tmp.path().join("m").join("mod.rs");
assert!(
msg.contains(&tests.display().to_string())
&& msg.contains(&parent.display().to_string()),
"message must name the module file and its parent: {msg}"
);
}
#[test]
fn missing_declaration_fails_naming_both_files() {
let tmp = tempfile::TempDir::new().unwrap();
let tests = synthetic_module(tmp.path(), "m/mod.rs", "fn helper() {}\n", false);
let msg = declared_under_test_cfg(&tests).expect_err("no declaration is rejected");
let parent = tmp.path().join("m").join("mod.rs");
assert!(
msg.contains("mod tests;")
&& msg.contains(&tests.display().to_string())
&& msg.contains(&parent.display().to_string()),
"message must name the missing item, the module file and its parent: {msg}"
);
}
#[test]
fn production_half_stops_at_the_inline_test_module() {
let src = "fn a() {}\n\n#[cfg(all(test, unix))]\n/// docs\n#[allow(dead_code)]\nmod tests {\n fn b() {}\n}\n";
assert_eq!(production_half(src), "fn a() {}\n\n");
}
#[test]
fn production_half_keeps_the_items_after_a_sibling_declaration() {
let src = "#[cfg(test)]\nmod tests;\n\nfn later() {}\n";
assert_eq!(production_half(src), src);
}
#[test]
fn production_half_ignores_non_test_and_nested_cfgs() {
let src = "#[cfg(unix)]\nmod posix {\n #[cfg(test)]\n mod inner {}\n}\n#[cfg(not(test))]\nfn prod() {}\n";
assert_eq!(production_half(src), src);
}
#[test]
fn name_rules_match_the_awk_lexer() {
for path in [
"crates/demo/src/tests.rs",
"crates/demo/src/foo_tests.rs",
"crates/demo/src/_tests.rs",
"crates/demo/tests/integration.rs",
"crates/demo/tests/nested/case.rs",
] {
assert!(is_test_source_path(Path::new(path)), "{path}");
}
for path in [
"crates/demo/src/lib.rs",
"crates/demo/src/mytests.rs",
"crates/demo/src/Foo_tests.rs",
"crates/demo/src/tests.rs.bak",
"crates/demo/src/process/tests/mod.rs",
"crates/demo/tests",
"src/tests/helper.rs",
] {
assert!(!is_test_source_path(Path::new(path)), "{path}");
}
}
}