git_perf/
filter.rs

1use anyhow::{Context, Result};
2use regex::Regex;
3
4/// Compile filter patterns into regex objects
5pub fn compile_filters(patterns: &[String]) -> Result<Vec<Regex>> {
6    patterns
7        .iter()
8        .map(|pattern| {
9            Regex::new(pattern).with_context(|| format!("Invalid regex pattern: '{}'", pattern))
10        })
11        .collect()
12}
13
14/// Convert measurement names to anchored regex patterns for exact matching
15/// This escapes special regex characters and adds ^ and $ anchors
16#[must_use]
17pub fn measurements_to_anchored_regex(measurements: &[String]) -> Vec<String> {
18    measurements
19        .iter()
20        .map(|m| format!("^{}$", regex::escape(m)))
21        .collect()
22}
23
24/// Combine measurements (as exact matches) and filter patterns into a single list
25/// Measurements are converted to anchored regex patterns for exact matching
26#[must_use]
27pub fn combine_measurements_and_filters(
28    measurements: &[String],
29    filter_patterns: &[String],
30) -> Vec<String> {
31    let mut combined = measurements_to_anchored_regex(measurements);
32    combined.extend_from_slice(filter_patterns);
33    combined
34}
35
36/// Check if a measurement name matches any of the compiled filters
37/// Returns true if filters is empty (no filters = match all)
38#[must_use]
39pub fn matches_any_filter(name: &str, filters: &[Regex]) -> bool {
40    if filters.is_empty() {
41        return true; // No filters = match all
42    }
43    filters.iter().any(|re| re.is_match(name))
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    #[test]
51    fn test_compile_valid_filters() {
52        let patterns = vec!["bench.*".to_string(), "test_.*".to_string()];
53        let result = compile_filters(&patterns);
54        assert!(result.is_ok());
55        assert_eq!(result.unwrap().len(), 2);
56    }
57
58    #[test]
59    fn test_compile_invalid_regex() {
60        let patterns = vec!["[invalid".to_string()];
61        let result = compile_filters(&patterns);
62        assert!(result.is_err());
63    }
64
65    #[test]
66    fn test_matches_any_filter_empty() {
67        let filters = vec![];
68        assert!(matches_any_filter("anything", &filters));
69    }
70
71    #[test]
72    fn test_matches_any_filter_single_match() {
73        let patterns = vec!["bench.*".to_string()];
74        let filters = compile_filters(&patterns).unwrap();
75        assert!(matches_any_filter("benchmark_x64", &filters));
76        assert!(!matches_any_filter("test_foo", &filters));
77    }
78
79    #[test]
80    fn test_matches_any_filter_or_logic() {
81        let patterns = vec!["bench.*".to_string(), "test_.*".to_string()];
82        let filters = compile_filters(&patterns).unwrap();
83        assert!(matches_any_filter("benchmark_x64", &filters));
84        assert!(matches_any_filter("test_foo", &filters));
85        assert!(!matches_any_filter("other_thing", &filters));
86    }
87
88    #[test]
89    fn test_anchored_patterns() {
90        let patterns = vec!["^bench.*$".to_string()];
91        let filters = compile_filters(&patterns).unwrap();
92        assert!(matches_any_filter("benchmark_x64", &filters));
93        assert!(!matches_any_filter("my_benchmark_x64", &filters));
94    }
95
96    #[test]
97    fn test_complex_regex() {
98        let patterns = vec![r"bench_.*_v\d+".to_string()];
99        let filters = compile_filters(&patterns).unwrap();
100        assert!(matches_any_filter("bench_foo_v1", &filters));
101        assert!(matches_any_filter("bench_bar_v23", &filters));
102        assert!(!matches_any_filter("bench_baz_vX", &filters));
103    }
104
105    #[test]
106    fn test_measurements_to_anchored_regex() {
107        let measurements = vec![
108            "benchmark_x64".to_string(),
109            "test.with.dots".to_string(),
110            "name[with]brackets".to_string(),
111        ];
112        let anchored = measurements_to_anchored_regex(&measurements);
113
114        assert_eq!(anchored.len(), 3);
115        assert_eq!(anchored[0], "^benchmark_x64$");
116        assert_eq!(anchored[1], r"^test\.with\.dots$");
117        assert_eq!(anchored[2], r"^name\[with\]brackets$");
118    }
119
120    #[test]
121    fn test_measurements_to_anchored_regex_matches_exactly() {
122        let measurements = vec!["benchmark".to_string()];
123        let anchored = measurements_to_anchored_regex(&measurements);
124        let filters = compile_filters(&anchored).unwrap();
125
126        // Should match exact name
127        assert!(matches_any_filter("benchmark", &filters));
128
129        // Should NOT match partial matches
130        assert!(!matches_any_filter("benchmark_x64", &filters));
131        assert!(!matches_any_filter("my_benchmark", &filters));
132    }
133
134    #[test]
135    fn test_combine_measurements_and_filters() {
136        let measurements = vec!["exact_match".to_string()];
137        let filters = vec!["pattern.*".to_string()];
138
139        let combined = combine_measurements_and_filters(&measurements, &filters);
140
141        assert_eq!(combined.len(), 2);
142        assert_eq!(combined[0], "^exact_match$");
143        assert_eq!(combined[1], "pattern.*");
144    }
145
146    #[test]
147    fn test_combine_measurements_and_filters_matching() {
148        let measurements = vec!["benchmark_x64".to_string()];
149        let filters = vec!["test_.*".to_string()];
150
151        let combined = combine_measurements_and_filters(&measurements, &filters);
152        let compiled = compile_filters(&combined).unwrap();
153
154        // Should match exact measurement name
155        assert!(matches_any_filter("benchmark_x64", &compiled));
156
157        // Should match filter pattern
158        assert!(matches_any_filter("test_foo", &compiled));
159        assert!(matches_any_filter("test_bar", &compiled));
160
161        // Should NOT match other names
162        assert!(!matches_any_filter("benchmark_arm64", &compiled));
163        assert!(!matches_any_filter("other", &compiled));
164    }
165}