use std::collections::BTreeSet;
use std::path::Path;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::check::{Key, Violation};
const HEADER: &str = "# Legalized architecture violations. Generated by `easement baseline`.\n\
# Fix them and regenerate: the gate fails on new violations and on stale entries.\n";
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Baseline {
pub violations: BTreeSet<Key>,
}
impl Baseline {
pub fn load(path: &Path) -> Result<Option<Self>> {
if !path.exists() {
return Ok(None);
}
let text = std::fs::read_to_string(path)
.with_context(|| format!("cannot read baseline {}", path.display()))?;
let baseline = serde_norway::from_str(&text)
.with_context(|| format!("invalid baseline {}", path.display()))?;
Ok(Some(baseline))
}
pub fn from_violations(violations: &[Violation]) -> Self {
Self {
violations: violations.iter().map(Violation::key).collect(),
}
}
pub fn save(&self, path: &Path) -> Result<()> {
let body = serde_norway::to_string(self)?;
std::fs::write(path, format!("{HEADER}{body}"))
.with_context(|| format!("cannot write baseline {}", path.display()))
}
}
pub struct Verdict {
pub new: Vec<Violation>,
pub stale: Vec<Key>,
pub legalized: usize,
}
impl Verdict {
pub fn judge(
violations: Vec<Violation>,
current: &BTreeSet<Key>,
baseline: Option<&Baseline>,
) -> Self {
let Some(baseline) = baseline else {
return Self {
new: violations,
stale: Vec::new(),
legalized: 0,
};
};
let stale = baseline.violations.difference(current).cloned().collect();
let (legal, new): (Vec<_>, Vec<_>) = violations
.into_iter()
.partition(|v| baseline.violations.contains(&v.key()));
Self {
new,
stale,
legalized: legal.len(),
}
}
pub fn passed(&self) -> bool {
self.new.is_empty() && self.stale.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn v(file: &str, to: &str) -> Violation {
Violation {
rule: crate::check::RuleId::Dependency,
file: file.into(),
line: 1,
message: String::new(),
because: None,
from: "a".into(),
to: to.into(),
target: format!("{to}/x"),
specifier: String::new(),
evidence: Vec::new(),
}
}
#[test]
fn one_baseline_entry_legalizes_every_use_of_its_key() {
let mut second = v("f1", "b");
second.line = 9;
let baseline = Baseline::from_violations(&[v("f1", "b")]);
let found = vec![v("f1", "b"), second];
let current = found.iter().map(Violation::key).collect();
let verdict = Verdict::judge(found, ¤t, Some(&baseline));
assert_eq!(verdict.legalized, 2);
assert!(verdict.passed());
}
#[test]
fn ratchet_fails_on_new_and_stale() {
let baseline = Baseline::from_violations(&[v("f1", "b"), v("f2", "b")]);
let found = vec![v("f1", "b"), v("f3", "c")];
let current = found.iter().map(Violation::key).collect();
let verdict = Verdict::judge(found, ¤t, Some(&baseline));
assert_eq!(verdict.legalized, 1);
assert_eq!(verdict.new.len(), 1);
assert_eq!(verdict.new[0].file, "f3");
assert_eq!(verdict.stale.len(), 1);
assert_eq!(verdict.stale[0].file, "f2");
assert!(!verdict.passed());
}
}