use crate::scanner::result::FileRecord;
const TEST_PATTERNS: &[&str] = &[
"_test.cpp",
"_test.cc",
"Tests.cs",
"Test.cs",
"_test.cs",
"_test.dart",
"_test.exs",
"_test.go",
"Spec.hs",
"Test.hs",
"Test.java",
"Tests.java",
"IT.java",
".test.js",
".spec.js",
".integration.test.js",
"Test.kt",
"Tests.kt",
"Spec.kt",
"_spec.lua",
"_test.lua",
"Test.php",
"_test.php",
"_test.py",
"tests.py",
"test-",
"_test.rb",
"_spec.rb",
"_test.rs",
"tests.rs",
"Spec.scala",
"Test.scala",
"Suite.scala",
".bats",
"_test.sh",
".test.sh",
"Tests.swift",
"Test.swift",
"Spec.swift",
".test.ts",
".spec.ts",
".integration.test.ts",
".e2e.test.ts",
".test.tsx",
".spec.tsx",
"test.zig",
"_test.zig",
"test_",
"__tests__/",
];
pub fn is_test_file(relative_path: &str) -> bool {
TEST_PATTERNS
.iter()
.any(|pat| relative_path.contains(pat) || relative_path.ends_with(pat))
}
pub fn map_test_files(files: &mut [FileRecord]) {
let mut by_base: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
for file in files.iter() {
if !is_test_file(&file.relative_path) {
continue;
}
let base = leading_token(&file.file_name);
by_base
.entry(base)
.or_default()
.push(file.relative_path.clone());
}
for file in files.iter_mut() {
if is_test_file(&file.relative_path) {
continue;
}
let base = leading_token(&file.file_name);
let candidates = match by_base.get(&base) {
Some(c) if !c.is_empty() => c,
_ => continue,
};
let source_dir = parent_dir_owned(&file.relative_path);
let best = candidates
.iter()
.max_by_key(|cand| common_prefix_components(cand, &source_dir))
.cloned();
file.corresponding_test_file = best;
}
}
fn leading_token(file_name: &str) -> String {
file_name
.split('.')
.next()
.unwrap_or(file_name)
.to_ascii_lowercase()
}
fn parent_dir_owned(path: &str) -> String {
crate::scanner::extensions::parent_dir(path).to_string()
}
fn common_prefix_components(a: &str, b: &str) -> usize {
a.split('/')
.zip(b.split('/'))
.take_while(|(x, y)| x == y)
.count()
}
#[cfg(test)]
mod tests {
use super::*;
fn record(path: &str) -> FileRecord {
FileRecord {
id: path.to_string(),
relative_path: path.to_string(),
file_name: path.rsplit('/').next().unwrap_or(path).to_string(),
language: "ts".to_string(),
line_count: 1,
size_bytes: 1,
last_modified_unix_ms: 0,
imports: Vec::new(),
churn_score: 0.0,
corresponding_test_file: None,
}
}
#[test]
fn pairs_source_with_same_dir_test() {
let mut files = vec![
record("src/routes/accounts.ts"),
record("src/routes/__tests__/accounts.test.ts"),
record("src/routes/__tests__/accounts.integration.test.ts"),
];
map_test_files(&mut files);
let src = files
.iter()
.find(|f| f.relative_path == "src/routes/accounts.ts")
.unwrap();
assert!(src.corresponding_test_file.is_some());
assert!(src
.corresponding_test_file
.as_deref()
.unwrap()
.ends_with(".test.ts"));
}
#[test]
fn pairs_swift_source_with_swift_tests() {
let mut files = vec![record("Sources/Foo.swift"), record("Tests/Foo.Tests.swift")];
map_test_files(&mut files);
let src = files
.iter()
.find(|f| f.relative_path == "Sources/Foo.swift")
.unwrap();
assert_eq!(
src.corresponding_test_file.as_deref(),
Some("Tests/Foo.Tests.swift")
);
}
#[test]
fn skips_when_leading_token_differs() {
let mut files = vec![record("foo.py"), record("test_foo.py")];
map_test_files(&mut files);
let src = files.iter().find(|f| f.relative_path == "foo.py").unwrap();
assert!(src.corresponding_test_file.is_none());
}
}