use super::*;
/// Strip ANSI escape sequences so assertions work under both plain and colored builds.
fn strip_ansi(s: &str) -> String {
let mut out = String::new();
let mut in_escape = false;
for ch in s.chars() {
if in_escape {
if ch.is_ascii_alphabetic() {
in_escape = false;
}
} else if ch == '\x1b' {
in_escape = true;
} else {
out.push(ch);
}
}
out
}
fn parser_with_flags_and_option() -> ArgParser {
ArgBuilder::new()
.flag(Flag::new("alpha").desc("alpha flag").short('a'))
.flag(Flag::new("bravo").desc("bravo flag").short('b'))
.flag(Flag::new("charlie").desc("charlie flag").short('c'))
.option(
Opt::new("width")
.placeholder("N")
.desc("width option")
.short('w'),
)
.build()
.unwrap()
}
#[test]
fn combined_short_flags_abc() {
let parser = parser_with_flags_and_option();
let result = parser.parse(&["-abc".into()]).unwrap();
assert!(result.get_flag("alpha"));
assert!(result.get_flag("bravo"));
assert!(result.get_flag("charlie"));
}
#[test]
fn attached_value_w10() {
let parser = parser_with_flags_and_option();
let result = parser.parse(&["-w10".into()]).unwrap();
assert_eq!(result.get_option("width"), Some("10"));
}
#[test]
fn combined_flags_then_option_with_attached_value() {
let parser = parser_with_flags_and_option();
let result = parser.parse(&["-abcw10".into()]).unwrap();
assert!(result.get_flag("alpha"));
assert!(result.get_flag("bravo"));
assert!(result.get_flag("charlie"));
assert_eq!(result.get_option("width"), Some("10"));
}
#[test]
fn combined_flags_then_option_next_token() {
let parser = parser_with_flags_and_option();
let result = parser.parse(&["-abcw".into(), "10".into()]).unwrap();
assert!(result.get_flag("alpha"));
assert!(result.get_flag("bravo"));
assert!(result.get_flag("charlie"));
assert_eq!(result.get_option("width"), Some("10"));
}
#[test]
fn repeated_flag_vvv() {
let parser = ArgBuilder::new()
.flag(Flag::new("verbose").desc("verbose flag").short('v'))
.build()
.unwrap();
let result = parser.parse(&["-vvv".into()]).unwrap();
assert!(result.get_flag("verbose"));
}
#[test]
fn unknown_char_in_cluster() {
let parser = parser_with_flags_and_option();
let err = parser.parse(&["-abx".into()]).unwrap_err();
match err {
ParseError::UnknownArgument(msg) => {
assert!(
msg.contains('x'),
"error should mention unknown char 'x': {msg}"
);
}
other => panic!("expected UnknownArgument, got: {other}"),
}
}
#[test]
fn missing_value_option_at_end_of_cluster() {
let parser = parser_with_flags_and_option();
let err = parser.parse(&["-abcw".into()]).unwrap_err();
match err {
ParseError::MissingValue(name) => assert_eq!(name, "width"),
other => panic!("expected MissingValue, got: {other}"),
}
}
#[test]
fn combined_token_after_double_dash_is_positional() {
let parser = parser_with_flags_and_option();
let result = parser.parse(&["--".into(), "-abc".into()]).unwrap();
assert!(!result.get_flag("alpha"));
assert!(!result.get_flag("bravo"));
assert!(!result.get_flag("charlie"));
assert_eq!(result.get_positionals(), &["-abc".to_string()]);
}
// --- Subcommand parsing edge case tests ---
fn parent_with_subcommands() -> ArgParser {
let build_parser = ArgBuilder::new()
.flag(
Flag::new("release")
.desc("Build in release mode")
.short('r'),
)
.build()
.unwrap();
let test_parser = ArgBuilder::new()
.flag(Flag::new("verbose").desc("Verbose test output").short('v'))
.build()
.unwrap();
ArgBuilder::new()
.name("myapp")
.flag(Flag::new("debug").desc("Enable debug mode").short('d'))
.subcommand("build", "Compile the project", build_parser)
.subcommand("test", "Run tests", test_parser)
.build()
.unwrap()
}
#[test]
fn subcommand_empty_args_returns_no_subcommand() {
let parser = parent_with_subcommands();
let err = parser.parse(&[]).unwrap_err();
match err {
ParseError::NoSubcommand(names) => {
assert!(names.contains("build"));
assert!(names.contains("test"));
}
other => panic!("expected NoSubcommand, got: {other}"),
}
}
#[test]
fn subcommand_unknown_name_returns_unknown_subcommand() {
let parser = parent_with_subcommands();
let err = parser.parse(&["deploy".into()]).unwrap_err();
assert_eq!(err, ParseError::UnknownSubcommand("deploy".into()));
}
#[test]
fn subcommand_unknown_flag_after_subcommand() {
let parser = parent_with_subcommands();
let err = parser
.parse(&["build".into(), "--unknown".into()])
.unwrap_err();
match err {
ParseError::UnknownArgument(token) => assert_eq!(token, "--unknown"),
other => panic!("expected UnknownArgument, got: {other}"),
}
}
#[test]
fn subcommand_help_before_subcommand_returns_parent_help() {
let parser = parent_with_subcommands();
let err = parser.parse(&["--help".into()]).unwrap_err();
match err {
ParseError::HelpRequested(text) => {
assert!(
text.contains("myapp"),
"parent help should contain program name"
);
}
other => panic!("expected HelpRequested, got: {other}"),
}
}
#[test]
fn subcommand_help_after_subcommand_returns_subcommand_help() {
let parser = parent_with_subcommands();
let err = parser
.parse(&["build".into(), "--help".into()])
.unwrap_err();
match err {
ParseError::HelpRequested(text) => {
// Subcommand help should mention the subcommand's own flags
assert!(
text.contains("release"),
"subcommand help should contain its flags"
);
}
other => panic!("expected HelpRequested, got: {other}"),
}
}
// --- Duplicate argument validation tests ---
#[test]
fn duplicate_short_flag_flag() {
let err = ArgBuilder::new()
.flag(Flag::new("verbose").desc("verbose").short('v'))
.flag(Flag::new("version").desc("version").short('v'))
.build()
.unwrap_err();
assert!(
matches!(err, ParseError::InvalidFormat(ref msg) if msg.contains("-v")),
"expected duplicate short error, got: {err}"
);
}
#[test]
fn duplicate_short_flag_option() {
let err = ArgBuilder::new()
.flag(Flag::new("verbose").desc("verbose").short('v'))
.option(
Opt::new("value")
.placeholder("VAL")
.desc("a value")
.short('v'),
)
.build()
.unwrap_err();
assert!(
matches!(err, ParseError::InvalidFormat(ref msg) if msg.contains("-v")),
"expected duplicate short error, got: {err}"
);
}
#[test]
fn duplicate_long_flag_option() {
let err = ArgBuilder::new()
.flag(Flag::new("output").desc("output flag"))
.option(Opt::new("output").placeholder("FILE").desc("output option"))
.build()
.unwrap_err();
assert!(
matches!(err, ParseError::InvalidFormat(ref msg) if msg.contains("--output")),
"expected duplicate long error, got: {err}"
);
}
#[test]
fn no_duplicate_when_shorts_differ() {
ArgBuilder::new()
.flag(Flag::new("verbose").desc("verbose").short('v'))
.flag(Flag::new("debug").desc("debug").short('d'))
.option(
Opt::new("output")
.placeholder("FILE")
.desc("output")
.short('o'),
)
.build()
.unwrap();
}
// --- Help text alignment tests ---
#[test]
fn help_single_flag_has_column_gap() {
let parser = ArgBuilder::new()
.name("app")
.flag(Flag::new("verbose").desc("Enable verbose").short('v'))
.build()
.unwrap();
let help = strip_ansi(&parser.help_text());
assert!(help.contains(" -v, --verbose Enable verbose\n"));
}
#[test]
fn help_flags_mixed_short_aligned() {
let parser = ArgBuilder::new()
.name("app")
.flag(Flag::new("v").desc("short name").short('v'))
.flag(Flag::new("very-long-flag").desc("long name"))
.build()
.unwrap();
let help = strip_ansi(&parser.help_text());
let lines: Vec<&str> = help
.lines()
.filter(|l| l.starts_with(" "))
.filter(|l| l.contains("--"))
.collect();
assert_eq!(lines.len(), 2);
// Both descriptions should start at the same column
let col0 = lines[0].find("short name").unwrap();
let col1 = lines[1].find("long name").unwrap();
assert_eq!(col0, col1, "descriptions should be aligned: {lines:?}");
}
#[test]
fn help_empty_parser_no_sections() {
let parser = ArgBuilder::new()
.name("app")
.description("My app")
.build()
.unwrap();
let help = strip_ansi(&parser.help_text());
assert!(!help.contains("Options:"));
assert!(!help.contains("Positional arguments:"));
assert!(!help.contains("Subcommands:"));
assert!(help.contains("My app"));
assert!(help.contains("Usage: app"));
}
#[test]
fn help_varying_option_lengths_aligned() {
let parser = ArgBuilder::new()
.name("app")
.option(
Opt::new("o")
.placeholder("FILE")
.desc("output file")
.short('o'),
)
.option(
Opt::new("very-long-option")
.placeholder("VAL")
.desc("some value"),
)
.build()
.unwrap();
let help = strip_ansi(&parser.help_text());
let option_lines: Vec<&str> = help
.lines()
.skip_while(|l| !l.contains("Options:"))
.skip(1)
.take_while(|l| l.starts_with(" "))
.collect();
assert_eq!(option_lines.len(), 2);
let col0 = option_lines[0].find("output file").unwrap();
let col1 = option_lines[1].find("some value").unwrap();
assert_eq!(
col0, col1,
"option descriptions should be aligned: {option_lines:?}"
);
}
// --- Duplicate option tests ---
#[test]
fn duplicate_non_multi_option_errors() {
let parser = ArgBuilder::new()
.option(
Opt::new("output")
.placeholder("FILE")
.desc("Output file")
.short('o'),
)
.build()
.unwrap();
let err = parser
.parse(&[
"--output".into(),
"a.txt".into(),
"--output".into(),
"b.txt".into(),
])
.unwrap_err();
assert!(
matches!(err, ParseError::DuplicateOption(ref name) if name == "output"),
"expected DuplicateOption, got: {err}"
);
}
#[test]
fn duplicate_multi_option_is_allowed() {
let parser = ArgBuilder::new()
.option(
Opt::new("tag")
.placeholder("TAG")
.desc("A tag")
.short('t')
.multi(),
)
.build()
.unwrap();
let result = parser
.parse(&["--tag".into(), "a".into(), "--tag".into(), "b".into()])
.unwrap();
assert_eq!(result.get_option_values("tag"), &["a", "b"]);
}
// --- Version flag interaction tests ---
#[test]
fn help_before_version_returns_help() {
let parser = ArgBuilder::new()
.name("myapp")
.version("1.0.0")
.build()
.unwrap();
let err = parser
.parse(&["--help".into(), "--version".into()])
.unwrap_err();
assert!(
matches!(err, ParseError::HelpRequested(_)),
"expected HelpRequested, got: {err}"
);
}
#[test]
fn version_before_help_returns_version() {
let parser = ArgBuilder::new()
.name("myapp")
.version("1.0.0")
.build()
.unwrap();
let err = parser
.parse(&["--version".into(), "--help".into()])
.unwrap_err();
match err {
ParseError::VersionRequested(text) => assert_eq!(text, "myapp 1.0.0"),
other => panic!("expected VersionRequested, got: {other}"),
}
}
#[test]
fn double_dash_then_version_is_positional() {
let parser = ArgBuilder::new().version("1.0.0").build().unwrap();
let result = parser.parse(&["--".into(), "--version".into()]).unwrap();
assert_eq!(result.get_positionals(), &["--version".to_string()]);
}
#[test]
fn double_dash_then_short_v_is_positional() {
let parser = ArgBuilder::new().version("1.0.0").build().unwrap();
let result = parser.parse(&["--".into(), "-V".into()]).unwrap();
assert_eq!(result.get_positionals(), &["-V".to_string()]);
}
#[test]
fn version_before_subcommand_returns_parent_version() {
let sub = ArgBuilder::new()
.flag(Flag::new("verbose").desc("verbose").short('v'))
.build()
.unwrap();
let parser = ArgBuilder::new()
.name("myapp")
.version("2.0.0")
.subcommand("run", "Run stuff", sub)
.build()
.unwrap();
let err = parser
.parse(&["--version".into(), "run".into()])
.unwrap_err();
match err {
ParseError::VersionRequested(text) => assert_eq!(text, "myapp 2.0.0"),
other => panic!("expected VersionRequested, got: {other}"),
}
}
#[test]
fn version_after_subcommand_handled_by_subcommand() {
let sub = ArgBuilder::new().version("0.1.0").build().unwrap();
let parser = ArgBuilder::new()
.name("myapp")
.version("2.0.0")
.subcommand("run", "Run stuff", sub)
.build()
.unwrap();
let err = parser
.parse(&["run".into(), "--version".into()])
.unwrap_err();
match err {
ParseError::VersionRequested(text) => {
// Subcommand has no name, so just version string
assert_eq!(text, "0.1.0");
}
other => panic!("expected VersionRequested from subcommand, got: {other}"),
}
}
// ── free::parse_loose_from coverage ──────────────────────────────────────
#[test]
fn loose_long_option_with_value() {
let args = vec!["--output".into(), "file.txt".into()];
let result = crate::nanoargs::free::parse_loose_from(&args).unwrap();
assert_eq!(result.get_option("output"), Some("file.txt"));
}
#[test]
fn loose_long_flag_no_value() {
let args = vec!["--verbose".into(), "--debug".into()];
let result = crate::nanoargs::free::parse_loose_from(&args).unwrap();
assert!(result.get_flag("verbose"));
assert!(result.get_flag("debug"));
}
#[test]
fn loose_long_option_equals_syntax() {
let args = vec!["--output=file.txt".into()];
let result = crate::nanoargs::free::parse_loose_from(&args).unwrap();
assert_eq!(result.get_option("output"), Some("file.txt"));
}
#[test]
fn loose_short_option_with_value() {
let args = vec!["-o".into(), "file.txt".into()];
let result = crate::nanoargs::free::parse_loose_from(&args).unwrap();
assert_eq!(result.get_option("o"), Some("file.txt"));
}
#[test]
fn loose_short_flag_no_value() {
let args = vec!["-v".into(), "-d".into()];
let result = crate::nanoargs::free::parse_loose_from(&args).unwrap();
assert!(result.get_flag("v"));
assert!(result.get_flag("d"));
}
#[test]
fn loose_short_option_equals_syntax() {
let args = vec!["-o=file.txt".into()];
let result = crate::nanoargs::free::parse_loose_from(&args).unwrap();
assert_eq!(result.get_option("o"), Some("file.txt"));
}
#[test]
fn loose_positional_args() {
let args = vec!["hello".into(), "world".into()];
let result = crate::nanoargs::free::parse_loose_from(&args).unwrap();
assert_eq!(result.get_positionals(), &["hello", "world"]);
}
#[test]
fn loose_double_dash_stops_parsing() {
let args = vec!["--verbose".into(), "--".into(), "--not-a-flag".into()];
let result = crate::nanoargs::free::parse_loose_from(&args).unwrap();
assert!(result.get_flag("verbose"));
assert_eq!(result.get_positionals(), &["--not-a-flag"]);
}
#[test]
fn loose_help_flag_returns_error() {
let args = vec!["--help".into()];
let err = crate::nanoargs::free::parse_loose_from(&args).unwrap_err();
match err {
ParseError::HelpRequested(text) => assert!(text.contains("Usage")),
other => panic!("expected HelpRequested, got: {other}"),
}
}
#[test]
fn loose_short_help_flag_returns_error() {
let args = vec!["-h".into()];
let err = crate::nanoargs::free::parse_loose_from(&args).unwrap_err();
match err {
ParseError::HelpRequested(_) => {}
other => panic!("expected HelpRequested, got: {other}"),
}
}
#[test]
fn loose_mixed_args() {
let args = vec![
"--verbose".into(),
"-o".into(),
"out.txt".into(),
"positional".into(),
"--".into(),
"--rest".into(),
];
let result = crate::nanoargs::free::parse_loose_from(&args).unwrap();
assert!(result.get_flag("verbose"));
assert_eq!(result.get_option("o"), Some("out.txt"));
assert_eq!(result.get_positionals(), &["positional", "--rest"]);
}
#[test]
fn loose_empty_args() {
let result = crate::nanoargs::free::parse_loose_from(&[]).unwrap();
assert!(result.get_positionals().is_empty());
assert!(!result.get_flag("anything"));
}
#[test]
fn loose_flag_when_next_starts_with_dash() {
// --output followed by -v should treat --output as a flag
let args = vec!["--output".into(), "-v".into()];
let result = crate::nanoargs::free::parse_loose_from(&args).unwrap();
assert!(result.get_flag("output"));
assert!(result.get_flag("v"));
}
// ── ParseResultBuilder coverage ──────────────────────────────────────────
#[test]
fn result_builder_new_and_multi_option() {
let result = ParseResultBuilder::new()
.multi_option("tags", "a")
.multi_option("tags", "b")
.multi_option("tags", "c")
.build();
assert_eq!(result.get_option_values("tags"), &["a", "b", "c"]);
// get_option returns last value
assert_eq!(result.get_option("tags"), Some("c"));
}
#[test]
fn result_builder_subcommand() {
let sub = ParseResultBuilder::new().flag("verbose", true).build();
let result = ParseResultBuilder::new().subcommand("run", sub).build();
assert_eq!(result.subcommand(), Some("run"));
assert!(result.subcommand_result().unwrap().get_flag("verbose"));
}
// ── OptionError Display coverage ─────────────────────────────────────────
#[test]
fn option_error_missing_display() {
let err = OptionError::Missing {
option: "port".into(),
};
assert_eq!(
err.to_string(),
"option --port is required but was not provided"
);
}
#[test]
fn option_error_parse_failed_display() {
let err = OptionError::ParseFailed {
option: "port".into(),
message: "invalid digit".into(),
};
assert_eq!(err.to_string(), "option --port: invalid digit");
}
#[test]
fn option_error_is_std_error() {
let err = OptionError::Missing { option: "x".into() };
// Verify it implements std::error::Error
let _: &dyn std::error::Error = &err;
}
// ── debug_assert! typo-detection tests ─────────────────────────────────────
#[test]
#[should_panic(expected = "unknown flag")]
fn debug_assert_get_flag_unknown_name_panics() {
let parser = ArgBuilder::new()
.flag(Flag::new("verbose"))
.build()
.unwrap();
let result = parser.parse(&[]).unwrap();
let _ = result.get_flag("verbos"); // typo → panic
}
#[test]
#[should_panic(expected = "unknown option")]
fn debug_assert_get_option_unknown_name_panics() {
let parser = ArgBuilder::new()
.option(Opt::new("output"))
.build()
.unwrap();
let result = parser.parse(&[]).unwrap();
let _ = result.get_option("outpt"); // typo → panic
}
#[test]
#[should_panic(expected = "unknown option")]
fn debug_assert_get_option_values_unknown_name_panics() {
let parser = ArgBuilder::new()
.option(Opt::new("files").multi())
.build()
.unwrap();
let result = parser.parse(&[]).unwrap();
let _ = result.get_option_values("filez"); // typo → panic
}
#[test]
fn debug_assert_get_flag_known_name_does_not_panic() {
let parser = ArgBuilder::new()
.flag(Flag::new("verbose"))
.build()
.unwrap();
let result = parser.parse(&[]).unwrap();
assert!(!result.get_flag("verbose")); // correct name → no panic
}
#[test]
fn debug_assert_get_option_known_name_does_not_panic() {
let parser = ArgBuilder::new()
.option(Opt::new("output"))
.build()
.unwrap();
let result = parser.parse(&[]).unwrap();
assert_eq!(result.get_option("output"), None); // correct name → no panic
}
#[test]
fn debug_assert_loose_parse_skips_check() {
// parse_loose has no schema, so any name should work without panic
let result = crate::nanoargs::free::parse_loose_from(&["--foo".into(), "bar".into()]).unwrap();
assert!(!result.get_flag("nonexistent")); // no schema → no panic
assert_eq!(result.get_option("also_nonexistent"), None);
}
#[test]
#[should_panic(expected = "unknown flag")]
fn debug_assert_builder_result_catches_typo() {
// ParseResultBuilder also sets known names
let result = ParseResultBuilder::new().flag("verbose", true).build();
let _ = result.get_flag("verbos"); // typo → panic
}
#[test]
fn debug_assert_builder_result_known_name_ok() {
let result = ParseResultBuilder::new()
.flag("verbose", true)
.option("output", "file.txt")
.build();
assert!(result.get_flag("verbose"));
assert_eq!(result.get_option("output"), Some("file.txt"));
}
// ── Subcommand-level group and conflict tests ──────────────────────────────
#[test]
fn subcommand_group_violation_on_subcommand_args() {
// Subcommand parser has a group; none of its members provided → GroupViolation
let sub_parser = ArgBuilder::new()
.flag(Flag::new("json").desc("JSON output"))
.flag(Flag::new("csv").desc("CSV output"))
.group("output format", &["json", "csv"])
.build()
.unwrap();
let parent = ArgBuilder::new()
.subcommand("export", "export data", sub_parser)
.build()
.unwrap();
let result = parent.parse(&["export".into()]);
assert!(result.is_err());
let err = result.unwrap_err();
match err {
ParseError::GroupViolation { group, members } => {
assert_eq!(group, "output format");
assert_eq!(members, vec!["json", "csv"]);
}
other => panic!("Expected GroupViolation, got: {other:?}"),
}
}
#[test]
fn subcommand_group_satisfied_on_subcommand_args() {
// Subcommand parser has a group; one member provided → success
let sub_parser = ArgBuilder::new()
.flag(Flag::new("json").desc("JSON output"))
.flag(Flag::new("csv").desc("CSV output"))
.group("output format", &["json", "csv"])
.build()
.unwrap();
let parent = ArgBuilder::new()
.subcommand("export", "export data", sub_parser)
.build()
.unwrap();
let result = parent.parse(&["export".into(), "--json".into()]).unwrap();
let sub = result.subcommand_result().unwrap();
assert!(sub.get_flag("json"));
assert!(!sub.get_flag("csv"));
}
#[test]
fn subcommand_conflict_violation_on_subcommand_args() {
// Subcommand parser has a conflict; two members provided → ConflictViolation
let sub_parser = ArgBuilder::new()
.flag(Flag::new("json").desc("JSON output"))
.flag(Flag::new("csv").desc("CSV output"))
.conflict("output format", &["json", "csv"])
.build()
.unwrap();
let parent = ArgBuilder::new()
.subcommand("export", "export data", sub_parser)
.build()
.unwrap();
let result = parent.parse(&["export".into(), "--json".into(), "--csv".into()]);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::ConflictViolation { conflict, provided } => {
assert_eq!(conflict, "output format");
assert_eq!(provided, vec!["json", "csv"]);
}
other => panic!("Expected ConflictViolation, got: {other:?}"),
}
}
#[test]
fn subcommand_conflict_ok_with_one_member() {
// Subcommand parser has a conflict; only one member provided → success
let sub_parser = ArgBuilder::new()
.flag(Flag::new("json").desc("JSON output"))
.flag(Flag::new("csv").desc("CSV output"))
.conflict("output format", &["json", "csv"])
.build()
.unwrap();
let parent = ArgBuilder::new()
.subcommand("export", "export data", sub_parser)
.build()
.unwrap();
let result = parent.parse(&["export".into(), "--csv".into()]).unwrap();
let sub = result.subcommand_result().unwrap();
assert!(sub.get_flag("csv"));
assert!(!sub.get_flag("json"));
}
#[test]
fn parent_group_validated_against_global_args_not_subcommand_args() {
// Parent has a group on global flags; subcommand has its own flags.
// Providing the global group member satisfies the parent group,
// even though the subcommand doesn't know about it.
let sub_parser = ArgBuilder::new()
.flag(Flag::new("detail").desc("show details"))
.build()
.unwrap();
let parent = ArgBuilder::new()
.flag(Flag::new("verbose").desc("verbose"))
.flag(Flag::new("quiet").desc("quiet"))
.group("verbosity", &["verbose", "quiet"])
.subcommand("run", "run something", sub_parser)
.build()
.unwrap();
// Provide global group member before subcommand → success
let result = parent.parse(&["--verbose".into(), "run".into()]).unwrap();
assert!(result.get_flag("verbose"));
assert_eq!(result.subcommand(), Some("run"));
}
#[test]
fn parent_group_violation_when_no_global_member_provided() {
// Parent has a group on global flags; no global member provided → GroupViolation
let sub_parser = ArgBuilder::new()
.flag(Flag::new("detail").desc("show details"))
.build()
.unwrap();
let parent = ArgBuilder::new()
.flag(Flag::new("verbose").desc("verbose"))
.flag(Flag::new("quiet").desc("quiet"))
.group("verbosity", &["verbose", "quiet"])
.subcommand("run", "run something", sub_parser)
.build()
.unwrap();
let result = parent.parse(&["run".into()]);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::GroupViolation { group, .. } => assert_eq!(group, "verbosity"),
other => panic!("Expected GroupViolation, got: {other:?}"),
}
}
#[test]
fn parent_conflict_validated_against_global_args() {
// Parent has a conflict on global flags; both provided → ConflictViolation
let sub_parser = ArgBuilder::new().build().unwrap();
let parent = ArgBuilder::new()
.flag(Flag::new("verbose").desc("verbose"))
.flag(Flag::new("quiet").desc("quiet"))
.conflict("verbosity", &["verbose", "quiet"])
.subcommand("run", "run something", sub_parser)
.build()
.unwrap();
let result = parent.parse(&["--verbose".into(), "--quiet".into(), "run".into()]);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::ConflictViolation { conflict, .. } => assert_eq!(conflict, "verbosity"),
other => panic!("Expected ConflictViolation, got: {other:?}"),
}
}
#[test]
fn independent_groups_on_parent_and_subcommand() {
// Parent and subcommand each have their own group.
// Parent group is checked against global args; subcommand group against subcommand args.
let sub_parser = ArgBuilder::new()
.flag(Flag::new("json").desc("JSON"))
.flag(Flag::new("csv").desc("CSV"))
.group("format", &["json", "csv"])
.build()
.unwrap();
let parent = ArgBuilder::new()
.flag(Flag::new("verbose").desc("verbose"))
.flag(Flag::new("quiet").desc("quiet"))
.group("verbosity", &["verbose", "quiet"])
.subcommand("export", "export data", sub_parser)
.build()
.unwrap();
// Both groups satisfied
let result = parent
.parse(&["--verbose".into(), "export".into(), "--json".into()])
.unwrap();
assert!(result.get_flag("verbose"));
let sub = result.subcommand_result().unwrap();
assert!(sub.get_flag("json"));
}
#[test]
fn independent_conflicts_on_parent_and_subcommand() {
// Parent and subcommand each have their own conflict set.
let sub_parser = ArgBuilder::new()
.flag(Flag::new("json").desc("JSON"))
.flag(Flag::new("csv").desc("CSV"))
.conflict("format", &["json", "csv"])
.build()
.unwrap();
let parent = ArgBuilder::new()
.flag(Flag::new("verbose").desc("verbose"))
.flag(Flag::new("quiet").desc("quiet"))
.conflict("verbosity", &["verbose", "quiet"])
.subcommand("export", "export data", sub_parser)
.build()
.unwrap();
// Parent conflict OK (one member), subcommand conflict OK (one member)
let result = parent
.parse(&["--verbose".into(), "export".into(), "--csv".into()])
.unwrap();
assert!(result.get_flag("verbose"));
let sub = result.subcommand_result().unwrap();
assert!(sub.get_flag("csv"));
// Subcommand conflict violated (both members)
let result2 = parent.parse(&[
"--verbose".into(),
"export".into(),
"--json".into(),
"--csv".into(),
]);
assert!(result2.is_err());
match result2.unwrap_err() {
ParseError::ConflictViolation { conflict, .. } => assert_eq!(conflict, "format"),
other => panic!("Expected ConflictViolation on subcommand, got: {other:?}"),
}
}