mod common;
use nanoargs::{extract, OptionError, ParseResultBuilder};
use proptest::prelude::*;
proptest! {
#[test]
fn prop1_required_field_extraction_succeeds_with_valid_values(
value in any::<u32>(),
) {
let result = ParseResultBuilder::new()
.option("port", &value.to_string())
.build();
let extracted: Result<_, OptionError> = extract!(result, {
port: u32,
});
let e = extracted.unwrap();
prop_assert_eq!(e.port, value);
}
}
proptest! {
#[test]
fn prop2_required_field_extraction_fails_when_absent(
_dummy in any::<u8>(),
) {
let result = ParseResultBuilder::new().build();
let extracted = extract!(result, { port: u32 });
match extracted {
Err(OptionError::Missing { option }) => {
prop_assert_eq!(option, "port");
}
other => {
prop_assert!(false, "Expected Err(Missing), got {:?}", other);
}
}
}
}
proptest! {
#[test]
fn prop5_flag_field_reflects_parse_result_state(
flag_val in any::<bool>(),
) {
let result = ParseResultBuilder::new()
.flag("verbose", flag_val)
.build();
let extracted = extract!(result, {
verbose: bool,
});
let e = extracted.unwrap();
prop_assert_eq!(e.verbose, flag_val);
}
}
proptest! {
#[test]
fn prop4_optional_field_extraction_reflects_presence(
value in any::<u32>(),
present in any::<bool>(),
) {
let mut builder = ParseResultBuilder::new();
if present {
builder = builder.option("level", &value.to_string());
}
let result = builder.build();
let extracted = extract!(result, {
level: Option<u32>,
});
let e = extracted.unwrap();
if present {
prop_assert_eq!(e.level, Some(value));
} else {
prop_assert_eq!(e.level, None);
}
}
}
proptest! {
#[test]
fn prop6_default_field_extraction_succeeds_with_valid_values(
value in any::<u32>(),
default_val in any::<u32>(),
) {
let result = ParseResultBuilder::new()
.option("jobs", &value.to_string())
.build();
let extracted = extract!(result, {
jobs: u32 = default_val,
});
let e = extracted.unwrap();
prop_assert_eq!(e.jobs, value);
}
}
proptest! {
#[test]
fn prop7_default_field_uses_default_when_absent(
default_val in any::<u32>(),
) {
let result = ParseResultBuilder::new().build();
let extracted = extract!(result, {
jobs: u32 = default_val,
});
let e = extracted.unwrap();
prop_assert_eq!(e.jobs, default_val);
}
}
proptest! {
#[test]
fn prop8_multi_value_field_extraction_preserves_order_and_completeness(
values in prop::collection::vec(any::<u32>(), 0..20),
) {
let mut builder = ParseResultBuilder::new();
for v in &values {
builder = builder.multi_option("tags", &v.to_string());
}
let result = builder.build();
let extracted = extract!(result, {
tags: Vec<u32>,
});
let e = extracted.unwrap();
prop_assert_eq!(e.tags, values);
}
}
proptest! {
#[test]
fn prop3_unparseable_values_produce_parse_failed_required(
bad in "[a-zA-Z][a-zA-Z0-9]{0,10}",
) {
let result = ParseResultBuilder::new()
.option("port", &bad)
.build();
let extracted = extract!(result, { port: u32 });
match extracted {
Err(OptionError::ParseFailed { option, .. }) => {
prop_assert_eq!(option, "port");
}
other => prop_assert!(false, "Expected ParseFailed, got {:?}", other),
}
}
#[test]
fn prop3_unparseable_values_produce_parse_failed_optional(
bad in "[a-zA-Z][a-zA-Z0-9]{0,10}",
) {
let result = ParseResultBuilder::new()
.option("level", &bad)
.build();
let extracted = extract!(result, { level: Option<u32> });
match extracted {
Err(OptionError::ParseFailed { option, .. }) => {
prop_assert_eq!(option, "level");
}
other => prop_assert!(false, "Expected ParseFailed, got {:?}", other),
}
}
#[test]
fn prop3_unparseable_values_produce_parse_failed_default(
bad in "[a-zA-Z][a-zA-Z0-9]{0,10}",
) {
let result = ParseResultBuilder::new()
.option("jobs", &bad)
.build();
let extracted = extract!(result, { jobs: u32 = 4 });
match extracted {
Err(OptionError::ParseFailed { option, .. }) => {
prop_assert_eq!(option, "jobs");
}
other => prop_assert!(false, "Expected ParseFailed, got {:?}", other),
}
}
#[test]
fn prop3_unparseable_values_produce_parse_failed_multi(
bad in "[a-zA-Z][a-zA-Z0-9]{0,10}",
) {
let result = ParseResultBuilder::new()
.multi_option("tags", &bad)
.build();
let extracted = extract!(result, { tags: Vec<u32> });
match extracted {
Err(OptionError::ParseFailed { option, .. }) => {
prop_assert_eq!(option, "tags");
}
other => prop_assert!(false, "Expected ParseFailed, got {:?}", other),
}
}
}
#[test]
fn test_underscore_to_hyphen_name_mapping() {
let result = ParseResultBuilder::new().option("listen-port", "8080").build();
let extracted = extract!(result, { listen_port: u16 });
let e = extracted.unwrap();
assert_eq!(e.listen_port, 8080);
}
#[test]
fn test_custom_name_override_required() {
let result = ParseResultBuilder::new().option("p", "3000").build();
let extracted = extract!(result, { port: u16 as "p" });
let e = extracted.unwrap();
assert_eq!(e.port, 3000);
}
#[test]
fn test_custom_name_with_default_present() {
let result = ParseResultBuilder::new().option("j", "8").build();
let extracted = extract!(result, { jobs: u32 as "j" = 4 });
let e = extracted.unwrap();
assert_eq!(e.jobs, 8);
}
#[test]
fn test_custom_name_with_default_absent() {
let result = ParseResultBuilder::new().build();
let extracted = extract!(result, { jobs: u32 as "j" = 4 });
let e = extracted.unwrap();
assert_eq!(e.jobs, 4);
}
#[test]
fn test_parse_result_accessible_after_extract_by_reference() {
let result = ParseResultBuilder::new().option("port", "8080").flag("verbose", true).build();
let extracted = extract!(&result, { port: u16 });
let e = extracted.unwrap();
assert_eq!(e.port, 8080);
assert!(result.get_flag("verbose"));
assert_eq!(result.get_option("port"), Some("8080"));
}
#[test]
fn test_fail_fast_error_ordering() {
let result = ParseResultBuilder::new().build();
let extracted = extract!(result, { first: u32, second: u32 });
match extracted {
Err(OptionError::Missing { option }) => {
assert_eq!(option, "first");
}
other => panic!("Expected Err(Missing) for 'first', got {:?}", other),
}
}
#[test]
fn test_mixed_field_types_single_extraction() {
let result = ParseResultBuilder::new()
.option("host", "localhost")
.option("port", "9090")
.flag("verbose", true)
.option("j", "2")
.multi_option("tags", "a")
.multi_option("tags", "b")
.build();
let extracted = extract!(result, {
host: String,
port: u16,
verbose: bool,
jobs: u32 as "j" = 4,
tags: Vec<String>,
});
let e = extracted.unwrap();
assert_eq!(e.host, "localhost");
assert_eq!(e.port, 9090);
assert!(e.verbose);
assert_eq!(e.jobs, 2);
assert_eq!(e.tags, vec!["a".to_string(), "b".to_string()]);
}
proptest! {
#[test]
fn prop_pos1_required_positional_extraction_succeeds(
value in any::<u32>(),
) {
let result = ParseResultBuilder::new()
.positional(&value.to_string())
.build();
let extracted: Result<_, OptionError> = extract!(result, {
input: u32 as @pos,
});
let e = extracted.unwrap();
prop_assert_eq!(e.input, value);
}
}
#[test]
fn test_required_positional_missing_error() {
let result = ParseResultBuilder::new().build();
let extracted = extract!(result, { input: u32 as @pos });
match extracted {
Err(OptionError::Missing { option }) => {
assert_eq!(option, "input");
}
other => panic!("Expected Err(Missing), got {:?}", other),
}
}
proptest! {
#[test]
fn prop_pos2_optional_positional_extraction_reflects_presence(
value in any::<u32>(),
present in any::<bool>(),
) {
let mut builder = ParseResultBuilder::new();
if present {
builder = builder.positional(&value.to_string());
}
let result = builder.build();
let extracted: Result<_, OptionError> = extract!(result, {
extra: Option<u32> as @pos,
});
let e = extracted.unwrap();
if present {
prop_assert_eq!(e.extra, Some(value));
} else {
prop_assert_eq!(e.extra, None);
}
}
}
proptest! {
#[test]
fn prop_pos3_default_positional_uses_value_or_default(
value in any::<u32>(),
default_val in any::<u32>(),
present in any::<bool>(),
) {
let mut builder = ParseResultBuilder::new();
if present {
builder = builder.positional(&value.to_string());
}
let result = builder.build();
let extracted: Result<_, OptionError> = extract!(result, {
mode: u32 as @pos = default_val,
});
let e = extracted.unwrap();
if present {
prop_assert_eq!(e.mode, value);
} else {
prop_assert_eq!(e.mode, default_val);
}
}
}
proptest! {
#[test]
fn prop_pos4_remaining_positionals_preserves_order_and_completeness(
values in prop::collection::vec(any::<u32>(), 0..20),
) {
let mut builder = ParseResultBuilder::new();
for v in &values {
builder = builder.positional(&v.to_string());
}
let result = builder.build();
let extracted: Result<_, OptionError> = extract!(result, {
files: Vec<u32> as @pos,
});
let e = extracted.unwrap();
prop_assert_eq!(e.files, values);
}
}
proptest! {
#[test]
fn prop_pos5_multiple_positionals_get_correct_indices(
first_val in any::<u32>(),
second_val in any::<u32>(),
) {
let result = ParseResultBuilder::new()
.positional(&first_val.to_string())
.positional(&second_val.to_string())
.build();
let extracted: Result<_, OptionError> = extract!(result, {
first: u32 as @pos,
second: u32 as @pos,
});
let e = extracted.unwrap();
prop_assert_eq!(e.first, first_val);
prop_assert_eq!(e.second, second_val);
}
}
proptest! {
#[test]
fn prop_pos6_unparseable_required_positional(
bad in "[a-zA-Z][a-zA-Z0-9]{0,10}",
) {
let result = ParseResultBuilder::new()
.positional(&bad)
.build();
let extracted = extract!(result, { input: u32 as @pos });
match extracted {
Err(OptionError::ParseFailed { option, .. }) => {
prop_assert_eq!(option, "input");
}
other => prop_assert!(false, "Expected ParseFailed, got {:?}", other),
}
}
#[test]
fn prop_pos6_unparseable_optional_positional(
bad in "[a-zA-Z][a-zA-Z0-9]{0,10}",
) {
let result = ParseResultBuilder::new()
.positional(&bad)
.build();
let extracted = extract!(result, { extra: Option<u32> as @pos });
match extracted {
Err(OptionError::ParseFailed { option, .. }) => {
prop_assert_eq!(option, "extra");
}
other => prop_assert!(false, "Expected ParseFailed, got {:?}", other),
}
}
#[test]
fn prop_pos6_unparseable_default_positional(
bad in "[a-zA-Z][a-zA-Z0-9]{0,10}",
) {
let result = ParseResultBuilder::new()
.positional(&bad)
.build();
let extracted = extract!(result, { mode: u32 as @pos = 42 });
match extracted {
Err(OptionError::ParseFailed { option, .. }) => {
prop_assert_eq!(option, "mode");
}
other => prop_assert!(false, "Expected ParseFailed, got {:?}", other),
}
}
#[test]
fn prop_pos6_unparseable_vec_positional(
bad in "[a-zA-Z][a-zA-Z0-9]{0,10}",
) {
let result = ParseResultBuilder::new()
.positional(&bad)
.build();
let extracted = extract!(result, { files: Vec<u32> as @pos });
match extracted {
Err(OptionError::ParseFailed { option, .. }) => {
prop_assert_eq!(option, "files");
}
other => prop_assert!(false, "Expected ParseFailed, got {:?}", other),
}
}
}
#[test]
fn test_mixed_positional_and_non_positional_fields() {
let result = ParseResultBuilder::new()
.flag("verbose", true)
.option("host", "localhost")
.positional("input.txt")
.option("port", "9090")
.positional("output.txt")
.multi_option("tags", "a")
.multi_option("tags", "b")
.build();
let extracted = extract!(result, {
verbose: bool,
src: String as @pos,
host: String,
dest: String as @pos,
port: u16,
tags: Vec<String>,
});
let e = extracted.unwrap();
assert!(e.verbose);
assert_eq!(e.src, "input.txt");
assert_eq!(e.host, "localhost");
assert_eq!(e.dest, "output.txt");
assert_eq!(e.port, 9090);
assert_eq!(e.tags, vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn test_vec_positional_followed_by_required_positional_gets_missing() {
let result = ParseResultBuilder::new().positional("1").positional("2").positional("3").build();
let extracted = extract!(result, {
all: Vec<u32> as @pos,
leftover: u32 as @pos,
});
match extracted {
Err(OptionError::Missing { option }) => {
assert_eq!(option, "leftover");
}
other => panic!(
"Expected Err(Missing) for 'leftover' after Vec consumed all, got {:?}",
other
),
}
}