1use crate::analysis_result::AnalysisResult;
2
3const BASELINE: i32 = 100;
4const MAX_SCORE: i32 = 145;
5const BONUS_THRESHOLD: i32 = 90;
6
7#[derive(Debug, Clone, PartialEq)]
9pub struct ScanScore {
10 pub score: i32,
11 pub grade: &'static str,
12}
13
14impl ScanScore {
15 pub fn calculate(results: &[AnalysisResult]) -> Self {
23 let penalties: i32 = results
24 .iter()
25 .filter(|r| r.score_impact < 0)
26 .map(|r| r.score_impact)
27 .sum();
28
29 let round1 = (BASELINE + penalties).clamp(0, BASELINE);
30
31 let bonuses: i32 = if round1 >= BONUS_THRESHOLD {
32 results
33 .iter()
34 .filter(|r| r.score_impact > 0)
35 .map(|r| r.score_impact)
36 .sum()
37 } else {
38 0
39 };
40
41 let score = (round1 + bonuses).clamp(0, MAX_SCORE);
42 Self {
43 score,
44 grade: Self::grade(score),
45 }
46 }
47
48 pub fn grade(score: i32) -> &'static str {
49 match score {
50 100..=i32::MAX => "A+",
51 90..=99 => "A",
52 85..=89 => "A-",
53 80..=84 => "B+",
54 70..=79 => "B",
55 65..=69 => "B-",
56 60..=64 => "C+",
57 50..=59 => "C",
58 45..=49 => "C-",
59 40..=44 => "D+",
60 30..=39 => "D",
61 25..=29 => "D-",
62 _ => "F",
63 }
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70 use crate::severity::Severity;
71
72 fn result(score_impact: i32) -> AnalysisResult {
73 AnalysisResult::new(Severity::Ok, "check", "comment").with_score(score_impact)
74 }
75
76 #[test]
77 fn perfect_score_with_no_results() {
78 let score = ScanScore::calculate(&[]);
79 assert_eq!(score.score, 100);
80 assert_eq!(score.grade, "A+");
81 }
82
83 #[test]
84 fn penalty_is_deducted_from_baseline() {
85 let score = ScanScore::calculate(&[result(-25)]);
86 assert_eq!(score.score, 75);
87 assert_eq!(score.grade, "B");
88 }
89
90 #[test]
91 fn multiple_penalties_are_summed() {
92 let score = ScanScore::calculate(&[result(-20), result(-20), result(-5)]);
93 assert_eq!(score.score, 55);
94 assert_eq!(score.grade, "C");
95 }
96
97 #[test]
98 fn score_is_clamped_to_zero() {
99 let score = ScanScore::calculate(&[result(-200)]);
100 assert_eq!(score.score, 0);
101 assert_eq!(score.grade, "F");
102 }
103
104 #[test]
105 fn bonus_applied_when_round1_score_is_90_or_more() {
106 let score = ScanScore::calculate(&[result(10)]);
107 assert_eq!(score.score, 110);
108 assert_eq!(score.grade, "A+");
109 }
110
111 #[test]
112 fn bonus_not_applied_when_round1_score_below_90() {
113 let score = ScanScore::calculate(&[result(-20), result(10)]);
115 assert_eq!(score.score, 80);
116 assert_eq!(score.grade, "B+");
117 }
118
119 #[test]
120 fn score_is_clamped_to_max() {
121 let score = ScanScore::calculate(&[result(100)]);
122 assert_eq!(score.score, MAX_SCORE);
123 }
124
125 #[test]
126 fn grade_boundaries() {
127 assert_eq!(ScanScore::grade(100), "A+");
128 assert_eq!(ScanScore::grade(99), "A");
129 assert_eq!(ScanScore::grade(90), "A");
130 assert_eq!(ScanScore::grade(89), "A-");
131 assert_eq!(ScanScore::grade(85), "A-");
132 assert_eq!(ScanScore::grade(84), "B+");
133 assert_eq!(ScanScore::grade(80), "B+");
134 assert_eq!(ScanScore::grade(79), "B");
135 assert_eq!(ScanScore::grade(70), "B");
136 assert_eq!(ScanScore::grade(69), "B-");
137 assert_eq!(ScanScore::grade(65), "B-");
138 assert_eq!(ScanScore::grade(64), "C+");
139 assert_eq!(ScanScore::grade(60), "C+");
140 assert_eq!(ScanScore::grade(59), "C");
141 assert_eq!(ScanScore::grade(50), "C");
142 assert_eq!(ScanScore::grade(49), "C-");
143 assert_eq!(ScanScore::grade(45), "C-");
144 assert_eq!(ScanScore::grade(44), "D+");
145 assert_eq!(ScanScore::grade(40), "D+");
146 assert_eq!(ScanScore::grade(39), "D");
147 assert_eq!(ScanScore::grade(30), "D");
148 assert_eq!(ScanScore::grade(29), "D-");
149 assert_eq!(ScanScore::grade(25), "D-");
150 assert_eq!(ScanScore::grade(24), "F");
151 assert_eq!(ScanScore::grade(0), "F");
152 }
153}