rustrails_support/
backtrace_cleaner.rs1use regex::Regex;
2
3#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
5pub enum BacktraceCleanerError {
6 #[error("invalid regex: {0}")]
8 InvalidRegex(String),
9}
10
11#[derive(Debug, Clone, Default)]
13pub struct BacktraceCleaner {
14 filters: Vec<Regex>,
15 silencers: Vec<Regex>,
16}
17
18impl BacktraceCleaner {
19 #[must_use]
21 pub fn new() -> Self {
22 Self::default()
23 }
24
25 pub fn add_filter(&mut self, pattern: &str) -> Result<(), BacktraceCleanerError> {
27 let regex = Regex::new(pattern)
28 .map_err(|error| BacktraceCleanerError::InvalidRegex(error.to_string()))?;
29 self.filters.push(regex);
30 Ok(())
31 }
32
33 pub fn add_silencer(&mut self, pattern: &str) -> Result<(), BacktraceCleanerError> {
35 let regex = Regex::new(pattern)
36 .map_err(|error| BacktraceCleanerError::InvalidRegex(error.to_string()))?;
37 self.silencers.push(regex);
38 Ok(())
39 }
40
41 #[must_use]
43 pub fn clean(&self, backtrace: &str) -> Vec<String> {
44 if backtrace.trim().is_empty() {
45 return Vec::new();
46 }
47
48 backtrace
49 .lines()
50 .filter(|line| !line.trim().is_empty())
51 .filter(|line| {
52 self.filters.is_empty() || self.filters.iter().any(|regex| regex.is_match(line))
53 })
54 .filter(|line| !self.silencers.iter().any(|regex| regex.is_match(line)))
55 .map(ToOwned::to_owned)
56 .collect()
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::{BacktraceCleaner, BacktraceCleanerError};
63
64 const BACKTRACE: &str =
65 "app/models/user.rs:12\nlib/vendor/gem.rb:4\napp/controllers/home.rs:7\n";
66
67 #[test]
68 fn clean_returns_empty_for_empty_backtrace() {
69 let cleaner = BacktraceCleaner::new();
70 assert!(cleaner.clean("").is_empty());
71 }
72
73 #[test]
74 fn clean_returns_all_lines_when_no_rules_are_configured() {
75 let cleaner = BacktraceCleaner::new();
76 assert_eq!(
77 cleaner.clean(BACKTRACE),
78 vec![
79 String::from("app/models/user.rs:12"),
80 String::from("lib/vendor/gem.rb:4"),
81 String::from("app/controllers/home.rs:7"),
82 ],
83 );
84 }
85
86 #[test]
87 fn add_filter_keeps_only_matching_lines() {
88 let mut cleaner = BacktraceCleaner::new();
89 cleaner.add_filter("^app/").expect("filter should compile");
90
91 assert_eq!(
92 cleaner.clean(BACKTRACE),
93 vec![
94 String::from("app/models/user.rs:12"),
95 String::from("app/controllers/home.rs:7"),
96 ],
97 );
98 }
99
100 #[test]
101 fn add_silencer_removes_matching_lines() {
102 let mut cleaner = BacktraceCleaner::new();
103 cleaner
104 .add_silencer("vendor")
105 .expect("silencer should compile");
106
107 assert_eq!(
108 cleaner.clean(BACKTRACE),
109 vec![
110 String::from("app/models/user.rs:12"),
111 String::from("app/controllers/home.rs:7"),
112 ],
113 );
114 }
115
116 #[test]
117 fn silencers_run_after_filters() {
118 let mut cleaner = BacktraceCleaner::new();
119 cleaner.add_filter("^app/").expect("filter should compile");
120 cleaner
121 .add_silencer("controllers")
122 .expect("silencer should compile");
123
124 assert_eq!(
125 cleaner.clean(BACKTRACE),
126 vec![String::from("app/models/user.rs:12")]
127 );
128 }
129
130 #[test]
131 fn invalid_filter_regex_returns_a_typed_error() {
132 let mut cleaner = BacktraceCleaner::new();
133 let error = cleaner
134 .add_filter("[")
135 .expect_err("invalid regex should fail");
136
137 assert!(matches!(error, BacktraceCleanerError::InvalidRegex(_)));
138 }
139
140 #[test]
141 fn invalid_silencer_regex_returns_a_typed_error() {
142 let mut cleaner = BacktraceCleaner::new();
143 let error = cleaner
144 .add_silencer("[")
145 .expect_err("invalid regex should fail");
146
147 assert!(matches!(error, BacktraceCleanerError::InvalidRegex(_)));
148 }
149
150 #[test]
151 fn clean_preserves_original_line_order() {
152 let mut cleaner = BacktraceCleaner::new();
153 cleaner.add_filter(".+").expect("filter should compile");
154
155 assert_eq!(cleaner.clean(BACKTRACE)[0], "app/models/user.rs:12");
156 assert_eq!(cleaner.clean(BACKTRACE)[1], "lib/vendor/gem.rb:4");
157 }
158
159 #[test]
160 fn blank_lines_are_discarded() {
161 let cleaner = BacktraceCleaner::new();
162 let cleaned = cleaner.clean("first\n\nsecond\n");
163
164 assert_eq!(cleaned, vec![String::from("first"), String::from("second")]);
165 }
166}