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 let path_str = attr
93 .path()
94 .segments
95 .iter()
96 .map(|seg| seg.ident.to_string())
97 .collect::<Vec<_>>()
98 .join("::");
99
100 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}