pub(crate) mod markdown;
pub(crate) mod text;
use crate::threshold::ThresholdSource;
use crate::verdict::{PackageOutcome, Status};
fn format_lines(outcome: &PackageOutcome) -> String {
match outcome.status {
Status::NoCoverableLines => "(no lines)".to_owned(),
Status::UnexpectedCoverableLines => {
let n = outcome.totals.count;
if n == 1 { "1 line".to_owned() } else { format!("{n} lines") }
}
_ => outcome.percent().map_or_else(|| "(no data)".to_owned(), |p| format!("{p:.1}%")),
}
}
fn format_threshold(outcome: &PackageOutcome) -> String {
match outcome.status {
Status::NoCoverableLines | Status::UnexpectedCoverableLines => "(no lines)".to_owned(),
_ => format!("{:.1}%", outcome.threshold.min_lines_percent),
}
}
fn format_delta(outcome: &PackageOutcome) -> String {
if matches!(outcome.status, Status::NoCoverableLines | Status::UnexpectedCoverableLines) {
return "—".to_owned();
}
let Some(pct) = outcome.percent() else {
return "—".to_owned();
};
let delta = pct - outcome.threshold.min_lines_percent;
let rounded = (delta * 10.0).round() / 10.0;
if rounded > 0.0 {
format!("+{rounded:.1}pp")
} else if rounded < 0.0 {
format!("{rounded:.1}pp")
} else {
"0.0pp".to_owned()
}
}
fn format_status_text(status: Status) -> &'static str {
match status {
Status::Ok => "OK",
Status::Fail => "FAIL",
Status::NoData => "NO DATA",
Status::NoCoverableLines => "EMPTY",
Status::UnexpectedCoverableLines => "NOT EMPTY",
}
}
fn format_status_markdown(status: Status) -> &'static str {
match status {
Status::Ok => "✅",
Status::Fail | Status::UnexpectedCoverableLines => "❌",
Status::NoData => "💥",
Status::NoCoverableLines => "➖",
}
}
fn format_source(source: ThresholdSource) -> &'static str {
source.as_str()
}
fn count_failures(outcomes: &[PackageOutcome]) -> usize {
outcomes.iter().filter(|o| o.status == Status::Fail).count()
}
fn count_no_data(outcomes: &[PackageOutcome]) -> usize {
outcomes.iter().filter(|o| o.status == Status::NoData).count()
}
fn count_unexpected_coverable_lines(outcomes: &[PackageOutcome]) -> usize {
outcomes.iter().filter(|o| o.status == Status::UnexpectedCoverableLines).count()
}
fn result_summary(outcomes: &[PackageOutcome]) -> String {
let mut clauses: Vec<String> = Vec::new();
let failures = count_failures(outcomes);
if failures > 0 {
clauses.push(format!("{} below threshold", packages(failures)));
}
let unexpected = count_unexpected_coverable_lines(outcomes);
if unexpected > 0 {
clauses.push(format!("{} with unexpected coverable lines", packages(unexpected)));
}
let no_data = count_no_data(outcomes);
if no_data > 0 {
clauses.push(format!("{} with no attributed coverage data", packages(no_data)));
}
if clauses.is_empty() {
"all packages meet their threshold.".to_owned()
} else {
format!("{}.", clauses.join(", "))
}
}
fn plural(n: usize, singular: &str, plural: &str) -> String {
if n == 1 {
format!("{n} {singular}")
} else {
format!("{n} {plural}")
}
}
fn packages(n: usize) -> String {
plural(n, "package", "packages")
}
fn files(n: usize) -> String {
plural(n, "file", "files")
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::aggregate::LineTotals;
use crate::threshold::Threshold;
fn outcome(count: u32, covered: u32, threshold: f64) -> PackageOutcome {
PackageOutcome {
name: "x".to_owned(),
threshold: Threshold {
min_lines_percent: threshold,
source: ThresholdSource::Package,
},
totals: LineTotals { count, covered },
status: Status::Ok,
}
}
fn outcome_with_status(count: u32, covered: u32, status: Status) -> PackageOutcome {
PackageOutcome {
name: "x".to_owned(),
threshold: Threshold {
min_lines_percent: 0.0,
source: ThresholdSource::Package,
},
totals: LineTotals { count, covered },
status,
}
}
#[test]
fn format_delta_renders_positive_with_sign() {
assert_eq!(format_delta(&outcome(100, 95, 80.0)), "+15.0pp");
}
#[test]
fn format_delta_renders_negative_with_sign() {
assert_eq!(format_delta(&outcome(100, 60, 80.0)), "-20.0pp");
}
#[test]
fn format_delta_collapses_sub_precision_noise_to_unsigned_zero() {
let o = outcome(100, 82, 82.0);
assert_eq!(format_delta(&o), "0.0pp");
let mut o = outcome(100, 82, 82.0);
o.totals = LineTotals {
count: 100_000_000,
covered: 82_000_001,
};
assert_eq!(format_delta(&o), "0.0pp");
let mut o = outcome(100, 82, 82.0);
o.totals = LineTotals {
count: 100_000_000,
covered: 81_999_999,
};
assert_eq!(format_delta(&o), "0.0pp");
}
#[test]
fn format_delta_returns_dash_for_no_data() {
let o = outcome(0, 0, 80.0);
assert_eq!(format_delta(&o), "—");
}
#[test]
fn plural_helpers_pick_singular_only_for_one() {
assert_eq!(packages(0), "0 packages");
assert_eq!(packages(1), "1 package");
assert_eq!(packages(2), "2 packages");
assert_eq!(files(0), "0 files");
assert_eq!(files(1), "1 file");
assert_eq!(files(2), "2 files");
}
#[test]
fn format_lines_for_assertion_statuses() {
assert_eq!(format_lines(&outcome_with_status(0, 0, Status::NoCoverableLines)), "(no lines)");
assert_eq!(format_lines(&outcome_with_status(1, 0, Status::UnexpectedCoverableLines)), "1 line");
assert_eq!(
format_lines(&outcome_with_status(7, 0, Status::UnexpectedCoverableLines)),
"7 lines"
);
}
#[test]
fn format_threshold_and_delta_for_assertion_statuses() {
for status in [Status::NoCoverableLines, Status::UnexpectedCoverableLines] {
let o = outcome_with_status(3, 1, status);
assert_eq!(format_threshold(&o), "(no lines)");
assert_eq!(format_delta(&o), "—");
}
}
#[test]
fn status_labels_cover_assertion_variants() {
assert_eq!(format_status_text(Status::NoCoverableLines), "EMPTY");
assert_eq!(format_status_text(Status::UnexpectedCoverableLines), "NOT EMPTY");
assert_eq!(format_status_markdown(Status::NoCoverableLines), "➖");
assert_eq!(format_status_markdown(Status::UnexpectedCoverableLines), "❌");
}
#[test]
fn result_summary_reports_each_category() {
assert_eq!(
result_summary(&[outcome_with_status(10, 10, Status::Ok)]),
"all packages meet their threshold."
);
assert_eq!(
result_summary(&[outcome_with_status(10, 1, Status::Fail)]),
"1 package below threshold."
);
assert_eq!(
result_summary(&[outcome_with_status(3, 0, Status::UnexpectedCoverableLines)]),
"1 package with unexpected coverable lines."
);
assert_eq!(
result_summary(&[outcome_with_status(0, 0, Status::NoData)]),
"1 package with no attributed coverage data."
);
assert_eq!(
result_summary(&[outcome_with_status(0, 0, Status::NoCoverableLines)]),
"all packages meet their threshold."
);
}
#[test]
fn result_summary_joins_all_failing_categories() {
let outcomes = vec![
outcome_with_status(10, 1, Status::Fail),
outcome_with_status(3, 0, Status::UnexpectedCoverableLines),
outcome_with_status(0, 0, Status::NoData),
];
assert_eq!(
result_summary(&outcomes),
"1 package below threshold, 1 package with unexpected coverable lines, 1 package with no attributed coverage data."
);
}
#[test]
fn count_helpers_select_their_status() {
let outcomes = vec![
outcome_with_status(10, 1, Status::Fail),
outcome_with_status(3, 0, Status::UnexpectedCoverableLines),
outcome_with_status(0, 0, Status::NoData),
outcome_with_status(0, 0, Status::NoCoverableLines),
];
assert_eq!(count_failures(&outcomes), 1);
assert_eq!(count_unexpected_coverable_lines(&outcomes), 1);
assert_eq!(count_no_data(&outcomes), 1);
}
}