1 | use log::LevelFilter; |
2 | use std::error::Error; |
3 | use std::fmt::{Display, Formatter}; |
4 | |
5 | use crate::Directive; |
6 | use crate::FilterOp; |
7 | |
8 | #[derive (Default, Debug)] |
9 | pub(crate) struct ParseResult { |
10 | pub(crate) directives: Vec<Directive>, |
11 | pub(crate) filter: Option<FilterOp>, |
12 | pub(crate) errors: Vec<String>, |
13 | } |
14 | |
15 | impl ParseResult { |
16 | fn add_directive(&mut self, directive: Directive) { |
17 | self.directives.push(directive); |
18 | } |
19 | |
20 | fn set_filter(&mut self, filter: FilterOp) { |
21 | self.filter = Some(filter); |
22 | } |
23 | |
24 | fn add_error(&mut self, message: String) { |
25 | self.errors.push(message); |
26 | } |
27 | |
28 | pub(crate) fn ok(self) -> Result<(Vec<Directive>, Option<FilterOp>), ParseError> { |
29 | let Self { |
30 | directives, |
31 | filter, |
32 | errors, |
33 | } = self; |
34 | if let Some(error) = errors.into_iter().next() { |
35 | Err(ParseError { details: error }) |
36 | } else { |
37 | Ok((directives, filter)) |
38 | } |
39 | } |
40 | } |
41 | |
42 | /// Error during logger directive parsing process. |
43 | #[derive (Debug, Clone, PartialEq, Eq, Hash)] |
44 | pub struct ParseError { |
45 | details: String, |
46 | } |
47 | |
48 | impl Display for ParseError { |
49 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
50 | write!(f, "error parsing logger filter: {}" , self.details) |
51 | } |
52 | } |
53 | |
54 | impl Error for ParseError {} |
55 | |
56 | /// Parse a logging specification string (e.g: `crate1,crate2::mod3,crate3::x=error/foo`) |
57 | /// and return a vector with log directives. |
58 | pub(crate) fn parse_spec(spec: &str) -> ParseResult { |
59 | let mut result = ParseResult::default(); |
60 | |
61 | let mut parts = spec.split('/' ); |
62 | let mods = parts.next(); |
63 | let filter = parts.next(); |
64 | if parts.next().is_some() { |
65 | result.add_error(format!("invalid logging spec ' {spec}' (too many '/'s)" )); |
66 | return result; |
67 | } |
68 | if let Some(m) = mods { |
69 | for s in m.split(',' ).map(|ss| ss.trim()) { |
70 | if s.is_empty() { |
71 | continue; |
72 | } |
73 | let mut parts = s.split('=' ); |
74 | let (log_level, name) = |
75 | match (parts.next(), parts.next().map(|s| s.trim()), parts.next()) { |
76 | (Some(part0), None, None) => { |
77 | // if the single argument is a log-level string or number, |
78 | // treat that as a global fallback |
79 | match part0.parse() { |
80 | Ok(num) => (num, None), |
81 | Err(_) => (LevelFilter::max(), Some(part0)), |
82 | } |
83 | } |
84 | (Some(part0), Some("" ), None) => (LevelFilter::max(), Some(part0)), |
85 | (Some(part0), Some(part1), None) => { |
86 | if let Ok(num) = part1.parse() { |
87 | (num, Some(part0)) |
88 | } else { |
89 | result.add_error(format!("invalid logging spec ' {part1}'" )); |
90 | continue; |
91 | } |
92 | } |
93 | _ => { |
94 | result.add_error(format!("invalid logging spec ' {s}'" )); |
95 | continue; |
96 | } |
97 | }; |
98 | |
99 | result.add_directive(Directive { |
100 | name: name.map(|s| s.to_owned()), |
101 | level: log_level, |
102 | }); |
103 | } |
104 | } |
105 | |
106 | if let Some(filter) = filter { |
107 | match FilterOp::new(filter) { |
108 | Ok(filter_op) => result.set_filter(filter_op), |
109 | Err(err) => result.add_error(format!("invalid regex filter - {err}" )), |
110 | } |
111 | } |
112 | |
113 | result |
114 | } |
115 | |
116 | #[cfg (test)] |
117 | mod tests { |
118 | use crate::ParseError; |
119 | use log::LevelFilter; |
120 | use snapbox::{assert_data_eq, str, Data, IntoData}; |
121 | |
122 | use super::{parse_spec, ParseResult}; |
123 | |
124 | impl IntoData for ParseError { |
125 | fn into_data(self) -> Data { |
126 | self.to_string().into_data() |
127 | } |
128 | } |
129 | |
130 | #[test ] |
131 | fn parse_spec_valid() { |
132 | let ParseResult { |
133 | directives: dirs, |
134 | filter, |
135 | errors, |
136 | } = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug" ); |
137 | |
138 | assert_eq!(dirs.len(), 3); |
139 | assert_eq!(dirs[0].name, Some("crate1::mod1" .to_owned())); |
140 | assert_eq!(dirs[0].level, LevelFilter::Error); |
141 | |
142 | assert_eq!(dirs[1].name, Some("crate1::mod2" .to_owned())); |
143 | assert_eq!(dirs[1].level, LevelFilter::max()); |
144 | |
145 | assert_eq!(dirs[2].name, Some("crate2" .to_owned())); |
146 | assert_eq!(dirs[2].level, LevelFilter::Debug); |
147 | assert!(filter.is_none()); |
148 | |
149 | assert!(errors.is_empty()); |
150 | } |
151 | |
152 | #[test ] |
153 | fn parse_spec_invalid_crate() { |
154 | // test parse_spec with multiple = in specification |
155 | let ParseResult { |
156 | directives: dirs, |
157 | filter, |
158 | errors, |
159 | } = parse_spec("crate1::mod1=warn=info,crate2=debug" ); |
160 | |
161 | assert_eq!(dirs.len(), 1); |
162 | assert_eq!(dirs[0].name, Some("crate2" .to_owned())); |
163 | assert_eq!(dirs[0].level, LevelFilter::Debug); |
164 | assert!(filter.is_none()); |
165 | |
166 | assert_eq!(errors.len(), 1); |
167 | assert_data_eq!( |
168 | &errors[0], |
169 | str!["invalid logging spec 'crate1::mod1=warn=info'" ] |
170 | ); |
171 | } |
172 | |
173 | #[test ] |
174 | fn parse_spec_invalid_level() { |
175 | // test parse_spec with 'noNumber' as log level |
176 | let ParseResult { |
177 | directives: dirs, |
178 | filter, |
179 | errors, |
180 | } = parse_spec("crate1::mod1=noNumber,crate2=debug" ); |
181 | |
182 | assert_eq!(dirs.len(), 1); |
183 | assert_eq!(dirs[0].name, Some("crate2" .to_owned())); |
184 | assert_eq!(dirs[0].level, LevelFilter::Debug); |
185 | assert!(filter.is_none()); |
186 | |
187 | assert_eq!(errors.len(), 1); |
188 | assert_data_eq!(&errors[0], str!["invalid logging spec 'noNumber'" ]); |
189 | } |
190 | |
191 | #[test ] |
192 | fn parse_spec_string_level() { |
193 | // test parse_spec with 'warn' as log level |
194 | let ParseResult { |
195 | directives: dirs, |
196 | filter, |
197 | errors, |
198 | } = parse_spec("crate1::mod1=wrong,crate2=warn" ); |
199 | |
200 | assert_eq!(dirs.len(), 1); |
201 | assert_eq!(dirs[0].name, Some("crate2" .to_owned())); |
202 | assert_eq!(dirs[0].level, LevelFilter::Warn); |
203 | assert!(filter.is_none()); |
204 | |
205 | assert_eq!(errors.len(), 1); |
206 | assert_data_eq!(&errors[0], str!["invalid logging spec 'wrong'" ]); |
207 | } |
208 | |
209 | #[test ] |
210 | fn parse_spec_empty_level() { |
211 | // test parse_spec with '' as log level |
212 | let ParseResult { |
213 | directives: dirs, |
214 | filter, |
215 | errors, |
216 | } = parse_spec("crate1::mod1=wrong,crate2=" ); |
217 | |
218 | assert_eq!(dirs.len(), 1); |
219 | assert_eq!(dirs[0].name, Some("crate2" .to_owned())); |
220 | assert_eq!(dirs[0].level, LevelFilter::max()); |
221 | assert!(filter.is_none()); |
222 | |
223 | assert_eq!(errors.len(), 1); |
224 | assert_data_eq!(&errors[0], str!["invalid logging spec 'wrong'" ]); |
225 | } |
226 | |
227 | #[test ] |
228 | fn parse_spec_empty_level_isolated() { |
229 | // test parse_spec with "" as log level (and the entire spec str) |
230 | let ParseResult { |
231 | directives: dirs, |
232 | filter, |
233 | errors, |
234 | } = parse_spec("" ); // should be ignored |
235 | assert_eq!(dirs.len(), 0); |
236 | assert!(filter.is_none()); |
237 | assert!(errors.is_empty()); |
238 | } |
239 | |
240 | #[test ] |
241 | fn parse_spec_blank_level_isolated() { |
242 | // test parse_spec with a white-space-only string specified as the log |
243 | // level (and the entire spec str) |
244 | let ParseResult { |
245 | directives: dirs, |
246 | filter, |
247 | errors, |
248 | } = parse_spec(" " ); // should be ignored |
249 | assert_eq!(dirs.len(), 0); |
250 | assert!(filter.is_none()); |
251 | assert!(errors.is_empty()); |
252 | } |
253 | |
254 | #[test ] |
255 | fn parse_spec_blank_level_isolated_comma_only() { |
256 | // The spec should contain zero or more comma-separated string slices, |
257 | // so a comma-only string should be interpreted as two empty strings |
258 | // (which should both be treated as invalid, so ignored). |
259 | let ParseResult { |
260 | directives: dirs, |
261 | filter, |
262 | errors, |
263 | } = parse_spec("," ); // should be ignored |
264 | assert_eq!(dirs.len(), 0); |
265 | assert!(filter.is_none()); |
266 | assert!(errors.is_empty()); |
267 | } |
268 | |
269 | #[test ] |
270 | fn parse_spec_blank_level_isolated_comma_blank() { |
271 | // The spec should contain zero or more comma-separated string slices, |
272 | // so this bogus spec should be interpreted as containing one empty |
273 | // string and one blank string. Both should both be treated as |
274 | // invalid, so ignored. |
275 | let ParseResult { |
276 | directives: dirs, |
277 | filter, |
278 | errors, |
279 | } = parse_spec(", " ); // should be ignored |
280 | assert_eq!(dirs.len(), 0); |
281 | assert!(filter.is_none()); |
282 | assert!(errors.is_empty()); |
283 | } |
284 | |
285 | #[test ] |
286 | fn parse_spec_blank_level_isolated_blank_comma() { |
287 | // The spec should contain zero or more comma-separated string slices, |
288 | // so this bogus spec should be interpreted as containing one blank |
289 | // string and one empty string. Both should both be treated as |
290 | // invalid, so ignored. |
291 | let ParseResult { |
292 | directives: dirs, |
293 | filter, |
294 | errors, |
295 | } = parse_spec(" ," ); // should be ignored |
296 | assert_eq!(dirs.len(), 0); |
297 | assert!(filter.is_none()); |
298 | assert!(errors.is_empty()); |
299 | } |
300 | |
301 | #[test ] |
302 | fn parse_spec_global() { |
303 | // test parse_spec with no crate |
304 | let ParseResult { |
305 | directives: dirs, |
306 | filter, |
307 | errors, |
308 | } = parse_spec("warn,crate2=debug" ); |
309 | assert_eq!(dirs.len(), 2); |
310 | assert_eq!(dirs[0].name, None); |
311 | assert_eq!(dirs[0].level, LevelFilter::Warn); |
312 | assert_eq!(dirs[1].name, Some("crate2" .to_owned())); |
313 | assert_eq!(dirs[1].level, LevelFilter::Debug); |
314 | assert!(filter.is_none()); |
315 | assert!(errors.is_empty()); |
316 | } |
317 | |
318 | #[test ] |
319 | fn parse_spec_global_bare_warn_lc() { |
320 | // test parse_spec with no crate, in isolation, all lowercase |
321 | let ParseResult { |
322 | directives: dirs, |
323 | filter, |
324 | errors, |
325 | } = parse_spec("warn" ); |
326 | assert_eq!(dirs.len(), 1); |
327 | assert_eq!(dirs[0].name, None); |
328 | assert_eq!(dirs[0].level, LevelFilter::Warn); |
329 | assert!(filter.is_none()); |
330 | assert!(errors.is_empty()); |
331 | } |
332 | |
333 | #[test ] |
334 | fn parse_spec_global_bare_warn_uc() { |
335 | // test parse_spec with no crate, in isolation, all uppercase |
336 | let ParseResult { |
337 | directives: dirs, |
338 | filter, |
339 | errors, |
340 | } = parse_spec("WARN" ); |
341 | assert_eq!(dirs.len(), 1); |
342 | assert_eq!(dirs[0].name, None); |
343 | assert_eq!(dirs[0].level, LevelFilter::Warn); |
344 | assert!(filter.is_none()); |
345 | assert!(errors.is_empty()); |
346 | } |
347 | |
348 | #[test ] |
349 | fn parse_spec_global_bare_warn_mixed() { |
350 | // test parse_spec with no crate, in isolation, mixed case |
351 | let ParseResult { |
352 | directives: dirs, |
353 | filter, |
354 | errors, |
355 | } = parse_spec("wArN" ); |
356 | assert_eq!(dirs.len(), 1); |
357 | assert_eq!(dirs[0].name, None); |
358 | assert_eq!(dirs[0].level, LevelFilter::Warn); |
359 | assert!(filter.is_none()); |
360 | assert!(errors.is_empty()); |
361 | } |
362 | |
363 | #[test ] |
364 | fn parse_spec_valid_filter() { |
365 | let ParseResult { |
366 | directives: dirs, |
367 | filter, |
368 | errors, |
369 | } = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug/abc" ); |
370 | assert_eq!(dirs.len(), 3); |
371 | assert_eq!(dirs[0].name, Some("crate1::mod1" .to_owned())); |
372 | assert_eq!(dirs[0].level, LevelFilter::Error); |
373 | |
374 | assert_eq!(dirs[1].name, Some("crate1::mod2" .to_owned())); |
375 | assert_eq!(dirs[1].level, LevelFilter::max()); |
376 | |
377 | assert_eq!(dirs[2].name, Some("crate2" .to_owned())); |
378 | assert_eq!(dirs[2].level, LevelFilter::Debug); |
379 | assert!(filter.is_some() && filter.unwrap().to_string() == "abc" ); |
380 | assert!(errors.is_empty()); |
381 | } |
382 | |
383 | #[test ] |
384 | fn parse_spec_invalid_crate_filter() { |
385 | let ParseResult { |
386 | directives: dirs, |
387 | filter, |
388 | errors, |
389 | } = parse_spec("crate1::mod1=error=warn,crate2=debug/a.c" ); |
390 | |
391 | assert_eq!(dirs.len(), 1); |
392 | assert_eq!(dirs[0].name, Some("crate2" .to_owned())); |
393 | assert_eq!(dirs[0].level, LevelFilter::Debug); |
394 | assert!(filter.is_some() && filter.unwrap().to_string() == "a.c" ); |
395 | |
396 | assert_eq!(errors.len(), 1); |
397 | assert_data_eq!( |
398 | &errors[0], |
399 | str!["invalid logging spec 'crate1::mod1=error=warn'" ] |
400 | ); |
401 | } |
402 | |
403 | #[test ] |
404 | fn parse_spec_empty_with_filter() { |
405 | let ParseResult { |
406 | directives: dirs, |
407 | filter, |
408 | errors, |
409 | } = parse_spec("crate1/a*c" ); |
410 | assert_eq!(dirs.len(), 1); |
411 | assert_eq!(dirs[0].name, Some("crate1" .to_owned())); |
412 | assert_eq!(dirs[0].level, LevelFilter::max()); |
413 | assert!(filter.is_some() && filter.unwrap().to_string() == "a*c" ); |
414 | assert!(errors.is_empty()); |
415 | } |
416 | |
417 | #[test ] |
418 | fn parse_spec_with_multiple_filters() { |
419 | let ParseResult { |
420 | directives: dirs, |
421 | filter, |
422 | errors, |
423 | } = parse_spec("debug/abc/a.c" ); |
424 | assert!(dirs.is_empty()); |
425 | assert!(filter.is_none()); |
426 | |
427 | assert_eq!(errors.len(), 1); |
428 | assert_data_eq!( |
429 | &errors[0], |
430 | str!["invalid logging spec 'debug/abc/a.c' (too many '/'s)" ] |
431 | ); |
432 | } |
433 | |
434 | #[test ] |
435 | fn parse_spec_multiple_invalid_crates() { |
436 | // test parse_spec with multiple = in specification |
437 | let ParseResult { |
438 | directives: dirs, |
439 | filter, |
440 | errors, |
441 | } = parse_spec("crate1::mod1=warn=info,crate2=debug,crate3=error=error" ); |
442 | |
443 | assert_eq!(dirs.len(), 1); |
444 | assert_eq!(dirs[0].name, Some("crate2" .to_owned())); |
445 | assert_eq!(dirs[0].level, LevelFilter::Debug); |
446 | assert!(filter.is_none()); |
447 | |
448 | assert_eq!(errors.len(), 2); |
449 | assert_data_eq!( |
450 | &errors[0], |
451 | str!["invalid logging spec 'crate1::mod1=warn=info'" ] |
452 | ); |
453 | assert_data_eq!( |
454 | &errors[1], |
455 | str!["invalid logging spec 'crate3=error=error'" ] |
456 | ); |
457 | } |
458 | |
459 | #[test ] |
460 | fn parse_spec_multiple_invalid_levels() { |
461 | // test parse_spec with 'noNumber' as log level |
462 | let ParseResult { |
463 | directives: dirs, |
464 | filter, |
465 | errors, |
466 | } = parse_spec("crate1::mod1=noNumber,crate2=debug,crate3=invalid" ); |
467 | |
468 | assert_eq!(dirs.len(), 1); |
469 | assert_eq!(dirs[0].name, Some("crate2" .to_owned())); |
470 | assert_eq!(dirs[0].level, LevelFilter::Debug); |
471 | assert!(filter.is_none()); |
472 | |
473 | assert_eq!(errors.len(), 2); |
474 | assert_data_eq!(&errors[0], str!["invalid logging spec 'noNumber'" ]); |
475 | assert_data_eq!(&errors[1], str!["invalid logging spec 'invalid'" ]); |
476 | } |
477 | |
478 | #[test ] |
479 | fn parse_spec_invalid_crate_and_level() { |
480 | // test parse_spec with 'noNumber' as log level |
481 | let ParseResult { |
482 | directives: dirs, |
483 | filter, |
484 | errors, |
485 | } = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid" ); |
486 | |
487 | assert_eq!(dirs.len(), 1); |
488 | assert_eq!(dirs[0].name, Some("crate2" .to_owned())); |
489 | assert_eq!(dirs[0].level, LevelFilter::Debug); |
490 | assert!(filter.is_none()); |
491 | |
492 | assert_eq!(errors.len(), 2); |
493 | assert_data_eq!( |
494 | &errors[0], |
495 | str!["invalid logging spec 'crate1::mod1=debug=info'" ] |
496 | ); |
497 | assert_data_eq!(&errors[1], str!["invalid logging spec 'invalid'" ]); |
498 | } |
499 | |
500 | #[test ] |
501 | fn parse_error_message_single_error() { |
502 | let error = parse_spec("crate1::mod1=debug=info,crate2=debug" ) |
503 | .ok() |
504 | .unwrap_err(); |
505 | assert_data_eq!( |
506 | error, |
507 | str!["error parsing logger filter: invalid logging spec 'crate1::mod1=debug=info'" ] |
508 | ); |
509 | } |
510 | |
511 | #[test ] |
512 | fn parse_error_message_multiple_errors() { |
513 | let error = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid" ) |
514 | .ok() |
515 | .unwrap_err(); |
516 | assert_data_eq!( |
517 | error, |
518 | str!["error parsing logger filter: invalid logging spec 'crate1::mod1=debug=info'" ] |
519 | ); |
520 | } |
521 | } |
522 | |