Skip to main content

garbage_code_hunter/common/
severity.rs

1//! Shared severity level used by all analysis modules.
2
3use serde::{Deserialize, Serialize};
4
5/// Severity level for any detected issue.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
7pub enum Severity {
8    Critical,
9    High,
10    Medium,
11    Low,
12    Info,
13}
14
15impl Severity {
16    /// Returns the emoji representation for terminal output.
17    pub fn emoji(&self) -> &'static str {
18        match self {
19            Severity::Critical => "\u{1f480}",
20            Severity::High => "\u{1f621}",
21            Severity::Medium => "\u{26a0}\u{fe0f}",
22            Severity::Low => "\u{1f4a7}",
23            Severity::Info => "\u{2139}\u{fe0f}",
24        }
25    }
26
27    /// Returns the numeric penalty weight for scoring.
28    pub fn penalty(&self) -> f64 {
29        match self {
30            Severity::Critical => 10.0,
31            Severity::High => 5.0,
32            Severity::Medium => 2.0,
33            Severity::Low => 0.5,
34            Severity::Info => 0.0,
35        }
36    }
37
38    /// Alias for penalty() — used by commit_roaster which calls it weight().
39    pub fn weight(&self) -> f64 {
40        self.penalty()
41    }
42
43    /// Returns a human-readable label.
44    pub fn label(&self) -> &'static str {
45        match self {
46            Severity::Critical => "Critical",
47            Severity::High => "High",
48            Severity::Medium => "Medium",
49            Severity::Low => "Low",
50            Severity::Info => "Info",
51        }
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    // ── ordering ──────────────────────────────────────────────────
60
61    /// Objective: Verify PartialOrd orders by declaration order (Critical highest).
62    #[test]
63    fn test_severity_ordering_adjacent() {
64        assert!(Severity::Critical < Severity::High, "Critical < High");
65        assert!(Severity::High < Severity::Medium, "High < Medium");
66        assert!(Severity::Medium < Severity::Low, "Medium < Low");
67        assert!(Severity::Low < Severity::Info, "Low < Info");
68    }
69
70    /// Objective: Verify ordering is transitive across non-adjacent pairs.
71    #[test]
72    fn test_severity_ordering_transitive() {
73        assert!(Severity::Critical < Severity::Medium, "Critical < Medium");
74        assert!(Severity::Critical < Severity::Low, "Critical < Low");
75        assert!(Severity::Critical < Severity::Info, "Critical < Info");
76        assert!(Severity::High < Severity::Low, "High < Low");
77        assert!(Severity::High < Severity::Info, "High < Info");
78        assert!(Severity::Medium < Severity::Info, "Medium < Info");
79    }
80
81    /// Objective: Verify equality is reflexive.
82    #[test]
83    fn test_severity_ordering_equal() {
84        assert_eq!(Severity::Critical, Severity::Critical);
85        assert_eq!(Severity::High, Severity::High);
86        assert_eq!(Severity::Medium, Severity::Medium);
87        assert_eq!(Severity::Low, Severity::Low);
88        assert_eq!(Severity::Info, Severity::Info);
89    }
90
91    // ── emoji ─────────────────────────────────────────────────────
92
93    /// Objective: Verify each severity has a specific non-empty emoji.
94    #[test]
95    fn test_emoji_specific_values() {
96        assert_eq!(Severity::Critical.emoji(), "\u{1f480}", "Critical → skull");
97        assert_eq!(Severity::High.emoji(), "\u{1f621}", "High → angry");
98        assert_eq!(
99            Severity::Medium.emoji(),
100            "\u{26a0}\u{fe0f}",
101            "Medium → warning"
102        );
103        assert_eq!(Severity::Low.emoji(), "\u{1f4a7}", "Low → droplet");
104        assert_eq!(Severity::Info.emoji(), "\u{2139}\u{fe0f}", "Info → info");
105    }
106
107    // ── penalty ───────────────────────────────────────────────────
108
109    /// Objective: Verify penalty values match expected weights.
110    #[test]
111    fn test_penalty_values() {
112        assert_eq!(Severity::Critical.penalty(), 10.0);
113        assert_eq!(Severity::High.penalty(), 5.0);
114        assert_eq!(Severity::Medium.penalty(), 2.0);
115        assert_eq!(Severity::Low.penalty(), 0.5);
116        assert_eq!(Severity::Info.penalty(), 0.0);
117    }
118
119    /// Objective: Verify penalty ordering matches severity ordering.
120    #[test]
121    fn test_penalty_ordering_matches_severity() {
122        assert!(Severity::Critical.penalty() > Severity::High.penalty());
123        assert!(Severity::High.penalty() > Severity::Medium.penalty());
124        assert!(Severity::Medium.penalty() > Severity::Low.penalty());
125        assert!(Severity::Low.penalty() > Severity::Info.penalty());
126    }
127
128    // ── weight alias ──────────────────────────────────────────────
129
130    /// Objective: Verify weight() is an alias for penalty().
131    #[test]
132    fn test_weight_is_alias_for_penalty() {
133        for sev in [
134            Severity::Critical,
135            Severity::High,
136            Severity::Medium,
137            Severity::Low,
138            Severity::Info,
139        ] {
140            assert_eq!(
141                sev.weight(),
142                sev.penalty(),
143                "{:?}.weight() should equal .penalty()",
144                sev
145            );
146        }
147    }
148
149    // ── label ─────────────────────────────────────────────────────
150
151    /// Objective: Verify label for all variants.
152    #[test]
153    fn test_label_all_variants() {
154        assert_eq!(Severity::Critical.label(), "Critical");
155        assert_eq!(Severity::High.label(), "High");
156        assert_eq!(Severity::Medium.label(), "Medium");
157        assert_eq!(Severity::Low.label(), "Low");
158        assert_eq!(Severity::Info.label(), "Info");
159    }
160
161    /// Objective: Verify labels are all unique.
162    #[test]
163    fn test_label_unique() {
164        let labels: Vec<&str> = [
165            Severity::Critical,
166            Severity::High,
167            Severity::Medium,
168            Severity::Low,
169            Severity::Info,
170        ]
171        .iter()
172        .map(|s| s.label())
173        .collect();
174        let unique: std::collections::HashSet<&&str> = labels.iter().collect();
175        assert_eq!(unique.len(), labels.len(), "all labels must be unique");
176    }
177
178    // ── serde ─────────────────────────────────────────────────────
179
180    /// Objective: Verify Severity round-trips through JSON without data loss.
181    #[test]
182    fn test_severity_serde_roundtrip() {
183        for sev in [
184            Severity::Critical,
185            Severity::High,
186            Severity::Medium,
187            Severity::Low,
188            Severity::Info,
189        ] {
190            let json = serde_json::to_string(&sev).unwrap();
191            let parsed: Severity = serde_json::from_str(&json).unwrap();
192            assert_eq!(parsed, sev, "{sev:?} round-trips through JSON");
193        }
194    }
195
196    /// Objective: Verify Severity deserializes from JSON strings case-insensitively
197    /// (if serde is configured for it).
198    #[test]
199    fn test_severity_serde_from_string() {
200        // serde by default uses unit variants for enums — serialize as strings
201        let parsed: Severity = serde_json::from_str("\"Critical\"").unwrap();
202        assert_eq!(parsed, Severity::Critical);
203        let parsed: Severity = serde_json::from_str("\"Info\"").unwrap();
204        assert_eq!(parsed, Severity::Info);
205    }
206}