summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2024-04-24 23:51:54 +0200
committerJoel Klinghed <the_jk@spawned.biz>2024-04-25 00:19:52 +0200
commit4555ccb38a0fc325240c338e2046f24df1c82cef (patch)
tree974529d7c364ca838f00519146638e80fa3ec30b
parent92742afc7724fd502d65f8056f245c2401d3ce07 (diff)
args: Add tests
Also minor fix in LongOnlyParser, support "-" as argument.
-rw-r--r--src/args.rs481
1 files changed, 470 insertions, 11 deletions
diff --git a/src/args.rs b/src/args.rs
index 9f6fe3d..5081a15 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -2,6 +2,7 @@
use derive_builder::Builder;
use std::collections::HashMap;
+use std::io::{stdout, Write};
pub enum ValueRequirement {
None,
@@ -99,7 +100,15 @@ pub trait Parser {
options: &mut Options,
args: &mut dyn Iterator<Item = String>,
) -> Result<Arguments, String>;
- fn print_help(&self, options: &Options);
+
+ fn print_help(&self, options: &Options) {
+ let lock = std::sync::Mutex::new(stdout().lock());
+ self.print_help_fn(options, &|a| {
+ writeln!(lock.lock().unwrap(), "{}", a).unwrap();
+ });
+ }
+
+ fn print_help_fn(&self, options: &Options, println: &dyn Fn(String));
}
#[allow(dead_code)]
@@ -108,13 +117,20 @@ pub struct LongOnlyParser {}
#[allow(dead_code)]
pub struct ShortAndLongParser {}
-fn print_list(list: Vec<(String, &str)>) {
+fn print_list(list: Vec<(String, &str)>, println: &dyn Fn(String)) {
let mut left_len: usize = 0;
for (left, _) in &list {
left_len = std::cmp::max(left_len, left.len());
}
for (left, right) in &list {
- println!("{left:<left_len$} {right}");
+ let mut right_lines = right.split('\n');
+ println(format!(
+ "{left:<left_len$} {}",
+ right_lines.next().unwrap()
+ ));
+ for right_line in right_lines {
+ println(format!("{:<left_len$} {right_line}", ""));
+ }
}
}
@@ -134,7 +150,7 @@ impl Parser for LongOnlyParser {
let mut ret = Vec::new();
let program = args.next();
while let Some(arg) = args.next() {
- if arg.starts_with("-") {
+ if arg.len() >= 2 && arg.starts_with("-") {
if arg == "--" {
// All following arguments are just that.
while let Some(arg) = args.next() {
@@ -157,7 +173,7 @@ impl Parser for LongOnlyParser {
match option.value_req {
ValueRequirement::None => {
if value.is_some() {
- return Err(format!("option '{}' doesn't allow an argument", arg));
+ return Err(format!("option '-{name}' doesn't allow an argument"));
}
}
ValueRequirement::Required(_) => {
@@ -181,7 +197,7 @@ impl Parser for LongOnlyParser {
Ok(Arguments { program, args: ret })
}
- fn print_help(&self, options: &Options) {
+ fn print_help_fn(&self, options: &Options, println: &dyn Fn(String)) {
let mut lines = Vec::new();
for option in &options.options {
let left = match option.value_req {
@@ -191,7 +207,7 @@ impl Parser for LongOnlyParser {
};
lines.push((left, option.description()));
}
- print_list(lines);
+ print_list(lines, println);
}
}
@@ -234,7 +250,10 @@ impl Parser for ShortAndLongParser {
match option.value_req {
ValueRequirement::None => {
if value.is_some() {
- return Err(format!("option '{}' doesn't allow an argument", arg));
+ return Err(format!(
+ "option '--{}' doesn't allow an argument",
+ name
+ ));
}
}
ValueRequirement::Required(_) => {
@@ -261,7 +280,7 @@ impl Parser for ShortAndLongParser {
ValueRequirement::Required(_) => {
value = args.next();
if value.is_none() {
- return Err(format!("option requires an argument -- '{}", c));
+ return Err(format!("option requires an argument -- '{}'", c));
}
}
ValueRequirement::Optional(_) => {}
@@ -278,7 +297,7 @@ impl Parser for ShortAndLongParser {
Ok(Arguments { program, args: ret })
}
- fn print_help(&self, options: &Options) {
+ fn print_help_fn(&self, options: &Options, println: &dyn Fn(String)) {
let mut lines = Vec::new();
for option in &options.options {
let left: String;
@@ -309,6 +328,446 @@ impl Parser for ShortAndLongParser {
}
lines.push((left, option.description()));
}
- print_list(lines);
+ print_list(lines, println);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ struct Params {
+ options: Options,
+
+ no_value_idx: usize,
+ opt_value_idx: usize,
+ value_idx: usize,
+
+ long_no_value_idx: usize,
+ long_opt_value_idx: usize,
+ long_value_idx: usize,
+ }
+
+ fn params_new() -> Params {
+ let mut options = Options::new();
+ let no_value_idx = options.push(
+ OptionBuilder::default()
+ .short('V')
+ .long("version")
+ .description("display version and exit")
+ .build()
+ .unwrap(),
+ );
+ let opt_value_idx = options.push(
+ OptionBuilder::default()
+ .short('s')
+ .long("sort")
+ .description("sort by optional key")
+ .value(ValueRequirement::Optional("key"))
+ .build()
+ .unwrap(),
+ );
+ let value_idx = options.push(
+ OptionBuilder::default()
+ .short('I')
+ .long("input")
+ .description("input FILE")
+ .value(ValueRequirement::Required("FILE"))
+ .build()
+ .unwrap(),
+ );
+ let long_no_value_idx = options.push(
+ OptionBuilder::default()
+ .long("full-time")
+ .description("like -l -time-style=full-iso")
+ .value(ValueRequirement::None)
+ .build()
+ .unwrap(),
+ );
+ let long_opt_value_idx = options.push(
+ OptionBuilder::default()
+ .long("hyperlink")
+ .description("hyperlink file names WHEN")
+ .value(ValueRequirement::Optional("WHEN"))
+ .build()
+ .unwrap(),
+ );
+ let long_value_idx = options.push(
+ OptionBuilder::default()
+ .long("indicator-style")
+ .description(
+ "append indicator with style WORD to entry names:
+none (default), slash (-p),
+file-type (--file-type), classify (-F)",
+ )
+ .value(ValueRequirement::Required("WORD"))
+ .build()
+ .unwrap(),
+ );
+ return Params {
+ options,
+ no_value_idx,
+ opt_value_idx,
+ value_idx,
+ long_no_value_idx,
+ long_opt_value_idx,
+ long_value_idx,
+ };
+ }
+
+ fn assert_empty(parser: &Box<dyn Parser>) {
+ let mut params = params_new();
+ let args = parser
+ .run(&mut params.options, &mut [].iter().cloned())
+ .unwrap();
+ assert_eq!(args.program, None);
+ assert_eq!(args.args.len(), 0);
+ }
+
+ fn assert_program(parser: &Box<dyn Parser>) {
+ let mut params = params_new();
+ let args = parser
+ .run(
+ &mut params.options,
+ &mut ["test".to_string()].iter().cloned(),
+ )
+ .unwrap();
+ assert_eq!(args.program, Some("test".to_string()));
+ assert_eq!(args.args.len(), 0);
+ }
+
+ fn assert_program_arg(parser: &Box<dyn Parser>) {
+ let mut params = params_new();
+ let args = parser
+ .run(
+ &mut params.options,
+ &mut ["test", "file"].map(|s| s.to_string()).iter().cloned(),
+ )
+ .unwrap();
+ assert_eq!(args.program, Some("test".to_string()));
+ assert_eq!(args.args, vec!["file".to_string()]);
+ }
+
+ fn assert_options1(parser: &Box<dyn Parser>, input: Vec<&str>) {
+ let mut params = params_new();
+ let args = parser
+ .run(
+ &mut params.options,
+ &mut input.iter().map(|s| s.to_string()),
+ )
+ .unwrap();
+ assert_eq!(args.program, Some("test".to_string()));
+ assert_eq!(params.options[params.no_value_idx].is_set(), true);
+ assert_eq!(params.options[params.opt_value_idx].is_set(), true);
+ assert_eq!(params.options[params.opt_value_idx].value, None);
+ assert_eq!(params.options[params.value_idx].is_set(), true);
+ assert_eq!(
+ params.options[params.value_idx].value,
+ Some("foo".to_string())
+ );
+ assert_eq!(args.args, vec!["arg".to_string()]);
+ }
+
+ fn assert_options2(parser: &Box<dyn Parser>, input: Vec<&str>) {
+ let mut params = params_new();
+ let args = parser
+ .run(
+ &mut params.options,
+ &mut input.iter().map(|s| s.to_string()),
+ )
+ .unwrap();
+ assert_eq!(args.program, Some("test".to_string()));
+ assert_eq!(params.options[params.long_no_value_idx].is_set(), true);
+ assert_eq!(params.options[params.long_opt_value_idx].is_set(), true);
+ assert_eq!(
+ params.options[params.long_opt_value_idx].value,
+ Some("foo".to_string())
+ );
+ assert_eq!(params.options[params.long_value_idx].is_set(), true);
+ assert_eq!(
+ params.options[params.long_value_idx].value,
+ Some("bar".to_string())
+ );
+ assert_eq!(args.args, vec!["arg".to_string()]);
+ }
+
+ fn assert_args_with_dash(parser: &Box<dyn Parser>, input: Vec<&str>) {
+ let mut params = params_new();
+ let args = parser
+ .run(
+ &mut params.options,
+ &mut input.iter().map(|s| s.to_string()),
+ )
+ .unwrap();
+ assert_eq!(args.program, Some("test".to_string()));
+ assert_eq!(
+ params.options[params.value_idx].value,
+ Some("-foo".to_string())
+ );
+ assert_eq!(
+ args.args,
+ vec![
+ "-".to_string(),
+ "-sort".to_string(),
+ "-full-time".to_string(),
+ ]
+ );
+ }
+
+ fn assert_error(parser: &Box<dyn Parser>, input: Vec<&str>, error: &str) {
+ let mut params = params_new();
+ let maybe_args = parser.run(
+ &mut params.options,
+ &mut input.iter().map(|s| s.to_string()),
+ );
+ assert_eq!(maybe_args.is_err(), true);
+ unsafe {
+ assert_eq!(maybe_args.unwrap_err_unchecked(), error);
+ }
+ }
+
+ fn assert_help(parser: &Box<dyn Parser>, expected: &str) {
+ let params = params_new();
+ let out = std::sync::Mutex::new(String::new());
+ parser.print_help_fn(&params.options, &|line| {
+ let mut o = out.lock().unwrap();
+ *o += &line;
+ *o += "\n";
+ });
+ assert_eq!(*out.lock().unwrap(), expected);
+ }
+
+ #[test]
+ fn empty_longonly() {
+ let parser: Box<dyn Parser> = Box::new(LongOnlyParser::new());
+ assert_empty(&parser);
+ }
+
+ #[test]
+ fn empty_shortandlong() {
+ let parser: Box<dyn Parser> = Box::new(ShortAndLongParser::new());
+ assert_empty(&parser);
+ }
+
+ #[test]
+ fn program_longonly() {
+ let parser: Box<dyn Parser> = Box::new(LongOnlyParser::new());
+ assert_program(&parser);
+ }
+
+ #[test]
+ fn program_shortandlong() {
+ let parser: Box<dyn Parser> = Box::new(ShortAndLongParser::new());
+ assert_program(&parser);
+ }
+
+ #[test]
+ fn program_arg_longonly() {
+ let parser: Box<dyn Parser> = Box::new(LongOnlyParser::new());
+ assert_program_arg(&parser);
+ }
+
+ #[test]
+ fn program_arg_shortandlong() {
+ let parser: Box<dyn Parser> = Box::new(ShortAndLongParser::new());
+ assert_program_arg(&parser);
+ }
+
+ #[test]
+ fn options1_longonly() {
+ let parser: Box<dyn Parser> = Box::new(LongOnlyParser::new());
+ assert_options1(
+ &parser,
+ vec!["test", "-version", "-sort", "-input=foo", "arg"],
+ );
+ assert_options1(
+ &parser,
+ vec!["test", "-version", "-sort", "-input", "foo", "arg"],
+ );
+ }
+
+ #[test]
+ fn options1_shortandlong() {
+ let parser: Box<dyn Parser> = Box::new(ShortAndLongParser::new());
+ assert_options1(
+ &parser,
+ vec!["test", "--version", "--sort", "--input=foo", "arg"],
+ );
+ assert_options1(
+ &parser,
+ vec!["test", "--version", "--sort", "--input", "foo", "arg"],
+ );
+ assert_options1(&parser, vec!["test", "-V", "-s", "-I", "foo", "arg"]);
+ assert_options1(&parser, vec!["test", "-VsI", "foo", "arg"]);
+ }
+
+ #[test]
+ fn options2_longonly() {
+ let parser: Box<dyn Parser> = Box::new(LongOnlyParser::new());
+ assert_options2(
+ &parser,
+ vec![
+ "test",
+ "-full-time",
+ "-hyperlink=foo",
+ "-indicator-style=bar",
+ "arg",
+ ],
+ );
+ assert_options2(
+ &parser,
+ vec![
+ "test",
+ "-full-time",
+ "-hyperlink=foo",
+ "-indicator-style",
+ "bar",
+ "arg",
+ ],
+ );
+ }
+
+ #[test]
+ fn options2_shortandlong() {
+ let parser: Box<dyn Parser> = Box::new(ShortAndLongParser::new());
+ assert_options2(
+ &parser,
+ vec![
+ "test",
+ "--full-time",
+ "--hyperlink=foo",
+ "--indicator-style=bar",
+ "arg",
+ ],
+ );
+ assert_options2(
+ &parser,
+ vec![
+ "test",
+ "--full-time",
+ "--hyperlink=foo",
+ "--indicator-style",
+ "bar",
+ "arg",
+ ],
+ );
+ }
+
+ #[test]
+ fn args_with_dash_longonly() {
+ let parser: Box<dyn Parser> = Box::new(LongOnlyParser::new());
+ assert_args_with_dash(
+ &parser,
+ vec!["test", "-", "-input", "-foo", "--", "-sort", "-full-time"],
+ );
+ }
+
+ #[test]
+ fn args_with_dash_shortandlong() {
+ let parser: Box<dyn Parser> = Box::new(ShortAndLongParser::new());
+ assert_args_with_dash(
+ &parser,
+ vec!["test", "-", "--input", "-foo", "--", "-sort", "-full-time"],
+ );
+ }
+
+ #[test]
+ fn bad_option_longonly() {
+ let parser: Box<dyn Parser> = Box::new(LongOnlyParser::new());
+ assert_error(
+ &parser,
+ vec!["test", "-unknown"],
+ "unrecognized option '-unknown'",
+ );
+ }
+
+ #[test]
+ fn bad_option_shortandlong() {
+ let parser: Box<dyn Parser> = Box::new(ShortAndLongParser::new());
+ assert_error(
+ &parser,
+ vec!["test", "--unknown"],
+ "unrecognized option '--unknown'",
+ );
+ assert_error(&parser, vec!["test", "-U"], "invalid option -- 'U'");
+ }
+
+ #[test]
+ fn missing_arg_longonly() {
+ let parser: Box<dyn Parser> = Box::new(LongOnlyParser::new());
+ assert_error(
+ &parser,
+ vec!["test", "-input"],
+ "option '-input' requires an argument",
+ );
+ }
+
+ #[test]
+ fn missing_arg_shortandlong() {
+ let parser: Box<dyn Parser> = Box::new(ShortAndLongParser::new());
+ assert_error(
+ &parser,
+ vec!["test", "--input"],
+ "option '--input' requires an argument",
+ );
+ assert_error(
+ &parser,
+ vec!["test", "-I"],
+ "option requires an argument -- 'I'",
+ );
+ }
+
+ #[test]
+ fn unexpected_arg_longonly() {
+ let parser: Box<dyn Parser> = Box::new(LongOnlyParser::new());
+ assert_error(
+ &parser,
+ vec!["test", "-version=foo"],
+ "option '-version' doesn't allow an argument",
+ );
+ }
+
+ #[test]
+ fn unexpected_arg_shortandlong() {
+ let parser: Box<dyn Parser> = Box::new(ShortAndLongParser::new());
+ assert_error(
+ &parser,
+ vec!["test", "--version=foo"],
+ "option '--version' doesn't allow an argument",
+ );
+ }
+
+ #[test]
+ fn help_longonly() {
+ let parser: Box<dyn Parser> = Box::new(LongOnlyParser::new());
+ assert_help(
+ &parser,
+ "-version display version and exit
+-sort[=key] sort by optional key
+-input=FILE input FILE
+-full-time like -l -time-style=full-iso
+-hyperlink[=WHEN] hyperlink file names WHEN
+-indicator-style=WORD append indicator with style WORD to entry names:
+ none (default), slash (-p),
+ file-type (--file-type), classify (-F)
+",
+ );
+ }
+
+ #[test]
+ fn help_shortandlong() {
+ let parser: Box<dyn Parser> = Box::new(ShortAndLongParser::new());
+ assert_help(
+ &parser,
+ "-V, --version display version and exit
+-s, --sort[=key] sort by optional key
+-I, --input=FILE input FILE
+ --full-time like -l -time-style=full-iso
+ --hyperlink[=WHEN] hyperlink file names WHEN
+ --indicator-style=WORD append indicator with style WORD to entry names:
+ none (default), slash (-p),
+ file-type (--file-type), classify (-F)
+",
+ );
}
}