1use crate::lang::Lang;
2
3pub fn is_test_file(path: &str, content: &str) -> bool {
6 is_test_path(path)
7 || Lang::from_path(path).is_some_and(|lang| imports_test_framework(lang, content))
8}
9
10pub fn is_test_path(path: &str) -> bool {
11 let mut components: Vec<&str> = path.split('/').collect();
12 let Some(file) = components.pop() else {
13 return false;
14 };
15 components.iter().any(|dir| is_test_dir(dir)) || is_test_file_name(file)
16}
17
18fn is_test_dir(dir: &str) -> bool {
19 let lower = dir.to_ascii_lowercase();
20 matches!(
21 lower.as_str(),
22 "test" | "tests" | "__tests__" | "spec" | "specs" | "__mocks__" | "__snapshots__"
23 ) || dir.ends_with("Tests")
24}
25
26fn is_test_file_name(file: &str) -> bool {
27 let stem = file.split('.').next().unwrap_or(file);
28 let camel = ["Tests", "Test"]
31 .iter()
32 .any(|suffix| stem.len() > suffix.len() && stem.ends_with(suffix));
33 let marked = file.contains(".test.")
35 || file.contains(".spec.")
36 || stem.ends_with("_test")
37 || stem.ends_with("_spec")
38 || (stem.starts_with("test_") && file.ends_with(".py"))
40 || file == "conftest.py";
41 camel || marked
42}
43
44fn imports_test_framework(lang: Lang, content: &str) -> bool {
45 content.lines().map(str::trim_start).any(|line| match lang {
46 Lang::Swift => {
47 line.starts_with("@testable import ")
48 || matches!(
49 line,
50 "import XCTest" | "import Testing" | "import Quick" | "import Nimble"
51 )
52 }
53 Lang::Python => [
54 "import pytest",
55 "from pytest",
56 "import unittest",
57 "from unittest",
58 ]
59 .iter()
60 .any(|import| line.starts_with(import)),
61 Lang::TypeScript | Lang::Tsx | Lang::JavaScript => {
62 line.starts_with("import")
63 && ["vitest", "@jest/globals", "node:test", "@testing-library/"]
64 .iter()
65 .any(|module| {
66 line.contains(&format!("'{module}"))
67 || line.contains(&format!("\"{module}"))
68 })
69 }
70 Lang::Kotlin => ["org.junit", "kotlin.test", "io.kotest", "io.mockk"]
71 .iter()
72 .any(|package| line.starts_with(&format!("import {package}"))),
73 Lang::Go | Lang::Rust => false,
76 })
77}