use crate::domain::debt::{Debt, DebtError, Measured, Measurement, Recorded, dimensions};
use crate::domain::finding::Finding;
use crate::domain::gate_id::GateId;
use crate::domain::rule_id::RuleId;
use crate::gates::{GateCtx, GateError, Violation};
const TIGHTEN: &str = "run 'sdd debt tighten --apply'";
pub(crate) fn read_debt(ctx: &GateCtx) -> Result<Debt, GateError> {
Debt::read(&ctx.repo_root).map_err(GateError::Debt)
}
fn label(gate: GateId, dimension: &str) -> &'static str {
dimensions(gate)
.iter()
.find(|spec| spec.name == dimension)
.map_or("", |spec| spec.label)
}
pub(crate) fn judge(
debt: &Debt,
gate: GateId,
rule: RuleId,
measurements: &[Measurement],
fresh: impl Fn(&Measurement) -> Violation,
) -> Vec<Violation> {
let mut violations = Vec::new();
for measurement in measurements {
let recorded = debt.recorded(gate, &measurement.path, measurement.dimension);
let noun = label(gate, measurement.dimension);
let stale = |reason: String| {
Violation::Finding(Finding::on_file(
rule,
measurement.path.as_str(),
format!("{reason}; {TIGHTEN}"),
))
};
match (recorded, measurement.value) {
(None, _) => {
if measurement.violates() {
violations.push(fresh(measurement));
}
}
(Some(Recorded::Ceiling(ceiling)), Measured::Count { value, budget }) => {
if value <= budget {
violations.push(stale(format!(
"{value} {noun} is within the budget of {budget} and a ceiling of {ceiling} is recorded"
)));
} else if value > ceiling {
violations.push(Violation::Finding(Finding::on_file(
rule,
measurement.path.as_str(),
format!("{value} {noun}, recorded ceiling is {ceiling}"),
)));
} else if value < ceiling {
violations.push(stale(format!(
"{value} {noun} is below the recorded ceiling of {ceiling}"
)));
}
}
(Some(Recorded::Exception), Measured::Flag(holds)) => {
if !holds {
violations.push(stale(format!(
"the recorded exception for {noun} is corrected"
)));
}
}
(Some(Recorded::Ceiling(_)), Measured::Flag(_))
| (Some(Recorded::Exception), Measured::Count { .. }) => {
violations.push(stale(format!(
"the recorded entry for {noun} is not of the kind this gate measures"
)));
}
}
}
for (path, dimension, _) in debt.recorded_for(gate) {
let measured = measurements
.iter()
.any(|m| crate::domain::debt::normalize(&m.path) == path && m.dimension == dimension);
if !measured {
violations.push(Violation::Finding(Finding::on_file(
rule,
path.as_str(),
format!(
"a {} entry is recorded and this gate measures no such path; {TIGHTEN}",
label(gate, dimension)
),
)));
}
}
violations
}
impl From<DebtError> for GateError {
fn from(error: DebtError) -> Self {
Self::Debt(error)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fresh(m: &Measurement) -> Violation {
Violation::Finding(Finding::on_file(
RuleId::ChapterStaysWithinLineCap,
m.path.as_str(),
"fresh",
))
}
fn debt(ceiling: usize) -> Debt {
let mut debt = Debt::default();
debt.record(
GateId::ChapterSizeCap,
"method/a.md",
"lines",
Recorded::Ceiling(ceiling),
);
debt
}
fn lines(value: usize) -> Vec<Measurement> {
vec![Measurement::count(
GateId::ChapterSizeCap,
"./method/a.md",
"lines",
value,
200,
)]
}
fn judged(debt: &Debt, measurements: &[Measurement]) -> Vec<String> {
judge(
debt,
GateId::ChapterSizeCap,
RuleId::ChapterStaysWithinLineCap,
measurements,
fresh,
)
.iter()
.map(ToString::to_string)
.collect()
}
#[test]
fn a_measurement_at_its_ceiling_passes() {
assert!(judged(&debt(300), &lines(300)).is_empty());
}
#[test]
fn a_measurement_above_its_ceiling_fails_citing_the_budget_rule() {
let out = judged(&debt(300), &lines(301));
assert_eq!(
out,
vec![
"FAIL docs-format:chapter-stays-within-200-lines ./method/a.md: 301 lines, recorded ceiling is 300"
.to_string()
]
);
}
#[test]
fn a_measurement_below_its_ceiling_fails_naming_tighten() {
let out = judged(&debt(300), &lines(250));
assert_eq!(out.len(), 1);
assert!(out[0].contains("below the recorded ceiling of 300"));
assert!(out[0].contains("sdd debt tighten --apply"));
}
#[test]
fn a_measurement_within_the_budget_with_an_entry_fails_naming_tighten() {
let out = judged(&debt(300), &lines(150));
assert_eq!(out.len(), 1);
assert!(out[0].contains("within the budget"));
assert!(out[0].contains("sdd debt tighten --apply"));
}
#[test]
fn a_fresh_violation_prints_the_gates_own_message() {
assert_eq!(
judged(&Debt::default(), &lines(201)),
vec![
"FAIL docs-format:chapter-stays-within-200-lines ./method/a.md: fresh".to_string()
]
);
assert!(judged(&Debt::default(), &lines(200)).is_empty());
}
#[test]
fn an_entry_the_gate_did_not_measure_fails_naming_tighten() {
let out = judged(&debt(300), &[]);
assert_eq!(out.len(), 1);
assert!(out[0].contains("method/a.md"));
assert!(out[0].contains("measures no such path"));
}
}