use clap::CommandFactory;
fn leaf_paths() -> Vec<String> {
let cmd = anvil::commands::Cli::command();
let mut leaves = Vec::new();
collect(&cmd, String::new(), &mut leaves);
leaves.sort();
leaves
}
fn collect(cmd: &clap::Command, prefix: String, out: &mut Vec<String>) {
let subs: Vec<&clap::Command> = cmd.get_subcommands().collect();
if subs.is_empty() {
if !prefix.is_empty() {
out.push(prefix);
}
return;
}
for sub in subs {
let name = sub.get_name();
let path = if prefix.is_empty() {
name.to_string()
} else {
format!("{prefix} {name}")
};
collect(sub, path, out);
}
}
const COVERED_LEAVES: &[&str] = &[
"agent approve",
"agent list",
"agent reject",
"agent session",
"agent sessions",
"agent trigger",
"agent view",
"auth git-credential",
"auth login",
"auth logout",
"auth rotate",
"auth status",
"board create-column",
"board delete-column",
"board edit-column",
"board init",
"board list",
"branch list",
"ci cancel",
"ci delete-secret",
"ci job-view",
"ci list",
"ci run",
"ci secrets",
"ci set-secret",
"ci view",
"commit list",
"commit view",
"deploy create",
"deploy env create",
"deploy env list",
"deploy list",
"deploy status",
"epic add-child",
"epic auto-close",
"epic children",
"epic list",
"epic mark",
"epic remove-child",
"epic view",
"issue assign",
"issue close",
"issue comment",
"issue comments",
"issue create",
"issue create-milestone",
"issue edit",
"issue link",
"issue link-req",
"issue links",
"issue list",
"issue milestones",
"issue move",
"issue reopen",
"issue unassign",
"issue unlink",
"issue unlink-req",
"issue view",
"label add",
"label create",
"label delete",
"label edit",
"label list",
"label remove",
"milestone close",
"milestone create",
"milestone delete",
"milestone edit",
"milestone list",
"milestone reopen",
"milestone view",
"pr checkout",
"pr close",
"pr comment",
"pr create",
"pr diff",
"pr edit",
"pr list",
"pr merge",
"pr reopen",
"pr review",
"pr reviews",
"pr view",
"registry token create",
"registry token delete",
"registry token list",
"release assets",
"release changelog",
"release create",
"release delete",
"release delete-asset",
"release download",
"release list",
"release publish",
"release update",
"release upload",
"release view",
"repo clone",
"repo create",
"repo list",
"repo set-default",
"repo view",
"requirement applicability add",
"requirement applicability list",
"requirement applicability remove",
"requirement create",
"requirement delete",
"requirement export",
"requirement import",
"requirement link",
"requirement list",
"requirement matrix",
"requirement seed",
"requirement status",
"requirement unlink",
"requirement update",
"requirement view",
"runner configure",
"runner doctor",
"runner list",
"runner logs",
"runner remove",
"runner restart",
"runner service install",
"runner service list",
"runner service restart",
"runner service start",
"runner service status",
"runner service stop",
"runner service uninstall",
"runner start",
"runner status",
"runner stop",
"runner token",
"runner unconfigure",
"runner update",
"runner view",
"ssh-key add",
"ssh-key list",
"ssh-key remove",
"update",
];
#[test]
fn every_leaf_is_covered() {
let actual = leaf_paths();
let expected: Vec<String> = COVERED_LEAVES.iter().map(|s| s.to_string()).collect();
let missing: Vec<&String> = actual.iter().filter(|p| !expected.contains(p)).collect();
let stale: Vec<&String> = expected.iter().filter(|p| !actual.contains(p)).collect();
assert!(
missing.is_empty() && stale.is_empty(),
"leaf-command inventory drifted from the JSON contract.\n\
NEW leaves not yet covered (make run() return an output::Response, then add here): {missing:?}\n\
STALE entries no longer in the CLI (remove from COVERED_LEAVES): {stale:?}",
);
}