// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT // // cspell: ignore infi anda manµ someµ tomorro use std::time::Duration as StdDuration; use fundu::TimeUnit::*; use fundu::{ parse_duration, CustomDurationParser, CustomDurationParserBuilder, CustomTimeUnit, Duration, DurationParser, Multiplier, ParseError, TimeKeyword, TimeUnit, SYSTEMD_TIME_UNITS, }; use rstest::rstest; const YEAR: u64 = 60 * 60 * 24 * 365 + 60 * 60 * 24 / 4; // 365 days + day/4 const MONTH: u64 = YEAR / 12; #[rstest] #[case::empty_string("")] #[case::leading_whitespace(" 1")] #[case::trailing_whitespace("1 ")] #[case::trailing_invalid_character("1.1e1msNOPE")] #[case::only_whitespace(" \t\n")] #[case::only_point(".")] #[case::only_sign("+")] #[case::only_exponent("e-10")] #[case::sign_with_exponent("-e1")] #[case::sign_with_point_and_exponent("-.e1")] #[case::negative_seconds("-1")] #[case::negative_seconds_with_fraction("-1.0")] #[case::negative_nano_seconds("-0.000000001")] #[case::exponent_with_only_plus("2E+")] #[case::negative_number_high_exponent("-3E75")] #[case::negative_number_barely_not_zero("-1.e-18")] #[case::negative_number_1("-.00000000000019999999e-4")] #[case::negative_number_2("-.00000003e-2")] #[case::negative_large_input_with_high_exponent("-088888888888888888888888818288833333333333333333333333333333333333333333333388888888888888888880000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001616564957e-1027")] fn test_parse_duration_with_illegal_argument_then_error(#[case] source: &str) { let parser = DurationParser::builder().default_time_units().build(); assert!(parser.parse(source).is_err()); let parser = DurationParser::builder() .default_time_units() .parse_multiple(Some(&["and"])) .build(); assert!(parser.parse(source).is_err()); } #[rstest] #[case::simple_number("1", Duration::positive(1, 0))] #[case::trailing_zeros("10.010000000", Duration::positive(10, 10_000_000))] #[case::simple_zero("0", Duration::ZERO)] #[case::many_zeros(&"0".repeat(2000), Duration::ZERO)] #[case::many_leading_zeros(&format!("{}1", "0".repeat(2000)), Duration::positive(1, 0))] #[case::zero_point_zero("0.0", Duration::ZERO)] #[case::point_zero(".0", Duration::ZERO)] #[case::zero_point("0.", Duration::ZERO)] #[case::one_with_fraction_number("1.1", Duration::positive(1, 100_000_000))] #[case::leading_zero_max_nanos("0.999999999", Duration::positive(0, 999_999_999))] #[case::leading_number_max_nanos("1.999999999", Duration::positive(1, 999_999_999))] #[case::simple_number("1234.123456789", Duration::positive(1234, 123_456_789))] #[case::max_seconds(&u64::MAX.to_string(), Duration::positive(u64::MAX, 0))] #[case::leading_zeros("000000100", Duration::positive(100, 0))] #[case::leading_zeros_with_fraction("00000010.0", Duration::positive(10, 0))] #[case::negative_number_negative_exponent_below_attos("-9.99999999999E-19", Duration::ZERO)] #[case::negative_large_input_with_high_exponent_is_zero("-.000000000000000000000000000000000000011888888888888888888888888888880000000000003333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333888888888888888888888888888888888800003333333333333333333333333333333333333333333333333333333333333333333333333338888888888888888833333333333333333333333333333333333333333333333338888888888888833333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333338888888888888888888888888888888888888888888888885888888000000000000333333333333333333333333333338883333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333888888888888888888888888888888888888888888888888888888800000000000033333333333333333000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011414703819657838596", Duration::ZERO)] #[case::f64_max(&format!("{}", f64::MAX), Duration::MAX)] fn test_parse_duration_when_simple_arguments_are_valid( #[case] source: &str, #[case] expected: Duration, ) { let parser = DurationParser::builder().default_time_units().build(); assert_eq!(parser.parse(source), Ok(expected)); let parser = DurationParser::builder() .default_time_units() .parse_multiple(Some(&["and"])) // cov:excl-line .build(); assert_eq!(parser.parse(source), Ok(expected)); } #[rstest] #[case::seconds_overflow_when_negative_and_positive_exponent(&format!("-{}.11e1", u64::MAX as u128 * 100), Duration::MIN)] #[case::minimum_exponent_with_minimum_time_unit(&format!("1{}.0e{}ns", "0".repeat((i16::MAX as usize) + 1), i16::MIN), Duration::positive(0, 1))] #[case::seconds_overflow_when_positive_and_negative_exponent(&format!("{}e-1", u64::MAX as u128 * 100), Duration::MAX)] #[case::seconds_overflow_when_positive_and_positive_exponent(&format!("{}.11e1", u64::MAX), Duration::MAX)] #[case::seconds_overflow_when_negative_and_negative_exponent(&format!("-{}e-1", u64::MAX as u128 * 100), Duration::MIN)] #[case::minus_sign_whole_to_fract("1.00000001e-1", Duration::positive(0, 100_000_001))] #[case::zero("1.1e0", Duration::positive(1, 100_000_000))] #[case::point_and_then_exponent("1.e0", Duration::positive(1, 0))] #[case::negative_zero("1.1e-0", Duration::positive(1, 100_000_000))] #[case::simple("1.09e1", Duration::positive(10, 900_000_000))] #[case::simple_big_e("1.09E1", Duration::positive(10, 900_000_000))] #[case::two_e4("2e4", Duration::positive(20000, 0))] #[case::lower_than_nanos_min("0.0000000001e1", Duration::positive(0, 1))] #[case::higher_than_seconds_max(&format!("{}9.999999999e-1", u64::MAX), Duration::MAX)] #[case::plus_sign("0.1000000001e+1", Duration::positive(1, 1))] #[case::minus_sign_zero_to_fract("10.00000001e-1", Duration::positive(1, 1))] #[case::exponent_then_nineteen_zeros_in_fraction("1.0e-20", Duration::ZERO)] #[case::no_overflow_error_low("1.0e-32768", Duration::ZERO)] #[case::no_overflow_error_high("1.0e+32767", Duration::MAX)] #[case::maximum_exponent(&format!("0.{}9e+{}", "0".repeat(i16::MAX as usize), i16::MAX), Duration::positive(0, 900_000_000))] #[case::maximum_exponent_barely_not_zero(&format!(".{}1e{}", "0".repeat((i16::MAX as usize) + 8), i16::MAX), Duration::positive(0, 1))] #[case::maximum_exponent_barely_not_zero_with_time_unit(&format!(".{}1e{}y", "0".repeat((i16::MAX as usize) + 15), i16::MAX), Duration::positive(0, 3))] #[case::maximum_exponent_with_maximum_time_unit(&format!("0.{}9e+{}y", "0".repeat(i16::MAX as usize), i16::MAX), Duration::positive(28401840, 0))] #[case::minimum_exponent(&format!("1{}.0e{}", "0".repeat(i16::MIN.unsigned_abs() as usize), i16::MIN), Duration::positive(1, 0))] #[case::minimum_exponent_barely_not_max_duration(&format!("1{}.0e{}", "0".repeat((i16::MIN.unsigned_abs() as usize) + 19), i16::MIN), Duration::positive(10_000_000_000_000_000_000, 0))] #[case::minimum_exponent_barely_not_max_duration_with_time_unit(&format!("1{}.0e{}ns", "0".repeat((i16::MIN.unsigned_abs() as usize) + 28), i16::MIN), Duration::positive(10_000_000_000_000_000_000, 0))] fn test_parse_duration_when_arguments_contain_exponent( #[case] source: &str, #[case] expected: Duration, ) { let duration = DurationParser::with_all_time_units() .allow_negative(true) .parse(source) .unwrap(); assert_eq!(duration, expected); } #[rstest] #[case::exponent_without_mantissa("e1")] #[case::no_number("1e")] #[case::no_number_but_time_unit("6eMs")] #[case::invalid_number("1e+F")] #[case::exponent_overflow_error_high("1e32768")] #[case::exponent_overflow_error_low("1e-32769")] #[case::exponent_parse_i16_overflow_error(&format!("1e{}", i16::MIN as i32 - 1))] fn test_parse_duration_when_arguments_with_illegal_exponent_then_error(#[case] source: &str) { let parser = DurationParser::builder().all_time_units().build(); let result = parser.parse(source); assert!(result.is_err()); } #[rstest] #[case::no_rounding("1.99999999999999999", StdDuration::new(1, 999_999_999))] #[case::high_value_no_swallow_fract(&format!("{}.1", u64::MAX), StdDuration::new(u64::MAX, 100_000_000) )] fn test_parse_duration_when_precision_of_float_would_be_insufficient_then_still_parse_exact( #[case] source: &str, #[case] expected: StdDuration, ) { let duration = parse_duration(source).unwrap(); assert_eq!(duration, expected); } #[rstest] #[case::lower_than_min_nanos("1.0000000001", StdDuration::new(1, 0))] #[case::max_digits_of_nanos("1.99999999999", StdDuration::new(1, 999_999_999))] #[case::higher_than_max_seconds(&format!("{}", u64::MAX as u128 + 1), StdDuration::MAX)] #[case::higher_than_max_seconds_with_fraction(&format!("{}.0", u64::MAX as u128 + 1), StdDuration::MAX)] fn test_parse_duration_when_arguments_are_capped_then_max_duration_or_min_nanos( #[case] source: &str, #[case] expected: StdDuration, ) { let duration = parse_duration(source).unwrap(); assert_eq!(duration, expected); } #[rstest] #[case::plus_zero("+0", StdDuration::ZERO)] #[case::plus_zero_with_fraction("+0.0", StdDuration::ZERO)] #[case::minus_zero("-0", StdDuration::ZERO)] #[case::minus_zero_with_fraction("-0.0", StdDuration::ZERO)] #[case::plus_one_with_fraction("+1.0", StdDuration::new(1, 0))] fn test_parse_duration_when_arguments_have_a_sign( #[case] source: &str, #[case] expected: StdDuration, ) { let duration = parse_duration(source).unwrap(); assert_eq!(duration, expected); } #[rstest] #[case::infinity_short("inf")] #[case::infinity_short_with_sign("+inf")] #[case::infinity_short_case_insensitive("iNf")] #[case::infinity_long("infinity")] #[case::infinity_long_with_sign("+infinity")] #[case::infinity_long_case_insensitive("InfiNitY")] fn test_parse_duration_when_arguments_are_infinity_values(#[case] source: &str) { let duration = parse_duration(source).unwrap(); assert_eq!(duration, StdDuration::MAX); } #[rstest] #[case::negative_infinity_short("-inf", ParseError::NegativeNumber)] #[case::negative_infinity_long("-infinity", ParseError::NegativeNumber)] #[case::infinity_long_trailing_invalid("infinityINVALID", ParseError::Syntax(8, "Expected end of input but found: 'INVALID'".to_string()))] #[case::incomplete_infinity("infin", ParseError::Syntax(5, "Error parsing infinity: Premature end of input".to_string()))] #[case::infinity_with_number("inf1.0", ParseError::Syntax(3, "Error parsing infinity: Invalid character '1'".to_string()))] fn test_parse_duration_when_arguments_are_illegal_infinity_values_then_error( #[case] source: &str, #[case] expected: ParseError, ) { assert_eq!(parse_duration(source), Err(expected)); } #[rstest] #[case::max_attos_no_u64_overflow(&format!("0.{}y", "9".repeat(100)), Duration::positive(31557599, 999_999_999))] #[case::seconds("1s", Duration::positive(1, 0))] #[case::milli_seconds("1ms", Duration::positive(0, 1_000_000))] #[case::milli_seconds("1000ms", Duration::positive(1, 0))] #[case::micro_seconds("1Ms", Duration::positive(0, 1_000))] #[case::micro_seconds("1e-3Ms", Duration::positive(0, 1))] #[case::nano_seconds_time_unit("1ns", Duration::positive(0, 1))] #[case::minutes_fraction("0.1m", Duration::positive(6, 0))] #[case::minutes_negative_exponent("100.0e-3m", Duration::positive(6, 0))] #[case::minutes_underflow("0.0000000001m", Duration::positive(0, 6))] #[case::hours_underflow("0.000000000001h", Duration::positive(0, 3))] #[case::years_underflow("0.0000000000000001y", Duration::positive(0, 3))] #[case::minutes_overflow(&format!("{}m", u64::MAX), Duration::MAX)] #[case::time_unit_overflow_with_fraction(&format!("{}.00001m", u64::MAX), Duration::MAX)] #[case::negative_time_unit_overflow_with_fraction(&format!("-{}.00001m", u64::MAX), Duration::MIN)] #[case::time_unit_overflow_with_overflowing_fraction(&format!("{}.1d", u64::MAX), Duration::MAX)] #[case::negative_time_unit_overflow_with_overflowing_fraction(&format!("-{}.1d", u64::MAX), Duration::MIN)] fn test_parse_duration_when_time_units_are_given(#[case] source: &str, #[case] expected: Duration) { let duration = DurationParser::with_all_time_units() .allow_negative(true) .parse(source) .unwrap(); assert_eq!(duration, expected); } #[rstest] #[case::seconds("1s", vec![TimeUnit::Second])] #[case::hour("1h", vec![TimeUnit::Hour])] fn test_parser_when_time_units(#[case] source: &str, #[case] time_units: Vec) { _ = DurationParser::with_time_units(&time_units) .parse(source) .unwrap(); } #[rstest] #[case::minute_short("1s", vec![TimeUnit::Minute])] fn test_parser_when_time_units_are_not_present_then_error( #[case] source: &str, #[case] time_units: Vec, ) { assert!( DurationParser::with_time_units(time_units.as_slice()) .parse(source) .is_err() ); } #[rstest] #[case::empty("", ParseError::Empty)] #[case::only_space(" ", ParseError::InvalidInput(" ".to_string()))] #[case::space_before_number(" 123", ParseError::InvalidInput(" 123".to_string()))] #[case::space_at_end_of_input("123 ns ", ParseError::TimeUnit(4, "Invalid time unit: 'ns '".to_string()))] #[case::other_whitespace("123\tns", ParseError::TimeUnit(3, "Invalid time unit: '\tns'".to_string()))] fn test_parser_when_allow_time_unit_delimiter_then_error( #[case] input: &str, #[case] expected: ParseError, ) { assert_eq!( DurationParser::with_all_time_units() .allow_time_unit_delimiter(true) .set_inner_delimiter(|byte| byte == b' ') .parse(input) .unwrap_err(), expected ); } #[rstest] #[case::without_delimiter("123ns", |b : u8| b.is_ascii_whitespace(), Duration::positive(0, 123))] #[case::all_rust_whitespace("123 \t\n\x0C\rns", |b : u8| b.is_ascii_whitespace(), Duration::positive(0, 123))] fn test_parser_when_allow_time_unit_delimiter( #[case] input: &str, #[case] delimiter: fundu::Delimiter, #[case] expected: Duration, ) { assert_eq!( DurationParser::with_all_time_units() .allow_time_unit_delimiter(true) .set_inner_delimiter(delimiter) .parse(input) .unwrap(), expected ); } #[rstest] #[case::without_spaces("123ns", Duration::positive(0, 123))] #[case::single_space("123 ns", Duration::positive(0, 123))] #[case::multiple_spaces("123 ns", Duration::positive(0, 123))] fn test_parser_when_allow_delimiter_then_ok(#[case] input: &str, #[case] expected: Duration) { assert_eq!( DurationParser::with_all_time_units() .allow_time_unit_delimiter(true) .set_inner_delimiter(|byte| byte == b' ') .parse(input), Ok(expected) ); } #[rstest] #[case::nano_seconds("ns", Ok(Duration::positive(0, 1)))] #[case::exponent_without_mantissa("e1", Err(ParseError::InvalidInput("e1".to_string())))] #[case::just_point(".", Err(ParseError::Syntax(0, "Either the whole number part or the fraction must be present".to_string())))] fn test_parser_when_number_is_optional( #[case] input: &str, #[case] expected: Result, ) { assert_eq!( DurationParser::with_all_time_units() .number_is_optional(true) .parse(input), expected ); } #[rstest] #[case::starts_with_delimiter("\rd", ParseError::InvalidInput("\rd".to_string()))] fn test_parser_when_number_is_optional_and_allow_delimiter_then_error( #[case] input: &str, #[case] expected: ParseError, ) { assert_eq!( DurationParser::with_all_time_units() .number_is_optional(true) .allow_time_unit_delimiter(true) .parse(input), Err(expected) ); } #[rstest] #[case::whole_with_just_point("1.", Err(ParseError::Syntax(1, "No fraction allowed".to_string())))] #[case::fract_with_just_point(".1", Err(ParseError::Syntax(0, "No fraction allowed".to_string())))] #[case::just_point(".", Err(ParseError::Syntax(0, "No fraction allowed".to_string())))] fn test_parser_when_disable_fraction( #[case] input: &str, #[case] expected: Result, ) { assert_eq!( DurationParser::with_all_time_units() .disable_fraction(true) .parse(input), expected ); } #[rstest] #[case::whole_with_exponent("1e0", Err(ParseError::Syntax(1, "No exponent allowed".to_string())))] #[case::fract_with_exponent("0.1e0", Err(ParseError::Syntax(3, "No exponent allowed".to_string())))] #[case::exponent_without_number("1e", Err(ParseError::Syntax(1, "No exponent allowed".to_string())))] fn test_parser_when_disable_exponent( #[case] input: &str, #[case] expected: Result, ) { assert_eq!( DurationParser::with_all_time_units() .disable_exponent(true) .parse(input), expected ); } #[rstest] #[case::whole_with_exponent("i", Err(ParseError::InvalidInput("i".to_string())))] #[case::whole_with_exponent("in", Err(ParseError::InvalidInput("in".to_string())))] #[case::whole_with_exponent("inf", Err(ParseError::InvalidInput("inf".to_string())))] #[case::whole_with_exponent("+inf", Err(ParseError::InvalidInput("inf".to_string())))] #[case::whole_with_exponent("-inf", Err(ParseError::InvalidInput("inf".to_string())))] #[case::whole_with_exponent("infi", Err(ParseError::InvalidInput("infi".to_string())))] #[case::whole_with_exponent("infinity", Err(ParseError::InvalidInput("infinity".to_string())))] fn test_parser_when_disable_infinity( #[case] input: &str, #[case] expected: Result, ) { assert_eq!( DurationParser::with_all_time_units() .disable_infinity(true) .parse(input), expected ); } #[rstest] #[case::minute_short("1s", TimeUnit::Minute)] fn test_parser_when_custom_time_unit_then_error(#[case] source: &str, #[case] time_unit: TimeUnit) { assert!( DurationParser::with_time_units(&[time_unit]) .parse(source) .is_err() ); } #[rstest] #[case::time_unit_error_when_no_time_units( DurationParser::without_time_units(), "1y", ParseError::TimeUnit(1, "No time units allowed but found: 'y'".to_string()), "Time unit error: No time units allowed but found: 'y' at column 1" )] #[case::time_unit_error_when_all_time_units( DurationParser::with_all_time_units(), "1years", ParseError::TimeUnit(1, "Invalid time unit: 'years'".to_string()), "Time unit error: Invalid time unit: 'years' at column 1" )] #[case::negative_exponent_overflow_error( DurationParser::new(), "1e-32769", ParseError::NegativeExponentOverflow, "Negative exponent overflow: Minimum is -32768" )] #[case::positive_exponent_overflow_error( DurationParser::new(), "1e+32768", ParseError::PositiveExponentOverflow, "Positive exponent overflow: Maximum is +32767" )] #[case::negative_number_error( DurationParser::new(), "-1", ParseError::NegativeNumber, "Number was negative" )] #[case::negative_number_when_infinity_error( DurationParser::new(), "-inf", ParseError::NegativeNumber, "Number was negative" )] fn test_parse_error_messages( #[case] parser: DurationParser, #[case] input: &str, #[case] expected_error: ParseError, #[case] expected_string: &str, ) { assert_eq!(parser.parse(input), Err(expected_error)); assert_eq!( parser.parse(input).unwrap_err().to_string(), expected_string ); } #[rstest] #[case::nano_second(NanoSecond, Duration::positive(0, 1))] #[case::micro_second(MicroSecond, Duration::positive(0, 1_000))] #[case::milli_second(MilliSecond, Duration::positive(0, 1_000_000))] #[case::second(Second, Duration::positive(1, 0))] #[case::minute(Minute, Duration::positive(60, 0))] #[case::hour(Hour, Duration::positive(60 * 60, 0))] #[case::day(Day, Duration::positive(60 * 60 * 24, 0))] #[case::week(Week, Duration::positive(60 * 60 * 24 * 7, 0))] #[case::month(Month, Duration::positive(MONTH, 0))] #[case::year(Year, Duration::positive(YEAR, 0))] fn test_parser_setting_default_time_unit(#[case] time_unit: TimeUnit, #[case] expected: Duration) { assert_eq!( DurationParser::without_time_units() .default_unit(time_unit) .parse("1") .unwrap(), expected ); } #[rstest] #[case::zero_zero("0 0", Duration::ZERO)] #[case::zero_conjunction_zero("0 and 0", Duration::ZERO)] #[case::many_whitespace("0 \t\n\r 0", Duration::ZERO)] #[case::zero_one("0 1", Duration::positive(1, 0))] #[case::zero_one_conjunction("0 and 1", Duration::positive(1, 0))] #[case::one_zero("1 0", Duration::positive(1, 0))] #[case::one_zero_conjunction("1 and 0", Duration::positive(1, 0))] #[case::one_one("1 1", Duration::positive(2, 0))] #[case::one_one_conjunction("1 and 1", Duration::positive(2, 0))] #[case::one_one("1 1", Duration::positive(2, 0))] #[case::one_one_with_sign_as_delimiter("1+1", Duration::positive(2, 0))] #[case::two_with_time_units("1ns 1ns", Duration::positive(0, 2))] #[case::two_with_time_units_with_sign("1ns+1ns", Duration::positive(0, 2))] #[case::two_with_time_units_conjunction("1ns and 1ns", Duration::positive(0, 2))] #[case::two_with_time_units_conjunction_and_sign("1ns and+1ns", Duration::positive(0, 2))] #[case::two_with_time_units_without_delimiter("1ns1ns", Duration::positive(0, 2))] #[case::two_with_fraction_exponent_time_units( "1.123e9ns 1.987e9ns", Duration::positive(3, 110_000_000) )] #[case::two_with_fraction_exponent_time_units_conjunction( "1.123e9ns and 1.987e9ns", Duration::positive(3, 110_000_000) )] #[case::two_when_saturating(&format!("{0}s {0}s", u64::MAX), Duration::MAX)] #[case::multiple_mixed("1ns 1.001Ms1e1ms 9 .9 3m6h", Duration::positive(21789, 910_001_002))] #[case::multiple_mixed_with_sign_as_delimiter( "1ns+1.001Ms-1e1+9-.9+3m6h", Duration::positive(21778, 100_001_002) )] #[case::multiple_mixed_with_conjunction( "1ns and 1.001Ms and1e1ms and 9 .9 and 3m6h", Duration::positive(21789, 910_001_002) )] #[case::single_infinity_short("inf", Duration::MAX)] #[case::single_infinity_long("infinity", Duration::MAX)] #[case::multiple_infinity_short("inf inf", Duration::MAX)] #[case::multiple_infinity_long("infinity infinity", Duration::MAX)] #[case::multiple_infinity_mixed_short_then_long("inf infinity", Duration::MAX)] #[case::multiple_infinity_mixed_long_then_short("infinity inf", Duration::MAX)] #[case::multiple_infinity_short_conjunction("inf and inf", Duration::MAX)] #[case::multiple_infinity_long_conjunction("infinity and infinity", Duration::MAX)] fn test_parser_when_setting_parse_multiple(#[case] input: &str, #[case] expected: Duration) { let parser = DurationParser::builder() .all_time_units() .parse_multiple(Some(&["and"])) .allow_negative() .build(); assert_eq!(parser.parse(input), Ok(expected)) } #[rstest] #[case::empty("", ParseError::Empty)] #[case::only_conjunction("and", ParseError::InvalidInput("and".to_string()))] #[case::only_whitespace(" \t\n", ParseError::InvalidInput(" \t\n".to_owned()))] #[case::just_point(".", ParseError::Syntax(0, "Either the whole number part or the fraction must be present".to_string()))] #[case::just_time_unit("ns", ParseError::InvalidInput("ns".to_string()))] #[case::valid_then_invalid("1 a", ParseError::InvalidInput("a".to_string()))] #[case::end_with_space("1 1 ", ParseError::Syntax(3, "Input may not end with a delimiter".to_string()))] #[case::end_with_conjunction("1 and", ParseError::Syntax(2, "Input may not end with a conjunction but found: 'and'".to_string()))] #[case::end_with_wrong_conjunction("1 anda", ParseError::Syntax(5, "A conjunction must be separated by a delimiter, sign or digit but found: 'a'".to_string()))] #[case::end_with_conjunction_and_delimiter("1 and ", ParseError::Syntax(5, "Input may not end with a delimiter".to_string()))] #[case::valid_time_unit_end_with_delimiter("1s ", ParseError::Syntax(2, "Input may not end with a delimiter".to_string()))] #[case::two_end_with_conjunction("1 1 and", ParseError::Syntax(4, "Input may not end with a conjunction but found: 'and'".to_string()))] #[case::invalid_then_valid("a 1", ParseError::InvalidInput("a 1".to_string()))] #[case::multiple_invalid("a a", ParseError::InvalidInput("a a".to_string()))] #[case::infinity_then_space("inf ", ParseError::Syntax(3, "Input may not end with a delimiter".to_string()))] #[case::infinity_then_conjunction("inf and", ParseError::Syntax(4, "Input may not end with a conjunction but found: 'and'".to_string()))] #[case::infinity_short_then_number("inf1", ParseError::Syntax(3, "Error parsing infinity: Invalid character '1'".to_string()))] #[case::infinity_long_then_number("infinity1", ParseError::Syntax(8, "Error parsing infinity: Expected a delimiter but found '1'".to_string()))] #[case::premature_end_parsing_infinity("infi", ParseError::Syntax(0, "Error parsing infinity: 'infi' is an invalid identifier for infinity".to_string()))] fn test_parser_when_setting_parse_multiple_then_error( #[case] input: &str, #[case] expected: ParseError, ) { let parser = DurationParser::builder() .all_time_units() .parse_multiple(Some(&["and"])) .build(); assert_eq!(parser.parse(input), Err(expected)) } #[rstest] #[case::numbers_without_time_units("1 ns 1 s", Ok(Duration::positive(1, 1)))] #[case::sign_without_number("1s + 1s", Err(ParseError::InvalidInput(" 1s".to_owned())))] fn test_parser_when_parse_multiple_number_is_optional_allow_delimiter( #[case] input: &str, #[case] expected: Result, ) { let parser = DurationParser::builder() .all_time_units() .parse_multiple(Some(&["and"])) .number_is_optional() .allow_time_unit_delimiter() .build(); assert_eq!(parser.parse(input), expected) } #[rstest] #[case::just_sign( "+", Err(ParseError::Syntax(1, "Unexpected end of input. Sign without a number".to_owned())) )] #[case::just_sign_end_with_delimiter( "+ ", Err(ParseError::Syntax(1, "Input may not end with a delimiter".to_owned())) )] #[case::sign_without_delimiter("+1", Ok(Duration::positive(1, 0)))] #[case::sign_with_delimiter("+ 1", Ok(Duration::positive(1, 0)))] #[case::multiple_sign_with_delimiter("+2 - 1", Ok(Duration::positive(1, 0)))] #[case::multiple_sign_end_with_sign( "+2 - 1 +", Err(ParseError::Syntax(8, "Unexpected end of input. Sign without a number".to_owned())) )] #[case::multiple_sign_end_with_delimiter( "+2 - 1 + ", Err(ParseError::Syntax(8, "Input may not end with a delimiter".to_owned())) )] fn test_parser_when_parse_multiple_with_sign_delimiter( #[case] input: &str, #[case] expected: Result, ) { let parser = DurationParser::builder() .all_time_units() .parse_multiple(Some(&["and"])) .allow_time_unit_delimiter() .allow_sign_delimiter() .allow_negative() .build(); assert_eq!(parser.parse(input), expected) } #[test] fn test_parser_when_parse_multiple_number_is_optional_not_allow_delimiter() { let parser = DurationParser::builder() .all_time_units() .parse_multiple(Some(&["and"])) .number_is_optional() .build(); assert_eq!(parser.parse("1 ns 1 s"), Ok(Duration::positive(3, 1))) } #[test] fn test_parser_when_parse_multiple_with_invalid_delimiter() { let delimiter = |byte: u8| byte == 0xb5; let parser = CustomDurationParser::builder() .time_unit(CustomTimeUnit::with_default(MicroSecond, &["µ"])) .parse_multiple(None) .outer_delimiter(delimiter) .build(); // The delimiter will split the multibyte µ and produces invalid utf-8 // µ = 0xc2 0xb5 assert_eq!( parser.parse("1µ"), Err(ParseError::InvalidInput("µ".to_string())) ) } #[test] fn test_parser_when_allow_ago_with_invalid_delimiter() { let delimiter = |byte: u8| byte == 0xb5; let parser = CustomDurationParser::builder() .time_unit(CustomTimeUnit::with_default(MicroSecond, &["µ"])) .allow_ago() .inner_delimiter(delimiter) .build(); // The delimiter will split the multibyte µ and produces invalid utf-8 // µ = 0xc2 0xb5 assert!(parser.parse("1µ ago").is_err()); } #[test] fn test_parser_parse_multiple_and_keywords_with_invalid_delimiter() { let delimiter = |byte: u8| byte == 0xb5; let parser = CustomDurationParser::builder() .time_unit(CustomTimeUnit::with_default(MicroSecond, &["µ"])) .keyword(TimeKeyword::new(MicroSecond, &["manµ"], None)) .parse_multiple(None) .outer_delimiter(delimiter) .build(); // The delimiter will split the multibyte µ and produces invalid utf-8 // µ = 0xc2 0xb5 assert_eq!( parser.parse("someµ"), Err(ParseError::InvalidInput("someµ".to_string())) ) } #[rstest] #[case::only_numbers("1 1", Ok(Duration::positive(2, 0)))] #[case::with_time_units("1ns 1ns", Err(ParseError::InvalidInput("ns 1ns".to_string())))] #[case::number_then_with_time_unit("1 1ns", Err(ParseError::InvalidInput("ns".to_string())))] #[case::ago_without_time_unit("1 ago", Err(ParseError::InvalidInput("ago".to_string())))] fn test_parser_when_parse_multiple_without_time_units( #[case] input: &str, #[case] expected: Result, ) { let parser = CustomDurationParser::builder() .parse_multiple(Some(&["and"])) .allow_ago() .build(); assert_eq!(parser.parse(input), expected); } #[test] fn test_parser_when_parse_multiple_without_time_units_default_unit_is_not_seconds() { let parser = DurationParser::builder() .parse_multiple(Some(&["and"])) .default_unit(NanoSecond) .build(); assert_eq!(parser.parse("1 1"), Ok(Duration::positive(0, 2))); } #[rstest] #[case::nano_second(&["ns", "nsec"], Duration::positive(0, 1))] #[case::micro_second(&["us", "µs", "usec"], Duration::positive(0, 1000))] #[case::milli_second(&["ms", "msec"], Duration::positive(0, 1_000_000))] #[case::second(&["s", "sec", "second", "seconds"], Duration::positive(1, 0))] #[case::minute(&["m", "min", "minute", "minutes"], Duration::positive(60, 0))] #[case::hour(&["h", "hr", "hour", "hours"], Duration::positive(60 * 60, 0))] #[case::day(&["d", "day", "days"], Duration::positive(60 * 60 * 24, 0))] #[case::week(&["w", "week", "weeks"], Duration::positive(60 * 60 * 24 * 7, 0))] #[case::month(&["M", "month", "months"], Duration::positive(MONTH, 0))] #[case::year(&["y", "year", "years"], Duration::positive(YEAR, 0))] fn test_custom_duration_parser_parse_when_systemd_time_units( #[case] inputs: &[&str], #[case] expected: Duration, ) { let parser = CustomDurationParser::with_time_units(&SYSTEMD_TIME_UNITS); for input in inputs { assert_eq!(parser.parse(&format!("1{input}")), Ok(expected)); } } #[rstest] #[case::negative_zero("-0", Duration::ZERO)] #[case::negative_barely_not_zero("-0.000000001", Duration::negative(0, 1))] #[case::negative_barely_zero("-0.0000000001", Duration::negative(0, 0))] // FIXME: should be Duration::ZERO #[case::positive_zero("+0", Duration::ZERO)] #[case::positive_barely_not_zero("0.000000001", Duration::positive(0, 1))] #[case::positive_barely_zero("0.0000000001", Duration::ZERO)] #[case::zero_without_sign("0", Duration::ZERO)] #[case::negative_one("-1", Duration::negative(1, 0))] #[case::negative_one_with_fraction("-1.2", Duration::negative(1, 200_000_000))] #[case::negative_one_with_exponent("-1e-9", Duration::negative(0, 1))] #[case::negative_min_seconds(&format!("-{}", u64::MAX), Duration::negative(u64::MAX, 0))] #[case::negative_min_seconds_low_nanos(&format!("-{}.000000001", u64::MAX), Duration::negative(u64::MAX, 1))] #[case::negative_min_seconds_and_nanos(&format!("-{}.999999999", u64::MAX), Duration::MIN)] #[case::negative_min_nanos_10_digits(&format!("-{}.9999999999", u64::MAX), Duration::MIN)] #[case::negative_seconds_barely_saturate(&format!("-{}", u64::MAX as u128 + 1), Duration::MIN)] #[case::negative_some_mixed_number( "-1122334455.123456789e-4", Duration::negative(112233, 445512345) )] #[case::negative_years("-1.000000001y", Duration::negative(YEAR, YEAR as u32))] #[case::negative_high_value_saturate(&format!("-{}.{}e1000", "1".repeat(1000), "1".repeat(1000)), Duration::MIN)] #[case::positive_one("1", Duration::positive(1, 0))] #[case::positive_max_seconds(&format!("{}", u64::MAX), Duration::positive(u64::MAX, 0))] #[case::positive_max_seconds_low_nanos(&format!("{}.000000001", u64::MAX), Duration::positive(u64::MAX, 1))] #[case::positive_max_seconds_and_nanos(&format!("{}.999999999", u64::MAX), Duration::MAX)] #[case::positive_seconds_barely_saturate(&format!("{}", u64::MAX as u128 + 1), Duration::MAX)] #[case::positive_seconds_and_nanos_barely_saturate(&format!("{}.000000001", u64::MAX as u128 + 1), Duration::MAX)] #[case::positive_some_mixed_number( "1122334455.123456789e-4", Duration::positive(112233, 445512345) )] #[case::positive_years("1.000000001y", Duration::positive(YEAR, YEAR as u32))] #[case::positive_high_value_saturate(&format!("{}.{}e1000", "1".repeat(1000), "1".repeat(1000)), Duration::MAX)] #[case::time_unit_causes_saturate_negative(&format!("-{}y", u64::MAX), Duration::MIN)] fn test_parse_negative(#[case] source: &str, #[case] expected: Duration) { let actual = DurationParser::with_all_time_units() .allow_negative(true) .parse(source) .unwrap(); assert_eq!(actual, expected); } #[rstest] #[case::simple_zero("0", Duration::ZERO)] #[case::two_zero("0 0", Duration::ZERO)] #[case::two_one("1 1", Duration::positive(2, 0))] #[case::negative_and_positive_then_zero("-1 1", Duration::ZERO)] #[case::two_negative("-1 -1", Duration::negative(2, 0))] #[case::two_negative_conjunction("-1 and -1", Duration::negative(2, 0))] #[case::two_negative_with_time_units("-1ns -1s", Duration::negative(1, 1))] #[case::two_negative_with_time_units_conjunction("-1ns and -1s", Duration::negative(1, 1))] #[case::negative_and_positive_with_time_units("1ns -1s", Duration::negative(0, 999_999_999))] #[case::two_negative_mixed("-1.1ms -1e-9s", Duration::negative(0, 1_100_001))] #[case::two_negative_saturate_negative(&format!("-{}s -1s", u64::MAX), Duration::MIN)] #[case::two_positive_saturate_positive(&format!("{}s 1s", u64::MAX), Duration::MAX)] #[case::negative_infinity_short("-inf", Duration::MIN)] #[case::two_negative_infinity_short_then_saturate("-inf -inf", Duration::MIN)] #[case::negative_infinity_long("-infinity", Duration::MIN)] #[case::two_negative_infinity_long_then_saturate("-infinity -infinity", Duration::MIN)] #[case::two_negative_infinity_long_conjunction_then_saturate( "-infinity and -infinity", Duration::MIN )] #[case::two_positive_infinity_long_then_saturate_max("infinity infinity", Duration::MAX)] #[case::negative_infinity_and_positive_infinity_no_error("-inf +inf", Duration::ZERO)] #[case::negative_infinity_and_positive_infinity_conjunction("-inf and +inf", Duration::ZERO)] fn test_parse_negative_when_multiple(#[case] input: &str, #[case] expected: Duration) { assert_eq!( DurationParser::builder() .all_time_units() .parse_multiple(Some(&["and"])) .allow_negative() .build() .parse(input) .unwrap(), expected ); } #[rstest] #[case::i_without_number("i", "i")] #[case::big_i_without_number("I", "I")] #[case::simple_plus_i("i", "+i")] #[case::simple_i_one("i", "1i")] #[case::simple_i_with_fraction("i", "1.0i")] #[case::simple_i_with_exponent("i", "1.0e0i")] #[case::simple_inf("inf", "inf")] #[case::infi("infi", "infi")] #[case::infinity("infinity", "infinity")] #[case::one_infinity("infinity", "1infinity")] fn test_custom_parser_when_disable_infinity_then_no_problems_with_infinity_like_ids( #[case] id: &str, #[case] input: &str, ) { let ids = &[id]; let parser = CustomDurationParserBuilder::new() .disable_infinity() .number_is_optional() .time_units(&[CustomTimeUnit::with_default(NanoSecond, ids)]) .build(); assert_eq!(parser.parse(input), Ok(Duration::positive(0, 1))); } #[rstest] #[case::positive_zero("0s", Duration::ZERO)] #[case::negative_zero("-0s", Duration::ZERO)] #[case::positive_second("1s", Duration::negative(1, 0))] #[case::negative_second("-1s", Duration::positive(1, 0))] #[case::positive_two_seconds("2s", Duration::negative(2, 0))] #[case::negative_two_seconds("-2s", Duration::positive(2, 0))] #[case::positive_fraction("0.1s", Duration::negative(0, 100_000_000))] #[case::negative_fraction("-0.1s", Duration::positive(0, 100_000_000))] #[case::positive_overflow(&format!("{}s", u128::MAX), Duration::MIN)] #[case::negative_overflow(&format!("-{}s", u128::MAX), Duration::MAX)] fn test_custom_parser_when_negative_multiplier(#[case] input: &str, #[case] expected: Duration) { let parser = CustomDurationParserBuilder::new() .time_unit(CustomTimeUnit::new(Second, &["s"], Some(Multiplier(-1, 0)))) .allow_negative() .build(); let actual = parser.parse(input).unwrap(); assert_eq!(actual, expected); } #[rstest] #[case::positive_zero("0s")] #[case::negative_zero("-0s")] #[case::one("1s")] #[case::some("1234567.89s")] #[case::max(&format!("{}s", u64::MAX))] #[case::min(&format!("-{}s", u64::MAX))] fn test_custom_parser_when_multiplier_is_zero(#[case] input: &str) { let parser = CustomDurationParserBuilder::new() .time_unit(CustomTimeUnit::new(Second, &["s"], Some(Multiplier(0, 0)))) .allow_negative() .build(); assert_eq!(parser.parse(input), Ok(Duration::ZERO)); } #[rstest] #[case::yesterday("yesterday", Duration::negative(60 * 60 * 24, 0))] #[case::negative_yesterday("-yesterday", Duration::positive(60 * 60 * 24, 0))] #[case::positive_yesterday("+yesterday", Duration::negative(60 * 60 * 24, 0))] #[case::tomorrow("tomorrow", Duration::positive(60 * 60 * 24, 0))] #[case::negative_tomorrow("-tomorrow", Duration::negative(60 * 60 * 24, 0))] #[case::today("today", Duration::ZERO)] #[case::negative_today("-today", Duration::ZERO)] fn test_custom_parser_with_keywords(#[case] input: &str, #[case] expected: Duration) { let parser = CustomDurationParserBuilder::new() .keyword(TimeKeyword::new( Day, &["yesterday"], Some(Multiplier(-1, 0)), )) .keyword(TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0)))) .keyword(TimeKeyword::new(Day, &["today"], Some(Multiplier(0, 0)))) .time_units(&[ CustomTimeUnit::with_default(NanoSecond, &["ns"]), CustomTimeUnit::with_default(Second, &["s", "second"]), ]) .allow_negative() .build(); let actual = parser.parse(input).unwrap(); assert_eq!(actual, expected); } #[rstest] #[case::leading_space(" tomorrow", ParseError::InvalidInput(" tomorrow".to_string()))] #[case::trailing_space("tomorrow ", ParseError::InvalidInput("tomorrow ".to_string()))] #[case::incomplete_keyword("tomorro", ParseError::InvalidInput("tomorro".to_string()))] #[case::number("1tomorrow", ParseError::TimeUnit(1, "Invalid time unit: 'tomorrow'".to_string()))] #[case::number_with_space("1 tomorrow", ParseError::TimeUnit(1, "Invalid time unit: ' tomorrow'".to_string()))] fn test_custom_parser_with_keywords_then_error(#[case] input: &str, #[case] expected: ParseError) { let parser = CustomDurationParserBuilder::new() .keyword(TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0)))) .time_units(&[ CustomTimeUnit::with_default(NanoSecond, &["ns"]), CustomTimeUnit::with_default(Second, &["s", "second"]), ]) .build(); assert_eq!(parser.parse(input), Err(expected)); } #[rstest] #[case::single_tomorrow("tomorrow", Duration::positive(60 * 60 * 24, 0))] #[case::tomorrow_then_number("tomorrow1.1", Duration::positive(60 * 60 * 24 + 1, 100_000_000))] #[case::two_tomorrow("tomorrow tomorrow", Duration::positive(60 * 60 * 24 * 2, 0))] #[case::yesterday_tomorrow("yesterday tomorrow", Duration::ZERO)] #[case::number_tomorrow("1 tomorrow", Duration::positive(60 * 60 * 24 + 1, 0))] #[case::fraction_tomorrow(".1 tomorrow", Duration::positive(60 * 60 * 24, 100_000_000))] #[case::number_conjunction_keyword(".1 and tomorrow", Duration::positive(60 * 60 * 24, 100_000_000))] #[case::keyword_conjunction_number("tomorrow and .1", Duration::positive(60 * 60 * 24, 100_000_000))] #[case::keyword_conjunction_no_delimiter_number("tomorrow and1.1", Duration::positive(60 * 60 * 24 + 1, 100_000_000))] fn test_custom_parser_with_keywords_when_parse_multiple( #[case] input: &str, #[case] expected: Duration, ) { let parser = CustomDurationParserBuilder::new() .keyword(TimeKeyword::new( Day, &["yesterday"], Some(Multiplier(-1, 0)), )) .keyword(TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0)))) .time_units(&[ CustomTimeUnit::with_default(NanoSecond, &["ns"]), CustomTimeUnit::with_default(Second, &["s", "second"]), ]) .parse_multiple(Some(&["and"])) .allow_negative() .build(); assert_eq!(parser.parse(input), Ok(expected)); } #[rstest] #[case::nano_second("ns", Duration::positive(0, 1))] #[case::two_times_nano_second("ns ns", Duration::positive(0, 2))] #[case::two_times_nano_second_with_conjunction("ns and ns", Duration::positive(0, 2))] #[case::tomorrow_nano_second("tomorrow ns", Duration::positive(60 * 60 * 24, 1))] #[case::tomorrow_conjunction_nano_second("tomorrow and ns", Duration::positive(60 * 60 * 24, 1))] #[case::nano_second_tomorrow("ns tomorrow", Duration::positive(60 * 60 * 24, 1))] #[case::nano_second_conjunction_tomorrow("ns and tomorrow", Duration::positive(60 * 60 * 24, 1))] #[case::one_tomorrow_nano_second("1 tomorrow ns", Duration::positive(60 * 60 * 24 + 1, 1))] #[case::one_tomorrow_nano_second_with_conjunctions("1 and tomorrow and ns", Duration::positive(60 * 60 * 24 + 1, 1))] fn test_custom_parser_with_keywords_when_parse_multiple_and_number_is_optional( #[case] input: &str, #[case] expected: Duration, ) { let parser = CustomDurationParserBuilder::new() .keyword(TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0)))) .time_units(&[ CustomTimeUnit::with_default(NanoSecond, &["ns"]), CustomTimeUnit::with_default(Second, &["s", "second"]), ]) .parse_multiple(Some(&["and"])) .number_is_optional() .build(); assert_eq!(parser.parse(input), Ok(expected)); } #[rstest] #[case::zero(Multiplier(0, 0), Duration::ZERO)] #[case::max_exponent(Multiplier(1, i16::MAX), Duration::MAX)] #[case::min_exponent(Multiplier(1, i16::MIN), Duration::ZERO)] #[case::high_exponent(Multiplier(1, 19), Duration::positive(10_000_000_000_000_000_000, 0))] #[case::low_exponent(Multiplier(1, -19), Duration::ZERO)] #[case::barely_lower_than_nano(Multiplier(1, -10), Duration::ZERO)] #[case::high_exponent_then_saturate_max(Multiplier(1, 20), Duration::MAX)] #[case::high_exponent_then_saturate_min(Multiplier(-1, 20), Duration::MIN)] #[case::low_exponent_then_zero(Multiplier(1, -18), Duration::ZERO)] #[case::some_positive_multiplier( Multiplier(1234567, 8), Duration::positive(123_456_700_000_000, 0) )] #[case::some_negative_multiplier( Multiplier(-1234567, 8), Duration::negative(123_456_700_000_000, 0) )] fn test_custom_parser_multiplier_when_number_is_optional_and_time_unit_without_number( #[case] multiplier: Multiplier, #[case] expected: Duration, ) { let parser = CustomDurationParserBuilder::new() .time_unit(CustomTimeUnit::new(Second, &["my"], Some(multiplier))) .number_is_optional() .allow_negative() .build(); assert_eq!(parser.parse("my"), Ok(expected)); } #[test] fn test_custom_parser_with_negative_keyword_when_not_allow_negative_then_error() { let parser = CustomDurationParserBuilder::new() .keyword(TimeKeyword::new( Day, &["yesterday"], Some(Multiplier(-1, 0)), )) .build(); assert_eq!(parser.parse("yesterday"), Err(ParseError::NegativeNumber)); } #[rstest] #[case::one("1", Duration::positive(1, 0))] #[case::second("1s", Duration::positive(1, 0))] #[case::seconds_ago("1s ago", Duration::negative(1, 0))] #[case::seconds_ago_big("1s AGO", Duration::negative(1, 0))] #[case::seconds_ago_mixed("1s aGO", Duration::negative(1, 0))] #[case::negative_seconds_ago("-1s ago", Duration::positive(1, 0))] #[case::day_ago("1day ago", Duration::negative(60 * 60 * 24, 0))] #[case::with_delimiter_between_number_and_time_unit("1 s ago", Duration::negative(1, 0))] fn test_custom_parser_with_allow_ago(#[case] input: &str, #[case] expected: Duration) { let parser = CustomDurationParserBuilder::new() .time_units(&SYSTEMD_TIME_UNITS) .allow_time_unit_delimiter() .allow_ago() .allow_negative() .build(); assert_eq!(parser.parse(input), Ok(expected)); } #[rstest] #[case::ago_without_time_unit("1 ago", ParseError::TimeUnit(2, "Invalid time unit: 'ago'".to_string()))] #[case::ago_as_time_unit("1 ago", ParseError::TimeUnit(2, "Invalid time unit: 'ago'".to_string()))] #[case::just_ago("ago", ParseError::InvalidInput("ago".to_string()))] #[case::incomplete_ago("1s ag", ParseError::TimeUnit(1, "Invalid time unit: 's ag'".to_string()))] #[case::one_second_twice("1s 1s", ParseError::TimeUnit(1, "Invalid time unit: 's 1s'".to_string()))] fn test_custom_parser_with_allow_ago_then_error(#[case] input: &str, #[case] expected: ParseError) { let parser = CustomDurationParserBuilder::new() .time_units(&SYSTEMD_TIME_UNITS) .allow_time_unit_delimiter() .allow_ago() .allow_negative() .build(); assert_eq!(parser.parse(input), Err(expected)); } #[rstest] #[case::seconds_without_ago("1s", Duration::positive(1, 0))] #[case::seconds("1s ago", Duration::negative(1, 0))] #[case::twice_seconds("1s ago 1s ago", Duration::negative(2, 0))] #[case::seconds_and_day("1s ago 1day ago", Duration::negative(60 * 60 * 24 + 1, 0))] #[case::two_without_delimiter_between_multiple("1s ago1s ago", Duration::negative(2, 0))] #[case::two_with_delimiter_between_number_and_time_unit( "1 s ago 1 day ago", Duration::negative(60 * 60 * 24 + 1, 0) )] #[case::two_with_conjunction_between_number_and_time_unit( "1 s ago and 1 day ago", Duration::negative(60 * 60 * 24 + 1, 0) )] #[case::without_ago_conjunction_and_with_ago( "1 s and 1 day ago", Duration::negative(60 * 60 * 24 - 1, 0) )] #[case::without_ago_and_with_ago( "1 s 1 day ago", Duration::negative(60 * 60 * 24 - 1, 0) )] #[case::with_ago_and_without_ago( "1s ago 1 day", Duration::positive(60 * 60 * 24 - 1, 0) )] #[case::with_ago_conjunction_without_ago( "1s ago and 1 day", Duration::positive(60 * 60 * 24 - 1, 0) )] fn test_custom_parser_with_allow_ago_when_parse_multiple( #[case] input: &str, #[case] expected: Duration, ) { let parser = CustomDurationParserBuilder::new() .time_units(&SYSTEMD_TIME_UNITS) .allow_time_unit_delimiter() .allow_ago() .allow_negative() .parse_multiple(Some(&["and"])) .build(); assert_eq!(parser.parse(input), Ok(expected)); }