#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Skipped,
Pass,
Warn,
Error,
}
impl Severity {
pub fn as_str(self) -> &'static str {
match self {
Self::Skipped => "skip",
Self::Pass => "pass",
Self::Warn => "warn",
Self::Error => "fail",
}
}
}
#[derive(Debug, Clone)]
pub struct CheckResult {
#[allow(dead_code)]
pub check_id: String,
pub check_name: String,
pub severity: Severity,
pub message: String,
pub suggestion: Option<String>,
pub location: Option<(String, Option<usize>)>,
}
impl CheckResult {
pub fn pass(check_id: &str, check_name: &str, message: impl Into<String>) -> Self {
Self {
check_id: check_id.to_string(),
check_name: check_name.to_string(),
severity: Severity::Pass,
message: message.into(),
suggestion: None,
location: None,
}
}
pub fn warn(
check_id: &str,
check_name: &str,
message: impl Into<String>,
suggestion: impl Into<String>,
) -> Self {
Self {
check_id: check_id.to_string(),
check_name: check_name.to_string(),
severity: Severity::Warn,
message: message.into(),
suggestion: Some(suggestion.into()),
location: None,
}
}
pub fn fail(
check_id: &str,
check_name: &str,
message: impl Into<String>,
suggestion: impl Into<String>,
) -> Self {
Self {
check_id: check_id.to_string(),
check_name: check_name.to_string(),
severity: Severity::Error,
message: message.into(),
suggestion: Some(suggestion.into()),
location: None,
}
}
pub fn skipped(check_id: &str, check_name: &str, message: impl Into<String>) -> Self {
Self {
check_id: check_id.to_string(),
check_name: check_name.to_string(),
severity: Severity::Skipped,
message: message.into(),
suggestion: None,
location: None,
}
}
pub fn is_critical_failure(&self) -> bool {
matches!(self.severity, Severity::Error)
}
}
#[derive(Debug, Default)]
pub struct VerifyReport {
pub results: Vec<CheckResult>,
}
impl VerifyReport {
pub fn push(&mut self, r: CheckResult) {
self.results.push(r);
}
pub fn has_critical_failure(&self) -> bool {
self.results.iter().any(CheckResult::is_critical_failure)
}
pub fn counts(&self) -> (usize, usize, usize, usize) {
let mut pass = 0;
let mut warn = 0;
let mut fail = 0;
let mut skip = 0;
for r in &self.results {
match r.severity {
Severity::Pass => pass += 1,
Severity::Warn => warn += 1,
Severity::Error => fail += 1,
Severity::Skipped => skip += 1,
}
}
(pass, warn, fail, skip)
}
pub fn discoverability_score(&self) -> u8 {
let mut credits = 0.0_f32;
let mut eligible = 0;
for r in &self.results {
match r.severity {
Severity::Pass => {
credits += 1.0;
eligible += 1;
}
Severity::Warn => {
credits += 0.5;
eligible += 1;
}
Severity::Error => {
eligible += 1;
}
Severity::Skipped => {}
}
}
if eligible == 0 {
return 0;
}
(credits / eligible as f32 * 100.0).round() as u8
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mk(id: &str, sev: Severity) -> CheckResult {
CheckResult {
check_id: id.to_string(),
check_name: id.to_string(),
severity: sev,
message: String::new(),
suggestion: None,
location: None,
}
}
#[test]
fn score_all_pass_is_100() {
let mut r = VerifyReport::default();
r.push(mk("a", Severity::Pass));
r.push(mk("b", Severity::Pass));
assert_eq!(r.discoverability_score(), 100);
}
#[test]
fn score_one_warn_out_of_two_is_75() {
let mut r = VerifyReport::default();
r.push(mk("a", Severity::Pass));
r.push(mk("b", Severity::Warn));
assert_eq!(r.discoverability_score(), 75);
}
#[test]
fn score_error_lowers_below_100() {
let mut r = VerifyReport::default();
r.push(mk("a", Severity::Pass));
r.push(mk("b", Severity::Pass));
r.push(mk("c", Severity::Error));
assert_eq!(r.discoverability_score(), 67);
}
#[test]
fn score_all_skipped_is_zero() {
let mut r = VerifyReport::default();
r.push(mk("a", Severity::Skipped));
r.push(mk("b", Severity::Skipped));
assert_eq!(r.discoverability_score(), 0);
}
#[test]
fn score_empty_report_is_zero() {
let r = VerifyReport::default();
assert_eq!(r.discoverability_score(), 0);
}
#[test]
fn score_skipped_excluded_from_denominator() {
let mut r = VerifyReport::default();
r.push(mk("a", Severity::Skipped));
r.push(mk("b", Severity::Warn));
assert_eq!(r.discoverability_score(), 50);
}
#[test]
fn score_all_errors_is_zero() {
let mut r = VerifyReport::default();
r.push(mk("a", Severity::Error));
r.push(mk("b", Severity::Error));
assert_eq!(r.discoverability_score(), 0);
}
}