mago_reporting/
lib.rs

1//! Issue reporting and formatting for Mago.
2//!
3//! This crate provides functionality for reporting code issues identified by the linter and analyzer.
4//! It includes support for multiple output formats, baseline filtering, and rich terminal output.
5//!
6//! # Core Types
7//!
8//! - [`Issue`]: Represents a single code issue with severity level, annotations, and optional fixes
9//! - [`IssueCollection`]: A collection of issues with filtering and sorting capabilities
10//! - [`reporter::Reporter`]: Handles formatting and outputting issues in various formats
11//! - [`baseline::Baseline`]: Manages baseline files to filter out known issues
12
13use std::cmp::Ordering;
14use std::collections::hash_map::Entry;
15use std::iter::Once;
16use std::str::FromStr;
17
18use ahash::HashMap;
19use schemars::JsonSchema;
20use serde::Deserialize;
21use serde::Serialize;
22use strum::Display;
23use strum::VariantNames;
24
25use mago_database::file::FileId;
26use mago_fixer::FixPlan;
27use mago_span::Span;
28
29mod formatter;
30mod internal;
31
32pub mod baseline;
33pub mod color;
34pub mod error;
35pub mod output;
36pub mod reporter;
37
38pub use color::ColorChoice;
39pub use formatter::ReportingFormat;
40pub use output::ReportingTarget;
41
42/// Represents the kind of annotation associated with an issue.
43#[derive(Debug, PartialEq, Eq, Ord, Copy, Clone, Hash, PartialOrd, Deserialize, Serialize)]
44pub enum AnnotationKind {
45    /// A primary annotation, typically highlighting the main source of the issue.
46    Primary,
47    /// A secondary annotation, providing additional context or related information.
48    Secondary,
49}
50
51/// An annotation associated with an issue, providing additional context or highlighting specific code spans.
52#[derive(Debug, PartialEq, Eq, Ord, Clone, Hash, PartialOrd, Deserialize, Serialize)]
53pub struct Annotation {
54    /// An optional message associated with the annotation.
55    pub message: Option<String>,
56    /// The kind of annotation.
57    pub kind: AnnotationKind,
58    /// The code span that the annotation refers to.
59    pub span: Span,
60}
61
62/// Represents the severity level of an issue.
63#[derive(
64    Debug, PartialEq, Eq, Ord, Copy, Clone, Hash, PartialOrd, Deserialize, Serialize, Display, VariantNames, JsonSchema,
65)]
66#[strum(serialize_all = "lowercase")]
67pub enum Level {
68    /// A note, providing additional information or context.
69    #[serde(alias = "note")]
70    Note,
71    /// A help message, suggesting possible solutions or further actions.
72    #[serde(alias = "help")]
73    Help,
74    /// A warning, indicating a potential problem that may need attention.
75    #[serde(alias = "warning", alias = "warn")]
76    Warning,
77    /// An error, indicating a problem that prevents the code from functioning correctly.
78    #[serde(alias = "error", alias = "err")]
79    Error,
80}
81
82impl FromStr for Level {
83    type Err = ();
84
85    fn from_str(s: &str) -> Result<Self, Self::Err> {
86        match s.to_lowercase().as_str() {
87            "note" => Ok(Self::Note),
88            "help" => Ok(Self::Help),
89            "warning" => Ok(Self::Warning),
90            "error" => Ok(Self::Error),
91            _ => Err(()),
92        }
93    }
94}
95
96/// Represents an issue identified in the code.
97#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
98pub struct Issue {
99    /// The severity level of the issue.
100    pub level: Level,
101    /// An optional code associated with the issue.
102    pub code: Option<String>,
103    /// The main message describing the issue.
104    pub message: String,
105    /// Additional notes related to the issue.
106    pub notes: Vec<String>,
107    /// An optional help message suggesting possible solutions or further actions.
108    pub help: Option<String>,
109    /// An optional link to external resources for more information about the issue.
110    pub link: Option<String>,
111    /// Annotations associated with the issue, providing additional context or highlighting specific code spans.
112    pub annotations: Vec<Annotation>,
113    /// Modification suggestions that can be applied to fix the issue.
114    pub suggestions: Vec<(FileId, FixPlan)>,
115}
116
117/// A collection of issues.
118#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
119pub struct IssueCollection {
120    issues: Vec<Issue>,
121}
122
123impl AnnotationKind {
124    /// Returns `true` if this annotation kind is primary.
125    #[inline]
126    pub const fn is_primary(&self) -> bool {
127        matches!(self, AnnotationKind::Primary)
128    }
129
130    /// Returns `true` if this annotation kind is secondary.
131    #[inline]
132    pub const fn is_secondary(&self) -> bool {
133        matches!(self, AnnotationKind::Secondary)
134    }
135}
136
137impl Annotation {
138    /// Creates a new annotation with the given kind and span.
139    ///
140    /// # Examples
141    ///
142    /// ```
143    /// use mago_reporting::{Annotation, AnnotationKind};
144    /// use mago_database::file::FileId;
145    /// use mago_span::Span;
146    /// use mago_span::Position;
147    ///
148    /// let file = FileId::zero();
149    /// let start = Position::new(0);
150    /// let end = Position::new(5);
151    /// let span = Span::new(file, start, end);
152    /// let annotation = Annotation::new(AnnotationKind::Primary, span);
153    /// ```
154    pub fn new(kind: AnnotationKind, span: Span) -> Self {
155        Self { message: None, kind, span }
156    }
157
158    /// Creates a new primary annotation with the given span.
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use mago_reporting::{Annotation, AnnotationKind};
164    /// use mago_database::file::FileId;
165    /// use mago_span::Span;
166    /// use mago_span::Position;
167    ///
168    /// let file = FileId::zero();
169    /// let start = Position::new(0);
170    /// let end = Position::new(5);
171    /// let span = Span::new(file, start, end);
172    /// let annotation = Annotation::primary(span);
173    /// ```
174    pub fn primary(span: Span) -> Self {
175        Self::new(AnnotationKind::Primary, span)
176    }
177
178    /// Creates a new secondary annotation with the given span.
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// use mago_reporting::{Annotation, AnnotationKind};
184    /// use mago_database::file::FileId;
185    /// use mago_span::Span;
186    /// use mago_span::Position;
187    ///
188    /// let file = FileId::zero();
189    /// let start = Position::new(0);
190    /// let end = Position::new(5);
191    /// let span = Span::new(file, start, end);
192    /// let annotation = Annotation::secondary(span);
193    /// ```
194    pub fn secondary(span: Span) -> Self {
195        Self::new(AnnotationKind::Secondary, span)
196    }
197
198    /// Sets the message of this annotation.
199    ///
200    /// # Examples
201    ///
202    /// ```
203    /// use mago_reporting::{Annotation, AnnotationKind};
204    /// use mago_database::file::FileId;
205    /// use mago_span::Span;
206    /// use mago_span::Position;
207    ///
208    /// let file = FileId::zero();
209    /// let start = Position::new(0);
210    /// let end = Position::new(5);
211    /// let span = Span::new(file, start, end);
212    /// let annotation = Annotation::primary(span).with_message("This is a primary annotation");
213    /// ```
214    #[must_use]
215    pub fn with_message(mut self, message: impl Into<String>) -> Self {
216        self.message = Some(message.into());
217
218        self
219    }
220
221    /// Returns `true` if this annotation is a primary annotation.
222    pub fn is_primary(&self) -> bool {
223        self.kind == AnnotationKind::Primary
224    }
225}
226
227impl Level {
228    /// Downgrades the level to the next lower severity.
229    ///
230    /// This function maps levels to their less severe counterparts:
231    ///
232    /// - `Error` becomes `Warning`
233    /// - `Warning` becomes `Help`
234    /// - `Help` becomes `Note`
235    /// - `Note` remains as `Note`
236    ///
237    /// # Examples
238    ///
239    /// ```
240    /// use mago_reporting::Level;
241    ///
242    /// let level = Level::Error;
243    /// assert_eq!(level.downgrade(), Level::Warning);
244    ///
245    /// let level = Level::Warning;
246    /// assert_eq!(level.downgrade(), Level::Help);
247    ///
248    /// let level = Level::Help;
249    /// assert_eq!(level.downgrade(), Level::Note);
250    ///
251    /// let level = Level::Note;
252    /// assert_eq!(level.downgrade(), Level::Note);
253    /// ```
254    pub fn downgrade(&self) -> Self {
255        match self {
256            Level::Error => Level::Warning,
257            Level::Warning => Level::Help,
258            Level::Help | Level::Note => Level::Note,
259        }
260    }
261}
262
263impl Issue {
264    /// Creates a new issue with the given level and message.
265    ///
266    /// # Examples
267    ///
268    /// ```
269    /// use mago_reporting::{Issue, Level};
270    ///
271    /// let issue = Issue::new(Level::Error, "This is an error");
272    /// ```
273    pub fn new(level: Level, message: impl Into<String>) -> Self {
274        Self {
275            level,
276            code: None,
277            message: message.into(),
278            annotations: Vec::new(),
279            notes: Vec::new(),
280            help: None,
281            link: None,
282            suggestions: Vec::new(),
283        }
284    }
285
286    /// Creates a new error issue with the given message.
287    ///
288    /// # Examples
289    ///
290    /// ```
291    /// use mago_reporting::Issue;
292    ///
293    /// let issue = Issue::error("This is an error");
294    /// ```
295    pub fn error(message: impl Into<String>) -> Self {
296        Self::new(Level::Error, message)
297    }
298
299    /// Creates a new warning issue with the given message.
300    ///
301    /// # Examples
302    ///
303    /// ```
304    /// use mago_reporting::Issue;
305    ///
306    /// let issue = Issue::warning("This is a warning");
307    /// ```
308    pub fn warning(message: impl Into<String>) -> Self {
309        Self::new(Level::Warning, message)
310    }
311
312    /// Creates a new help issue with the given message.
313    ///
314    /// # Examples
315    ///
316    /// ```
317    /// use mago_reporting::Issue;
318    ///
319    /// let issue = Issue::help("This is a help message");
320    /// ```
321    pub fn help(message: impl Into<String>) -> Self {
322        Self::new(Level::Help, message)
323    }
324
325    /// Creates a new note issue with the given message.
326    ///
327    /// # Examples
328    ///
329    /// ```
330    /// use mago_reporting::Issue;
331    ///
332    /// let issue = Issue::note("This is a note");
333    /// ```
334    pub fn note(message: impl Into<String>) -> Self {
335        Self::new(Level::Note, message)
336    }
337
338    /// Adds a code to this issue.
339    ///
340    /// # Examples
341    ///
342    /// ```
343    /// use mago_reporting::{Issue, Level};
344    ///
345    /// let issue = Issue::error("This is an error").with_code("E0001");
346    /// ```
347    #[must_use]
348    pub fn with_code(mut self, code: impl Into<String>) -> Self {
349        self.code = Some(code.into());
350
351        self
352    }
353
354    /// Add an annotation to this issue.
355    ///
356    /// # Examples
357    ///
358    /// ```
359    /// use mago_reporting::{Issue, Annotation, AnnotationKind};
360    /// use mago_database::file::FileId;
361    /// use mago_span::Span;
362    /// use mago_span::Position;
363    ///
364    /// let file = FileId::zero();
365    /// let start = Position::new(0);
366    /// let end = Position::new(5);
367    /// let span = Span::new(file, start, end);
368    ///
369    /// let issue = Issue::error("This is an error").with_annotation(Annotation::primary(span));
370    /// ```
371    #[must_use]
372    pub fn with_annotation(mut self, annotation: Annotation) -> Self {
373        self.annotations.push(annotation);
374
375        self
376    }
377
378    #[must_use]
379    pub fn with_annotations(mut self, annotation: impl IntoIterator<Item = Annotation>) -> Self {
380        self.annotations.extend(annotation);
381
382        self
383    }
384
385    /// Add a note to this issue.
386    ///
387    /// # Examples
388    ///
389    /// ```
390    /// use mago_reporting::Issue;
391    ///
392    /// let issue = Issue::error("This is an error").with_note("This is a note");
393    /// ```
394    #[must_use]
395    pub fn with_note(mut self, note: impl Into<String>) -> Self {
396        self.notes.push(note.into());
397
398        self
399    }
400
401    /// Add a help message to this issue.
402    ///
403    /// This is useful for providing additional context to the user on how to resolve the issue.
404    ///
405    /// # Examples
406    ///
407    /// ```
408    /// use mago_reporting::Issue;
409    ///
410    /// let issue = Issue::error("This is an error").with_help("This is a help message");
411    /// ```
412    #[must_use]
413    pub fn with_help(mut self, help: impl Into<String>) -> Self {
414        self.help = Some(help.into());
415
416        self
417    }
418
419    /// Add a link to this issue.
420    ///
421    /// # Examples
422    ///
423    /// ```
424    /// use mago_reporting::Issue;
425    ///
426    /// let issue = Issue::error("This is an error").with_link("https://example.com");
427    /// ```
428    #[must_use]
429    pub fn with_link(mut self, link: impl Into<String>) -> Self {
430        self.link = Some(link.into());
431
432        self
433    }
434
435    /// Add a code modification suggestion to this issue.
436    #[must_use]
437    pub fn with_suggestion(mut self, file_id: FileId, plan: FixPlan) -> Self {
438        self.suggestions.push((file_id, plan));
439
440        self
441    }
442
443    /// Take the code modification suggestion from this issue.
444    #[must_use]
445    pub fn take_suggestions(&mut self) -> Vec<(FileId, FixPlan)> {
446        self.suggestions.drain(..).collect()
447    }
448}
449
450impl IssueCollection {
451    pub fn new() -> Self {
452        Self { issues: Vec::new() }
453    }
454
455    pub fn from(issues: impl IntoIterator<Item = Issue>) -> Self {
456        Self { issues: issues.into_iter().collect() }
457    }
458
459    pub fn push(&mut self, issue: Issue) {
460        self.issues.push(issue);
461    }
462
463    pub fn extend(&mut self, issues: impl IntoIterator<Item = Issue>) {
464        self.issues.extend(issues);
465    }
466
467    pub fn shrink_to_fit(&mut self) {
468        self.issues.shrink_to_fit();
469    }
470
471    pub fn is_empty(&self) -> bool {
472        self.issues.is_empty()
473    }
474
475    pub fn len(&self) -> usize {
476        self.issues.len()
477    }
478
479    /// Filters the issues in the collection to only include those with a severity level
480    /// lower than or equal to the given level.
481    pub fn with_maximum_level(self, level: Level) -> Self {
482        Self { issues: self.issues.into_iter().filter(|issue| issue.level <= level).collect() }
483    }
484
485    /// Filters the issues in the collection to only include those with a severity level
486    ///  higher than or equal to the given level.
487    pub fn with_minimum_level(self, level: Level) -> Self {
488        Self { issues: self.issues.into_iter().filter(|issue| issue.level >= level).collect() }
489    }
490
491    /// Returns `true` if the collection contains any issues with a severity level
492    ///  higher than or equal to the given level.
493    pub fn has_minimum_level(&self, level: Level) -> bool {
494        self.issues.iter().any(|issue| issue.level >= level)
495    }
496
497    /// Returns the number of issues in the collection with the given severity level.
498    pub fn get_level_count(&self, level: Level) -> usize {
499        self.issues.iter().filter(|issue| issue.level == level).count()
500    }
501
502    /// Returns the highest severity level of the issues in the collection.
503    pub fn get_highest_level(&self) -> Option<Level> {
504        self.issues.iter().map(|issue| issue.level).max()
505    }
506
507    /// Returns the lowest severity level of the issues in the collection.
508    pub fn get_lowest_level(&self) -> Option<Level> {
509        self.issues.iter().map(|issue| issue.level).min()
510    }
511
512    pub fn filter_out_ignored(&mut self, ignore: &[String]) {
513        self.issues.retain(|issue| if let Some(code) = &issue.code { !ignore.contains(code) } else { true });
514    }
515
516    pub fn filter_retain_codes(&mut self, retain_codes: &[String]) {
517        self.issues.retain(|issue| if let Some(code) = &issue.code { retain_codes.contains(code) } else { false });
518    }
519
520    pub fn take_suggestions(&mut self) -> impl Iterator<Item = (FileId, FixPlan)> + '_ {
521        self.issues.iter_mut().flat_map(|issue| issue.take_suggestions())
522    }
523
524    pub fn filter_fixable(self) -> Self {
525        Self { issues: self.issues.into_iter().filter(|issue| !issue.suggestions.is_empty()).collect() }
526    }
527
528    /// Sorts the issues in the collection.
529    ///
530    /// The issues are sorted by severity level in descending order,
531    /// then by code in ascending order, and finally by the primary annotation span.
532    pub fn sorted(self) -> Self {
533        let mut issues = self.issues;
534
535        issues.sort_by(|a, b| match a.level.cmp(&b.level) {
536            Ordering::Greater => Ordering::Greater,
537            Ordering::Less => Ordering::Less,
538            Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
539                Ordering::Less => Ordering::Less,
540                Ordering::Greater => Ordering::Greater,
541                Ordering::Equal => {
542                    let a_span = a
543                        .annotations
544                        .iter()
545                        .find(|annotation| annotation.is_primary())
546                        .map(|annotation| annotation.span);
547
548                    let b_span = b
549                        .annotations
550                        .iter()
551                        .find(|annotation| annotation.is_primary())
552                        .map(|annotation| annotation.span);
553
554                    match (a_span, b_span) {
555                        (Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
556                        (Some(_), None) => Ordering::Less,
557                        (None, Some(_)) => Ordering::Greater,
558                        (None, None) => Ordering::Equal,
559                    }
560                }
561            },
562        });
563
564        Self { issues }
565    }
566
567    pub fn iter(&self) -> impl Iterator<Item = &Issue> {
568        self.issues.iter()
569    }
570
571    pub fn to_fix_plans(self) -> HashMap<FileId, FixPlan> {
572        let mut plans: HashMap<FileId, FixPlan> = HashMap::default();
573        for issue in self.issues.into_iter().filter(|issue| !issue.suggestions.is_empty()) {
574            for suggestion in issue.suggestions.into_iter() {
575                match plans.entry(suggestion.0) {
576                    Entry::Occupied(mut occupied_entry) => {
577                        occupied_entry.get_mut().merge(suggestion.1);
578                    }
579                    Entry::Vacant(vacant_entry) => {
580                        vacant_entry.insert(suggestion.1);
581                    }
582                }
583            }
584        }
585
586        plans
587    }
588}
589
590impl IntoIterator for IssueCollection {
591    type Item = Issue;
592
593    type IntoIter = std::vec::IntoIter<Issue>;
594
595    fn into_iter(self) -> Self::IntoIter {
596        self.issues.into_iter()
597    }
598}
599
600impl Default for IssueCollection {
601    fn default() -> Self {
602        Self::new()
603    }
604}
605
606impl IntoIterator for Issue {
607    type Item = Issue;
608    type IntoIter = Once<Issue>;
609
610    fn into_iter(self) -> Self::IntoIter {
611        std::iter::once(self)
612    }
613}
614
615impl FromIterator<Issue> for IssueCollection {
616    fn from_iter<T: IntoIterator<Item = Issue>>(iter: T) -> Self {
617        Self { issues: iter.into_iter().collect() }
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624
625    #[test]
626    pub fn test_highest_collection_level() {
627        let mut collection = IssueCollection::from(vec![]);
628        assert_eq!(collection.get_highest_level(), None);
629
630        collection.push(Issue::note("note"));
631        assert_eq!(collection.get_highest_level(), Some(Level::Note));
632
633        collection.push(Issue::help("help"));
634        assert_eq!(collection.get_highest_level(), Some(Level::Help));
635
636        collection.push(Issue::warning("warning"));
637        assert_eq!(collection.get_highest_level(), Some(Level::Warning));
638
639        collection.push(Issue::error("error"));
640        assert_eq!(collection.get_highest_level(), Some(Level::Error));
641    }
642
643    #[test]
644    pub fn test_level_downgrade() {
645        assert_eq!(Level::Error.downgrade(), Level::Warning);
646        assert_eq!(Level::Warning.downgrade(), Level::Help);
647        assert_eq!(Level::Help.downgrade(), Level::Note);
648        assert_eq!(Level::Note.downgrade(), Level::Note);
649    }
650
651    #[test]
652    pub fn test_issue_collection_with_maximum_level() {
653        let mut collection = IssueCollection::from(vec![
654            Issue::error("error"),
655            Issue::warning("warning"),
656            Issue::help("help"),
657            Issue::note("note"),
658        ]);
659
660        collection = collection.with_maximum_level(Level::Warning);
661        assert_eq!(collection.len(), 3);
662        assert_eq!(
663            collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
664            vec![Level::Warning, Level::Help, Level::Note]
665        );
666    }
667
668    #[test]
669    pub fn test_issue_collection_with_minimum_level() {
670        let mut collection = IssueCollection::from(vec![
671            Issue::error("error"),
672            Issue::warning("warning"),
673            Issue::help("help"),
674            Issue::note("note"),
675        ]);
676
677        collection = collection.with_minimum_level(Level::Warning);
678        assert_eq!(collection.len(), 2);
679        assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
680    }
681
682    #[test]
683    pub fn test_issue_collection_has_minimum_level() {
684        let mut collection = IssueCollection::from(vec![]);
685
686        assert!(!collection.has_minimum_level(Level::Error));
687        assert!(!collection.has_minimum_level(Level::Warning));
688        assert!(!collection.has_minimum_level(Level::Help));
689        assert!(!collection.has_minimum_level(Level::Note));
690
691        collection.push(Issue::note("note"));
692
693        assert!(!collection.has_minimum_level(Level::Error));
694        assert!(!collection.has_minimum_level(Level::Warning));
695        assert!(!collection.has_minimum_level(Level::Help));
696        assert!(collection.has_minimum_level(Level::Note));
697
698        collection.push(Issue::help("help"));
699
700        assert!(!collection.has_minimum_level(Level::Error));
701        assert!(!collection.has_minimum_level(Level::Warning));
702        assert!(collection.has_minimum_level(Level::Help));
703        assert!(collection.has_minimum_level(Level::Note));
704
705        collection.push(Issue::warning("warning"));
706
707        assert!(!collection.has_minimum_level(Level::Error));
708        assert!(collection.has_minimum_level(Level::Warning));
709        assert!(collection.has_minimum_level(Level::Help));
710        assert!(collection.has_minimum_level(Level::Note));
711
712        collection.push(Issue::error("error"));
713
714        assert!(collection.has_minimum_level(Level::Error));
715        assert!(collection.has_minimum_level(Level::Warning));
716        assert!(collection.has_minimum_level(Level::Help));
717        assert!(collection.has_minimum_level(Level::Note));
718    }
719
720    #[test]
721    pub fn test_issue_collection_level_count() {
722        let mut collection = IssueCollection::from(vec![]);
723
724        assert_eq!(collection.get_level_count(Level::Error), 0);
725        assert_eq!(collection.get_level_count(Level::Warning), 0);
726        assert_eq!(collection.get_level_count(Level::Help), 0);
727        assert_eq!(collection.get_level_count(Level::Note), 0);
728
729        collection.push(Issue::error("error"));
730
731        assert_eq!(collection.get_level_count(Level::Error), 1);
732        assert_eq!(collection.get_level_count(Level::Warning), 0);
733        assert_eq!(collection.get_level_count(Level::Help), 0);
734        assert_eq!(collection.get_level_count(Level::Note), 0);
735
736        collection.push(Issue::warning("warning"));
737
738        assert_eq!(collection.get_level_count(Level::Error), 1);
739        assert_eq!(collection.get_level_count(Level::Warning), 1);
740        assert_eq!(collection.get_level_count(Level::Help), 0);
741        assert_eq!(collection.get_level_count(Level::Note), 0);
742
743        collection.push(Issue::help("help"));
744
745        assert_eq!(collection.get_level_count(Level::Error), 1);
746        assert_eq!(collection.get_level_count(Level::Warning), 1);
747        assert_eq!(collection.get_level_count(Level::Help), 1);
748        assert_eq!(collection.get_level_count(Level::Note), 0);
749
750        collection.push(Issue::note("note"));
751
752        assert_eq!(collection.get_level_count(Level::Error), 1);
753        assert_eq!(collection.get_level_count(Level::Warning), 1);
754        assert_eq!(collection.get_level_count(Level::Help), 1);
755        assert_eq!(collection.get_level_count(Level::Note), 1);
756    }
757}