debtmap/testing/rust/
flaky_detector.rs

1use super::RustFlakinessType;
2use syn::spanned::Spanned;
3use syn::visit::Visit;
4use syn::{Expr, ItemFn, PathSegment, Stmt};
5
6/// Detects patterns that can cause test flakiness in Rust tests
7pub struct FlakyDetector {
8    indicators: Vec<FlakyIndicator>,
9}
10
11#[derive(Debug, Clone)]
12pub struct FlakyIndicator {
13    pub flakiness_type: RustFlakinessType,
14    pub line: usize,
15    pub explanation: String,
16}
17
18impl FlakyDetector {
19    pub fn new() -> Self {
20        Self {
21            indicators: Vec::new(),
22        }
23    }
24
25    /// Detect flaky patterns in a test function
26    pub fn detect_flaky_patterns(&mut self, func: &ItemFn) -> Vec<FlakyIndicator> {
27        self.indicators.clear();
28        self.visit_block(&func.block);
29        self.indicators.clone()
30    }
31
32    /// Check if any flaky patterns were detected
33    pub fn has_flaky_patterns(&self) -> bool {
34        !self.indicators.is_empty()
35    }
36
37    /// Get count of flaky patterns
38    pub fn flaky_pattern_count(&self) -> usize {
39        self.indicators.len()
40    }
41
42    /// Check if path represents a timing-related function
43    fn is_timing_related(&self, segments: &[&PathSegment]) -> bool {
44        let path_str = segments
45            .iter()
46            .map(|seg| seg.ident.to_string())
47            .collect::<Vec<_>>()
48            .join("::");
49
50        // Timing dependencies
51        path_str.contains("thread::sleep")
52            || path_str.contains("sleep")
53            || path_str.contains("Instant::now")
54            || path_str.contains("SystemTime::now")
55            || path_str.contains("Duration")
56    }
57
58    /// Check if path represents random value generation
59    fn is_random_related(&self, segments: &[&PathSegment]) -> bool {
60        let path_str = segments
61            .iter()
62            .map(|seg| seg.ident.to_string())
63            .collect::<Vec<_>>()
64            .join("::");
65
66        path_str.contains("rand")
67            || path_str.contains("random")
68            || path_str.contains("Uuid::new")
69            || path_str.contains("uuid")
70    }
71
72    /// Check if path represents external dependencies
73    fn is_external_dependency(&self, segments: &[&PathSegment]) -> bool {
74        let path_str = segments
75            .iter()
76            .map(|seg| seg.ident.to_string())
77            .collect::<Vec<_>>()
78            .join("::");
79
80        path_str.contains("reqwest")
81            || path_str.contains("hyper")
82            || path_str.contains("http")
83            || path_str.contains("Client")
84            || path_str.contains("Request")
85    }
86
87    /// Check if path represents filesystem operations
88    fn is_filesystem_related(&self, segments: &[&PathSegment]) -> bool {
89        let path_str = segments
90            .iter()
91            .map(|seg| seg.ident.to_string())
92            .collect::<Vec<_>>()
93            .join("::");
94
95        path_str.contains("File::")
96            || path_str.contains("fs::")
97            || path_str.contains("read")
98            || path_str.contains("write")
99            || path_str.contains("OpenOptions")
100    }
101
102    /// Check if path represents network operations
103    fn is_network_related(&self, segments: &[&PathSegment]) -> bool {
104        let path_str = segments
105            .iter()
106            .map(|seg| seg.ident.to_string())
107            .collect::<Vec<_>>()
108            .join("::");
109
110        path_str.contains("TcpStream")
111            || path_str.contains("UdpSocket")
112            || path_str.contains("bind")
113            || path_str.contains("connect")
114            || path_str.contains("listen")
115    }
116
117    /// Check if path represents threading operations
118    fn is_threading_related(&self, segments: &[&PathSegment]) -> bool {
119        let path_str = segments
120            .iter()
121            .map(|seg| seg.ident.to_string())
122            .collect::<Vec<_>>()
123            .join("::");
124
125        path_str.contains("thread::spawn")
126            || path_str.contains("spawn")
127            || path_str.contains("Arc")
128            || path_str.contains("Mutex")
129            || path_str.contains("RwLock")
130            || path_str.contains("Channel")
131    }
132
133    /// Check if path represents HashMap iteration (non-deterministic ordering)
134    fn is_hash_ordering_issue(&self, segments: &[&PathSegment]) -> bool {
135        let path_str = segments
136            .iter()
137            .map(|seg| seg.ident.to_string())
138            .collect::<Vec<_>>()
139            .join("::");
140
141        path_str.contains("HashMap::iter") || path_str.contains("HashSet::iter")
142    }
143
144    /// Extract path segments from an expression
145    fn extract_path_segments(expr: &Expr) -> Vec<&PathSegment> {
146        match expr {
147            Expr::Path(expr_path) => expr_path.path.segments.iter().collect(),
148            Expr::Call(call) => Self::extract_path_segments(&call.func),
149            Expr::MethodCall(_method) => vec![],
150            _ => vec![],
151        }
152    }
153
154    /// Analyze expression for flaky patterns
155    fn analyze_expr(&mut self, expr: &Expr, line: usize) {
156        let segments = Self::extract_path_segments(expr);
157
158        if self.is_timing_related(&segments) {
159            self.indicators.push(FlakyIndicator {
160                flakiness_type: RustFlakinessType::TimingDependency,
161                line,
162                explanation: "Test uses timing-dependent code (sleep, Instant::now) which can cause flakiness".to_string(),
163            });
164        }
165
166        if self.is_random_related(&segments) {
167            self.indicators.push(FlakyIndicator {
168                flakiness_type: RustFlakinessType::RandomValue,
169                line,
170                explanation: "Test uses random values which can cause non-deterministic behavior"
171                    .to_string(),
172            });
173        }
174
175        if self.is_external_dependency(&segments) {
176            self.indicators.push(FlakyIndicator {
177                flakiness_type: RustFlakinessType::ExternalDependency,
178                line,
179                explanation: "Test depends on external services which can be unreliable"
180                    .to_string(),
181            });
182        }
183
184        if self.is_filesystem_related(&segments) {
185            self.indicators.push(FlakyIndicator {
186                flakiness_type: RustFlakinessType::FileSystemDependency,
187                line,
188                explanation:
189                    "Test performs filesystem operations which can fail in different environments"
190                        .to_string(),
191            });
192        }
193
194        if self.is_network_related(&segments) {
195            self.indicators.push(FlakyIndicator {
196                flakiness_type: RustFlakinessType::NetworkDependency,
197                line,
198                explanation: "Test uses network operations which can be unreliable".to_string(),
199            });
200        }
201
202        if self.is_threading_related(&segments) {
203            self.indicators.push(FlakyIndicator {
204                flakiness_type: RustFlakinessType::ThreadingIssue,
205                line,
206                explanation: "Test uses threading which can cause race conditions and flakiness"
207                    .to_string(),
208            });
209        }
210
211        if self.is_hash_ordering_issue(&segments) {
212            self.indicators.push(FlakyIndicator {
213                flakiness_type: RustFlakinessType::HashOrdering,
214                line,
215                explanation: "Test iterates HashMap/HashSet which has non-deterministic ordering"
216                    .to_string(),
217            });
218        }
219    }
220}
221
222impl Default for FlakyDetector {
223    fn default() -> Self {
224        Self::new()
225    }
226}
227
228impl<'ast> Visit<'ast> for FlakyDetector {
229    fn visit_expr(&mut self, expr: &'ast Expr) {
230        let line = expr.span().start().line;
231        self.analyze_expr(expr, line);
232        syn::visit::visit_expr(self, expr);
233    }
234
235    fn visit_stmt(&mut self, stmt: &'ast Stmt) {
236        if let Stmt::Expr(expr, _) = stmt {
237            let line = expr.span().start().line;
238            self.analyze_expr(expr, line);
239        }
240        syn::visit::visit_stmt(self, stmt);
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use syn::parse_quote;
248
249    #[test]
250    fn test_detect_sleep() {
251        let func: ItemFn = parse_quote! {
252            #[test]
253            fn test_timing() {
254                std::thread::sleep(std::time::Duration::from_millis(100));
255                assert!(true);
256            }
257        };
258
259        let mut detector = FlakyDetector::new();
260        let indicators = detector.detect_flaky_patterns(&func);
261        assert!(!indicators.is_empty());
262        assert!(indicators
263            .iter()
264            .any(|i| matches!(i.flakiness_type, RustFlakinessType::TimingDependency)));
265    }
266
267    #[test]
268    fn test_detect_random() {
269        let func: ItemFn = parse_quote! {
270            #[test]
271            fn test_random() {
272                use rand::Rng;
273                let value = rand::thread_rng().gen_range(0..100);
274                assert!(value < 100);
275            }
276        };
277
278        let mut detector = FlakyDetector::new();
279        let indicators = detector.detect_flaky_patterns(&func);
280        assert!(indicators
281            .iter()
282            .any(|i| matches!(i.flakiness_type, RustFlakinessType::RandomValue)));
283    }
284
285    #[test]
286    fn test_detect_network() {
287        let func: ItemFn = parse_quote! {
288            #[test]
289            fn test_network() {
290                let client = reqwest::Client::new();
291                assert!(true);
292            }
293        };
294
295        let mut detector = FlakyDetector::new();
296        let indicators = detector.detect_flaky_patterns(&func);
297        assert!(indicators
298            .iter()
299            .any(|i| matches!(i.flakiness_type, RustFlakinessType::ExternalDependency)));
300    }
301
302    #[test]
303    fn test_detect_filesystem() {
304        let func: ItemFn = parse_quote! {
305            #[test]
306            fn test_file() {
307                std::fs::write("/tmp/test.txt", "data").unwrap();
308                assert!(true);
309            }
310        };
311
312        let mut detector = FlakyDetector::new();
313        let indicators = detector.detect_flaky_patterns(&func);
314        assert!(indicators
315            .iter()
316            .any(|i| matches!(i.flakiness_type, RustFlakinessType::FileSystemDependency)));
317    }
318
319    #[test]
320    fn test_detect_threading() {
321        let func: ItemFn = parse_quote! {
322            #[test]
323            fn test_thread() {
324                std::thread::spawn(|| {
325                    // do work
326                });
327                assert!(true);
328            }
329        };
330
331        let mut detector = FlakyDetector::new();
332        let indicators = detector.detect_flaky_patterns(&func);
333        assert!(indicators
334            .iter()
335            .any(|i| matches!(i.flakiness_type, RustFlakinessType::ThreadingIssue)));
336    }
337
338    #[test]
339    fn test_no_flaky_patterns() {
340        let func: ItemFn = parse_quote! {
341            #[test]
342            fn test_clean() {
343                let x = 42;
344                assert_eq!(x, 42);
345            }
346        };
347
348        let mut detector = FlakyDetector::new();
349        let indicators = detector.detect_flaky_patterns(&func);
350        assert!(indicators.is_empty());
351    }
352
353    #[test]
354    fn test_multiple_flaky_patterns() {
355        let func: ItemFn = parse_quote! {
356            #[test]
357            fn test_multiple() {
358                std::thread::sleep(std::time::Duration::from_millis(100));
359                let value = rand::thread_rng().gen_range(0..100);
360                assert!(value < 100);
361            }
362        };
363
364        let mut detector = FlakyDetector::new();
365        let indicators = detector.detect_flaky_patterns(&func);
366        assert!(indicators.len() >= 2);
367    }
368}