use clap::{Arg, ArgAction, ArgGroup, Command};
use serde_json::json;
use serial_test::serial;
use standout::cli::{App, ExitStatus, HelpResult, Output, RunErrorKind, SuccessKind};
use standout::topics::{Topic, TopicType};
use standout::Theme;
use standout_test::TestHarness;
fn flat_required_command() -> Command {
Command::new("app")
.about("Flat app")
.arg(Arg::new("range").help("A revision range"))
.arg(
Arg::new("staged")
.long("staged")
.action(ArgAction::SetTrue)
.help("Use the staged diff"),
)
.group(
ArgGroup::new("target")
.args(["range", "staged"])
.required(true),
)
}
fn flat_app(help_word: bool) -> App {
App::builder()
.help_handling(true)
.help_word(help_word)
.add_topic(Topic::new(
"Ranges",
"A range is two revisions separated by two dots.",
TopicType::Text,
Some("ranges".to_string()),
))
.command(
"",
|m, _ctx| {
Ok(Output::Render(json!({
"range": m.get_one::<String>("range").cloned().unwrap_or_default(),
})))
},
"range={{ range }}",
)
.unwrap()
.build()
.unwrap()
}
fn subcommand_command() -> Command {
Command::new("app")
.about("Subcommand app")
.arg(
Arg::new("file")
.short('f')
.action(ArgAction::Set)
.help("A file to read"),
)
.subcommand(Command::new("list").about("List the things"))
}
fn subcommand_app() -> App {
App::builder()
.help_handling(true)
.command("list", |_m, _ctx| Ok(Output::Render(json!({}))), "listed")
.unwrap()
.build()
.unwrap()
}
fn configured_help(app: &App, cmd: Command, args: &[&str]) -> String {
match app.get_matches_from(cmd, args) {
HelpResult::Help(h) | HelpResult::PagedHelp(h) => h,
other => panic!("expected rendered help, got {other:?}"),
}
}
#[test]
#[serial]
fn the_help_word_renders_themed_help_through_run() {
let result = TestHarness::new().text_output().run(
&flat_app(true),
flat_required_command(),
["app", "help"],
);
result.assert_success();
assert_eq!(result.success_kind(), Some(SuccessKind::ClapHelp));
result.assert_stdout_contains("Flat app");
result.assert_stdout_contains("USAGE");
}
#[test]
#[serial]
fn the_help_flags_render_themed_help_through_run() {
for flag in ["--help", "-h"] {
let result = TestHarness::new().text_output().run(
&flat_app(false),
flat_required_command(),
["app", flag],
);
result.assert_success();
assert_eq!(result.success_kind(), Some(SuccessKind::ClapHelp));
result.assert_stdout_contains("USAGE");
}
}
#[test]
#[serial]
fn the_help_word_renders_a_topic_through_run() {
let result = TestHarness::new().text_output().run(
&flat_app(true),
flat_required_command(),
["app", "help", "ranges"],
);
result.assert_success();
result.assert_stdout_contains("two revisions separated by two dots");
}
#[test]
#[serial]
fn the_help_word_renders_a_subcommands_help_through_run() {
let app = subcommand_app();
let word =
TestHarness::new()
.text_output()
.run(&app, subcommand_command(), ["app", "help", "list"]);
word.assert_success();
word.assert_stdout_contains("List the things");
drop(word);
let flag =
TestHarness::new()
.text_output()
.run(&app, subcommand_command(), ["app", "list", "--help"]);
flag.assert_success();
flag.assert_stdout_contains("List the things");
}
#[test]
#[serial]
fn the_help_word_honours_the_output_flag_through_run() {
let tagged = TestHarness::new().run(
&flat_app(true),
flat_required_command(),
["app", "help", "--output", "term-debug"],
);
tagged.assert_success();
tagged.assert_stdout_contains("[header]USAGE[/header]");
drop(tagged);
let json = TestHarness::new().run(
&flat_app(true),
flat_required_command(),
["app", "help", "--output", "json"],
);
json.assert_success();
json.assert_stdout_contains("USAGE");
assert!(
!json.stdout().contains("[header]"),
"structured modes strip style tags:\n{}",
json.stdout()
);
}
#[test]
#[serial]
fn the_output_flag_reaches_the_word_but_not_the_flags() {
let app = flat_app(true);
let word = TestHarness::new().no_color().run(
&app,
flat_required_command(),
["app", "help", "--output", "term-debug"],
);
word.assert_stdout_contains("[header]USAGE[/header]");
drop(word);
let flag = TestHarness::new().no_color().run(
&app,
flat_required_command(),
["app", "--help", "--output", "term-debug"],
);
flag.assert_success();
flag.assert_stdout_contains("USAGE");
assert!(
!flag.stdout().contains("[header]"),
"`--help` renders in Auto, so the requested mode is not applied:\n{}",
flag.stdout()
);
}
#[test]
#[serial]
fn a_pager_request_rides_back_as_a_typed_success() {
let result = TestHarness::new().text_output().run(
&flat_app(true),
flat_required_command(),
["app", "help", "--page"],
);
result.assert_success();
assert_eq!(result.success_kind(), Some(SuccessKind::PagedHelp));
result.assert_stdout_contains("USAGE");
}
fn assert_is_root_help(rendered: &str) {
assert!(
rendered.contains("Subcommand app"),
"expected the root's help, got:\n{rendered}"
);
}
#[test]
#[serial]
fn an_option_value_is_not_read_as_the_targeted_command() {
let app = subcommand_app();
let args = ["app", "--output-file-path", "list", "--help"];
let dispatched = TestHarness::new().run(&app, subcommand_command(), args);
dispatched.assert_success();
assert_is_root_help(dispatched.stdout());
drop(dispatched);
assert_is_root_help(&configured_help(&app, subcommand_command(), &args));
}
#[test]
#[serial]
fn a_short_option_value_is_not_read_as_the_targeted_command() {
let result = TestHarness::new().run(
&subcommand_app(),
subcommand_command(),
["app", "-f", "list", "--help"],
);
result.assert_success();
assert_is_root_help(result.stdout());
}
#[test]
#[serial]
fn the_walk_stops_where_the_help_request_is() {
for flag in ["--help", "-h"] {
let result = TestHarness::new().run(
&subcommand_app(),
subcommand_command(),
["app", flag, "list"],
);
result.assert_success();
assert_is_root_help(result.stdout());
}
}
fn broken_theme() -> Theme {
Theme::new().add("header", "no-such-style")
}
fn app_with_a_broken_theme() -> App {
App::builder()
.help_handling(true)
.theme(broken_theme())
.command("list", |_m, _ctx| Ok(Output::Render(json!({}))), "listed")
.unwrap()
.build()
.unwrap()
}
#[test]
#[serial]
fn a_help_that_cannot_be_rendered_is_not_a_usage_error() {
for args in [
&["app", "help"][..],
&["app", "--help"][..],
&["app", "-h"][..],
] {
let result = TestHarness::new().run(&app_with_a_broken_theme(), subcommand_command(), args);
result.assert_error();
result.assert_error_kind(RunErrorKind::Render);
result.assert_exit_status(ExitStatus::FAILURE);
result.assert_error_contains("failed to render help");
}
}
#[test]
#[serial]
fn a_render_failure_is_not_disguised_as_an_unrecognized_topic() {
let result = TestHarness::new().run(
&app_with_a_broken_theme(),
subcommand_command(),
["app", "help", "list"],
);
result.assert_error();
result.assert_error_kind(RunErrorKind::Render);
result.assert_error_contains("failed to render help");
assert!(
!result
.error()
.unwrap_or_default()
.contains("wasn't recognized"),
"a broken theme is not an unknown topic: {:?}",
result.error()
);
}
#[test]
#[serial]
fn both_entry_points_render_the_same_help() {
let app = flat_app(true);
for args in [
["app", "help", "--output", "text"],
["app", "--help", "--output", "text"],
] {
let dispatched = TestHarness::new().run(&app, flat_required_command(), args);
dispatched.assert_success();
let configured = configured_help(&app, flat_required_command(), &args);
assert_eq!(
dispatched.stdout(),
configured,
"entry points disagree for {args:?}"
);
}
}
#[test]
#[serial]
fn a_flat_command_keeps_the_word_as_data_through_run_without_the_opt_in() {
let result = TestHarness::new().text_output().run(
&flat_app(false),
flat_required_command(),
["app", "help"],
);
result.assert_success();
result.assert_stdout_eq("range=help");
}
#[test]
#[serial]
fn the_escape_delivers_the_literal_word_through_run() {
let result = TestHarness::new().run(
&flat_app(true),
flat_required_command(),
["app", "--", "help"],
);
result.assert_success();
result.assert_stdout_eq("range=help");
}
#[test]
#[serial]
fn a_normal_invocation_is_untouched_through_run() {
let result = TestHarness::new().text_output().run(
&flat_app(true),
flat_required_command(),
["app", "main..HEAD"],
);
result.assert_success();
result.assert_stdout_eq("range=main..HEAD");
}