use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TestClassification {
Production,
UnitTest,
IntegrationTest,
TestFixture,
TestHelper,
}
pub fn is_dev_file(path: &str) -> bool {
path.contains("__tests__")
|| path.contains("stories")
|| path.contains(".stories.")
|| path.contains("story.")
|| path.contains("fixture")
|| path.contains("fixtures")
}
pub fn detect_language(ext: &str) -> String {
match ext {
"ts" | "tsx" => "ts".to_string(),
"js" | "jsx" | "mjs" | "cjs" => "js".to_string(),
"rs" => "rs".to_string(),
"py" => "py".to_string(),
"dart" => "dart".to_string(),
"css" => "css".to_string(),
other => other.to_string(),
}
}
pub fn is_test_path(path: &str) -> bool {
let lower = path.to_ascii_lowercase();
lower.contains("__tests__")
|| lower.contains(".test.")
|| lower.contains(".spec.")
|| lower.ends_with("_test.rs")
|| lower.ends_with("_tests.rs")
|| lower.ends_with("_test.go")
|| lower.ends_with("_test.dart")
|| lower.starts_with("test_")
|| lower.contains("/tests/")
|| lower.starts_with("tests/")
|| lower.contains("/test_")
}
pub fn should_exclude_from_reports(path: &str) -> bool {
let lower = path.to_ascii_lowercase();
lower.contains("/tests/")
|| lower.starts_with("tests/")
|| lower.contains("__tests__")
|| lower.contains("__mocks__")
|| lower.contains("/fixtures/")
|| lower.contains("/fixture/")
|| lower.contains("__fixtures__")
|| lower.contains("/mocks/")
|| lower.contains("/mock/")
|| lower.contains(".test.")
|| lower.contains(".spec.")
|| lower.ends_with("_test.rs")
|| lower.ends_with("_tests.rs")
|| lower.ends_with("_test.ts")
|| lower.ends_with("_test.tsx")
|| lower.ends_with("_test.js")
|| lower.ends_with("_test.jsx")
|| lower.ends_with("_test.go")
|| lower.ends_with("_test.dart")
|| lower.ends_with("_test.py")
|| lower.contains("/test_utils/")
|| lower.contains("/test_helpers/")
|| lower.contains("/testing/")
}
fn is_story_path(path: &str) -> bool {
let lower = path.to_ascii_lowercase();
lower.contains("stories") || lower.contains(".story.") || lower.contains(".stories.")
}
fn is_generated_path(path: &str) -> bool {
let lower = path.to_ascii_lowercase();
lower.contains("generated")
|| lower.contains("codegen")
|| lower.contains("/gen/")
|| lower.ends_with(".gen.ts")
|| lower.ends_with(".gen.tsx")
|| lower.ends_with(".gen.rs")
|| lower.ends_with(".g.rs")
|| lower.ends_with(".g.dart")
|| lower.ends_with(".freezed.dart")
|| lower.ends_with(".gr.dart")
|| lower.ends_with(".pb.dart")
|| lower.ends_with(".pbjson.dart")
|| lower.ends_with(".pbenum.dart")
|| lower.ends_with(".pbserver.dart")
|| lower.ends_with(".config.dart")
}
pub fn classify_test_path(path: &Path) -> TestClassification {
let path_str = path.to_str().unwrap_or("");
let lower = path_str.to_ascii_lowercase();
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
let filename_lower = filename.to_ascii_lowercase();
if (lower.contains("fixture") || lower.contains("fixtures") || lower.contains("mock"))
&& !filename_lower.starts_with("test_")
{
return TestClassification::TestFixture;
}
let is_python = filename_lower.ends_with(".py");
let is_python_test_file = is_python && filename_lower.starts_with("test_");
if !is_python_test_file
&& (filename_lower.contains("test_helper")
|| filename_lower.contains("test_utils")
|| filename_lower == "setup.py"
|| lower.contains("testing/"))
{
return TestClassification::TestHelper;
}
if (lower.starts_with("tests/") || lower.contains("/tests/")) && !lower.contains("__tests__") {
return TestClassification::IntegrationTest;
}
if lower.contains("__tests__")
|| lower.contains(".test.")
|| lower.contains(".spec.")
|| lower.ends_with("_test.rs")
|| lower.ends_with("_tests.rs")
|| lower.ends_with("_test.go")
|| lower.ends_with("_test.dart")
|| filename_lower.starts_with("test_") || lower.contains("/test_")
{
return TestClassification::UnitTest;
}
TestClassification::Production
}
pub fn has_test_code(content: &str, lang: &str) -> bool {
match lang {
"rs" | "rust" => content.contains("#[cfg(test)]") || content.contains("#[test]"),
"ts" | "js" | "tsx" | "jsx" => {
content.contains("describe(") || content.contains("it(") || content.contains("test(")
}
"py" | "python" => {
content.contains("def test_")
|| content.contains("import unittest")
|| content.contains("import pytest")
}
"go" => content.contains("func Test") || content.contains("testing.T"),
_ => false,
}
}
pub fn test_patterns(lang: &str) -> Vec<&'static str> {
match lang {
"rs" | "rust" => vec!["*_test.rs", "*_tests.rs", "tests/**/*.rs"],
"ts" | "tsx" | "js" | "jsx" => vec![
"*.test.ts",
"*.test.tsx",
"*.test.js",
"*.test.jsx",
"*.spec.ts",
"*.spec.tsx",
"*.spec.js",
"*.spec.jsx",
"__tests__/**/*",
],
"py" | "python" => vec!["test_*.py", "*_test.py", "tests/**/*.py"],
"go" => vec!["*_test.go"],
"dart" => vec!["*_test.dart", "test/**/*.dart"],
_ => vec![],
}
}
pub fn file_kind(path: &str) -> (String, bool, bool) {
let generated = is_generated_path(path);
let test = is_test_path(path);
let story = is_story_path(path);
let lower = path.to_ascii_lowercase();
let config = lower.contains("config/")
|| lower.contains("/config/")
|| lower.ends_with("config.ts")
|| lower.ends_with("config.tsx")
|| lower.ends_with("config.js")
|| lower.ends_with("config.rs")
|| lower.ends_with(".config.ts")
|| lower.ends_with(".config.js")
|| lower.ends_with(".config.json");
let kind = if generated {
"generated"
} else if test {
"test"
} else if story {
"story"
} else if config {
"config"
} else {
"code"
};
(kind.to_string(), test, generated)
}
pub fn language_from_path(path: &str) -> String {
let ext = Path::new(path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default()
.to_lowercase();
detect_language(&ext)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_dev_and_non_dev_files() {
assert!(is_dev_file("features/__tests__/thing.ts"));
assert!(is_dev_file("components/Button.stories.tsx"));
assert!(is_dev_file("fixtures/foo.rs"));
assert!(!is_dev_file("src/app.tsx"));
}
#[test]
fn classifies_file_kinds_and_flags() {
let (kind, test, is_generated) = file_kind("src/generated/foo.gen.ts");
assert_eq!(kind, "generated");
assert!(!test);
assert!(is_generated);
let (kind, test, is_generated) = file_kind("src/components/Button.story.tsx");
assert_eq!(kind, "story");
assert!(!is_generated);
assert!(!test);
let (kind, test, _) = file_kind("src/__tests__/foo.test.ts");
assert_eq!(kind, "test");
assert!(test);
let (kind, _, _) = file_kind("config/vite.config.ts");
assert_eq!(kind, "config");
let (kind, _, _) = file_kind("src/features/app.tsx");
assert_eq!(kind, "code");
}
#[test]
fn detects_language_from_path() {
assert_eq!(language_from_path("foo/bar.tsx"), "ts");
assert_eq!(language_from_path("foo/bar.rs"), "rs");
assert_eq!(language_from_path("foo/bar.py"), "py");
assert_eq!(language_from_path("foo/bar.css"), "css");
assert_eq!(language_from_path("foo/bar.unknown"), "unknown");
}
#[test]
fn detect_language_all_extensions() {
assert_eq!(detect_language("ts"), "ts");
assert_eq!(detect_language("tsx"), "ts");
assert_eq!(detect_language("js"), "js");
assert_eq!(detect_language("jsx"), "js");
assert_eq!(detect_language("mjs"), "js");
assert_eq!(detect_language("cjs"), "js");
assert_eq!(detect_language("rs"), "rs");
assert_eq!(detect_language("py"), "py");
assert_eq!(detect_language("go"), "go");
assert_eq!(detect_language("css"), "css");
assert_eq!(detect_language("html"), "html");
}
#[test]
fn is_dev_file_variations() {
assert!(is_dev_file("src/__tests__/Button.test.ts"));
assert!(is_dev_file("__tests__/unit/helper.ts"));
assert!(is_dev_file("components/stories/Button.tsx"));
assert!(is_dev_file("Button.stories.tsx"));
assert!(is_dev_file("Button.story.tsx"));
assert!(is_dev_file("test/fixtures/data.json"));
assert!(is_dev_file("fixture/mock.ts"));
assert!(!is_dev_file("src/components/Button.tsx"));
assert!(!is_dev_file("lib/utils.ts"));
assert!(!is_dev_file("src/store/index.ts"));
}
#[test]
fn is_test_path_variations() {
assert!(is_test_path("src/__tests__/foo.ts"));
assert!(is_test_path("src/Button.test.tsx"));
assert!(is_test_path("utils.spec.ts"));
assert!(is_test_path("lib_test.rs"));
assert!(is_test_path("module_tests.rs"));
assert!(is_test_path("SRC/__TESTS__/FOO.TS")); assert!(is_test_path("test_parser.py"));
assert!(is_test_path("src/test_utils.py"));
assert!(is_test_path("tests/api/test.rs"));
assert!(is_test_path("src/tests/integration.py"));
assert!(!is_test_path("src/Button.tsx"));
assert!(!is_test_path("testing.ts")); }
#[test]
fn is_story_path_variations() {
assert!(is_story_path("Button.stories.tsx"));
assert!(is_story_path("Button.story.tsx"));
assert!(is_story_path("components/stories/Button.tsx"));
assert!(is_story_path("BUTTON.STORIES.TSX"));
assert!(!is_story_path("src/Button.tsx"));
assert!(!is_story_path("history.ts")); }
#[test]
fn is_generated_path_variations() {
assert!(is_generated_path("src/generated/types.ts"));
assert!(is_generated_path("lib/codegen/schema.ts"));
assert!(is_generated_path("out/gen/api.ts"));
assert!(is_generated_path("types.gen.ts"));
assert!(is_generated_path("api.gen.tsx"));
assert!(is_generated_path("schema.gen.rs"));
assert!(is_generated_path("proto.g.rs"));
assert!(is_generated_path("SRC/GENERATED/FOO.TS"));
assert!(!is_generated_path("src/utils.ts"));
assert!(!is_generated_path("generic.ts")); }
#[test]
fn file_kind_config_variations() {
let (kind, _, _) = file_kind("src/config/app.ts");
assert_eq!(kind, "config");
let (kind, _, _) = file_kind("vite.config.ts");
assert_eq!(kind, "config");
let (kind, _, _) = file_kind("tailwind.config.js");
assert_eq!(kind, "config");
let (kind, _, _) = file_kind("app.config.json");
assert_eq!(kind, "config");
}
#[test]
fn file_kind_priority_generated_over_test() {
let (kind, test, generated) = file_kind("__tests__/generated/mock.gen.ts");
assert_eq!(kind, "generated");
assert!(test); assert!(generated);
}
#[test]
fn file_kind_priority_test_over_story() {
let (kind, test, _) = file_kind("Button.stories.test.ts");
assert_eq!(kind, "test");
assert!(test);
}
#[test]
fn language_from_path_edge_cases() {
assert_eq!(language_from_path("Makefile"), "");
assert_eq!(language_from_path("src/noext"), "");
assert_eq!(language_from_path(".gitignore"), "");
assert_eq!(language_from_path(".env"), "");
assert_eq!(language_from_path("file.test.ts"), "ts");
assert_eq!(language_from_path("app.module.tsx"), "ts"); }
#[test]
fn classify_test_path_unit_tests() {
assert_eq!(
classify_test_path(Path::new("src/components/Button.test.tsx")),
TestClassification::UnitTest
);
assert_eq!(
classify_test_path(Path::new("utils.spec.ts")),
TestClassification::UnitTest
);
assert_eq!(
classify_test_path(Path::new("src/__tests__/helper.ts")),
TestClassification::UnitTest
);
assert_eq!(
classify_test_path(Path::new("src/parser_test.rs")),
TestClassification::UnitTest
);
assert_eq!(
classify_test_path(Path::new("lib/module_tests.rs")),
TestClassification::UnitTest
);
assert_eq!(
classify_test_path(Path::new("test_utils.py")),
TestClassification::UnitTest
);
assert_eq!(
classify_test_path(Path::new("handler_test.go")),
TestClassification::UnitTest
);
}
#[test]
fn classify_test_path_integration_tests() {
assert_eq!(
classify_test_path(Path::new("tests/api/endpoints.rs")),
TestClassification::IntegrationTest
);
assert_eq!(
classify_test_path(Path::new("tests/integration/database.rs")),
TestClassification::IntegrationTest
);
assert_eq!(
classify_test_path(Path::new("src/__tests__/integration.test.ts")),
TestClassification::UnitTest
);
}
#[test]
fn classify_test_path_fixtures() {
assert_eq!(
classify_test_path(Path::new("tests/fixtures/data.json")),
TestClassification::TestFixture
);
assert_eq!(
classify_test_path(Path::new("__tests__/fixture/mock.ts")),
TestClassification::TestFixture
);
assert_eq!(
classify_test_path(Path::new("test/mock/server.rs")),
TestClassification::TestFixture
);
}
#[test]
fn classify_test_path_helpers() {
assert_eq!(
classify_test_path(Path::new("tests/test_helper.rs")),
TestClassification::TestHelper
);
assert_eq!(
classify_test_path(Path::new("__tests__/test_utils.ts")),
TestClassification::TestHelper
);
assert_eq!(
classify_test_path(Path::new("testing/setup.py")),
TestClassification::TestHelper
);
}
#[test]
fn classify_test_path_production() {
assert_eq!(
classify_test_path(Path::new("src/components/Button.tsx")),
TestClassification::Production
);
assert_eq!(
classify_test_path(Path::new("lib/parser.rs")),
TestClassification::Production
);
assert_eq!(
classify_test_path(Path::new("utils/helpers.py")),
TestClassification::Production
);
}
#[test]
fn has_test_code_rust() {
let code_with_test_module = r#"
fn main() {}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
"#;
assert!(has_test_code(code_with_test_module, "rs"));
let code_with_test_fn = r#"
#[test]
fn test_something() {}
"#;
assert!(has_test_code(code_with_test_fn, "rust"));
let production_code = r#"
fn parse(input: &str) -> Result<(), Error> {
Ok(())
}
"#;
assert!(!has_test_code(production_code, "rs"));
}
#[test]
fn has_test_code_typescript() {
let jest_test = r#"
describe('Button', () => {
it('should render', () => {
expect(true).toBe(true);
});
});
"#;
assert!(has_test_code(jest_test, "ts"));
let vitest_test = r#"
test('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
"#;
assert!(has_test_code(vitest_test, "tsx"));
let production = r#"
export function add(a: number, b: number): number {
return a + b;
}
"#;
assert!(!has_test_code(production, "ts"));
}
#[test]
fn has_test_code_python() {
let unittest_code = r#"
import unittest
class TestMath(unittest.TestCase):
def test_addition(self):
self.assertEqual(1 + 1, 2)
"#;
assert!(has_test_code(unittest_code, "py"));
let pytest_code = r#"
import pytest
def test_addition():
assert 1 + 1 == 2
"#;
assert!(has_test_code(pytest_code, "python"));
let test_function = r#"
def test_something():
pass
"#;
assert!(has_test_code(test_function, "py"));
let production = r#"
def add(a, b):
return a + b
"#;
assert!(!has_test_code(production, "py"));
}
#[test]
fn has_test_code_go() {
let go_test = r#"
package main
import "testing"
func TestAdd(t *testing.T) {
result := Add(1, 2)
if result != 3 {
t.Errorf("Expected 3, got %d", result)
}
}
"#;
assert!(has_test_code(go_test, "go"));
let production = r#"
package main
func Add(a, b int) int {
return a + b
}
"#;
assert!(!has_test_code(production, "go"));
}
#[test]
fn test_patterns_all_languages() {
let rust_patterns = test_patterns("rs");
assert!(rust_patterns.contains(&"*_test.rs"));
assert!(rust_patterns.contains(&"*_tests.rs"));
assert!(rust_patterns.contains(&"tests/**/*.rs"));
let ts_patterns = test_patterns("ts");
assert!(ts_patterns.contains(&"*.test.ts"));
assert!(ts_patterns.contains(&"*.spec.tsx"));
assert!(ts_patterns.contains(&"__tests__/**/*"));
let py_patterns = test_patterns("py");
assert!(py_patterns.contains(&"test_*.py"));
assert!(py_patterns.contains(&"*_test.py"));
assert!(py_patterns.contains(&"tests/**/*.py"));
let go_patterns = test_patterns("go");
assert!(go_patterns.contains(&"*_test.go"));
let unknown_patterns = test_patterns("unknown");
assert!(unknown_patterns.is_empty());
}
}