use crate::lang::Lang;
pub fn is_test_file(path: &str, content: &str) -> bool {
is_test_path(path)
|| Lang::from_path(path).is_some_and(|lang| imports_test_framework(lang, content))
}
pub fn is_test_path(path: &str) -> bool {
let mut components: Vec<&str> = path.split('/').collect();
let Some(file) = components.pop() else {
return false;
};
components.iter().any(|dir| is_test_dir(dir)) || is_test_file_name(file)
}
fn is_test_dir(dir: &str) -> bool {
let lower = dir.to_ascii_lowercase();
matches!(
lower.as_str(),
"test" | "tests" | "__tests__" | "spec" | "specs" | "__mocks__" | "__snapshots__"
) || dir.ends_with("Tests")
}
fn is_test_file_name(file: &str) -> bool {
let stem = file.split('.').next().unwrap_or(file);
let camel = ["Tests", "Test"]
.iter()
.any(|suffix| stem.len() > suffix.len() && stem.ends_with(suffix));
let marked = file.contains(".test.")
|| file.contains(".spec.")
|| stem.ends_with("_test")
|| stem.ends_with("_spec")
|| (stem.starts_with("test_") && file.ends_with(".py"))
|| file == "conftest.py";
camel || marked
}
fn imports_test_framework(lang: Lang, content: &str) -> bool {
content.lines().map(str::trim_start).any(|line| match lang {
Lang::Swift => {
line.starts_with("@testable import ")
|| matches!(
line,
"import XCTest" | "import Testing" | "import Quick" | "import Nimble"
)
}
Lang::Python => [
"import pytest",
"from pytest",
"import unittest",
"from unittest",
]
.iter()
.any(|import| line.starts_with(import)),
Lang::TypeScript | Lang::Tsx | Lang::JavaScript => {
line.starts_with("import")
&& ["vitest", "@jest/globals", "node:test", "@testing-library/"]
.iter()
.any(|module| {
line.contains(&format!("'{module}"))
|| line.contains(&format!("\"{module}"))
})
}
Lang::Kotlin => ["org.junit", "kotlin.test", "io.kotest", "io.mockk"]
.iter()
.any(|package| line.starts_with(&format!("import {package}"))),
Lang::Go | Lang::Rust => false,
})
}