1use crate::adapters::{TestCase, TestRunResult, TestStatus, TestSuite};
2
3#[derive(Debug, Clone)]
5pub struct TestFilter {
6 include: Vec<FilterPattern>,
8 exclude: Vec<FilterPattern>,
10 status_filter: Vec<TestStatus>,
12 suite_filter: Option<String>,
14}
15
16#[derive(Debug, Clone)]
18pub enum FilterPattern {
19 Exact(String),
21 Prefix(String),
23 Suffix(String),
25 Contains(String),
27 Glob(Vec<GlobSegment>),
29}
30
31#[derive(Debug, Clone)]
33pub enum GlobSegment {
34 Literal(String),
36 Wildcard,
38}
39
40impl TestFilter {
41 pub fn new() -> Self {
43 Self {
44 include: Vec::new(),
45 exclude: Vec::new(),
46 status_filter: Vec::new(),
47 suite_filter: None,
48 }
49 }
50
51 pub fn include(mut self, pattern: &str) -> Self {
53 self.include.push(FilterPattern::parse(pattern));
54 self
55 }
56
57 pub fn include_csv(mut self, patterns: &str) -> Self {
59 for pattern in patterns.split(',') {
60 let pattern = pattern.trim();
61 if !pattern.is_empty() {
62 self.include.push(FilterPattern::parse(pattern));
63 }
64 }
65 self
66 }
67
68 pub fn exclude(mut self, pattern: &str) -> Self {
70 self.exclude.push(FilterPattern::parse(pattern));
71 self
72 }
73
74 pub fn exclude_csv(mut self, patterns: &str) -> Self {
76 for pattern in patterns.split(',') {
77 let pattern = pattern.trim();
78 if !pattern.is_empty() {
79 self.exclude.push(FilterPattern::parse(pattern));
80 }
81 }
82 self
83 }
84
85 pub fn status(mut self, status: TestStatus) -> Self {
87 self.status_filter.push(status);
88 self
89 }
90
91 pub fn suite(mut self, name: &str) -> Self {
93 self.suite_filter = Some(name.to_string());
94 self
95 }
96
97 pub fn is_active(&self) -> bool {
99 !self.include.is_empty()
100 || !self.exclude.is_empty()
101 || !self.status_filter.is_empty()
102 || self.suite_filter.is_some()
103 }
104
105 pub fn matches(&self, test: &TestCase, suite_name: &str) -> bool {
107 if let Some(ref sf) = self.suite_filter
109 && !suite_name.contains(sf)
110 {
111 return false;
112 }
113
114 if !self.status_filter.is_empty() && !self.status_filter.contains(&test.status) {
116 return false;
117 }
118
119 if self.exclude.iter().any(|p| p.matches(&test.name)) {
121 return false;
122 }
123
124 if !self.include.is_empty() && !self.include.iter().any(|p| p.matches(&test.name)) {
126 return false;
127 }
128
129 true
130 }
131
132 pub fn apply(&self, result: &TestRunResult) -> TestRunResult {
134 if !self.is_active() {
135 return result.clone();
136 }
137
138 let suites = result
139 .suites
140 .iter()
141 .filter_map(|suite| {
142 if let Some(ref sf) = self.suite_filter
144 && !suite.name.contains(sf)
145 {
146 return None;
147 }
148
149 let tests: Vec<TestCase> = suite
150 .tests
151 .iter()
152 .filter(|t| self.matches(t, &suite.name))
153 .cloned()
154 .collect();
155
156 if tests.is_empty() {
157 None
158 } else {
159 Some(TestSuite {
160 name: suite.name.clone(),
161 tests,
162 })
163 }
164 })
165 .collect();
166
167 TestRunResult {
168 suites,
169 duration: result.duration,
170 raw_exit_code: result.raw_exit_code,
171 }
172 }
173}
174
175impl Default for TestFilter {
176 fn default() -> Self {
177 Self::new()
178 }
179}
180
181impl FilterPattern {
182 pub fn parse(pattern: &str) -> Self {
184 if !pattern.contains('*') {
185 return FilterPattern::Exact(pattern.to_string());
186 }
187
188 if pattern.starts_with('*') && !pattern[1..].contains('*') {
190 return FilterPattern::Suffix(pattern[1..].to_string());
191 }
192
193 if pattern.ends_with('*') && !pattern[..pattern.len() - 1].contains('*') {
195 return FilterPattern::Prefix(pattern[..pattern.len() - 1].to_string());
196 }
197
198 if pattern.starts_with('*')
200 && pattern.ends_with('*')
201 && !pattern[1..pattern.len() - 1].contains('*')
202 {
203 return FilterPattern::Contains(pattern[1..pattern.len() - 1].to_string());
204 }
205
206 let segments = parse_glob_segments(pattern);
208 FilterPattern::Glob(segments)
209 }
210
211 pub fn matches(&self, name: &str) -> bool {
213 match self {
214 FilterPattern::Exact(s) => name == s,
215 FilterPattern::Prefix(p) => name.starts_with(p),
216 FilterPattern::Suffix(s) => name.ends_with(s),
217 FilterPattern::Contains(s) => name.contains(s),
218 FilterPattern::Glob(segments) => glob_match(segments, name),
219 }
220 }
221}
222
223fn parse_glob_segments(pattern: &str) -> Vec<GlobSegment> {
225 let mut segments = Vec::new();
226 let mut current = String::new();
227
228 for ch in pattern.chars() {
229 if ch == '*' {
230 if !current.is_empty() {
231 segments.push(GlobSegment::Literal(std::mem::take(&mut current)));
232 }
233 if !matches!(segments.last(), Some(GlobSegment::Wildcard)) {
235 segments.push(GlobSegment::Wildcard);
236 }
237 } else {
238 current.push(ch);
239 }
240 }
241 if !current.is_empty() {
242 segments.push(GlobSegment::Literal(current));
243 }
244
245 segments
246}
247
248fn glob_match(segments: &[GlobSegment], s: &str) -> bool {
250 glob_match_recursive(segments, s, 0, 0, 0)
251}
252
253fn glob_match_recursive(
254 segments: &[GlobSegment],
255 s: &str,
256 seg_idx: usize,
257 str_idx: usize,
258 depth: usize,
259) -> bool {
260 const MAX_DEPTH: usize = 1024;
262 if depth > MAX_DEPTH {
263 return false;
264 }
265
266 if seg_idx == segments.len() {
267 return str_idx == s.len();
268 }
269
270 match &segments[seg_idx] {
271 GlobSegment::Literal(lit) => {
272 if s[str_idx..].starts_with(lit) {
273 glob_match_recursive(segments, s, seg_idx + 1, str_idx + lit.len(), depth + 1)
274 } else {
275 false
276 }
277 }
278 GlobSegment::Wildcard => {
279 if seg_idx + 1 == segments.len() {
282 return true;
283 }
284 if let Some(GlobSegment::Literal(next_lit)) = segments.get(seg_idx + 1) {
287 let remaining = &s[str_idx..];
288 let mut search_from = 0;
289 while let Some(pos) = remaining[search_from..].find(next_lit.as_str()) {
290 let abs_pos = str_idx + search_from + pos;
291 if glob_match_recursive(segments, s, seg_idx + 1, abs_pos, depth + 1) {
292 return true;
293 }
294 search_from += pos + 1;
295 }
296 false
297 } else {
298 for i in str_idx..=s.len() {
300 if glob_match_recursive(segments, s, seg_idx + 1, i, depth + 1) {
301 return true;
302 }
303 }
304 false
305 }
306 }
307 }
308}
309
310pub fn build_filter(include: Option<&str>, exclude: Option<&str>, failed_only: bool) -> TestFilter {
312 let mut filter = TestFilter::new();
313
314 if let Some(inc) = include {
315 filter = filter.include_csv(inc);
316 }
317 if let Some(exc) = exclude {
318 filter = filter.exclude_csv(exc);
319 }
320 if failed_only {
321 filter = filter.status(TestStatus::Failed);
322 }
323
324 filter
325}
326
327pub struct FilterSummary {
329 pub total_before: usize,
331 pub total_after: usize,
333 pub filtered_out: usize,
335 pub suites_removed: usize,
337}
338
339pub fn filter_with_summary(
341 filter: &TestFilter,
342 result: &TestRunResult,
343) -> (TestRunResult, FilterSummary) {
344 let total_before = result.total_tests();
345 let suites_before = result.suites.len();
346
347 let filtered = filter.apply(result);
348
349 let total_after = filtered.total_tests();
350 let suites_after = filtered.suites.len();
351
352 let summary = FilterSummary {
353 total_before,
354 total_after,
355 filtered_out: total_before.saturating_sub(total_after),
356 suites_removed: suites_before.saturating_sub(suites_after),
357 };
358
359 (filtered, summary)
360}
361
362pub fn failed_test_names(result: &TestRunResult) -> Vec<String> {
364 let mut names = Vec::new();
365 for suite in &result.suites {
366 for test in &suite.tests {
367 if test.status == TestStatus::Failed {
368 names.push(test.name.clone());
369 }
370 }
371 }
372 names
373}
374
375pub fn matching_test_names(filter: &TestFilter, result: &TestRunResult) -> Vec<String> {
377 let mut names = Vec::new();
378 for suite in &result.suites {
379 for test in &suite.tests {
380 if filter.matches(test, &suite.name) {
381 names.push(test.name.clone());
382 }
383 }
384 }
385 names
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use std::time::Duration;
392
393 fn make_test(name: &str, status: TestStatus) -> TestCase {
394 TestCase {
395 name: name.into(),
396 status,
397 duration: Duration::from_millis(1),
398 error: None,
399 }
400 }
401
402 fn make_suite(name: &str, tests: Vec<TestCase>) -> TestSuite {
403 TestSuite {
404 name: name.into(),
405 tests,
406 }
407 }
408
409 fn make_result(suites: Vec<TestSuite>) -> TestRunResult {
410 TestRunResult {
411 suites,
412 duration: Duration::from_millis(100),
413 raw_exit_code: 0,
414 }
415 }
416
417 #[test]
420 fn pattern_exact() {
421 let p = FilterPattern::parse("test_add");
422 assert!(p.matches("test_add"));
423 assert!(!p.matches("test_add_extra"));
424 assert!(!p.matches("test"));
425 }
426
427 #[test]
428 fn pattern_prefix() {
429 let p = FilterPattern::parse("test_*");
430 assert!(p.matches("test_add"));
431 assert!(p.matches("test_"));
432 assert!(!p.matches("other_add"));
433 }
434
435 #[test]
436 fn pattern_suffix() {
437 let p = FilterPattern::parse("*_add");
438 assert!(p.matches("test_add"));
439 assert!(p.matches("_add"));
440 assert!(!p.matches("test_sub"));
441 }
442
443 #[test]
444 fn pattern_contains() {
445 let p = FilterPattern::parse("*divide*");
446 assert!(p.matches("test_divide_by_zero"));
447 assert!(p.matches("divide"));
448 assert!(!p.matches("test_add"));
449 }
450
451 #[test]
452 fn pattern_glob() {
453 let p = FilterPattern::parse("test_*_basic_*");
454 assert!(p.matches("test_math_basic_add"));
455 assert!(p.matches("test_str_basic_concat"));
456 assert!(!p.matches("test_math_advanced_add"));
457 }
458
459 #[test]
460 fn pattern_exact_no_wildcard() {
461 let p = FilterPattern::parse("hello");
462 assert!(matches!(p, FilterPattern::Exact(_)));
463 }
464
465 #[test]
468 fn filter_include_single() {
469 let filter = TestFilter::new().include("test_add");
470 let test = make_test("test_add", TestStatus::Passed);
471 assert!(filter.matches(&test, "suite"));
472
473 let test2 = make_test("test_sub", TestStatus::Passed);
474 assert!(!filter.matches(&test2, "suite"));
475 }
476
477 #[test]
478 fn filter_include_csv() {
479 let filter = TestFilter::new().include_csv("test_add, test_sub");
480 assert!(filter.matches(&make_test("test_add", TestStatus::Passed), "s"));
481 assert!(filter.matches(&make_test("test_sub", TestStatus::Passed), "s"));
482 assert!(!filter.matches(&make_test("test_mul", TestStatus::Passed), "s"));
483 }
484
485 #[test]
486 fn filter_exclude() {
487 let filter = TestFilter::new().exclude("*slow*");
488 assert!(filter.matches(&make_test("test_fast", TestStatus::Passed), "s"));
489 assert!(!filter.matches(&make_test("test_slow_add", TestStatus::Passed), "s"));
490 }
491
492 #[test]
493 fn filter_status() {
494 let filter = TestFilter::new().status(TestStatus::Failed);
495 assert!(!filter.matches(&make_test("test", TestStatus::Passed), "s"));
496 assert!(filter.matches(&make_test("test", TestStatus::Failed), "s"));
497 }
498
499 #[test]
500 fn filter_suite() {
501 let filter = TestFilter::new().suite("MathTest");
502 assert!(filter.matches(&make_test("test", TestStatus::Passed), "MathTest"));
503 assert!(!filter.matches(&make_test("test", TestStatus::Passed), "StringTest"));
504 }
505
506 #[test]
507 fn filter_combined() {
508 let filter = TestFilter::new()
509 .include("test_*")
510 .exclude("*slow*")
511 .status(TestStatus::Failed);
512
513 assert!(!filter.matches(&make_test("test_add", TestStatus::Passed), "s")); assert!(filter.matches(&make_test("test_add", TestStatus::Failed), "s")); assert!(!filter.matches(&make_test("test_slow", TestStatus::Failed), "s")); assert!(!filter.matches(&make_test("other", TestStatus::Failed), "s")); }
518
519 #[test]
520 fn filter_empty_matches_all() {
521 let filter = TestFilter::new();
522 assert!(!filter.is_active());
523 assert!(filter.matches(&make_test("anything", TestStatus::Passed), "any"));
524 }
525
526 #[test]
529 fn apply_filter_to_result() {
530 let result = make_result(vec![make_suite(
531 "tests",
532 vec![
533 make_test("test_add", TestStatus::Passed),
534 make_test("test_sub", TestStatus::Passed),
535 make_test("test_div", TestStatus::Failed),
536 ],
537 )]);
538
539 let filter = TestFilter::new().status(TestStatus::Failed);
540 let filtered = filter.apply(&result);
541
542 assert_eq!(filtered.total_tests(), 1);
543 assert_eq!(filtered.suites[0].tests[0].name, "test_div");
544 }
545
546 #[test]
547 fn apply_filter_removes_empty_suites() {
548 let result = make_result(vec![
549 make_suite("MathTest", vec![make_test("test_add", TestStatus::Passed)]),
550 make_suite(
551 "StringTest",
552 vec![make_test("test_upper", TestStatus::Passed)],
553 ),
554 ]);
555
556 let filter = TestFilter::new().suite("MathTest");
557 let filtered = filter.apply(&result);
558
559 assert_eq!(filtered.suites.len(), 1);
560 assert_eq!(filtered.suites[0].name, "MathTest");
561 }
562
563 #[test]
564 fn apply_no_filter_returns_clone() {
565 let result = make_result(vec![make_suite(
566 "tests",
567 vec![make_test("test_add", TestStatus::Passed)],
568 )]);
569
570 let filter = TestFilter::new();
571 let filtered = filter.apply(&result);
572
573 assert_eq!(filtered.total_tests(), result.total_tests());
574 }
575
576 #[test]
579 fn glob_segments_parsing() {
580 let segs = parse_glob_segments("test_*_basic_*");
581 assert_eq!(segs.len(), 4);
582 assert!(matches!(&segs[0], GlobSegment::Literal(s) if s == "test_"));
583 assert!(matches!(&segs[1], GlobSegment::Wildcard));
584 assert!(matches!(&segs[2], GlobSegment::Literal(s) if s == "_basic_"));
585 assert!(matches!(&segs[3], GlobSegment::Wildcard));
586 }
587
588 #[test]
589 fn glob_match_basic() {
590 let segs = parse_glob_segments("hello");
591 assert!(glob_match(&segs, "hello"));
592 assert!(!glob_match(&segs, "hell"));
593 }
594
595 #[test]
596 fn glob_match_wildcard() {
597 let segs = parse_glob_segments("*");
598 assert!(glob_match(&segs, "anything"));
599 assert!(glob_match(&segs, ""));
600 }
601
602 #[test]
603 fn glob_match_complex() {
604 let segs = parse_glob_segments("test_*_*_end");
605 assert!(glob_match(&segs, "test_a_b_end"));
606 assert!(glob_match(&segs, "test_foo_bar_end"));
607 assert!(!glob_match(&segs, "test_end"));
608 }
609
610 #[test]
613 fn build_filter_basic() {
614 let filter = build_filter(Some("test_*"), Some("*slow*"), false);
615 assert!(filter.is_active());
616 assert!(filter.matches(&make_test("test_fast", TestStatus::Passed), "s"));
617 assert!(!filter.matches(&make_test("test_slow", TestStatus::Passed), "s"));
618 }
619
620 #[test]
621 fn build_filter_failed_only() {
622 let filter = build_filter(None, None, true);
623 assert!(filter.is_active());
624 assert!(!filter.matches(&make_test("test", TestStatus::Passed), "s"));
625 assert!(filter.matches(&make_test("test", TestStatus::Failed), "s"));
626 }
627
628 #[test]
629 fn build_filter_none() {
630 let filter = build_filter(None, None, false);
631 assert!(!filter.is_active());
632 }
633
634 #[test]
635 fn filter_with_summary_test() {
636 let result = make_result(vec![make_suite(
637 "tests",
638 vec![
639 make_test("test_a", TestStatus::Passed),
640 make_test("test_b", TestStatus::Failed),
641 make_test("test_c", TestStatus::Passed),
642 ],
643 )]);
644
645 let filter = TestFilter::new().status(TestStatus::Failed);
646 let (filtered, summary) = filter_with_summary(&filter, &result);
647
648 assert_eq!(summary.total_before, 3);
649 assert_eq!(summary.total_after, 1);
650 assert_eq!(summary.filtered_out, 2);
651 assert_eq!(filtered.total_failed(), 1);
652 }
653
654 #[test]
655 fn failed_test_names_test() {
656 let result = make_result(vec![make_suite(
657 "tests",
658 vec![
659 make_test("test_a", TestStatus::Passed),
660 make_test("test_b", TestStatus::Failed),
661 make_test("test_c", TestStatus::Failed),
662 ],
663 )]);
664
665 let names = failed_test_names(&result);
666 assert_eq!(names, vec!["test_b", "test_c"]);
667 }
668
669 #[test]
670 fn matching_test_names_test() {
671 let result = make_result(vec![make_suite(
672 "tests",
673 vec![
674 make_test("test_add", TestStatus::Passed),
675 make_test("test_sub", TestStatus::Passed),
676 make_test("other", TestStatus::Passed),
677 ],
678 )]);
679
680 let filter = TestFilter::new().include("test_*");
681 let names = matching_test_names(&filter, &result);
682 assert_eq!(names, vec!["test_add", "test_sub"]);
683 }
684
685 #[test]
686 fn exclude_csv_multiple() {
687 let filter = TestFilter::new().exclude_csv("*slow*, *flaky*, *skip*");
688 assert!(filter.matches(&make_test("test_fast", TestStatus::Passed), "s"));
689 assert!(!filter.matches(&make_test("test_slow", TestStatus::Passed), "s"));
690 assert!(!filter.matches(&make_test("test_flaky", TestStatus::Passed), "s"));
691 assert!(!filter.matches(&make_test("test_skip_me", TestStatus::Passed), "s"));
692 }
693}