use serde::Serialize;
use super::analyzer::Grade;
use super::diff::ScoreDiff;
use crate::report_helpers;
const GREEN: &str = "\x1b[32m";
const RED: &str = "\x1b[31m";
const YELLOW: &str = "\x1b[33m";
const BOLD: &str = "\x1b[1m";
const RESET: &str = "\x1b[0m";
fn colored_delta(delta: f64) -> String {
if delta > 0.05 {
format!("{GREEN}+{delta:.1}{RESET}")
} else if delta < -0.05 {
format!("{RED}{delta:.1}{RESET}")
} else {
format!("{YELLOW} 0.0{RESET}")
}
}
fn colored_int_delta(delta: i64) -> String {
if delta > 0 {
format!("{GREEN}+{delta}{RESET}")
} else if delta < 0 {
format!("{RED}{delta}{RESET}")
} else {
format!("{YELLOW} 0{RESET}")
}
}
pub fn print_report(diff: &ScoreDiff) {
let separator = report_helpers::separator(72);
println!("Score Diff vs {BOLD}{}{RESET}", diff.git_ref);
println!("{separator}");
let grade_change = if diff.before_grade == diff.after_grade {
diff.after_grade.as_str().to_string()
} else {
format!(
"{} → {}",
diff.before_grade.as_str(),
diff.after_grade.as_str()
)
};
println!(
" Overall Score: {:.1} → {:.1} ({}) Grade: {grade_change}",
diff.overall.before,
diff.overall.after,
colored_delta(diff.overall.delta),
);
let files_delta = diff.files_after as i64 - diff.files_before as i64;
let loc_delta = diff.loc_after as i64 - diff.loc_before as i64;
println!(
" Files: {} → {} ({}) LOC: {} → {} ({})",
diff.files_before,
diff.files_after,
colored_int_delta(files_delta),
diff.loc_before,
diff.loc_after,
colored_int_delta(loc_delta),
);
println!("{separator}");
println!(
" {:<25} {:>6} {:>10} {:>10} {:>7}",
"Dimension", "Weight", "Before", "After", "Delta"
);
println!("{separator}");
for d in &diff.dimensions {
println!(
" {:<25} {:>5.0}% {:>5.1} {:<3} {:>5.1} {:<3} {}",
d.name,
d.weight * 100.0,
d.before_score,
d.before_grade.as_str(),
d.after_score,
d.after_grade.as_str(),
colored_delta(d.delta),
);
}
println!("{separator}");
}
#[derive(Serialize)]
struct JsonScoreSnapshot {
score: f64,
grade: Grade,
files: usize,
loc: usize,
}
#[derive(Serialize)]
struct JsonDimensionDelta {
name: String,
weight: f64,
before_score: f64,
before_grade: Grade,
after_score: f64,
after_grade: Grade,
delta: f64,
}
#[derive(Serialize)]
struct JsonScoreDiff {
git_ref: String,
before: JsonScoreSnapshot,
after: JsonScoreSnapshot,
delta: f64,
dimensions: Vec<JsonDimensionDelta>,
}
pub fn print_json(diff: &ScoreDiff) -> Result<(), Box<dyn std::error::Error>> {
let json = JsonScoreDiff {
git_ref: diff.git_ref.clone(),
before: JsonScoreSnapshot {
score: diff.overall.before,
grade: diff.before_grade,
files: diff.files_before,
loc: diff.loc_before,
},
after: JsonScoreSnapshot {
score: diff.overall.after,
grade: diff.after_grade,
files: diff.files_after,
loc: diff.loc_after,
},
delta: diff.overall.delta,
dimensions: diff
.dimensions
.iter()
.map(|d| JsonDimensionDelta {
name: d.name.clone(),
weight: d.weight,
before_score: d.before_score,
before_grade: d.before_grade,
after_score: d.after_score,
after_grade: d.after_grade,
delta: d.delta,
})
.collect(),
};
report_helpers::print_json_stdout(&json)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::score::analyzer::{DimensionScore, Grade, ProjectScore, score_to_grade};
use crate::score::diff::compute_diff;
fn make_project_score(score: f64, files: usize, loc: usize, dim_score: f64) -> ProjectScore {
let grade = score_to_grade(score);
let dim_grade = score_to_grade(dim_score);
ProjectScore {
score,
grade,
files_analyzed: files,
total_loc: loc,
dimensions: vec![
DimensionScore {
name: "Cognitive Complexity",
weight: 0.50,
score: dim_score,
grade: dim_grade,
},
DimensionScore {
name: "Duplication",
weight: 0.50,
score: dim_score,
grade: dim_grade,
},
],
needs_attention: vec![],
}
}
#[test]
fn print_report_improvement_does_not_panic() {
let before = make_project_score(70.0, 10, 1000, 70.0);
let after = make_project_score(80.0, 12, 1200, 80.0);
let diff = compute_diff("HEAD~1", &before, &after);
print_report(&diff);
}
#[test]
fn print_report_regression_does_not_panic() {
let before = make_project_score(80.0, 10, 1000, 80.0);
let after = make_project_score(70.0, 8, 900, 70.0);
let diff = compute_diff("main", &before, &after);
print_report(&diff);
}
#[test]
fn print_report_no_change_does_not_panic() {
let before = make_project_score(85.0, 10, 1000, 85.0);
let after = make_project_score(85.0, 10, 1000, 85.0);
let diff = compute_diff("v1.0", &before, &after);
print_report(&diff);
}
#[test]
fn print_json_does_not_panic() {
let before = make_project_score(75.0, 5, 500, 75.0);
let after = make_project_score(85.0, 7, 700, 85.0);
let diff = compute_diff("abc123", &before, &after);
print_json(&diff).unwrap();
}
#[test]
fn print_json_regression_does_not_panic() {
let before = make_project_score(90.0, 10, 1000, 90.0);
let after = make_project_score(60.0, 10, 1000, 60.0);
let diff = compute_diff("v2.0", &before, &after);
print_json(&diff).unwrap();
}
#[test]
fn colored_delta_positive() {
let s = colored_delta(5.0);
assert!(s.contains('+'), "positive delta should have + prefix: {s}");
}
#[test]
fn colored_delta_negative() {
let s = colored_delta(-5.0);
assert!(s.contains('-'), "negative delta should have - prefix: {s}");
}
#[test]
fn colored_delta_near_zero() {
let s = colored_delta(0.0);
assert!(s.contains("0.0"), "zero delta should show 0.0: {s}");
}
#[test]
fn colored_int_delta_positive() {
let s = colored_int_delta(3);
assert!(
s.contains('+'),
"positive int delta should have + prefix: {s}"
);
}
#[test]
fn colored_int_delta_negative() {
let s = colored_int_delta(-3);
assert!(
s.contains('-'),
"negative int delta should have - prefix: {s}"
);
}
#[test]
fn colored_int_delta_zero() {
let s = colored_int_delta(0);
assert!(s.contains('0'), "zero int delta should show 0: {s}");
}
#[test]
fn same_grade_shows_single_grade() {
let before = make_project_score(80.0, 10, 1000, 80.0);
let after = make_project_score(81.0, 10, 1000, 81.0);
let diff = compute_diff("HEAD", &before, &after);
assert_eq!(diff.before_grade, Grade::B);
assert_eq!(diff.after_grade, Grade::B);
print_report(&diff);
}
#[test]
fn different_grades_shows_transition() {
let before = make_project_score(70.0, 10, 1000, 70.0); let after = make_project_score(80.0, 10, 1000, 80.0); let diff = compute_diff("HEAD", &before, &after);
assert_eq!(diff.before_grade, Grade::C);
assert_eq!(diff.after_grade, Grade::B);
print_report(&diff);
}
}