mod common;
use nanoargs::{Opt, OptionDef, Pos, PositionalDef, Validator};
use proptest::prelude::*;
use common::{arb_safe_description, arb_safe_identifier};
proptest! {
#[test]
fn prop_opt_builder_stores_validator(
long in arb_safe_identifier(),
desc in arb_safe_description(),
use_validator in any::<bool>(),
) {
let opt = Opt::new(&long).desc(&desc);
let opt = if use_validator {
opt.validate(Validator::new(|_| Ok(())))
} else {
opt
};
let def: OptionDef = opt.into();
prop_assert_eq!(def.validator.is_some(), use_validator);
}
}
proptest! {
#[test]
fn prop_pos_builder_stores_validator(
name in arb_safe_identifier(),
desc in arb_safe_description(),
use_validator in any::<bool>(),
) {
let pos = Pos::new(&name).desc(&desc);
let pos = if use_validator {
pos.validate(Validator::new(|_| Ok(())))
} else {
pos
};
let def: PositionalDef = pos.into();
prop_assert_eq!(def.validator.is_some(), use_validator);
}
}
use common::arb_value_string;
use nanoargs::{ArgBuilder, ParseError};
proptest! {
#[test]
fn prop_option_validation_verdict_matches_parse_outcome(
long in arb_safe_identifier(),
value in arb_value_string(),
accept in any::<bool>(),
) {
let err_msg = format!("rejected: {}", value);
let err_msg_clone = err_msg.clone();
let value_clone = value.clone();
let validator = Validator::new(move |v| {
if accept || v != value_clone { Ok(()) } else { Err(err_msg_clone.clone()) }
});
let parser = ArgBuilder::new()
.option(Opt::new(&long).validate(validator))
.build()
.unwrap();
let args = vec![format!("--{}", long), value.clone()];
let result = parser.parse(args);
if accept {
prop_assert!(result.is_ok(), "Expected Ok but got {:?}", result);
} else {
match result {
Err(ParseError::ValidationFailed { name, message }) => {
prop_assert_eq!(name, long);
prop_assert_eq!(message, err_msg);
}
other => prop_assert!(false, "Expected ValidationFailed but got {:?}", other),
}
}
}
}
proptest! {
#[test]
fn prop_positional_validation_verdict_matches_parse_outcome(
name in arb_safe_identifier(),
value in arb_value_string(),
accept in any::<bool>(),
) {
let err_msg = format!("rejected: {}", value);
let err_msg_clone = err_msg.clone();
let value_clone = value.clone();
let validator = Validator::new(move |v| {
if accept || v != value_clone { Ok(()) } else { Err(err_msg_clone.clone()) }
});
let parser = ArgBuilder::new()
.positional(Pos::new(&name).validate(validator))
.build()
.unwrap();
let args = vec![value.clone()];
let result = parser.parse(args);
if accept {
prop_assert!(result.is_ok(), "Expected Ok but got {:?}", result);
} else {
match result {
Err(ParseError::ValidationFailed { name: n, message }) => {
prop_assert_eq!(n, name);
prop_assert_eq!(message, err_msg);
}
other => prop_assert!(false, "Expected ValidationFailed but got {:?}", other),
}
}
}
}
proptest! {
#[test]
fn prop_multi_value_option_validates_each_value(
long in arb_safe_identifier(),
values in prop::collection::vec(arb_value_string(), 1..5),
reject_idx in any::<prop::sample::Index>(),
) {
let reject_pos = reject_idx.index(values.len());
let bad_value = values[reject_pos].clone();
let validator = Validator::new(move |v| {
if v == bad_value { Err(format!("bad: {}", v)) } else { Ok(()) }
});
let parser = ArgBuilder::new()
.option(Opt::new(&long).multi().validate(validator))
.build()
.unwrap();
let mut args = Vec::new();
for v in &values {
args.push(format!("--{}", long));
args.push(v.clone());
}
let result = parser.parse(args);
match result {
Err(ParseError::ValidationFailed { name, .. }) => {
prop_assert_eq!(name, long);
}
other => prop_assert!(false, "Expected ValidationFailed but got {:?}", other),
}
}
}
proptest! {
#[test]
fn prop_default_values_are_validated(
long in arb_safe_identifier(),
default_val in arb_value_string(),
) {
let bad = default_val.clone();
let validator = Validator::new(move |v| {
if v == bad { Err(format!("bad default: {}", v)) } else { Ok(()) }
});
let parser = ArgBuilder::new()
.option(Opt::new(&long).default(&default_val).validate(validator))
.build()
.unwrap();
let result = parser.parse(vec![]);
match result {
Err(ParseError::ValidationFailed { name, message }) => {
prop_assert_eq!(name, long);
prop_assert!(message.contains(&default_val));
}
other => prop_assert!(false, "Expected ValidationFailed but got {:?}", other),
}
}
}
proptest! {
#[test]
fn prop_validation_failed_display_contains_name_and_message(
name in arb_safe_identifier(),
message in arb_safe_description(),
) {
let err = ParseError::ValidationFailed {
name: name.clone(),
message: message.clone(),
};
let display = format!("{}", err);
prop_assert!(display.contains(&name), "Display '{}' should contain name '{}'", display, name);
prop_assert!(display.contains(&message), "Display '{}' should contain message '{}'", display, message);
}
}
use nanoargs::{one_of, range};
proptest! {
#[test]
fn prop_range_validator_correctness(
min in -1000i64..1000,
max in -1000i64..1000,
value_str in prop_oneof![
any::<i64>().prop_map(|n| n.to_string()),
"[a-zA-Z]{1,6}",
],
) {
prop_assume!(min <= max);
let v = range(min, max);
let result = v.validate(&value_str);
match value_str.parse::<i64>() {
Ok(n) if n >= min && n <= max => {
prop_assert!(result.is_ok(), "Expected Ok for {} in [{}..{}], got {:?}", n, min, max, result);
}
Ok(_) => {
prop_assert!(result.is_err(), "Expected Err for out-of-range value {}", value_str);
}
Err(_) => {
prop_assert!(result.is_err(), "Expected Err for non-numeric '{}'", value_str);
}
}
}
}
proptest! {
#[test]
fn prop_one_of_validator_correctness(
allowed in prop::collection::vec("[a-z]{1,6}", 1..6),
input in "[a-z]{1,6}",
) {
let refs: Vec<&str> = allowed.iter().map(|s| s.as_str()).collect();
let v = one_of(&refs);
let result = v.validate(&input);
if allowed.contains(&input) {
prop_assert!(result.is_ok(), "Expected Ok for '{}' in {:?}", input, allowed);
} else {
prop_assert!(result.is_err(), "Expected Err for '{}' not in {:?}", input, allowed);
}
}
}
proptest! {
#[test]
fn prop_range_validator_hint_format(
min in any::<i64>(),
max in any::<i64>(),
) {
prop_assume!(min <= max);
let v = range(min, max);
let expected = format!("[{}..{}]", min, max);
prop_assert_eq!(v.hint(), Some(expected.as_str()));
}
}
proptest! {
#[test]
fn prop_one_of_validator_hint_format(
allowed in prop::collection::vec("[a-z]{1,6}", 1..6),
) {
let refs: Vec<&str> = allowed.iter().map(|s| s.as_str()).collect();
let v = one_of(&refs);
let expected = allowed.join("|");
prop_assert_eq!(v.hint(), Some(expected.as_str()));
}
}
use common::strip_ansi_inline;
proptest! {
#[test]
fn prop_help_text_reflects_option_validator_hint(
long in arb_safe_identifier(),
desc in arb_safe_description(),
hint in arb_safe_description(),
use_hint in any::<bool>(),
) {
let validator = if use_hint {
Validator::with_hint(&hint, |_| Ok(()))
} else {
Validator::new(|_| Ok(()))
};
let parser = ArgBuilder::new()
.option(Opt::new(&long).desc(&desc).validate(validator))
.build()
.unwrap();
let help = parser.help_text();
let plain = strip_ansi_inline(&help);
if use_hint {
prop_assert!(
plain.contains(&hint),
"Help text should contain hint '{}' but got:\n{}",
hint,
plain
);
} else {
prop_assert!(
plain.contains(&desc),
"Help text should contain description '{}' but got:\n{}",
desc,
plain
);
}
}
#[test]
fn prop_help_text_reflects_positional_validator_hint(
name in arb_safe_identifier(),
desc in arb_safe_description(),
hint in arb_safe_description(),
use_hint in any::<bool>(),
) {
let validator = if use_hint {
Validator::with_hint(&hint, |_| Ok(()))
} else {
Validator::new(|_| Ok(()))
};
let parser = ArgBuilder::new()
.positional(Pos::new(&name).desc(&desc).validate(validator))
.build()
.unwrap();
let help = parser.help_text();
let plain = strip_ansi_inline(&help);
if use_hint {
prop_assert!(
plain.contains(&hint),
"Help text should contain hint '{}' but got:\n{}",
hint,
plain
);
} else {
prop_assert!(
plain.contains(&desc),
"Help text should contain description '{}' but got:\n{}",
desc,
plain
);
}
}
}