use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Protection {
Direct,
Named,
Users,
None,
}
const LAYOUT_DIRS: &[&str] = &["lib", "src", "test", "tests", "spec", "__tests__"];
const TEST_AFFIXES: &[&str] = &["_test", ".test", "_spec", ".spec", "Test", "Spec"];
pub fn place(path: &Path) -> Vec<String> {
let mut place: Vec<String> = path
.parent()
.into_iter()
.flat_map(|dir| dir.components())
.filter_map(|c| c.as_os_str().to_str())
.filter(|c| !LAYOUT_DIRS.contains(c))
.map(str::to_string)
.collect();
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_default();
let name = TEST_AFFIXES
.iter()
.find_map(|affix| stem.strip_suffix(affix))
.or_else(|| stem.strip_prefix("test_"))
.unwrap_or(stem);
place.push(name.to_string());
place
}
fn a_directory_apart(short: &[String], long: &[String]) -> bool {
let mut rest = long.iter();
long.len() <= short.len() + 1 && short.iter().all(|dir| rest.any(|other| other == dir))
}
fn named_after(test: &[String], source: &[String]) -> bool {
let (Some((test_name, test_dirs)), Some((name, dirs))) =
(test.split_last(), source.split_last())
else {
return false;
};
let beside = test_dirs != dirs
&& (a_directory_apart(test_dirs, dirs) || a_directory_apart(dirs, test_dirs));
let after_directory = dirs
.split_last()
.is_some_and(|(last, above)| last == test_name && a_directory_apart(test_dirs, above));
(test_name == name && beside) || after_directory
}
pub struct Tests {
direct: Vec<Vec<usize>>,
named: Vec<Vec<usize>>,
}
impl Tests {
pub fn of(
files: &[PathBuf],
mut referring: Vec<Vec<usize>>,
is_test: impl Fn(usize) -> bool,
is_source: impl Fn(usize) -> bool,
) -> Self {
let places: Vec<Vec<String>> = files.iter().map(|f| place(f)).collect();
let mut by_name: HashMap<&str, Vec<usize>> = HashMap::new();
for test in (0..files.len()).filter(|&f| is_test(f)) {
if let Some(name) = places[test].last() {
by_name.entry(name).or_default().push(test);
}
}
let mut named = vec![Vec::new(); files.len()];
for source in (0..files.len()).filter(|&f| is_source(f)) {
let place = &places[source];
let candidates = place
.iter()
.rev()
.take(2)
.filter_map(|name| by_name.get(name.as_str()))
.flatten();
for &test in candidates {
if places[test] == *place {
referring[source].push(test);
} else if named_after(&places[test], place) {
named[source].push(test);
}
}
referring[source].sort_unstable();
referring[source].dedup();
}
Self {
direct: referring,
named,
}
}
pub fn direct(&self, file: usize) -> &[usize] {
&self.direct[file]
}
pub fn own(&self, file: usize) -> impl Iterator<Item = usize> + '_ {
self.direct[file].iter().chain(&self.named[file]).copied()
}
pub fn protection(&self, file: usize, users: &[usize]) -> Protection {
if !self.direct[file].is_empty() {
Protection::Direct
} else if !self.named[file].is_empty() {
Protection::Named
} else if users.iter().any(|&user| self.own(user).next().is_some()) {
Protection::Users
} else {
Protection::None
}
}
}
#[cfg(test)]
#[path = "protection_test.rs"]
mod tests;