clap/tests/builder/groups.rs
Ed Page f70ebe89a7 fix!: Require explicit help/version disabling
Before we introduced actions, it required specific setups to engage with
claps version and help printing.  With actions making that more
explicit, we don't get as much benefit from our multiple, obscure, ways
of users customizing help

Before
- Modify existing help or version with `mut_arg` which would
  automatically be pushed down the command tree like `global(true)`
- Create an new help or version and have it treated as if it was the
  built-in on (I think)
- Use the same flags as built-in and have the built-in flags
  automatically disabled
- Users could explicitly disable the built-in functionality and do what
  they want

Now
- `mut_arg` no longer works as we define help and version flags at the
  end
- If someone defines a flag that overlaps with the built-ins by id,
  long, or short, a debug assert will tell them to explicitly disable
  the built-in
- Any customization has to be done by a user providing their own.  To
  propagate through the command tree, they need to set `global(true)`.

Benefits
- Hopefully, this makes it less confusing on how to override help
  behavior.  Someone creates an arg and we then tell them how to disable
  the built-in
- This greatly simplifies the arg handling by pushing more
  responsibility onto the developer in what are hopefully just corner
  cases
- This removes about 1Kb from .text

Fixes #3405
Fixes #4033
2022-08-10 20:33:21 -05:00

331 lines
10 KiB
Rust

use super::utils;
use clap::{arg, error::ErrorKind, Arg, ArgAction, ArgGroup, Command};
static REQ_GROUP_USAGE: &str = "error: The following required arguments were not provided:
<base|--delete>
USAGE:
clap-test <base|--delete>
For more information try --help
";
static REQ_GROUP_CONFLICT_USAGE: &str =
"error: The argument '--delete' cannot be used with '<base>'
USAGE:
clap-test <base|--delete>
For more information try --help
";
static REQ_GROUP_CONFLICT_ONLY_OPTIONS: &str =
"error: The argument '--delete' cannot be used with '--all'
USAGE:
clap-test <--all|--delete>
For more information try --help
";
#[test]
fn required_group_missing_arg() {
let result = Command::new("group")
.arg(arg!(-f --flag "some flag"))
.arg(arg!( -c --color "some other flag"))
.group(ArgGroup::new("req").args(&["flag", "color"]).required(true))
.try_get_matches_from(vec![""]);
assert!(result.is_err());
let err = result.err().unwrap();
assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic = "Argument group 'req' contains non-existent argument"]
fn non_existing_arg() {
let _ = Command::new("group")
.arg(arg!(-f --flag "some flag"))
.arg(arg!(-c --color "some other flag"))
.group(ArgGroup::new("req").args(&["flg", "color"]).required(true))
.try_get_matches_from(vec![""]);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic = "Argument group name must be unique\n\n\t'req' is already in use"]
fn unique_group_name() {
let _ = Command::new("group")
.arg(arg!(-f --flag "some flag"))
.arg(arg!(-c --color "some other flag"))
.group(ArgGroup::new("req").args(&["flag"]).required(true))
.group(ArgGroup::new("req").args(&["color"]).required(true))
.try_get_matches_from(vec![""]);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic = "Argument group name '' must not conflict with argument name"]
fn groups_new_of_arg_name() {
let _ = Command::new("group")
.arg(Arg::new("a").long("a").group("a"))
.try_get_matches_from(vec!["", "--a"]);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic = "Argument group name 'a' must not conflict with argument name"]
fn arg_group_new_of_arg_name() {
let _ = Command::new("group")
.arg(Arg::new("a").long("a").group("a"))
.group(ArgGroup::new("a"))
.try_get_matches_from(vec!["", "--a"]);
}
#[test]
fn group_single_value() {
let res = Command::new("group")
.arg(arg!(-c --color [color] "some option"))
.arg(arg!(-n --hostname <name> "another option").required(false))
.group(ArgGroup::new("grp").args(&["hostname", "color"]))
.try_get_matches_from(vec!["", "-c", "blue"]);
assert!(res.is_ok(), "{}", res.unwrap_err());
let m = res.unwrap();
assert!(m.contains_id("grp"));
assert_eq!(
m.get_one::<String>("grp").map(|v| v.as_str()).unwrap(),
"blue"
);
}
#[test]
fn group_empty() {
let res = Command::new("group")
.arg(arg!(-c --color [color] "some option"))
.arg(arg!(-n --hostname <name> "another option").required(false))
.group(ArgGroup::new("grp").args(&["hostname", "color"]))
.try_get_matches_from(vec![""]);
assert!(res.is_ok(), "{}", res.unwrap_err());
let m = res.unwrap();
assert!(!m.contains_id("grp"));
assert!(m.get_one::<String>("grp").map(|v| v.as_str()).is_none());
}
#[test]
fn group_required_flags_empty() {
let result = Command::new("group")
.arg(arg!(-c --color "some option"))
.arg(arg!(-n --hostname <name> "another option").required(false))
.group(
ArgGroup::new("grp")
.required(true)
.args(&["hostname", "color"]),
)
.try_get_matches_from(vec![""]);
assert!(result.is_err());
let err = result.err().unwrap();
assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
}
#[test]
fn group_multi_value_single_arg() {
let res = Command::new("group")
.arg(arg!(-c --color <color> "some option").num_args(1..))
.arg(arg!(-n --hostname <name> "another option").required(false))
.group(ArgGroup::new("grp").args(&["hostname", "color"]))
.try_get_matches_from(vec!["", "-c", "blue", "red", "green"]);
assert!(res.is_ok(), "{:?}", res.unwrap_err().kind());
let m = res.unwrap();
assert!(m.contains_id("grp"));
assert_eq!(
&*m.get_many::<String>("grp")
.unwrap()
.map(|v| v.as_str())
.collect::<Vec<_>>(),
&["blue", "red", "green"]
);
}
#[test]
fn empty_group() {
let r = Command::new("empty_group")
.arg(arg!(-f --flag "some flag"))
.group(ArgGroup::new("vers").required(true))
.try_get_matches_from(vec!["empty_prog"]);
assert!(r.is_err());
let err = r.err().unwrap();
assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
}
#[test]
fn req_group_usage_string() {
let cmd = Command::new("req_group")
.arg(arg!([base] "Base commit"))
.arg(arg!(
-d --delete "Remove the base commit information"
))
.group(
ArgGroup::new("base_or_delete")
.args(&["base", "delete"])
.required(true),
);
utils::assert_output(cmd, "clap-test", REQ_GROUP_USAGE, true);
}
#[test]
fn req_group_with_conflict_usage_string() {
let cmd = Command::new("req_group")
.arg(arg!([base] "Base commit").conflicts_with("delete"))
.arg(arg!(
-d --delete "Remove the base commit information"
))
.group(
ArgGroup::new("base_or_delete")
.args(&["base", "delete"])
.required(true),
);
utils::assert_output(
cmd,
"clap-test --delete base",
REQ_GROUP_CONFLICT_USAGE,
true,
);
}
#[test]
fn req_group_with_conflict_usage_string_only_options() {
let cmd = Command::new("req_group")
.arg(arg!(-a --all "All").conflicts_with("delete"))
.arg(arg!(
-d --delete "Remove the base commit information"
))
.group(
ArgGroup::new("all_or_delete")
.args(&["all", "delete"])
.required(true),
);
utils::assert_output(
cmd,
"clap-test --delete --all",
REQ_GROUP_CONFLICT_ONLY_OPTIONS,
true,
);
}
#[test]
fn required_group_multiple_args() {
let result = Command::new("group")
.arg(arg!(-f --flag "some flag").action(ArgAction::SetTrue))
.arg(arg!(-c --color "some other flag").action(ArgAction::SetTrue))
.group(
ArgGroup::new("req")
.args(&["flag", "color"])
.required(true)
.multiple(true),
)
.try_get_matches_from(vec!["group", "-f", "-c"]);
assert!(result.is_ok(), "{}", result.unwrap_err());
let m = result.unwrap();
assert!(*m.get_one::<bool>("flag").expect("defaulted by clap"));
assert!(*m.get_one::<bool>("color").expect("defaulted by clap"));
}
#[test]
fn group_multiple_args_error() {
let result = Command::new("group")
.arg(arg!(-f --flag "some flag"))
.arg(arg!(-c --color "some other flag"))
.group(ArgGroup::new("req").args(&["flag", "color"]))
.try_get_matches_from(vec!["group", "-f", "-c"]);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.kind(), ErrorKind::ArgumentConflict);
}
#[test]
fn group_overrides_required() {
let command = Command::new("group")
.arg(arg!(--foo <FOO>))
.arg(arg!(--bar <BAR>))
.group(ArgGroup::new("group").args(&["foo", "bar"]).required(true));
let result = command.try_get_matches_from(vec!["group", "--foo", "value"]);
assert!(result.is_ok(), "{}", result.unwrap_err());
let m = result.unwrap();
assert!(m.contains_id("foo"));
assert!(!m.contains_id("bar"));
}
#[test]
fn group_usage_use_val_name() {
static GROUP_USAGE_USE_VAL_NAME: &str = "prog
USAGE:
prog <A>
ARGS:
<A>
OPTIONS:
-h, --help Print help information
";
let cmd = Command::new("prog")
.arg(Arg::new("a").value_name("A"))
.group(ArgGroup::new("group").arg("a").required(true));
utils::assert_output(cmd, "prog --help", GROUP_USAGE_USE_VAL_NAME, false);
}
#[test]
fn group_acts_like_arg() {
let result = Command::new("prog")
.arg(
Arg::new("debug")
.long("debug")
.group("mode")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("verbose")
.long("verbose")
.group("mode")
.action(ArgAction::SetTrue),
)
.try_get_matches_from(vec!["prog", "--debug"]);
assert!(result.is_ok(), "{}", result.unwrap_err());
let m = result.unwrap();
assert!(m.contains_id("mode"));
}
/* This is used to be fixed in a hack, we need to find a better way to fix it.
#[test]
fn issue_1794() {
let cmd = clap::Command::new("hello")
.bin_name("deno")
.arg(Arg::new("option1").long("option1").action(ArgAction::SetTrue))
.arg(Arg::new("pos1").action(ArgAction::Set))
.arg(Arg::new("pos2").action(ArgAction::Set))
.group(
ArgGroup::new("arg1")
.args(&["pos1", "option1"])
.required(true),
);
let m = cmd.clone().try_get_matches_from(&["cmd", "pos1", "pos2"]).unwrap();
assert_eq!(m.get_one::<String>("pos1").map(|v| v.as_str()), Some("pos1"));
assert_eq!(m.get_one::<String>("pos2").map(|v| v.as_str()), Some("pos2"));
assert!(!*m.get_one::<bool>("option1").expect("defaulted by clap"));
let m = cmd
.clone()
.try_get_matches_from(&["cmd", "--option1", "positional"]).unwrap();
assert_eq!(m.get_one::<String>("pos1").map(|v| v.as_str()), None);
assert_eq!(m.get_one::<String>("pos2").map(|v| v.as_str()), Some("positional"));
assert!(*m.get_one::<bool>("option1").expect("defaulted by clap"));
}
*/