use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use syn::spanned::Spanned;
use syn::visit::Visit;
const LIMIT: usize = 400;
fn pinned() -> HashMap<&'static str, usize> {
HashMap::from([("src/session.rs", 408), ("src/vcs/jj.rs", 426), ("src/workspace.rs", 1159)])
}
fn test_only(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|a| {
a.path().segments.last().is_some_and(|s| s.ident == "test")
|| (a.path().is_ident("cfg") && a.parse_args::<syn::Meta>().is_ok_and(|m| m.path().is_ident("test")))
})
}
#[derive(Default)]
struct TestItems {
lines: Vec<(usize, usize)>,
out_of_line: Vec<OutOfLine>,
}
struct OutOfLine {
name: String,
path: Option<String>,
}
impl TestItems {
fn add(&mut self, attrs: &[syn::Attribute], item: &impl Spanned) -> bool {
if !test_only(attrs) {
return false;
}
let span = item.span();
self.lines.push((span.start().line, span.end().line));
true
}
}
fn path_attr(attrs: &[syn::Attribute]) -> Option<String> {
attrs.iter().find(|a| a.path().is_ident("path")).and_then(|a| match &a.meta {
syn::Meta::NameValue(nv) => match &nv.value {
syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) => Some(s.value()),
_ => None,
},
_ => None,
})
}
impl<'a> Visit<'a> for TestItems {
fn visit_item(&mut self, i: &'a syn::Item) {
if let syn::Item::Mod(m) = i
&& m.content.is_none()
&& test_only(&m.attrs)
{
self.out_of_line.push(OutOfLine { name: m.ident.to_string(), path: path_attr(&m.attrs) });
}
let attrs = match i {
syn::Item::Const(x) => &x.attrs,
syn::Item::Enum(x) => &x.attrs,
syn::Item::Fn(x) => &x.attrs,
syn::Item::Impl(x) => &x.attrs,
syn::Item::Macro(x) => &x.attrs,
syn::Item::Mod(x) => &x.attrs,
syn::Item::Static(x) => &x.attrs,
syn::Item::Struct(x) => &x.attrs,
syn::Item::Trait(x) => &x.attrs,
syn::Item::Type(x) => &x.attrs,
syn::Item::Use(x) => &x.attrs,
_ => return syn::visit::visit_item(self, i),
};
if !self.add(attrs, i) {
syn::visit::visit_item(self, i);
}
}
fn visit_impl_item_fn(&mut self, f: &'a syn::ImplItemFn) {
if !self.add(&f.attrs, f) {
syn::visit::visit_impl_item_fn(self, f);
}
}
}
fn test_items(source: &str) -> syn::Result<TestItems> {
let file = syn::parse_file(source)?;
let mut tests = TestItems::default();
tests.visit_file(&file);
Ok(tests)
}
fn read(path: &Path) -> String {
std::fs::read_to_string(path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()))
}
fn production_lines(source: &str) -> syn::Result<usize> {
let tests = test_items(source)?;
let total = source.lines().count();
Ok((1..=total).filter(|line| !tests.lines.iter().any(|(a, b)| (a..=b).contains(&line))).count())
}
fn module_files(file: &Path, m: &OutOfLine) -> Vec<PathBuf> {
let dir = file.parent().unwrap_or(Path::new(""));
if let Some(p) = &m.path {
return vec![dir.join(p)];
}
let owns_dir = matches!(file.file_name().and_then(|n| n.to_str()), Some("mod.rs" | "lib.rs" | "main.rs"));
let base = if owns_dir { dir.to_path_buf() } else { dir.join(file.file_stem().unwrap_or_default()) };
vec![base.join(format!("{}.rs", m.name)), base.join(&m.name).join("mod.rs")]
}
fn test_module_files(files: &[PathBuf]) -> HashSet<PathBuf> {
let mut roots = Vec::new();
for file in files {
let items = test_items(&read(file)).unwrap_or_else(|e| panic!("{} does not parse: {e}", file.display()));
for m in items.out_of_line {
roots.extend(module_files(file, &m));
}
}
files.iter().filter(|f| roots.iter().any(|r| *f == r || f.starts_with(children_dir(r)))).cloned().collect()
}
fn children_dir(module_file: &Path) -> PathBuf {
if module_file.file_name().is_some_and(|n| n == "mod.rs") {
module_file.parent().unwrap_or(Path::new("")).to_path_buf()
} else {
module_file.with_extension("")
}
}
fn crate_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf()
}
fn sources(dir: &Path, found: &mut Vec<PathBuf>) {
let entries = std::fs::read_dir(dir).unwrap_or_else(|e| panic!("cannot list {}: {e}", dir.display()));
for entry in entries {
let entry = entry.unwrap_or_else(|e| panic!("cannot list {}: {e}", dir.display()));
let path = entry.path();
let kind = entry.file_type().unwrap_or_else(|e| panic!("cannot stat {}: {e}", path.display()));
if kind.is_dir() {
sources(&path, found);
} else if kind.is_file() && path.extension().is_some_and(|e| e == "rs") {
found.push(path);
}
}
}
fn verdict(relative: &str, lines: usize, limit: usize, pinned: &HashMap<&'static str, usize>) -> Option<String> {
match pinned.get(relative) {
Some(&ceiling) if lines > ceiling => Some(format!(
"{relative}: {lines} lines, up from its pinned {ceiling}. It is already over the {limit}-line limit; split it rather than growing it further."
)),
Some(_) if lines <= limit => Some(format!(
"{relative}: down to {lines} lines, under the {limit} limit, so remove its entry from `pinned()` and let the real limit hold it there."
)),
Some(&ceiling) if lines < ceiling => Some(format!(
"{relative}: down to {lines} lines from its pinned {ceiling}. Lower its pin to {lines} so it cannot grow back."
)),
Some(_) => None,
None if lines > limit => Some(format!(
"{relative}: {lines} lines, over the {limit} limit. Split it into pieces that each do one thing (tests are not counted, so they are not the cause)."
)),
None => None,
}
}
#[test]
fn no_file_outgrows_its_limit() {
let root = crate_root();
let pinned = pinned();
let mut files = Vec::new();
sources(&root.join("src"), &mut files);
assert!(!files.is_empty(), "no `.rs` files under `src`: the walk is broken, not the tree");
let test_files = test_module_files(&files);
let mut failures = Vec::new();
for path in files.iter().filter(|f| !test_files.contains(*f)) {
let relative = path.strip_prefix(&root).unwrap_or(path).to_string_lossy().replace('\\', "/");
let lines = production_lines(&read(path)).unwrap_or_else(|e| panic!("{relative} does not parse: {e}"));
if let Some(f) = verdict(&relative, lines, LIMIT, &pinned) {
failures.push(f);
}
}
failures.sort();
assert!(failures.is_empty(), "\n{}\n", failures.join("\n"));
}
#[test]
fn the_ratchet_holds_a_pinned_file_to_its_size() {
let pinned = HashMap::from([("a.rs", 500)]);
assert!(verdict("a.rs", 500, 400, &pinned).is_none(), "at its ceiling is fine");
assert!(
verdict("a.rs", 450, 400, &pinned).is_some_and(|m| m.contains("Lower its pin to 450")),
"a shrink must lower the pin, or the file could grow back"
);
assert!(verdict("a.rs", 501, 400, &pinned).is_some_and(|m| m.contains("up from its pinned")), "a pinned file may not grow");
assert!(verdict("a.rs", 400, 400, &pinned).is_some_and(|m| m.contains("remove its entry")), "once under the limit the pin must go");
assert!(verdict("b.rs", 400, 400, &pinned).is_none(), "unpinned, at the limit");
assert!(verdict("b.rs", 401, 400, &pinned).is_some_and(|m| m.contains("over the")), "unpinned, over the limit");
}
#[test]
fn every_pinned_file_still_exists() {
let root = crate_root();
let missing: Vec<&str> = pinned().keys().copied().filter(|r| !root.join(r).exists()).collect();
assert!(missing.is_empty(), "pinned files no longer at these paths: {missing:?}");
}
#[test]
fn only_test_items_are_left_out_of_the_count() {
let count = |source: &str| production_lines(source).expect("parses");
assert_eq!(count("fn a() {}\nfn b() {}\n#[cfg(test)]\nmod tests {\n // lots\n}\n"), 2);
assert_eq!(count("fn a() {}\n"), 1, "a file with no tests counts whole");
assert_eq!(count(""), 0);
let cases: &[(&str, &str, usize)] = &[
("a test-only mod declaration is its own line, not the rest of the file", "mod real;\n#[cfg(test)]\nmod test_support;\n\nfn a() {}\nfn b() {}\n", 4),
("a test-only helper is the helper, not the rest of the file", "fn a() {}\n#[cfg(test)]\nfn helper() {}\nfn b() {}\nfn c() {}\n", 3),
("an attribute between the guard and the item goes with the item", "#[cfg(test)]\n#[path = \"t.rs\"]\nmod tests;\n\nfn a() {}\nfn b() {}\n", 3),
("a same-line test module body is still skipped", "fn a() {}\n#[cfg(test)] mod tests {\n fn t() {}\n}\n", 1),
("a one-line test module", "fn a() {}\n#[cfg(test)] mod tests { fn t() {} }\nfn b() {}\n", 2),
(
"production code after a test module still counts",
"fn a() {}\n#[cfg(test)]\nmod a_tests {\n #[test]\n fn t() {\n }\n}\n\nfn b() {}\nfn c() {}\n",
4,
),
("a `}` in column 0 inside a test's string does not end the module", "#[cfg(test)]\nmod tests {\n const FIX: &str = \"\n}\n\";\n fn t() {}\n}\nfn b() {}\n", 1),
("`#[cfg(test)]` in a string or comment is not an attribute", "// #[cfg(test)]\nconst A: &str = \"\n#[cfg(test)]\nmod x {\";\nfn b() {}\n", 5),
("a test-only method in a production impl", "impl A {\n fn a() {}\n #[cfg(test)]\n fn t() {}\n}\n", 3),
("doc comments go with their item", "/// tests\n#[cfg(test)]\nmod t {}\nfn b() {}\n", 1),
("an async test under a runtime's attribute is a test", "#[tokio::test]\nasync fn t() {\n}\nfn b() {}\n", 1),
];
for (why, source, expected) in cases {
assert_eq!(count(source), *expected, "{why}");
}
assert!(production_lines("fn a( {").is_err(), "a file that does not parse is an error, not a count");
}
#[test]
#[cfg(unix)]
fn the_walk_does_not_follow_symlinks() {
let root = std::env::temp_dir().join(format!("workon-walk-{}", std::process::id()));
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/a.rs"), "fn a() {}\n").unwrap();
std::os::unix::fs::symlink(&root, root.join("src/loop")).unwrap();
let mut found = Vec::new();
sources(&root.join("src"), &mut found);
std::fs::remove_dir_all(&root).unwrap();
assert_eq!(found, vec![root.join("src/a.rs")], "a link back to an ancestor is not walked into");
}
#[test]
fn a_test_only_module_file_is_found_from_its_declaration() {
let files: Vec<PathBuf> = [
"src/vcs/git/mod.rs",
"src/vcs/git/tests.rs",
"src/vcs/git/tests/helpers.rs",
"src/vcs/git/commands.rs",
"src/app.rs",
"src/app/fixtures/mod.rs",
"src/app/fixtures/data.rs",
"src/app/view.rs",
]
.iter()
.map(PathBuf::from)
.collect();
let beside_mod_rs = OutOfLine { name: "tests".into(), path: None };
assert!(module_files(Path::new("src/vcs/git/mod.rs"), &beside_mod_rs).contains(&PathBuf::from("src/vcs/git/tests.rs")));
let under_named_dir = OutOfLine { name: "fixtures".into(), path: None };
let roots = module_files(Path::new("src/app.rs"), &under_named_dir);
assert!(roots.contains(&PathBuf::from("src/app/fixtures/mod.rs")), "a non-mod.rs file's children live in a directory named after it");
let with_path = OutOfLine { name: "t".into(), path: Some("elsewhere/t.rs".into()) };
assert_eq!(module_files(Path::new("src/vcs/git/mod.rs"), &with_path), vec![PathBuf::from("src/vcs/git/elsewhere/t.rs")]);
let tests_dir = std::env::temp_dir().join(format!("workon-file-length-{}", std::process::id()));
for f in &files {
let p = tests_dir.join(f);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
let body = match f.to_str().unwrap() {
"src/vcs/git/mod.rs" => "mod commands;\n#[cfg(test)]\nmod tests;\n",
"src/app.rs" => "mod view;\n#[cfg(test)]\nmod fixtures;\n",
_ => "fn f() {}\n",
};
std::fs::write(&p, body).unwrap();
}
let on_disk: Vec<PathBuf> = files.iter().map(|f| tests_dir.join(f)).collect();
let found = test_module_files(&on_disk);
std::fs::remove_dir_all(&tests_dir).unwrap();
let expected: HashSet<PathBuf> =
["src/vcs/git/tests.rs", "src/vcs/git/tests/helpers.rs", "src/app/fixtures/mod.rs", "src/app/fixtures/data.rs"]
.iter()
.map(|f| tests_dir.join(f))
.collect();
assert_eq!(found, expected, "the test module files and everything under a test module's directory, and nothing else");
}