Skip to main content

declutter/
test_files.rs

1use crate::lang::Lang;
2
3/// Whether a changed file belongs to the test layer: by path convention first, then,
4/// for files outside test directories, by importing a test framework.
5pub 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    // CamelCase suffixes: FooTests.swift, FooTest.kt, FooSpec.swift, FooTests.cs.
29    let camel = ["Tests", "Test", "Spec"]
30        .iter()
31        .any(|suffix| stem.len() > suffix.len() && stem.ends_with(suffix));
32    // Dotted and snake_case markers: foo.test.ts, foo.spec.tsx, foo_test.go, test_foo.py.
33    let marked = file.contains(".test.")
34        || file.contains(".spec.")
35        || stem.ends_with("_test")
36        || stem.ends_with("_spec")
37        || stem.starts_with("test_")
38        || file == "conftest.py";
39    camel || marked
40}
41
42fn imports_test_framework(lang: Lang, content: &str) -> bool {
43    content.lines().map(str::trim_start).any(|line| match lang {
44        Lang::Swift => {
45            line.starts_with("@testable import ")
46                || line == "import XCTest"
47                || line == "import Testing"
48        }
49        Lang::Python => [
50            "import pytest",
51            "from pytest",
52            "import unittest",
53            "from unittest",
54        ]
55        .iter()
56        .any(|import| line.starts_with(import)),
57        Lang::TypeScript | Lang::Tsx | Lang::JavaScript => {
58            line.starts_with("import")
59                && ["vitest", "@jest/globals", "node:test", "@testing-library/"]
60                    .iter()
61                    .any(|module| {
62                        line.contains(&format!("'{module}"))
63                            || line.contains(&format!("\"{module}"))
64                    })
65        }
66    })
67}