debtmap/testing/rust/
flaky_detector.rs1use super::RustFlakinessType;
2use syn::spanned::Spanned;
3use syn::visit::Visit;
4use syn::{Expr, ItemFn, PathSegment, Stmt};
5
6pub 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 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 pub fn has_flaky_patterns(&self) -> bool {
34 !self.indicators.is_empty()
35 }
36
37 pub fn flaky_pattern_count(&self) -> usize {
39 self.indicators.len()
40 }
41
42 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 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 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 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 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 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 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 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 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 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 });
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}