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        if self.issues.contains(&issue) {
461            return; // Avoid duplicates
462        }
463
464        self.issues.push(issue);
465    }
466
467    pub fn extend(&mut self, issues: impl IntoIterator<Item = Issue>) {
468        self.issues.extend(issues);
469    }
470
471    pub fn shrink_to_fit(&mut self) {
472        self.issues.shrink_to_fit();
473    }
474
475    pub fn is_empty(&self) -> bool {
476        self.issues.is_empty()
477    }
478
479    pub fn len(&self) -> usize {
480        self.issues.len()
481    }
482
483    /// Filters the issues in the collection to only include those with a severity level
484    /// lower than or equal to the given level.
485    pub fn with_maximum_level(self, level: Level) -> Self {
486        Self { issues: self.issues.into_iter().filter(|issue| issue.level <= level).collect() }
487    }
488
489    /// Filters the issues in the collection to only include those with a severity level
490    ///  higher than or equal to the given level.
491    pub fn with_minimum_level(self, level: Level) -> Self {
492        Self { issues: self.issues.into_iter().filter(|issue| issue.level >= level).collect() }
493    }
494
495    /// Returns `true` if the collection contains any issues with a severity level
496    ///  higher than or equal to the given level.
497    pub fn has_minimum_level(&self, level: Level) -> bool {
498        self.issues.iter().any(|issue| issue.level >= level)
499    }
500
501    /// Returns the number of issues in the collection with the given severity level.
502    pub fn get_level_count(&self, level: Level) -> usize {
503        self.issues.iter().filter(|issue| issue.level == level).count()
504    }
505
506    /// Returns the highest severity level of the issues in the collection.
507    pub fn get_highest_level(&self) -> Option<Level> {
508        self.issues.iter().map(|issue| issue.level).max()
509    }
510
511    /// Returns the lowest severity level of the issues in the collection.
512    pub fn get_lowest_level(&self) -> Option<Level> {
513        self.issues.iter().map(|issue| issue.level).min()
514    }
515
516    pub fn filter_out_ignored(&mut self, ignore: &[String]) {
517        self.issues.retain(|issue| if let Some(code) = &issue.code { !ignore.contains(code) } else { true });
518    }
519
520    pub fn filter_retain_codes(&mut self, retain_codes: &[String]) {
521        self.issues.retain(|issue| if let Some(code) = &issue.code { retain_codes.contains(code) } else { false });
522    }
523
524    pub fn take_suggestions(&mut self) -> impl Iterator<Item = (FileId, FixPlan)> + '_ {
525        self.issues.iter_mut().flat_map(|issue| issue.take_suggestions())
526    }
527
528    pub fn filter_fixable(self) -> Self {
529        Self { issues: self.issues.into_iter().filter(|issue| !issue.suggestions.is_empty()).collect() }
530    }
531
532    /// Sorts the issues in the collection.
533    ///
534    /// The issues are sorted by severity level in descending order,
535    /// then by code in ascending order, and finally by the primary annotation span.
536    pub fn sorted(self) -> Self {
537        let mut issues = self.issues;
538
539        issues.sort_by(|a, b| match a.level.cmp(&b.level) {
540            Ordering::Greater => Ordering::Greater,
541            Ordering::Less => Ordering::Less,
542            Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
543                Ordering::Less => Ordering::Less,
544                Ordering::Greater => Ordering::Greater,
545                Ordering::Equal => {
546                    let a_span = a
547                        .annotations
548                        .iter()
549                        .find(|annotation| annotation.is_primary())
550                        .map(|annotation| annotation.span);
551
552                    let b_span = b
553                        .annotations
554                        .iter()
555                        .find(|annotation| annotation.is_primary())
556                        .map(|annotation| annotation.span);
557
558                    match (a_span, b_span) {
559                        (Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
560                        (Some(_), None) => Ordering::Less,
561                        (None, Some(_)) => Ordering::Greater,
562                        (None, None) => Ordering::Equal,
563                    }
564                }
565            },
566        });
567
568        Self { issues }
569    }
570
571    pub fn iter(&self) -> impl Iterator<Item = &Issue> {
572        self.issues.iter()
573    }
574
575    pub fn to_fix_plans(self) -> HashMap<FileId, FixPlan> {
576        let mut plans: HashMap<FileId, FixPlan> = HashMap::default();
577        for issue in self.issues.into_iter().filter(|issue| !issue.suggestions.is_empty()) {
578            for suggestion in issue.suggestions.into_iter() {
579                match plans.entry(suggestion.0) {
580                    Entry::Occupied(mut occupied_entry) => {
581                        occupied_entry.get_mut().merge(suggestion.1);
582                    }
583                    Entry::Vacant(vacant_entry) => {
584                        vacant_entry.insert(suggestion.1);
585                    }
586                }
587            }
588        }
589
590        plans
591    }
592}
593
594impl IntoIterator for IssueCollection {
595    type Item = Issue;
596
597    type IntoIter = std::vec::IntoIter<Issue>;
598
599    fn into_iter(self) -> Self::IntoIter {
600        self.issues.into_iter()
601    }
602}
603
604impl Default for IssueCollection {
605    fn default() -> Self {
606        Self::new()
607    }
608}
609
610impl IntoIterator for Issue {
611    type Item = Issue;
612    type IntoIter = Once<Issue>;
613
614    fn into_iter(self) -> Self::IntoIter {
615        std::iter::once(self)
616    }
617}
618
619impl FromIterator<Issue> for IssueCollection {
620    fn from_iter<T: IntoIterator<Item = Issue>>(iter: T) -> Self {
621        Self { issues: iter.into_iter().collect() }
622    }
623}
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628
629    #[test]
630    pub fn test_highest_collection_level() {
631        let mut collection = IssueCollection::from(vec![]);
632        assert_eq!(collection.get_highest_level(), None);
633
634        collection.push(Issue::note("note"));
635        assert_eq!(collection.get_highest_level(), Some(Level::Note));
636
637        collection.push(Issue::help("help"));
638        assert_eq!(collection.get_highest_level(), Some(Level::Help));
639
640        collection.push(Issue::warning("warning"));
641        assert_eq!(collection.get_highest_level(), Some(Level::Warning));
642
643        collection.push(Issue::error("error"));
644        assert_eq!(collection.get_highest_level(), Some(Level::Error));
645    }
646
647    #[test]
648    pub fn test_level_downgrade() {
649        assert_eq!(Level::Error.downgrade(), Level::Warning);
650        assert_eq!(Level::Warning.downgrade(), Level::Help);
651        assert_eq!(Level::Help.downgrade(), Level::Note);
652        assert_eq!(Level::Note.downgrade(), Level::Note);
653    }
654
655    #[test]
656    pub fn test_issue_collection_with_maximum_level() {
657        let mut collection = IssueCollection::from(vec![
658            Issue::error("error"),
659            Issue::warning("warning"),
660            Issue::help("help"),
661            Issue::note("note"),
662        ]);
663
664        collection = collection.with_maximum_level(Level::Warning);
665        assert_eq!(collection.len(), 3);
666        assert_eq!(
667            collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
668            vec![Level::Warning, Level::Help, Level::Note]
669        );
670    }
671
672    #[test]
673    pub fn test_issue_collection_with_minimum_level() {
674        let mut collection = IssueCollection::from(vec![
675            Issue::error("error"),
676            Issue::warning("warning"),
677            Issue::help("help"),
678            Issue::note("note"),
679        ]);
680
681        collection = collection.with_minimum_level(Level::Warning);
682        assert_eq!(collection.len(), 2);
683        assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
684    }
685
686    #[test]
687    pub fn test_issue_collection_has_minimum_level() {
688        let mut collection = IssueCollection::from(vec![]);
689
690        assert!(!collection.has_minimum_level(Level::Error));
691        assert!(!collection.has_minimum_level(Level::Warning));
692        assert!(!collection.has_minimum_level(Level::Help));
693        assert!(!collection.has_minimum_level(Level::Note));
694
695        collection.push(Issue::note("note"));
696
697        assert!(!collection.has_minimum_level(Level::Error));
698        assert!(!collection.has_minimum_level(Level::Warning));
699        assert!(!collection.has_minimum_level(Level::Help));
700        assert!(collection.has_minimum_level(Level::Note));
701
702        collection.push(Issue::help("help"));
703
704        assert!(!collection.has_minimum_level(Level::Error));
705        assert!(!collection.has_minimum_level(Level::Warning));
706        assert!(collection.has_minimum_level(Level::Help));
707        assert!(collection.has_minimum_level(Level::Note));
708
709        collection.push(Issue::warning("warning"));
710
711        assert!(!collection.has_minimum_level(Level::Error));
712        assert!(collection.has_minimum_level(Level::Warning));
713        assert!(collection.has_minimum_level(Level::Help));
714        assert!(collection.has_minimum_level(Level::Note));
715
716        collection.push(Issue::error("error"));
717
718        assert!(collection.has_minimum_level(Level::Error));
719        assert!(collection.has_minimum_level(Level::Warning));
720        assert!(collection.has_minimum_level(Level::Help));
721        assert!(collection.has_minimum_level(Level::Note));
722    }
723
724    #[test]
725    pub fn test_issue_collection_level_count() {
726        let mut collection = IssueCollection::from(vec![]);
727
728        assert_eq!(collection.get_level_count(Level::Error), 0);
729        assert_eq!(collection.get_level_count(Level::Warning), 0);
730        assert_eq!(collection.get_level_count(Level::Help), 0);
731        assert_eq!(collection.get_level_count(Level::Note), 0);
732
733        collection.push(Issue::error("error"));
734
735        assert_eq!(collection.get_level_count(Level::Error), 1);
736        assert_eq!(collection.get_level_count(Level::Warning), 0);
737        assert_eq!(collection.get_level_count(Level::Help), 0);
738        assert_eq!(collection.get_level_count(Level::Note), 0);
739
740        collection.push(Issue::warning("warning"));
741
742        assert_eq!(collection.get_level_count(Level::Error), 1);
743        assert_eq!(collection.get_level_count(Level::Warning), 1);
744        assert_eq!(collection.get_level_count(Level::Help), 0);
745        assert_eq!(collection.get_level_count(Level::Note), 0);
746
747        collection.push(Issue::help("help"));
748
749        assert_eq!(collection.get_level_count(Level::Error), 1);
750        assert_eq!(collection.get_level_count(Level::Warning), 1);
751        assert_eq!(collection.get_level_count(Level::Help), 1);
752        assert_eq!(collection.get_level_count(Level::Note), 0);
753
754        collection.push(Issue::note("note"));
755
756        assert_eq!(collection.get_level_count(Level::Error), 1);
757        assert_eq!(collection.get_level_count(Level::Warning), 1);
758        assert_eq!(collection.get_level_count(Level::Help), 1);
759        assert_eq!(collection.get_level_count(Level::Note), 1);
760    }
761}