debtmap/testing/
mod.rs

1pub mod assertion_detector;
2pub mod complexity_detector;
3pub mod flaky_detector;
4pub mod rust;
5pub mod timing_classifier;
6
7use crate::core::{DebtItem, DebtType, Priority};
8use std::path::{Path, PathBuf};
9use syn::{File, ItemFn};
10
11#[derive(Debug, Clone, PartialEq)]
12pub enum TestingAntiPattern {
13    TestWithoutAssertions {
14        test_name: String,
15        file: PathBuf,
16        line: usize,
17        has_setup: bool,
18        has_action: bool,
19        suggested_assertions: Vec<String>,
20    },
21    OverlyComplexTest {
22        test_name: String,
23        file: PathBuf,
24        line: usize,
25        complexity_score: u32,
26        complexity_sources: Vec<ComplexitySource>,
27        suggested_simplification: TestSimplification,
28    },
29    FlakyTestPattern {
30        test_name: String,
31        file: PathBuf,
32        line: usize,
33        flakiness_type: FlakinessType,
34        reliability_impact: ReliabilityImpact,
35        stabilization_suggestion: String,
36    },
37}
38
39#[derive(Debug, Clone, PartialEq)]
40pub enum ComplexitySource {
41    ExcessiveMocking,
42    NestedConditionals,
43    MultipleAssertions,
44    LoopInTest,
45    ExcessiveSetup,
46}
47
48#[derive(Debug, Clone, PartialEq)]
49pub enum TestSimplification {
50    ExtractHelper,
51    SplitTest,
52    ParameterizeTest,
53    SimplifySetup,
54    ReduceMocking,
55}
56
57#[derive(Debug, Clone, PartialEq)]
58pub enum FlakinessType {
59    TimingDependency,
60    RandomValues,
61    ExternalDependency,
62    FilesystemDependency,
63    NetworkDependency,
64    ThreadingIssue,
65}
66
67#[derive(Debug, Clone, PartialEq)]
68pub enum ReliabilityImpact {
69    Critical,
70    High,
71    Medium,
72    Low,
73}
74
75#[derive(Debug, Clone, PartialEq)]
76pub enum TestQualityImpact {
77    Critical,
78    High,
79    Medium,
80    Low,
81}
82
83pub trait TestingDetector {
84    fn detect_anti_patterns(&self, file: &File, path: &Path) -> Vec<TestingAntiPattern>;
85    fn detector_name(&self) -> &'static str;
86    fn assess_test_quality_impact(&self, pattern: &TestingAntiPattern) -> TestQualityImpact;
87}
88
89pub fn is_test_function(function: &ItemFn) -> bool {
90    function.attrs.iter().any(|attr| {
91        // Check if it's a test attribute
92        let path_str = attr
93            .path()
94            .segments
95            .iter()
96            .map(|seg| seg.ident.to_string())
97            .collect::<Vec<_>>()
98            .join("::");
99
100        // Match common test attributes
101        path_str == "test"
102            || path_str == "tokio::test"
103            || path_str == "async_std::test"
104            || path_str == "bench"
105            || path_str.ends_with("::test")
106    }) || function.sig.ident.to_string().starts_with("test_")
107        || function.sig.ident.to_string().ends_with("_test")
108}
109
110pub fn analyze_testing_patterns(file: &File, path: &Path) -> Vec<DebtItem> {
111    let detectors: Vec<Box<dyn TestingDetector>> = vec![
112        Box::new(assertion_detector::AssertionDetector::new()),
113        Box::new(complexity_detector::TestComplexityDetector::new()),
114        Box::new(flaky_detector::FlakyTestDetector::new()),
115    ];
116
117    let mut testing_items = Vec::new();
118
119    for detector in detectors {
120        let anti_patterns = detector.detect_anti_patterns(file, path);
121
122        for pattern in anti_patterns {
123            let impact = detector.assess_test_quality_impact(&pattern);
124            let debt_item = convert_testing_pattern_to_debt_item(pattern, impact, path);
125            testing_items.push(debt_item);
126        }
127    }
128
129    testing_items
130}
131
132fn convert_testing_pattern_to_debt_item(
133    pattern: TestingAntiPattern,
134    _impact: TestQualityImpact,
135    path: &Path,
136) -> DebtItem {
137    let (priority, message, context, line, debt_type) = match pattern {
138        TestingAntiPattern::TestWithoutAssertions {
139            test_name,
140            suggested_assertions,
141            line,
142            ..
143        } => (
144            Priority::High,
145            format!("Test '{}' has no assertions", test_name),
146            Some(format!(
147                "Add assertions: {}",
148                suggested_assertions.join(", ")
149            )),
150            line,
151            DebtType::TestQuality { issue_type: None },
152        ),
153        TestingAntiPattern::OverlyComplexTest {
154            test_name,
155            complexity_score,
156            suggested_simplification,
157            line,
158            ..
159        } => (
160            Priority::Medium,
161            format!(
162                "Test '{}' is overly complex (score: {})",
163                test_name, complexity_score
164            ),
165            Some(format!("Consider: {:?}", suggested_simplification)),
166            line,
167            DebtType::TestComplexity {
168                cyclomatic: 0,
169                cognitive: 0,
170            },
171        ),
172        TestingAntiPattern::FlakyTestPattern {
173            test_name,
174            flakiness_type,
175            stabilization_suggestion,
176            line,
177            ..
178        } => (
179            Priority::High,
180            format!(
181                "Test '{}' has flaky pattern: {:?}",
182                test_name, flakiness_type
183            ),
184            Some(stabilization_suggestion),
185            line,
186            DebtType::TestQuality { issue_type: None },
187        ),
188    };
189
190    DebtItem {
191        id: format!("testing-{}-{}", path.display(), line),
192        debt_type,
193        priority,
194        file: path.to_path_buf(),
195        line,
196        column: None,
197        message,
198        context,
199    }
200}