diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2024-04-24 23:51:54 +0200 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2024-04-25 00:19:52 +0200 |
| commit | 4555ccb38a0fc325240c338e2046f24df1c82cef (patch) | |
| tree | 974529d7c364ca838f00519146638e80fa3ec30b | |
| parent | 92742afc7724fd502d65f8056f245c2401d3ce07 (diff) | |
args: Add tests
Also minor fix in LongOnlyParser, support "-" as argument.
| -rw-r--r-- | src/args.rs | 481 |
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(¶ms.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) +", + ); } } |
