use std::sync::LazyLock;
use shep_core::protocol::{MIN_SUPPORTED, PROTOCOL_VERSION};
#[cfg(test)]
const HELP_GROUPS: &[(&str, &[&str])] = &[
(
"Run things",
&[
"start", "add", "serve", "stop", "restart", "reload", "delete", "stock", "enable",
"disable",
],
),
(
"See what's up",
&["flock", "describe", "bleats", "lookout", "fold", "barks"],
),
(
"Survive reboots",
&["save", "muster", "startup", "unstartup"],
),
("Talk to a sheep", &["trigger", "signal", "whisper"]),
(
"The shepherd",
&[
"ping", "kill", "reopen", "flush", "set", "get", "unset", "secret",
],
),
("Dogs and agents", &["dogs", "adopt", "rehome", "whistle"]),
("Foreground runs", &["runtime", "dev"]),
("Coming from pm2", &["import"]),
("Help", &["welcome", "init", "help", "completions", "style"]),
];
pub(super) const HELP_TEMPLATE: &str = "\
{about}
{usage-heading} {usage}
Getting started
shep start server.js start it and keep it alive
shep flock see what's running
shep bleats server follow its output
shep startup bring it back after a reboot
Run things start add serve stop restart reload delete stock enable disable
See what's up flock describe bleats lookout fold barks
Survive reboots save muster startup unstartup
Talk to a sheep trigger signal whisper
The shepherd ping kill reopen flush set get unset secret
Dogs and agents dogs adopt rehome whistle
Foreground runs runtime dev
Coming from pm2 import
Help welcome init help completions style
Aliases flock: list, ls bleats: logs lookout: dash stock: scale whisper: sendline
Upgrading cargo install shep replaces the binary, not the running shepherd: shep daemon reload
{options}{after-help}";
static VERSION_TEXT: LazyLock<String> = LazyLock::new(|| {
format!(
"{}\nspeaks protocol {PROTOCOL_VERSION}, accepts {MIN_SUPPORTED} and newer",
env!("CARGO_PKG_VERSION")
)
});
pub(super) fn version_text() -> &'static str {
VERSION_TEXT.as_str()
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use super::*;
use crate::cli::Cli;
#[test]
fn version_output_names_both_the_protocol_and_the_floor() {
let text = version_text();
assert!(
text.contains(&format!("protocol {PROTOCOL_VERSION}")),
"got {text}"
);
assert!(
text.contains(&format!("accepts {MIN_SUPPORTED}")),
"got {text}"
);
}
#[test]
fn the_rendered_version_names_the_protocol_and_the_floor_on_the_real_command() {
use clap::CommandFactory;
let top = Cli::command().render_long_version().to_string();
assert!(
top.contains(&format!("speaks protocol {PROTOCOL_VERSION}")),
"top-level --version lost the protocol line: {top}"
);
assert!(
top.contains(&format!("accepts {MIN_SUPPORTED} and newer")),
"top-level --version lost the floor line: {top}"
);
let mut top_command = Cli::command();
top_command.build();
let daemon = top_command
.find_subcommand("daemon")
.expect("shep daemon exists");
let reload = daemon
.find_subcommand("reload")
.expect("shep daemon reload exists");
let nested = reload.render_long_version().to_string();
assert!(
nested.contains(&format!("speaks protocol {PROTOCOL_VERSION}")),
"shep daemon reload --version lost the protocol line: {nested}"
);
assert!(
nested.contains(&format!("accepts {MIN_SUPPORTED} and newer")),
"shep daemon reload --version lost the floor line: {nested}"
);
}
fn workspace_web_dir() -> Option<PathBuf> {
let dir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../web"));
dir.is_dir().then(|| dir.to_path_buf())
}
fn read_workspace_web_file(relative: &str) -> Option<String> {
let dir = workspace_web_dir()?;
Some(
std::fs::read_to_string(dir.join(relative)).unwrap_or_else(|err| {
panic!("web/{relative} exists in the workspace but could not be read: {err}")
}),
)
}
#[test]
fn the_command_tree_parses_and_is_internally_consistent() {
use clap::CommandFactory;
Cli::command().debug_assert(); }
fn listed_verb_paths(generator: &str) -> Vec<String> {
let (_, rest) = generator
.split_once("VERBS=(")
.expect("the generator declares a VERBS array");
let (block, _) = rest.split_once(')').expect("the VERBS array closes");
block
.split('"')
.enumerate()
.flat_map(|(i, chunk)| {
if i % 2 == 1 {
vec![chunk.to_string()]
} else {
chunk.split_whitespace().map(str::to_string).collect()
}
})
.collect()
}
fn visible_command_paths(command: &clap::Command) -> Vec<String> {
fn walk(command: &clap::Command, prefix: &str, found: &mut Vec<String>) {
for sub in command.get_subcommands() {
if sub.is_hide_set() || sub.get_name() == "help" {
continue;
}
let path = if prefix.is_empty() {
sub.get_name().to_string()
} else {
format!("{prefix} {}", sub.get_name())
};
found.push(path.clone());
walk(sub, &path, found);
}
}
let mut found = Vec::new();
walk(command, "", &mut found);
found
}
#[test]
fn every_visible_verb_reaches_the_docs_site_generator() {
use clap::CommandFactory;
let Some(generator) = read_workspace_web_file("scripts/generate-cli-reference.sh") else {
return;
};
let listed = listed_verb_paths(&generator);
let command = Cli::command();
let missing: Vec<String> = visible_command_paths(&command)
.into_iter()
.filter(|path| !listed.contains(path))
.collect();
assert!(
missing.is_empty(),
"these verbs would be missing from the published CLI reference: {missing:?}\n\
add them to VERBS in web/scripts/generate-cli-reference.sh and re-run it"
);
}
#[test]
fn every_listed_verb_has_a_block_in_the_committed_reference() {
let (Some(generator), Some(reference)) = (
read_workspace_web_file("scripts/generate-cli-reference.sh"),
read_workspace_web_file("src/data/cli-reference.generated.txt"),
) else {
return;
};
let missing: Vec<String> = listed_verb_paths(&generator)
.into_iter()
.filter(|path| !reference.contains(&format!("\n@@VERB:{path}@@\n")))
.collect();
assert!(
missing.is_empty(),
"the committed CLI reference has no block for {missing:?}\n\
run 'cargo build --release' then ./web/scripts/generate-cli-reference.sh"
);
}
#[test]
fn every_visible_verb_appears_in_exactly_one_help_group() {
use clap::CommandFactory;
let command = Cli::command();
let visible: Vec<String> = command
.get_subcommands()
.filter(|s| !s.is_hide_set())
.map(|s| s.get_name().to_string())
.collect();
let filed: Vec<&str> = HELP_GROUPS
.iter()
.flat_map(|(_, verbs)| verbs.iter().copied())
.collect();
for verb in &visible {
let times = filed.iter().filter(|f| *f == verb).count();
assert_eq!(
times, 1,
"`{verb}` appears in {times} help groups; it must appear in exactly one"
);
}
for name in &filed {
assert!(
*name == "help" || visible.iter().any(|v| v == name),
"a help group names `{name}`, which is not a visible verb"
);
}
}
#[test]
fn the_help_template_names_every_visible_alias() {
use clap::CommandFactory;
let command = Cli::command();
let mut expected: Vec<String> = command
.get_subcommands()
.filter(|s| !s.is_hide_set())
.filter_map(|s| {
let aliases: Vec<&str> = s.get_visible_aliases().collect();
(!aliases.is_empty()).then(|| format!("{}: {}", s.get_name(), aliases.join(", ")))
})
.collect();
expected.sort();
let line = HELP_TEMPLATE
.lines()
.find(|l| l.starts_with("Aliases"))
.expect("HELP_TEMPLATE has an Aliases line");
for entry in &expected {
assert!(
line.contains(entry.as_str()),
"`--help`'s Aliases line does not name `{entry}`: {line}"
);
}
for token in line.trim_start_matches("Aliases").split_whitespace() {
if let Some(verb) = token.strip_suffix(':') {
assert!(
expected.iter().any(|e| e.starts_with(&format!("{verb}:"))),
"`--help` names aliases for `{verb}`, which has none"
);
}
}
}
#[test]
fn the_help_template_names_the_upgrade_path() {
let line = HELP_TEMPLATE
.lines()
.find(|l| l.starts_with("Upgrading"))
.expect("HELP_TEMPLATE has an Upgrading line");
assert_eq!(
line,
"Upgrading cargo install shep replaces the binary, not the running shepherd: shep daemon reload"
);
}
#[test]
fn the_help_template_and_the_group_table_agree() {
for (heading, verbs) in HELP_GROUPS {
let line = HELP_TEMPLATE
.lines()
.find(|l| l.starts_with(heading))
.unwrap_or_else(|| panic!("`{heading}` is missing from HELP_TEMPLATE"));
for verb in *verbs {
assert!(
line.split_whitespace().any(|w| w == *verb),
"`{verb}` is filed under `{heading}` but is not on that line: {line}"
);
}
}
}
#[test]
fn the_help_opens_with_a_worked_example() {
use clap::CommandFactory;
let help = Cli::command().render_long_help().to_string();
assert!(
help.contains("Getting started"),
"no getting-started block:\n{help}"
);
assert!(
help.contains("shep start server.js"),
"no worked example:\n{help}"
);
}
#[test]
fn home_is_the_last_global_option_a_reader_meets() {
use clap::CommandFactory;
let help = Cli::command().render_long_help().to_string();
let home = help.find("--home").expect("--home is still documented");
let format = help.find("--format").expect("--format is documented");
let quiet = help.find("--quiet").expect("--quiet is documented");
assert!(
home > format && home > quiet,
"--home must come after the options people actually choose:\n{help}"
);
}
#[test]
fn the_top_level_help_carries_no_implementation_notes() {
use clap::CommandFactory;
let help = Cli::command().render_long_help().to_string();
for leak in [
"bin_name",
"Phase 15",
"load-bearing",
"argv[0]",
"{options}",
"{all-args}",
"help_heading",
] {
assert!(
!help.contains(leak),
"`shep --help` still contains the internal note {leak:?}:\n{help}"
);
}
}
#[test]
fn the_top_level_help_has_no_dashes_or_doc_link_syntax() {
use clap::CommandFactory;
let help = Cli::command().render_long_help().to_string();
assert!(
!help.contains('\u{2014}'),
"an em dash reached --help, which this project's copy rules forbid:\n{help}"
);
assert!(
!help.contains('\u{2013}'),
"an en dash reached --help, which this project's copy rules forbid:\n{help}"
);
assert!(
!help.contains("[`"),
"Rust intra-doc-link syntax reached --help -- an aside meant for a reader of the \
source, not the terminal, belongs on a `//` comment rather than a `///` one:\n{help}"
);
}
}