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)
251}
252
253fn glob_match_recursive(segments: &[GlobSegment], s: &str, seg_idx: usize, str_idx: usize) -> bool {
254 if seg_idx == segments.len() {
255 return str_idx == s.len();
256 }
257
258 match &segments[seg_idx] {
259 GlobSegment::Literal(lit) => {
260 if s[str_idx..].starts_with(lit) {
261 glob_match_recursive(segments, s, seg_idx + 1, str_idx + lit.len())
262 } else {
263 false
264 }
265 }
266 GlobSegment::Wildcard => {
267 for i in str_idx..=s.len() {
269 if glob_match_recursive(segments, s, seg_idx + 1, i) {
270 return true;
271 }
272 }
273 false
274 }
275 }
276}
277
278pub fn build_filter(include: Option<&str>, exclude: Option<&str>, failed_only: bool) -> TestFilter {
280 let mut filter = TestFilter::new();
281
282 if let Some(inc) = include {
283 filter = filter.include_csv(inc);
284 }
285 if let Some(exc) = exclude {
286 filter = filter.exclude_csv(exc);
287 }
288 if failed_only {
289 filter = filter.status(TestStatus::Failed);
290 }
291
292 filter
293}
294
295pub struct FilterSummary {
297 pub total_before: usize,
299 pub total_after: usize,
301 pub filtered_out: usize,
303 pub suites_removed: usize,
305}
306
307pub fn filter_with_summary(
309 filter: &TestFilter,
310 result: &TestRunResult,
311) -> (TestRunResult, FilterSummary) {
312 let total_before = result.total_tests();
313 let suites_before = result.suites.len();
314
315 let filtered = filter.apply(result);
316
317 let total_after = filtered.total_tests();
318 let suites_after = filtered.suites.len();
319
320 let summary = FilterSummary {
321 total_before,
322 total_after,
323 filtered_out: total_before.saturating_sub(total_after),
324 suites_removed: suites_before.saturating_sub(suites_after),
325 };
326
327 (filtered, summary)
328}
329
330pub fn failed_test_names(result: &TestRunResult) -> Vec<String> {
332 let mut names = Vec::new();
333 for suite in &result.suites {
334 for test in &suite.tests {
335 if test.status == TestStatus::Failed {
336 names.push(test.name.clone());
337 }
338 }
339 }
340 names
341}
342
343pub fn matching_test_names(filter: &TestFilter, result: &TestRunResult) -> Vec<String> {
345 let mut names = Vec::new();
346 for suite in &result.suites {
347 for test in &suite.tests {
348 if filter.matches(test, &suite.name) {
349 names.push(test.name.clone());
350 }
351 }
352 }
353 names
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use std::time::Duration;
360
361 fn make_test(name: &str, status: TestStatus) -> TestCase {
362 TestCase {
363 name: name.into(),
364 status,
365 duration: Duration::from_millis(1),
366 error: None,
367 }
368 }
369
370 fn make_suite(name: &str, tests: Vec<TestCase>) -> TestSuite {
371 TestSuite {
372 name: name.into(),
373 tests,
374 }
375 }
376
377 fn make_result(suites: Vec<TestSuite>) -> TestRunResult {
378 TestRunResult {
379 suites,
380 duration: Duration::from_millis(100),
381 raw_exit_code: 0,
382 }
383 }
384
385 #[test]
388 fn pattern_exact() {
389 let p = FilterPattern::parse("test_add");
390 assert!(p.matches("test_add"));
391 assert!(!p.matches("test_add_extra"));
392 assert!(!p.matches("test"));
393 }
394
395 #[test]
396 fn pattern_prefix() {
397 let p = FilterPattern::parse("test_*");
398 assert!(p.matches("test_add"));
399 assert!(p.matches("test_"));
400 assert!(!p.matches("other_add"));
401 }
402
403 #[test]
404 fn pattern_suffix() {
405 let p = FilterPattern::parse("*_add");
406 assert!(p.matches("test_add"));
407 assert!(p.matches("_add"));
408 assert!(!p.matches("test_sub"));
409 }
410
411 #[test]
412 fn pattern_contains() {
413 let p = FilterPattern::parse("*divide*");
414 assert!(p.matches("test_divide_by_zero"));
415 assert!(p.matches("divide"));
416 assert!(!p.matches("test_add"));
417 }
418
419 #[test]
420 fn pattern_glob() {
421 let p = FilterPattern::parse("test_*_basic_*");
422 assert!(p.matches("test_math_basic_add"));
423 assert!(p.matches("test_str_basic_concat"));
424 assert!(!p.matches("test_math_advanced_add"));
425 }
426
427 #[test]
428 fn pattern_exact_no_wildcard() {
429 let p = FilterPattern::parse("hello");
430 assert!(matches!(p, FilterPattern::Exact(_)));
431 }
432
433 #[test]
436 fn filter_include_single() {
437 let filter = TestFilter::new().include("test_add");
438 let test = make_test("test_add", TestStatus::Passed);
439 assert!(filter.matches(&test, "suite"));
440
441 let test2 = make_test("test_sub", TestStatus::Passed);
442 assert!(!filter.matches(&test2, "suite"));
443 }
444
445 #[test]
446 fn filter_include_csv() {
447 let filter = TestFilter::new().include_csv("test_add, test_sub");
448 assert!(filter.matches(&make_test("test_add", TestStatus::Passed), "s"));
449 assert!(filter.matches(&make_test("test_sub", TestStatus::Passed), "s"));
450 assert!(!filter.matches(&make_test("test_mul", TestStatus::Passed), "s"));
451 }
452
453 #[test]
454 fn filter_exclude() {
455 let filter = TestFilter::new().exclude("*slow*");
456 assert!(filter.matches(&make_test("test_fast", TestStatus::Passed), "s"));
457 assert!(!filter.matches(&make_test("test_slow_add", TestStatus::Passed), "s"));
458 }
459
460 #[test]
461 fn filter_status() {
462 let filter = TestFilter::new().status(TestStatus::Failed);
463 assert!(!filter.matches(&make_test("test", TestStatus::Passed), "s"));
464 assert!(filter.matches(&make_test("test", TestStatus::Failed), "s"));
465 }
466
467 #[test]
468 fn filter_suite() {
469 let filter = TestFilter::new().suite("MathTest");
470 assert!(filter.matches(&make_test("test", TestStatus::Passed), "MathTest"));
471 assert!(!filter.matches(&make_test("test", TestStatus::Passed), "StringTest"));
472 }
473
474 #[test]
475 fn filter_combined() {
476 let filter = TestFilter::new()
477 .include("test_*")
478 .exclude("*slow*")
479 .status(TestStatus::Failed);
480
481 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")); }
486
487 #[test]
488 fn filter_empty_matches_all() {
489 let filter = TestFilter::new();
490 assert!(!filter.is_active());
491 assert!(filter.matches(&make_test("anything", TestStatus::Passed), "any"));
492 }
493
494 #[test]
497 fn apply_filter_to_result() {
498 let result = make_result(vec![make_suite(
499 "tests",
500 vec![
501 make_test("test_add", TestStatus::Passed),
502 make_test("test_sub", TestStatus::Passed),
503 make_test("test_div", TestStatus::Failed),
504 ],
505 )]);
506
507 let filter = TestFilter::new().status(TestStatus::Failed);
508 let filtered = filter.apply(&result);
509
510 assert_eq!(filtered.total_tests(), 1);
511 assert_eq!(filtered.suites[0].tests[0].name, "test_div");
512 }
513
514 #[test]
515 fn apply_filter_removes_empty_suites() {
516 let result = make_result(vec![
517 make_suite("MathTest", vec![make_test("test_add", TestStatus::Passed)]),
518 make_suite(
519 "StringTest",
520 vec![make_test("test_upper", TestStatus::Passed)],
521 ),
522 ]);
523
524 let filter = TestFilter::new().suite("MathTest");
525 let filtered = filter.apply(&result);
526
527 assert_eq!(filtered.suites.len(), 1);
528 assert_eq!(filtered.suites[0].name, "MathTest");
529 }
530
531 #[test]
532 fn apply_no_filter_returns_clone() {
533 let result = make_result(vec![make_suite(
534 "tests",
535 vec![make_test("test_add", TestStatus::Passed)],
536 )]);
537
538 let filter = TestFilter::new();
539 let filtered = filter.apply(&result);
540
541 assert_eq!(filtered.total_tests(), result.total_tests());
542 }
543
544 #[test]
547 fn glob_segments_parsing() {
548 let segs = parse_glob_segments("test_*_basic_*");
549 assert_eq!(segs.len(), 4);
550 assert!(matches!(&segs[0], GlobSegment::Literal(s) if s == "test_"));
551 assert!(matches!(&segs[1], GlobSegment::Wildcard));
552 assert!(matches!(&segs[2], GlobSegment::Literal(s) if s == "_basic_"));
553 assert!(matches!(&segs[3], GlobSegment::Wildcard));
554 }
555
556 #[test]
557 fn glob_match_basic() {
558 let segs = parse_glob_segments("hello");
559 assert!(glob_match(&segs, "hello"));
560 assert!(!glob_match(&segs, "hell"));
561 }
562
563 #[test]
564 fn glob_match_wildcard() {
565 let segs = parse_glob_segments("*");
566 assert!(glob_match(&segs, "anything"));
567 assert!(glob_match(&segs, ""));
568 }
569
570 #[test]
571 fn glob_match_complex() {
572 let segs = parse_glob_segments("test_*_*_end");
573 assert!(glob_match(&segs, "test_a_b_end"));
574 assert!(glob_match(&segs, "test_foo_bar_end"));
575 assert!(!glob_match(&segs, "test_end"));
576 }
577
578 #[test]
581 fn build_filter_basic() {
582 let filter = build_filter(Some("test_*"), Some("*slow*"), false);
583 assert!(filter.is_active());
584 assert!(filter.matches(&make_test("test_fast", TestStatus::Passed), "s"));
585 assert!(!filter.matches(&make_test("test_slow", TestStatus::Passed), "s"));
586 }
587
588 #[test]
589 fn build_filter_failed_only() {
590 let filter = build_filter(None, None, true);
591 assert!(filter.is_active());
592 assert!(!filter.matches(&make_test("test", TestStatus::Passed), "s"));
593 assert!(filter.matches(&make_test("test", TestStatus::Failed), "s"));
594 }
595
596 #[test]
597 fn build_filter_none() {
598 let filter = build_filter(None, None, false);
599 assert!(!filter.is_active());
600 }
601
602 #[test]
603 fn filter_with_summary_test() {
604 let result = make_result(vec![make_suite(
605 "tests",
606 vec![
607 make_test("test_a", TestStatus::Passed),
608 make_test("test_b", TestStatus::Failed),
609 make_test("test_c", TestStatus::Passed),
610 ],
611 )]);
612
613 let filter = TestFilter::new().status(TestStatus::Failed);
614 let (filtered, summary) = filter_with_summary(&filter, &result);
615
616 assert_eq!(summary.total_before, 3);
617 assert_eq!(summary.total_after, 1);
618 assert_eq!(summary.filtered_out, 2);
619 assert_eq!(filtered.total_failed(), 1);
620 }
621
622 #[test]
623 fn failed_test_names_test() {
624 let result = make_result(vec![make_suite(
625 "tests",
626 vec![
627 make_test("test_a", TestStatus::Passed),
628 make_test("test_b", TestStatus::Failed),
629 make_test("test_c", TestStatus::Failed),
630 ],
631 )]);
632
633 let names = failed_test_names(&result);
634 assert_eq!(names, vec!["test_b", "test_c"]);
635 }
636
637 #[test]
638 fn matching_test_names_test() {
639 let result = make_result(vec![make_suite(
640 "tests",
641 vec![
642 make_test("test_add", TestStatus::Passed),
643 make_test("test_sub", TestStatus::Passed),
644 make_test("other", TestStatus::Passed),
645 ],
646 )]);
647
648 let filter = TestFilter::new().include("test_*");
649 let names = matching_test_names(&filter, &result);
650 assert_eq!(names, vec!["test_add", "test_sub"]);
651 }
652
653 #[test]
654 fn exclude_csv_multiple() {
655 let filter = TestFilter::new().exclude_csv("*slow*, *flaky*, *skip*");
656 assert!(filter.matches(&make_test("test_fast", TestStatus::Passed), "s"));
657 assert!(!filter.matches(&make_test("test_slow", TestStatus::Passed), "s"));
658 assert!(!filter.matches(&make_test("test_flaky", TestStatus::Passed), "s"));
659 assert!(!filter.matches(&make_test("test_skip_me", TestStatus::Passed), "s"));
660 }
661}