use headwater_cli::{command, GLOBALS};
use std::path::{Path, PathBuf};
use std::process::Command as Process;
struct Ran {
code: Option<i32>,
out: String,
err: String,
}
struct Scratch(PathBuf);
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
impl std::ops::Deref for Scratch {
type Target = Path;
fn deref(&self) -> &Path {
&self.0
}
}
impl AsRef<Path> for Scratch {
fn as_ref(&self) -> &Path {
&self.0
}
}
impl AsRef<std::ffi::OsStr> for Scratch {
fn as_ref(&self) -> &std::ffi::OsStr {
self.0.as_os_str()
}
}
#[test]
fn a_scratch_directory_is_removed_when_its_case_panics() {
let at = std::env::temp_dir().join(format!("headwater-cli-help-{}-panics", std::process::id()));
let held = at.clone();
let unwound = std::panic::catch_unwind(move || {
let scratch = Scratch(held);
std::fs::create_dir_all(&scratch).expect("the directory is there");
assert!(scratch.is_dir(), "the directory was made");
panic!("a failed assertion in the case");
});
assert!(unwound.is_err(), "the closure panicked");
assert!(!at.exists(), "{} survived the panic", at.display());
}
fn ran(label: &str, arguments: &[&str]) -> Ran {
let at = Scratch(
std::env::temp_dir().join(format!("headwater-cli-help-{}-{label}", std::process::id())),
);
let _ = std::fs::remove_dir_all(&at);
std::fs::create_dir_all(&at).expect("the directory is there");
let output = Process::new(env!("CARGO_BIN_EXE_headwater"))
.args(arguments)
.current_dir(&at)
.output()
.expect("the binary runs");
Ran {
code: output.status.code(),
out: String::from_utf8_lossy(&output.stdout).into_owned(),
err: String::from_utf8_lossy(&output.stderr).into_owned(),
}
}
fn flat(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn help_of(words: &[&str]) -> String {
let mut root = command();
root.build();
let mut cursor = &mut root;
for word in words {
cursor = cursor
.find_subcommand_mut(word)
.unwrap_or_else(|| panic!("the parser answers to `{}`", words.join(" ")));
}
cursor.render_help().to_string()
}
#[test]
fn every_argument_the_parser_admits_says_what_it_is() {
fn walk(prefix: &str, command: &clap::Command, silent: &mut Vec<String>) {
for argument in command.get_arguments() {
let says = argument
.get_help()
.map(ToString::to_string)
.unwrap_or_default();
if says.trim().is_empty() {
silent.push(format!("{prefix}: {}", argument.get_id()));
}
}
for word in command.get_subcommands() {
walk(&format!("{prefix} {}", word.get_name()), word, silent);
}
}
let mut root = command();
root.build();
let mut silent = Vec::new();
walk(headwater_verbs::BINARY, &root, &mut silent);
assert!(
silent.is_empty(),
"{} arguments of this parser carry no help text, and a caller who meets one has nothing \
to read: {silent:#?}",
silent.len()
);
}
#[test]
fn the_long_description_of_a_verb_is_reachable_three_ways() {
let verb = headwater_verbs::parse("check").expect("`check` is a verb of this binary");
let routes = [
("flag-long", vec!["check", "--help"]),
("flag-short", vec!["check", "-h"]),
("verb", vec!["help", "check"]),
];
let mut rendered: Vec<(String, String)> = Vec::new();
for (label, arguments) in routes {
let ran = ran(label, &arguments);
assert_eq!(
ran.code,
Some(0),
"`headwater {}` is a question rather than a mistake:\n{}{}",
arguments.join(" "),
ran.out,
ran.err
);
assert_eq!(
ran.err,
"",
"`headwater {}` writes nothing to standard error",
arguments.join(" ")
);
assert!(
flat(&ran.out).contains(&flat(verb.description)),
"`headwater {}` carries the description the dispatch table holds for `check`:\n{}",
arguments.join(" "),
ran.out
);
rendered.push((arguments.join(" "), ran.out));
}
let (first, text) = &rendered[0];
for (other, also) in &rendered[1..] {
assert_eq!(
text, also,
"`headwater {first}` and `headwater {other}` print the same bytes"
);
}
}
#[test]
fn the_first_screen_prints_the_group_and_the_summary_the_table_carries() {
let screen = flat(&help_of(&[]));
for group in headwater_verbs::groups() {
assert!(
screen.contains(&format!("{group}:")),
"the first screen carries the heading `{group}:`"
);
}
for verb in headwater_verbs::VERBS {
assert!(
screen.contains(&flat(verb.summary)),
"the first screen carries `{}`'s summary from the dispatch table",
verb.name
);
assert!(
screen.contains(&format!(" {} ", verb.name)),
"the first screen names `{}`",
verb.name
);
}
}
#[test]
fn the_long_description_of_every_command_line_is_what_the_table_carries() {
for verb in headwater_verbs::VERBS {
let rendered = flat(&help_of(&[verb.name]));
assert!(
rendered.contains(&flat(verb.description)),
"`headwater {} --help` carries the description the table holds:\n{rendered}",
verb.name
);
for word in verb.words {
let rendered = flat(&help_of(&[verb.name, word.name]));
assert!(
rendered.contains(&flat(word.description)),
"`headwater {} {} --help` carries the description the table holds:\n{rendered}",
verb.name,
word.name
);
}
}
}
#[test]
fn every_global_flag_is_one_line_on_the_first_screen() {
let mut root = command();
root.build();
for argument in root.get_arguments() {
let id = argument.get_id().as_str();
assert!(
GLOBALS.iter().any(|one| one.covers(id)),
"`{id}` is printed on the first screen and `GLOBALS` does not name it, so the screen \
would carry it with no summary or with its whole description"
);
}
let screen = help_of(&[]);
let block: Vec<&str> = screen
.lines()
.skip_while(|line| *line != "Global flags:")
.skip(1)
.take_while(|line| !line.trim().is_empty())
.collect();
assert_eq!(
block.len(),
GLOBALS.len(),
"the first screen gives {} lines to {} global flags, and HW-DR-0042 holds it to one line \
each:\n{}",
block.len(),
GLOBALS.len(),
block.join("\n")
);
for one in GLOBALS {
assert!(
block.iter().any(|line| line.contains(one.summary)),
"the first screen carries `{}`'s summary on one line of its own",
one.name
);
}
}
#[test]
fn the_whole_of_every_global_flag_is_on_a_verb_page() {
let page = flat(&help_of(&["check"]));
for one in GLOBALS {
assert!(
page.contains(&flat(one.description)),
"`headwater check --help` carries the whole of `{}`, which the first screen no longer \
prints",
one.name
);
}
}
#[test]
fn the_first_screen_holds_the_height_the_ruling_names() {
let screen = help_of(&[]);
let lines = screen.lines().count();
assert!(
lines <= 64,
"`headwater --help` is {lines} lines, and HW-DR-0042 holds the first screen to 64:\n{screen}"
);
assert!(
lines > 20,
"`headwater --help` is {lines} lines, which is too few to carry {} verbs with a line each",
headwater_verbs::VERBS.len()
);
}
#[test]
fn every_global_answer_exits_zero_on_standard_output_alone() {
for (label, arguments) in [
("root-long", vec!["--help"]),
("root-short", vec!["-h"]),
("verb-long", vec!["check", "--help"]),
("verb-short", vec!["check", "-h"]),
("word-long", vec!["sweep", "plan", "--help"]),
("version-long", vec!["--version"]),
("version-short", vec!["-V"]),
("version-after-verb", vec!["check", "--version"]),
("help-verb", vec!["help"]),
("help-verb-word", vec!["help", "taxonomy", "diff"]),
] {
let ran = ran(label, &arguments);
let named = arguments.join(" ");
assert_eq!(
ran.code,
Some(0),
"`headwater {named}`:\n{}{}",
ran.out,
ran.err
);
assert_eq!(
ran.err, "",
"`headwater {named}` writes nothing to standard error"
);
assert!(
!ran.out.is_empty(),
"`headwater {named}` writes an answer to standard output"
);
}
}