Something went wrong. Try again.
Identities for entities did.bot
atproto agent llm did
Something went wrong. Try again.
7.6 kB · 203 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204//! The CLI reference in the workspace's `README.md`, made from each binary's//! own `--help`.//!//! Every binary a person runs has a `cli_reference` test that renders its//! section and fails while the README holds anything else, so a new//! subcommand or flag cannot land without its entry. A clap binary also fails//! on any subcommand or argument with no help text, since that text is the//! whole reference. `scripts/gen-cli-reference.sh` writes every section again.//!//! Behind the `reference` feature, which only those tests turn on.
use std::path::PathBuf;
use clap::Command;
/// Set to anything but empty, and a stale section is written rather than/// failed.pub const BLESS: &str = "DIDBOT_BLESS_README";
/// The column help text is wrapped at, whatever terminal the test runs in.const WIDTH: usize = 100;
/// What a developer runs when a section is stale.const FIX: &str = "run scripts/gen-cli-reference.sh and stage README.md";
/// Checks the section of a binary whose parser is clap.////// # Panics////// When a subcommand or argument has no help text, or the section is stale/// and [`BLESS`] is unset.pub fn check_command(binary: &str, command: Command) { let mut command = command.term_width(WIDTH); command.build(); let missing = undocumented(&command); assert!( missing.is_empty(), "{binary}: give these help text, which the README's CLI reference is made of:\n {}", missing.join("\n ") ); check(binary, §ion(binary, &pages(binary, &command)));}
/// Checks the section of a binary that prints a hand-written usage.////// # Panics////// When the section is stale and [`BLESS`] is unset.pub fn check_usage(binary: &str, usage: &str) { check( binary, §ion(binary, &[(binary.to_owned(), usage.to_owned())]), );}
fn check(binary: &str, section: &str) { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../README.md"); let readme = std::fs::read_to_string(&path).expect("the workspace README"); let Some(wanted) = stale(&readme, binary, section).unwrap_or_else(|why| panic!("{why}")) else { return; }; if std::env::var_os(BLESS).is_some_and(|set| !set.is_empty()) { std::fs::write(&path, wanted).expect("the workspace README is writable"); return; } panic!("README.md's CLI reference for {binary} is not what its --help prints: {FIX}");}
/// The README with `binary`'s section replaced by `section`, or `None` when/// it already holds exactly that.fn stale(readme: &str, binary: &str, section: &str) -> Result<Option<String>, String> { let begin = format!("<!-- cli-reference {binary} -->\n"); let end = format!("<!-- /cli-reference {binary} -->"); let missing = || format!("README.md has no `{begin}` … `{end}` pair to write {binary} into"); let start = readme.find(&begin).ok_or_else(missing)? + begin.len(); let stop = start + readme[start..].find(&end).ok_or_else(missing)?; if &readme[start..stop] == section { return Ok(None); } Ok(Some(format!( "{}{section}{}", &readme[..start], &readme[stop..] )))}
/// One collapsed block per binary, and in it each command's help under a/// heading naming what a person types.fn section(binary: &str, pages: &[(String, String)]) -> String { let mut out = format!("<details>\n<summary><code>{binary}</code></summary>\n\n"); for (title, help) in pages { let help: Vec<&str> = help.trim_end().lines().map(str::trim_end).collect(); out.push_str(&format!( "### `{title}`\n\n```text\n{}\n```\n\n", help.join("\n") )); } out.push_str("</details>\n"); out}
/// Every command a person can reach, each with its `--help`: the root under/// the binary's name, and each subcommand as a person types it.fn pages(binary: &str, command: &Command) -> Vec<(String, String)> { let mut out = Vec::new(); walk(command, &mut |sub| { let title = match sub.get_bin_name() { Some(typed) if !std::ptr::eq(sub, command) => typed, _ => binary, }; out.push((title.to_owned(), sub.clone().render_long_help().to_string())); }); out}
/// Every subcommand and argument with no help text, by where it sits.fn undocumented(command: &Command) -> Vec<String> { let mut out = Vec::new(); walk(command, &mut |command| { let title = command.get_bin_name().unwrap_or(command.get_name()); if command.get_about().is_none() && command.get_long_about().is_none() { out.push(title.to_owned()); } for arg in command.get_arguments().filter(|arg| !arg.is_hide_set()) { if arg.get_help().is_none() && arg.get_long_help().is_none() { out.push(format!("{title}: {}", arg.get_id())); } } }); out}
/// `visit` on `command` and every visible subcommand beneath it, skipping the/// `help` subcommand clap adds.fn walk(command: &Command, visit: &mut impl FnMut(&Command)) { visit(command); for sub in command.get_subcommands() { if !sub.is_hide_set() && sub.get_name() != "help" { walk(sub, visit); } }}
#[cfg(test)]mod tests { use clap::{Arg, Command};
use super::{pages, section, stale, undocumented};
fn tool() -> Command { Command::new("tool") .about("Does a thing") .subcommand(Command::new("list").about("Lists the things")) }
fn rendered(mut command: Command) -> String { command.build(); section("tool", &pages("tool", &command)) }
fn readme(section: &str) -> String { format!("# Title\n\n<!-- cli-reference tool -->\n{section}<!-- /cli-reference tool -->\n\nAfter.\n") }
/// A README written from one parser is current for it, and stale as soon /// as a subcommand is added without writing it again. The rewrite holds /// the new subcommand and leaves the rest of the file alone. #[test] fn a_new_subcommand_makes_the_written_reference_stale() { let written = readme(&rendered(tool())); assert_eq!(stale(&written, "tool", &rendered(tool())), Ok(None));
let grown = tool().subcommand(Command::new("export").about("Exports the things")); let fixed = stale(&written, "tool", &rendered(grown.clone())) .unwrap() .expect("the added subcommand is not in the README yet"); assert!(fixed.contains("### `tool export`"), "{fixed}"); assert!(fixed.starts_with("# Title\n\n") && fixed.ends_with("\n\nAfter.\n")); assert_eq!(stale(&fixed, "tool", &rendered(grown)), Ok(None)); }
/// A binary with no place in the README is refused rather than appended /// somewhere a person did not choose. #[test] fn a_readme_without_the_markers_is_refused() { let error = stale("# Title\n", "tool", &rendered(tool())).unwrap_err(); assert!(error.contains("<!-- cli-reference tool -->"), "{error}"); }
/// A flag or subcommand with no help text is named, so its section never /// carries an entry that says nothing. #[test] fn an_argument_or_subcommand_without_help_is_named() { let mut command = tool() .arg(Arg::new("quiet").long("quiet")) .subcommand(Command::new("export")); command.build(); assert_eq!( undocumented(&command), ["tool: quiet", "tool export"].map(str::to_owned) ); }}