use crate::merge::CrapEntry;
use anyhow::Result;
use comfy_table::{Attribute, Cell, CellAlignment, Table, presets::UTF8_FULL};
use std::io::Write;
pub(crate) struct CrateRollup {
pub(crate) name: String,
pub(crate) total: usize,
pub(crate) crappy: usize,
}
pub(crate) fn crate_rollups(
entries: &[CrapEntry],
threshold: f64,
) -> Vec<CrateRollup> {
use std::collections::BTreeMap;
let mut by_name: BTreeMap<String, (usize, usize)> = BTreeMap::new();
for e in entries {
if let Some(name) = &e.crate_name {
let slot = by_name.entry(name.clone()).or_default();
slot.0 += 1;
if e.crap > threshold {
slot.1 += 1;
}
}
}
by_name
.into_iter()
.map(|(name, (total, crappy))| CrateRollup {
name,
total,
crappy,
})
.collect()
}
pub(crate) fn has_crate_data(entries: &[CrapEntry]) -> bool {
entries.iter().any(|e| e.crate_name.is_some())
}
pub(crate) fn write_per_crate_human(
entries: &[CrapEntry],
threshold: f64,
out: &mut dyn Write,
) -> Result<()> {
let rollups = crate_rollups(entries, threshold);
if rollups.is_empty() {
return Ok(());
}
writeln!(out, "Per-crate summary:")?;
let mut table = Table::new();
table.load_preset(UTF8_FULL);
table.set_header(vec![
Cell::new("Crate").add_attribute(Attribute::Bold),
Cell::new("Functions").add_attribute(Attribute::Bold),
Cell::new("Crappy").add_attribute(Attribute::Bold),
]);
table
.column_mut(1)
.unwrap()
.set_cell_alignment(CellAlignment::Right);
table
.column_mut(2)
.unwrap()
.set_cell_alignment(CellAlignment::Right);
for r in &rollups {
table.add_row(vec![
Cell::new(&r.name),
Cell::new(r.total),
Cell::new(r.crappy),
]);
}
writeln!(out, "{table}")?;
Ok(())
}
pub(crate) fn write_per_crate_markdown(
entries: &[CrapEntry],
threshold: f64,
out: &mut dyn Write,
) -> Result<()> {
let rollups = crate_rollups(entries, threshold);
if rollups.is_empty() {
return Ok(());
}
writeln!(out, "## Per-crate summary")?;
writeln!(out)?;
writeln!(out, "| Crate | Functions | Crappy |")?;
writeln!(out, "|---|---:|---:|")?;
for r in &rollups {
writeln!(out, "| {} | {} | {} |", r.name, r.total, r.crappy)?;
}
writeln!(out)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn entry(
crate_name: Option<&str>,
function: &str,
crap: f64,
) -> CrapEntry {
CrapEntry {
file: PathBuf::from("src/lib.rs"),
function: function.into(),
line: 1,
cyclomatic: 1.0,
coverage: Some(100.0),
crap,
crate_name: crate_name.map(std::string::ToString::to_string),
}
}
#[test]
fn crate_rollups_aggregate_per_crate() {
let entries = vec![
entry(Some("alpha"), "a1", 1.0),
entry(Some("alpha"), "a2", 35.0), entry(Some("beta"), "b1", 5.0),
];
let rollups = crate_rollups(&entries, 30.0);
assert_eq!(rollups.len(), 2);
assert_eq!(rollups[0].name, "alpha");
assert_eq!(rollups[0].total, 2);
assert_eq!(rollups[0].crappy, 1);
assert_eq!(rollups[1].name, "beta");
assert_eq!(rollups[1].total, 1);
assert_eq!(rollups[1].crappy, 0);
}
#[test]
fn crate_rollups_ignore_untagged_entries() {
let entries = vec![
entry(None, "untagged", 5.0),
entry(Some("alpha"), "a1", 1.0),
];
let rollups = crate_rollups(&entries, 30.0);
assert_eq!(rollups.len(), 1);
assert_eq!(rollups[0].name, "alpha");
}
#[test]
fn crate_rollups_crappy_uses_strict_above() {
let entries = vec![
entry(Some("alpha"), "exactly", 30.0),
entry(Some("alpha"), "above", 30.1),
];
let rollups = crate_rollups(&entries, 30.0);
assert_eq!(
rollups[0].crappy, 1,
"exactly-at-threshold must NOT count as crappy"
);
}
#[test]
fn has_crate_data_detects_any_tagged_entry() {
let untagged = vec![entry(None, "x", 1.0), entry(None, "y", 2.0)];
let one_tagged = vec![entry(None, "x", 1.0), entry(Some("alpha"), "y", 2.0)];
assert!(!has_crate_data(&untagged));
assert!(has_crate_data(&one_tagged));
}
#[test]
fn write_per_crate_human_noop_when_no_crate_data() {
let entries = vec![entry(None, "x", 1.0)];
let mut buf = Vec::new();
write_per_crate_human(&entries, 30.0, &mut buf).unwrap();
assert!(
buf.is_empty(),
"no per-crate output when no entry has crate_name"
);
}
#[test]
fn write_per_crate_markdown_emits_gfm_table() {
let entries = vec![entry(Some("alpha"), "a1", 1.0)];
let mut buf = Vec::new();
write_per_crate_markdown(&entries, 30.0, &mut buf).unwrap();
let s = String::from_utf8(buf).unwrap();
assert!(s.contains("## Per-crate summary"));
assert!(s.contains("| Crate | Functions | Crappy |"));
assert!(s.contains("| alpha | 1 | 0 |"));
}
}