use crate::finding::{Finding, Severity};
use crate::instance::{Instance, Outcome as InstanceOutcome};
use crate::scope::Scope;
use headwater_census::census::Census;
pub const RULE: &str = "coverage.document_unchecked";
pub const SCOPE: Scope = Scope::corpus(false, false, false);
pub const VERSION: u32 = 1;
pub const EXPORTABLE_AS: crate::scope::ExportTargets = &[];
#[derive(Clone, Debug)]
pub struct Document {
pub path: String,
pub class: &'static str,
pub created: usize,
pub ran: usize,
pub skipped: Vec<(&'static str, String)>,
}
pub const BESIDE_THE_ROOT: &[&str] = &[crate::claim::STORE];
#[derive(Clone, Debug)]
pub struct Coverage {
pub documents: Vec<Document>,
pub instances: usize,
pub unaccounted: Vec<String>,
skips: Vec<(String, usize)>,
}
impl Coverage {
pub fn of(census: &Census, instances: &[Instance]) -> Self {
let mut documents: Vec<Document> = census
.rows
.iter()
.map(|row| Document {
path: row.path.clone(),
class: row.outcome.class(),
created: 0,
ran: 0,
skipped: Vec::new(),
})
.collect();
let mut unaccounted = Vec::new();
let mut skips: Vec<(String, usize)> = Vec::new();
for instance in instances {
if let InstanceOutcome::Skipped(ref reason) = instance.outcome {
match skips.iter_mut().find(|(known, _)| known == reason) {
Some((_, count)) => *count += 1,
None => skips.push((reason.clone(), 1)),
}
}
for path in instance.paths() {
let Some(document) = documents.iter_mut().find(|d| d.path == path) else {
if !BESIDE_THE_ROOT.contains(&path)
&& !census.outside.holds(path)
&& !unaccounted.iter().any(|known| known == path)
{
unaccounted.push(path.to_string());
}
continue;
};
if !instance.grain.routes() {
continue;
}
document.created += 1;
match instance.outcome {
InstanceOutcome::Skipped(ref reason) => {
document.skipped.push((instance.rule, reason.clone()));
}
_ => document.ran += 1,
}
}
}
Coverage {
documents,
instances: instances.len(),
unaccounted,
skips,
}
}
pub fn seen(&self) -> usize {
self.documents.len()
}
pub fn classified(&self) -> usize {
self.documents.iter().filter(|d| d.class == "typed").count()
}
pub fn generated(&self) -> usize {
self.documents
.iter()
.filter(|d| d.class == "generated")
.count()
}
pub fn checked(&self) -> usize {
self.documents
.iter()
.filter(|d| d.class == "typed" && d.ran > 0)
.count()
}
pub fn skips(&self) -> &[(String, usize)] {
&self.skips
}
pub fn skipped(&self) -> usize {
self.skips.iter().map(|(_, count)| count).sum()
}
pub fn findings(&self) -> Vec<Finding> {
self.documents
.iter()
.filter(|document| document.class == "typed" && document.ran == 0)
.map(|document| Finding {
rule: RULE,
severity: Severity::Warn,
obligation: None,
path: document.path.clone(),
line: 0,
column: 0,
message: match document.created {
0 => "this document is classified and no check instance was created for it"
.to_string(),
created => format!(
"this document is classified and all {created} of its check \
instances were skipped"
),
},
remediation: "check that the shelf pattern claims the right files, and that \
the kind declares something a check reads"
.to_string(),
patch: None,
})
.collect()
}
pub fn render(&self) -> String {
use std::fmt::Write;
let mut out = String::new();
let _ = writeln!(
out,
"{} seen, {} classified, {} checked, {} check instances",
self.seen(),
self.classified(),
self.checked(),
self.instances
);
if self.generated() > 0 {
let _ = writeln!(
out,
" {:5} generated, held to regeneration by `headwater generate --check`",
self.generated()
);
}
for (reason, count) in self.skips() {
let _ = writeln!(out, " {count:5} skipped: {reason}");
}
for path in &self.unaccounted {
let _ = writeln!(out, " a check read {path}, which the census never walked");
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::instance::Input;
use crate::scope::Grain;
use headwater_census::census::{Outcome as Walked, Row};
fn row(path: &str) -> Row {
Row {
path: path.to_string(),
outcome: Walked::NotADocument,
document: None,
digest: None,
}
}
fn input(path: &str) -> Input {
Input {
path: path.to_string(),
digest: None,
}
}
fn over(rows: Vec<Row>, instances: Vec<Instance>) -> Coverage {
Coverage::of(
&Census {
rows,
outside: Default::default(),
},
&instances,
)
}
#[test]
fn an_edge_scoped_skip_is_one_instance_and_two_routed_documents() {
let coverage = over(
vec![row("source.md"), row("target.md")],
vec![Instance::skipped(
"dependency.terminal",
Grain::Edge,
vec![input("source.md"), input("target.md")],
"no state to read at the target end",
)],
);
assert_eq!(coverage.instances, 1);
assert_eq!(coverage.skipped(), 1, "one instance reached no verdict");
assert_eq!(
coverage.skips(),
&[("no state to read at the target end".to_string(), 1)]
);
let routed: usize = coverage
.documents
.iter()
.map(|document| document.skipped.len())
.sum();
assert_eq!(routed, 2, "and the routing accounts for it at both ends");
}
#[test]
fn a_skip_routed_to_no_document_is_still_a_skip() {
let coverage = over(
vec![row("source.md")],
vec![Instance::skipped(
"coverage.document_unchecked",
Grain::Corpus,
vec![input("source.md")],
"the census carries no row this rule can read",
)],
);
assert_eq!(coverage.skipped(), 1);
let routed: usize = coverage
.documents
.iter()
.map(|document| document.skipped.len())
.sum();
assert_eq!(routed, 0, "and the routing sees none of it");
}
#[test]
fn an_unaccounted_path_is_listed_once_and_the_claim_store_not_at_all() {
let reading = |rule, grain, path| {
Instance::of(rule, grain, vec![input(path)], InstanceOutcome::Passed)
};
let coverage = over(
vec![row("source.md")],
vec![
reading("claim.unclaimed", Grain::Corpus, crate::claim::STORE),
reading("claim.empty", Grain::Corpus, crate::claim::STORE),
reading("facet.required.missing", Grain::Document, "stray.md"),
reading("link.target.missing", Grain::Document, "stray.md"),
],
);
assert_eq!(coverage.unaccounted, vec!["stray.md".to_string()]);
}
#[test]
fn a_run_that_skipped_nothing_has_a_number_for_it() {
let coverage = over(
vec![row("source.md")],
vec![Instance::of(
"facet.required.missing",
Grain::Document,
vec![input("source.md")],
InstanceOutcome::Passed,
)],
);
assert_eq!(coverage.skipped(), 0);
assert!(coverage.skips().is_empty());
}
}