use super::{
Arity, Boundary, DEFAULT_MODE, Occurrence, OptionError, OptionErrorKind, OptionSpec,
OptionValue, ParseMode, ParsedOptions, WrapperOccurrence, parse_options, row,
};
use crate::command_options_query::{positional_tokens, value_bytes, without_tokens};
use crate::command_test_support::{
ALL, AMEND, MESSAGE, SYNTHETIC_TABLE, assert_table_invariants, os_arguments, parse_synthetic,
};
use std::ffi::OsString;
const INCUMBENT_TABLE: &[OptionSpec] = &[
row(ALL, Some(b'a'), Some("all"), Arity::None, true),
row(MESSAGE, Some(b'm'), Some("message"), Arity::Required, true),
];
const KEEP_UNKNOWN: ParseMode = ParseMode {
keep_unknown: true,
stop_at_non_option: false,
};
fn parse(values: &[&str], mode: ParseMode) -> Result<ParsedOptions, OptionError> {
return parse_options(os_arguments(values).as_slice(), INCUMBENT_TABLE, mode, &[]);
}
fn message_from_next(token: usize) -> Occurrence {
return Occurrence {
id: MESSAGE,
negated: false,
value: Some(OptionValue::Detached { token: token + 1 }),
token,
};
}
fn all_at(token: usize) -> Occurrence {
return Occurrence {
id: ALL,
negated: false,
value: None,
token,
};
}
#[test]
fn takes_a_declared_option_value_and_keeps_following_positionals() {
let parsed: ParsedOptions = parse(&["-m", "msg", "a.txt"], DEFAULT_MODE).expect("valid");
assert_eq!(parsed.occurrences, vec![message_from_next(0)]);
assert_eq!(parsed.leading, vec![2]);
}
#[test]
fn counts_declared_flags_without_consuming_positionals() {
let single: ParsedOptions = parse(&["-a", "a.txt", "b.txt"], DEFAULT_MODE).expect("valid");
assert_eq!(single.occurrences, vec![all_at(0)]);
assert_eq!(single.leading, vec![1, 2]);
let repeated: ParsedOptions = parse(&["-a", "-a"], DEFAULT_MODE).expect("valid");
assert_eq!(repeated.occurrences, vec![all_at(0), all_at(1)]);
assert!(repeated.leading.is_empty());
}
#[test]
fn refuses_an_undeclared_option_as_git_does() {
for values in [
vec!["-q", "a.txt"],
vec!["-q", "-a"],
vec!["-q", "--", "a.txt"],
vec!["-q"],
vec!["--unknown=v", "a.txt"],
vec!["--unknown"],
] {
assert_eq!(
parse(values.as_slice(), DEFAULT_MODE),
Err(OptionError {
kind: OptionErrorKind::UnknownOption,
token: 0
}),
"{values:?}"
);
}
assert_eq!(
parse(&["-a", "-aq"], DEFAULT_MODE),
Err(OptionError {
kind: OptionErrorKind::UnknownOption,
token: 1
})
);
}
#[test]
fn keeps_an_undeclared_option_without_consuming_the_next_token() {
for values in [vec!["-q", "a.txt"], vec!["--unknown=v", "a.txt"]] {
let parsed: ParsedOptions = parse(values.as_slice(), KEEP_UNKNOWN).expect("kept");
assert_eq!(parsed.unknown, vec![0], "{values:?}");
assert_eq!(parsed.leading, vec![1], "{values:?}");
}
let mixed: ParsedOptions = parse(&["-aq"], KEEP_UNKNOWN).expect("kept");
assert_eq!(mixed.occurrences, vec![all_at(0)]);
assert_eq!(mixed.unknown, vec![0]);
let abbreviated: ParsedOptions =
parse(&["--mess=x", "--message=y"], KEEP_UNKNOWN).expect("kept");
assert_eq!(abbreviated.unknown, vec![0]);
assert_eq!(abbreviated.occurrences.len(), 1);
let plain: ParsedOptions = parse(&["--mess=x"], DEFAULT_MODE).expect("valid");
assert_eq!(plain.occurrences.len(), 1);
}
#[test]
fn takes_a_declared_joined_option_value() {
let arguments: Vec<OsString> = os_arguments(&["--message=msg", "a.txt", "--message="]);
let parsed: ParsedOptions =
parse_options(arguments.as_slice(), INCUMBENT_TABLE, DEFAULT_MODE, &[]).expect("valid");
let first: OptionValue = parsed.occurrences[0].value.expect("joined value");
assert_eq!(
first,
OptionValue::Attached {
token: 0,
offset: 10
}
);
assert_eq!(value_bytes(arguments.as_slice(), first), b"msg");
let empty: OptionValue = parsed.occurrences[1].value.expect("empty joined value");
assert_eq!(value_bytes(arguments.as_slice(), empty), b"");
assert_eq!(parsed.leading, vec![1]);
}
#[test]
fn names_the_offending_token_when_a_value_is_missing() {
for (values, token) in [
(vec!["-a", "-m"], 1),
(vec!["-m"], 0),
(vec!["--message"], 0),
] {
assert_eq!(
parse(values.as_slice(), DEFAULT_MODE),
Err(OptionError {
kind: OptionErrorKind::MissingValue,
token
}),
"{values:?}"
);
}
let error: OptionError = parse(&["-a", "-m"], DEFAULT_MODE).expect_err("refused");
assert!(error.to_string().contains("argument 1"), "{error}");
assert!(error.to_string().contains("MissingValue"), "{error}");
}
#[test]
fn takes_a_dash_led_token_as_a_declared_option_value() {
let parsed: ParsedOptions = parse(&["-m", "-a"], DEFAULT_MODE).expect("valid");
assert_eq!(parsed.occurrences, vec![message_from_next(0)]);
let terminator: ParsedOptions = parse(&["-m", "--", "x"], DEFAULT_MODE).expect("valid");
assert_eq!(terminator.boundary, Boundary::End);
assert_eq!(terminator.leading, vec![2]);
}
#[test]
fn separates_options_positionals_and_the_terminator() {
let after: ParsedOptions = parse(&["a.txt", "-m", "msg"], DEFAULT_MODE).expect("valid");
assert_eq!(after.leading, vec![0]);
assert_eq!(after.occurrences, vec![message_from_next(1)]);
let dash: ParsedOptions = parse(&["-", ""], DEFAULT_MODE).expect("valid");
assert_eq!(dash.leading, vec![0, 1]);
let terminated: ParsedOptions = parse(&["--", "-a"], DEFAULT_MODE).expect("valid");
assert!(terminated.occurrences.is_empty());
assert_eq!(terminated.boundary, Boundary::DashDash(0));
assert_eq!(positional_tokens(&terminated, 2), vec![1]);
let before: ParsedOptions = parse(&["-a", "--", "a.txt"], DEFAULT_MODE).expect("valid");
assert_eq!(before.occurrences, vec![all_at(0)]);
assert_eq!(positional_tokens(&before, 3), vec![2]);
}
#[test]
#[should_panic(expected = "nodash")]
fn rejects_a_declared_spelling_that_cannot_introduce_an_option() {
assert_table_invariants(&[row(ALL, None, Some("-nodash"), Arity::None, true)]);
}
#[test]
#[should_panic(expected = "duplicate short")]
fn rejects_a_table_with_a_repeated_short_letter() {
assert_table_invariants(&[
row(ALL, Some(b'a'), Some("all"), Arity::None, true),
row(AMEND, Some(b'a'), Some("amend"), Arity::None, true),
]);
}
#[test]
fn synthetic_tables_are_well_formed() {
assert_table_invariants(SYNTHETIC_TABLE);
assert_table_invariants(INCUMBENT_TABLE);
}
#[test]
fn reports_each_boundary_kind() {
let end: ParsedOptions = parse_synthetic(&["--end-of-options", "-a"]).expect("valid");
assert_eq!(end.boundary, Boundary::EndOfOptions(0));
assert_eq!(positional_tokens(&end, 2), vec![1]);
let stop: ParseMode = ParseMode {
keep_unknown: false,
stop_at_non_option: true,
};
let stopped: ParsedOptions = parse(&["-a", "file", "-a", "--"], stop).expect("valid");
assert_eq!(stopped.boundary, Boundary::NonOption(1));
assert_eq!(stopped.occurrences, vec![all_at(0)]);
assert_eq!(positional_tokens(&stopped, 4), vec![1, 2, 3]);
let complete: ParsedOptions = parse_synthetic(&["x", "-a"]).expect("valid");
assert_eq!(complete.boundary, Boundary::End);
assert_eq!(positional_tokens(&complete, 2), vec![0]);
}
#[test]
fn finds_wrapper_flags_in_option_position_only() {
let arguments: Vec<OsString> = os_arguments(&[
"--no-enforce-only",
"-m",
"--no-enforce-only",
"--cli-git-keep-going",
"--",
"--no-enforce-only",
]);
let wrapper: &[&[u8]] = &[b"--cli-git-keep-going", b"--no-enforce-only"];
let parsed: ParsedOptions =
parse_options(arguments.as_slice(), SYNTHETIC_TABLE, DEFAULT_MODE, wrapper).expect("ok");
assert_eq!(
parsed.wrapper,
vec![
WrapperOccurrence { flag: 1, token: 0 },
WrapperOccurrence { flag: 0, token: 3 },
]
);
assert_eq!(positional_tokens(&parsed, arguments.len()), vec![5]);
assert_eq!(
parse_synthetic(&["--no-enforce-only"]),
Err(OptionError {
kind: OptionErrorKind::UnknownOption,
token: 0
})
);
assert_eq!(
parse_synthetic(&["-a"]).expect("ok").wrapper,
Vec::<WrapperOccurrence>::new()
);
let mut full: Vec<OsString> = os_arguments(&["-C", "/r", "commit"]);
full.extend(arguments.clone());
assert_eq!(
without_tokens(full.as_slice(), 3, &[0, 3]),
os_arguments(&[
"-C",
"/r",
"commit",
"-m",
"--no-enforce-only",
"--",
"--no-enforce-only"
])
);
}
#[cfg(unix)]
#[test]
fn keeps_non_utf8_bytes_in_values_and_paths() {
use crate::command_test_support::byte_argument;
let arguments: Vec<OsString> = vec![
byte_argument(b"-m\xff\xfe"),
byte_argument(b"--message=\xff"),
byte_argument(b"path-\xff"),
OsString::from("-m"),
byte_argument(b"\x80"),
];
let snapshot: Vec<OsString> = arguments.clone();
let parsed: ParsedOptions =
parse_options(arguments.as_slice(), SYNTHETIC_TABLE, DEFAULT_MODE, &[]).expect("ok");
let values: Vec<&[u8]> = vec![
value_bytes(
arguments.as_slice(),
parsed.occurrences[0].value.expect("v"),
),
value_bytes(
arguments.as_slice(),
parsed.occurrences[1].value.expect("v"),
),
value_bytes(
arguments.as_slice(),
parsed.occurrences[2].value.expect("v"),
),
];
assert_eq!(values, vec![&b"\xff\xfe"[..], &b"\xff"[..], &b"\x80"[..]]);
assert_eq!(parsed.leading, vec![2]);
assert_eq!(arguments, snapshot);
assert_eq!(
parse_options(
&[byte_argument(b"-\xff")],
SYNTHETIC_TABLE,
DEFAULT_MODE,
&[]
),
Err(OptionError {
kind: OptionErrorKind::UnknownOption,
token: 0
})
);
}