use std::{collections::BTreeSet, path::PathBuf};
use clap::{Command, CommandFactory};
use dora_cli::Args;
const SNAPSHOT: &str = "cli-surface.txt";
const HEADER: &str = "\
# The `dora` command surface, frozen for dora 1.x (docs/api-rust.md).
#
# Generated -- do not edit by hand:
# UPDATE_CLI_SURFACE=1 cargo test -p dora-cli --test cli_surface
#
# One line per command, then one per argument. Hidden commands are included:
# `dora daemon` and `dora coordinator` are undocumented but scripted against.
# Removing any line is a breaking change; adding one is not.
";
fn surface() -> String {
let mut lines = BTreeSet::new();
walk(&Args::command(), "dora", &mut lines);
let body: Vec<_> = lines.into_iter().collect();
format!("{HEADER}\n{}\n", body.join("\n"))
}
fn walk(command: &Command, path: &str, lines: &mut BTreeSet<String>) {
lines.insert(path.to_owned());
for alias in command.get_all_aliases() {
lines.insert(format!("{path} | alias: {alias}"));
}
for arg in command.get_arguments() {
lines.insert(format!("{path} | {}", describe(arg)));
}
for sub in command.get_subcommands() {
walk(sub, &format!("{path} {}", sub.get_name()), lines);
}
}
fn describe(arg: &clap::Arg) -> String {
let mut spelling = Vec::new();
if let Some(short) = arg.get_short() {
spelling.push(format!("-{short}"));
}
if let Some(long) = arg.get_long() {
spelling.push(format!("--{long}"));
}
let mut item = if spelling.is_empty() {
format!("<{}>", arg.get_id())
} else {
spelling.join("/")
};
if arg.get_action().takes_values() && arg.get_long().is_some() {
item.push_str(" <value>");
}
if arg.is_required_set() {
item.push_str(" (required)");
}
item
}
fn snapshot_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SNAPSHOT)
}
#[test]
fn cli_surface_snapshot_is_current() {
let path = snapshot_path();
let current = surface();
if std::env::var_os("UPDATE_CLI_SURFACE").is_some() {
std::fs::write(&path, ¤t).expect("failed to write the CLI surface snapshot");
return;
}
let recorded = std::fs::read_to_string(&path).unwrap_or_else(|error| {
panic!(
"cannot read {}: {error}\n\
Generate it with: UPDATE_CLI_SURFACE=1 cargo test -p dora-cli --test cli_surface",
path.display()
)
});
if recorded != current {
let recorded_lines: BTreeSet<_> = recorded.lines().filter(|l| keep(l)).collect();
let current_lines: BTreeSet<_> = current.lines().filter(|l| keep(l)).collect();
let render = |lines: BTreeSet<&&str>| {
if lines.is_empty() {
"(none)".to_owned()
} else {
lines
.iter()
.map(|line| line.to_string())
.collect::<Vec<_>>()
.join("\n ")
}
};
panic!(
"the `dora` command surface changed.\n\n\
removed (breaking -- dora 1.x froze these):\n {}\n\n\
added (fine, but record it):\n {}\n\n\
Update the snapshot in this commit so the change is reviewed:\n \
UPDATE_CLI_SURFACE=1 cargo test -p dora-cli --test cli_surface",
render(recorded_lines.difference(¤t_lines).collect()),
render(current_lines.difference(&recorded_lines).collect()),
);
}
}
fn keep(line: &str) -> bool {
!line.trim().is_empty() && !line.trim_start().starts_with('#')
}
#[test]
fn snapshot_covers_the_command_tree() {
let text = surface();
for command in [
"dora build",
"dora start",
"dora run",
"dora up",
"dora daemon",
] {
assert!(
text.lines().any(|line| line == command),
"`{command}` is missing from the generated surface"
);
}
assert!(
text.lines().filter(|l| keep(l)).count() > 100,
"the generated surface is implausibly small: the walk is not reaching the tree"
);
}