use serde::Serialize;
use amont_runtime::registry;
use crate::scan::Repo;
pub fn all_checks() -> Vec<&'static str> {
registry::CHECKS.iter().map(|c| c.name).collect()
}
pub fn trigger_of(check: &str) -> &'static str {
registry::CHECKS
.iter()
.find(|c| c.name == check)
.map(|c| c.stage.as_str())
.unwrap_or("")
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CheckRollup {
pub name: &'static str,
pub trigger: &'static str,
pub applicable: usize,
pub active: usize,
pub skipped: usize,
pub inert: usize,
}
fn is_skipped(repo: &Repo, check: &str) -> bool {
repo.skips
.iter()
.any(|s| amont_runtime::skip_suppresses(check, &s.value))
}
pub fn rollup(repos: &[Repo]) -> Vec<CheckRollup> {
let managed: Vec<&Repo> = repos.iter().filter(|r| r.managed).collect();
registry::CHECKS
.iter()
.map(|check| {
let (mut applicable, mut skipped, mut inert) = (0, 0, 0);
for repo in &managed {
if repo.applicable.iter().any(|a| a == check.name) {
applicable += 1;
if is_skipped(repo, check.name) {
skipped += 1;
}
} else {
inert += 1;
}
}
CheckRollup {
name: check.name,
trigger: check.stage.as_str(),
applicable,
active: applicable - skipped,
skipped,
inert,
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scan::AgentsMdState;
use crate::shim::{BakeState, ShimState};
use std::path::PathBuf;
fn repo_with_files(files: &[&str], skips: &[&str], managed: bool) -> Repo {
let paths: Vec<String> = files.iter().map(|s| s.to_string()).collect();
let applicable = crate::scan::applicable_from_paths(&paths);
Repo {
path: PathBuf::from("r"),
managed,
shims: vec![ShimState::Ok { baked: "/b".into() }; 4],
baked: BakeState::Current,
stale_ours: Vec::new(),
foreign_subs: Vec::new(),
hook_pkgjson: false,
languages: Vec::new(),
applicable,
skips: skips.iter().map(|s| crate::skips::for_test(s)).collect(),
severities: Vec::new(),
declared: Vec::new(),
trusted: None,
agents_md: AgentsMdState::Missing,
hooks_dir: crate::scan::HooksDir::In {
path: std::path::PathBuf::from(".git/hooks"),
},
shares_hooks_with: None,
}
}
fn repo(langs: &[&str], skips: &[&str], managed: bool) -> Repo {
let mut files = Vec::new();
for l in langs {
match *l {
"rust" => files.extend(["src/main.rs", "Cargo.toml"]),
"js" => files.extend(["a.ts", "package.json"]),
"python" => files.extend(["a.py", "pyproject.toml"]),
"k8s" => files.extend(["k8s/a.yaml", "kustomization.yaml"]),
other => panic!("unknown language in fixture: {other}"),
}
}
repo_with_files(&files, skips, managed)
}
#[allow(dead_code)]
fn unused_repo(langs: &[&str], skips: &[&str], managed: bool) -> Repo {
Repo {
path: PathBuf::from("r"),
managed,
shims: vec![ShimState::Ok { baked: "/b".into() }; 4],
baked: BakeState::Current,
stale_ours: Vec::new(),
foreign_subs: Vec::new(),
hook_pkgjson: false,
languages: langs.iter().map(|s| s.to_string()).collect(),
applicable: Vec::new(),
skips: skips.iter().map(|s| crate::skips::for_test(s)).collect(),
severities: Vec::new(),
declared: Vec::new(),
trusted: None,
agents_md: AgentsMdState::Missing,
hooks_dir: crate::scan::HooksDir::In {
path: std::path::PathBuf::from(".git/hooks"),
},
shares_hooks_with: None,
}
}
fn find<'a>(rs: &'a [CheckRollup], name: &str) -> &'a CheckRollup {
rs.iter().find(|r| r.name == name).expect("check")
}
#[test]
fn a_check_with_no_matching_manifest_is_inert_not_failing() {
let rs = rollup(&[repo(&["python"], &[], true)]);
let clippy = find(&rs, "pre-commit-clippy");
assert_eq!(clippy.applicable, 0);
assert_eq!(clippy.inert, 1);
assert_eq!(clippy.active, 0);
assert_eq!(clippy.skipped, 0, "inert is not skipped");
}
#[test]
fn rows_sum_across() {
let rs = rollup(&[
repo(&["rust"], &[], true),
repo(&["js"], &[], true),
repo(&["python"], &[], true),
]);
for r in &rs {
assert_eq!(
r.applicable + r.inert,
3,
"{} must account for every managed repo",
r.name
);
assert_eq!(r.active + r.skipped, r.applicable, "{}", r.name);
}
}
#[test]
fn skips_resolve_as_the_dispatcher_resolves_them() {
let rs = rollup(&[repo(&["rust"], &["clippy"], true)]);
let clippy = find(&rs, "pre-commit-clippy");
assert_eq!(clippy.applicable, 1);
assert_eq!(clippy.skipped, 1);
assert_eq!(clippy.active, 0);
assert_eq!(find(&rs, "pre-commit-cargo-fmt").active, 1);
}
#[test]
fn unmanaged_repos_are_not_counted_at_all() {
let rs = rollup(&[repo(&["rust"], &[], false)]);
for r in &rs {
assert_eq!(r.applicable + r.inert, 0, "{}", r.name);
}
}
#[test]
fn only_genuinely_unconditional_checks_apply_everywhere() {
let rs = rollup(&[repo(&[], &[], true), repo(&["js"], &[], true)]);
assert_eq!(
find(&rs, "pre-push-branch-protect").applicable,
2,
"branch-protect has no file condition"
);
assert_eq!(
find(&rs, "pre-commit-merge-conflict").applicable,
2,
"nor does merge-conflict"
);
assert_eq!(
find(&rs, "pre-commit-ban-terms").applicable,
1,
"ban-terms only scans JS-ish files, whatever the old table said"
);
}
#[test]
fn the_table_covers_twenty_checks() {
assert_eq!(all_checks().len(), 20);
let mut names: Vec<&str> = all_checks();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), 20, "duplicate check name");
}
}