use help_probe::model::{ArgumentType, OptionSpec, OptionType};
use help_probe::parser::{
detect_help_flag, parse_arguments, parse_environment_variables, parse_examples,
parse_options_from_sections, parse_options_from_usage_blocks, parse_subcommands, parse_usages,
parse_validation_rules,
};
#[test]
fn detect_help_flag_works() {
let args = vec!["-h".to_string()];
assert!(detect_help_flag(&args));
let args = vec!["--help".to_string()];
assert!(detect_help_flag(&args));
let args = vec!["HELP".to_string()];
assert!(detect_help_flag(&args));
let args = vec!["run".to_string(), "--usage".to_string()];
assert!(detect_help_flag(&args));
let args = vec!["run".to_string()];
assert!(!detect_help_flag(&args));
}
#[test]
fn parse_usages_finds_usage_block() {
let stdout = b"Some header\nUsage: myprog [OPTIONS]\nMore help here\n";
let stderr = b"";
let blocks = parse_usages(stdout, stderr);
assert_eq!(blocks.len(), 1);
assert!(blocks[0].contains("Usage: myprog [OPTIONS]"));
}
#[test]
fn parse_options_from_usage_blocks_extracts_flags() {
let block = r#"
Options:
-h, --help Show help message
-v, --verbose Verbose output
--version Print version
"#;
let blocks = vec![block.to_string()];
let opts: Vec<OptionSpec> = parse_options_from_usage_blocks(&blocks);
assert!(
opts.iter()
.any(|o| o.short_flags.contains(&"-h".to_string())
&& o.long_flags.contains(&"--help".to_string()))
);
assert!(
opts.iter()
.any(|o| o.short_flags.contains(&"-v".to_string())
&& o.long_flags.contains(&"--verbose".to_string()))
);
assert!(
opts.iter()
.any(|o| o.long_flags.contains(&"--version".to_string()))
);
}
#[test]
fn parse_subcommands_finds_subcommands() {
let help_text = r#"
Some tool
SUBCOMMANDS:
build Build the project
run Run the project
Options:
-h, --help Show help
"#;
let subs = parse_subcommands(help_text, "");
assert!(subs.iter().any(|s| s.name == "build"));
assert!(subs.iter().any(|s| s.name == "run"));
let build = subs.iter().find(|s| s.name == "build").unwrap();
assert!(
build
.description
.as_ref()
.unwrap()
.contains("Build the project")
);
}
#[test]
fn parse_arguments_from_usage_line() {
let usage_block = r#"
Usage: mytool [OPTIONS] <FILE> [OUTPUT] <PATTERN>...
"#;
let blocks = vec![usage_block.to_string()];
let args = parse_arguments("", "", &blocks);
let file_arg = args.iter().find(|a| a.name == "FILE").unwrap();
assert!(file_arg.required);
assert!(!file_arg.variadic);
assert_eq!(file_arg.arg_type, Some(ArgumentType::Path));
let output_arg = args.iter().find(|a| a.name == "OUTPUT").unwrap();
assert!(!output_arg.required);
assert!(!output_arg.variadic);
let pattern_arg = args.iter().find(|a| a.name == "PATTERN").unwrap();
assert!(pattern_arg.required);
assert!(pattern_arg.variadic);
}
#[test]
fn parse_arguments_type_inference() {
let usage_block = r#"
Usage: mytool <FILE> <PORT> <URL> <EMAIL> <COUNT>
"#;
let blocks = vec![usage_block.to_string()];
let args = parse_arguments("", "", &blocks);
let file = args.iter().find(|a| a.name == "FILE").unwrap();
assert_eq!(file.arg_type, Some(ArgumentType::Path));
let port = args.iter().find(|a| a.name == "PORT").unwrap();
assert_eq!(port.arg_type, Some(ArgumentType::Number));
let url = args.iter().find(|a| a.name == "URL").unwrap();
assert_eq!(url.arg_type, Some(ArgumentType::Url));
let email = args.iter().find(|a| a.name == "EMAIL").unwrap();
assert_eq!(email.arg_type, Some(ArgumentType::Email));
}
#[test]
fn parse_options_with_enhanced_metadata() {
let block = r#"
Options:
-v, --verbose Enable verbose output (boolean)
-f, --file <FILE> Input file (takes argument)
--port <PORT> Server port (number, default: 8080)
--level {debug|info|warn} Log level (choice)
--config=<PATH> Config file path
"#;
let blocks = vec![block.to_string()];
let opts = parse_options_from_usage_blocks(&blocks);
let verbose = opts
.iter()
.find(|o| o.long_flags.contains(&"--verbose".to_string()))
.unwrap();
assert_eq!(verbose.option_type, OptionType::Boolean);
assert!(!verbose.takes_argument);
let file = opts
.iter()
.find(|o| o.long_flags.contains(&"--file".to_string()))
.unwrap();
assert!(file.takes_argument);
assert_eq!(file.argument_name, Some("FILE".to_string()));
assert_eq!(file.option_type, OptionType::Path);
let port = opts
.iter()
.find(|o| o.long_flags.contains(&"--port".to_string()))
.unwrap();
assert!(port.takes_argument);
assert_eq!(port.argument_name, Some("PORT".to_string()));
assert_eq!(port.option_type, OptionType::Number);
assert_eq!(port.default_value, Some("8080".to_string()));
let level = opts
.iter()
.find(|o| o.long_flags.contains(&"--level".to_string()))
.unwrap();
assert_eq!(level.option_type, OptionType::Choice);
assert!(!level.choices.is_empty());
assert!(level.choices.contains(&"debug".to_string()));
assert!(level.choices.contains(&"info".to_string()));
assert!(level.choices.contains(&"warn".to_string()));
}
#[test]
fn test_parse_environment_variables() {
let help_text = r#"
Some tool
Options:
--config <FILE> Config file (can be set via $CONFIG_FILE, default: config.json)
--debug Enable debug mode (uses DEBUG environment variable)
--port <PORT> Server port (set PORT to override, default: 8080)
Environment Variables:
CONFIG_FILE Path to config file
DEBUG Enable debug mode (true/false)
PORT Server port number
"#;
let options = vec![
OptionSpec {
short_flags: vec![],
long_flags: vec!["--config".to_string()],
description: Some(
"Config file (can be set via $CONFIG_FILE, default: config.json)".to_string(),
),
option_type: OptionType::Path,
required: false,
default_value: Some("config.json".to_string()),
takes_argument: true,
argument_name: Some("FILE".to_string()),
choices: vec![],
},
OptionSpec {
short_flags: vec![],
long_flags: vec!["--debug".to_string()],
description: Some("Enable debug mode (uses DEBUG environment variable)".to_string()),
option_type: OptionType::Boolean,
required: false,
default_value: None,
takes_argument: false,
argument_name: None,
choices: vec![],
},
];
let env_vars = parse_environment_variables(help_text, "", &options);
assert!(!env_vars.is_empty());
if let Some(config_file) = env_vars.iter().find(|e| e.name == "CONFIG_FILE") {
assert!(config_file.description.is_some() || config_file.option_mapped.is_some());
}
if let Some(debug) = env_vars.iter().find(|e| e.name == "DEBUG") {
assert!(debug.description.is_some() || debug.option_mapped.is_some());
}
if let Some(port) = env_vars.iter().find(|e| e.name == "PORT") {
assert!(port.description.is_some() || port.option_mapped.is_some());
}
}
#[test]
fn test_parse_validation_rules() {
use help_probe::model::{ArgumentSpec, ValidationType};
let options = vec![
OptionSpec {
short_flags: vec![],
long_flags: vec!["--port".to_string()],
description: Some("Server port (must be between 1-65535)".to_string()),
option_type: OptionType::Number,
required: false,
default_value: None,
takes_argument: true,
argument_name: Some("PORT".to_string()),
choices: vec![],
},
OptionSpec {
short_flags: vec![],
long_flags: vec!["--level".to_string()],
description: Some("Log level".to_string()),
option_type: OptionType::Choice,
required: false,
default_value: None,
takes_argument: true,
argument_name: Some("LEVEL".to_string()),
choices: vec!["debug".to_string(), "info".to_string(), "warn".to_string()],
},
];
let arguments = vec![
ArgumentSpec {
name: "EMAIL".to_string(),
description: Some("Email address (must be valid email)".to_string()),
required: true,
variadic: false,
arg_type: Some(ArgumentType::Email),
placeholder: Some("<EMAIL>".to_string()),
},
ArgumentSpec {
name: "COUNT".to_string(),
description: Some("Count (minimum 1, maximum 100)".to_string()),
required: false,
variadic: false,
arg_type: Some(ArgumentType::Number),
placeholder: Some("[COUNT]".to_string()),
},
];
let rules = parse_validation_rules("", "", &options, &arguments);
assert!(!rules.is_empty());
let required_rule = rules
.iter()
.find(|r| r.target == "EMAIL" && r.rule_type == ValidationType::Required);
assert!(required_rule.is_some());
let email_rule = rules
.iter()
.find(|r| r.target == "EMAIL" && r.rule_type == ValidationType::Format);
assert!(email_rule.is_some());
let range_rule = rules.iter().find(|r| r.rule_type == ValidationType::Range);
assert!(range_rule.is_some());
let choice_rule = rules.iter().find(|r| r.rule_type == ValidationType::Choice);
assert!(choice_rule.is_some());
}
#[test]
fn parse_examples_from_section() {
let help_text = r#"
Some tool
Usage: mytool [OPTIONS] <FILE>
Examples:
Basic usage:
$ mytool file.txt
Process a single file
Advanced usage with options:
$ mytool --verbose file1.txt file2.txt
Process multiple files with verbose output
"#;
let examples = parse_examples(help_text, "");
if examples.is_empty() {
return;
}
if let Some(basic) = examples.iter().find(|e| e.command.contains("file.txt")) {
assert!(basic.command.contains("mytool"));
}
if let Some(advanced) = examples.iter().find(|e| e.command.contains("--verbose")) {
assert!(advanced.command.contains("mytool"));
}
}
#[test]
fn test_parse_options_from_sections() {
let help_text = r#"
Some Tool
USAGE:
tool [OPTIONS]
OPTIONS:
-h, --help Show help message
-v, --verbose Verbose output
--version Print version
-f, --file <FILE> Input file
--output=<PATH> Output path with equals
SUBCOMMANDS:
build Build something
"#;
let opts = parse_options_from_sections(help_text, "");
assert!(
opts.iter()
.any(|o| o.short_flags.contains(&"-h".to_string())
&& o.long_flags.contains(&"--help".to_string()))
);
assert!(
opts.iter()
.any(|o| o.short_flags.contains(&"-v".to_string())
&& o.long_flags.contains(&"--verbose".to_string()))
);
assert!(
opts.iter()
.any(|o| o.long_flags.contains(&"--version".to_string()))
);
assert!(
opts.iter()
.any(|o| o.short_flags.contains(&"-f".to_string())
&& o.long_flags.contains(&"--file".to_string()))
);
}
#[test]
fn test_parse_arguments_from_section() {
let help_text = r#"
Some tool
Usage: mytool [OPTIONS] <FILE>
Arguments:
<FILE> Input file to process
[OUTPUT] Optional output file path
<PATTERN>... Search patterns (variadic)
"#;
let blocks = parse_usages(help_text.as_bytes(), b"");
let args = parse_arguments(help_text, "", &blocks);
let file_arg = args.iter().find(|a| a.name == "FILE");
assert!(file_arg.is_some(), "FILE argument not found");
let file_arg = file_arg.unwrap();
assert!(file_arg.required);
assert_eq!(file_arg.arg_type, Some(ArgumentType::Path));
let output_arg = args.iter().find(|a| a.name == "OUTPUT");
assert!(output_arg.is_some(), "OUTPUT argument not found");
let output_arg = output_arg.unwrap();
assert!(!output_arg.required);
let pattern_arg = args.iter().find(|a| a.name == "PATTERN");
assert!(pattern_arg.is_some(), "PATTERN argument not found");
let pattern_arg = pattern_arg.unwrap();
assert!(pattern_arg.required);
assert!(pattern_arg.variadic);
}