use alloc::string::{String, ToString};
use crate::corety::AzString;
use crate::props::formatter::PrintAsCssValue;
pub const TICKS_PER_SECOND: u64 = 60;
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[repr(C)]
pub enum CssDurationUnit {
#[default]
Milliseconds,
Ticks,
}
impl PrintAsCssValue for CssDurationUnit {
fn print_as_css_value(&self) -> String {
match self {
Self::Milliseconds => "ms".to_string(),
Self::Ticks => "t".to_string(),
}
}
}
impl crate::codegen::format::FormatAsRustCode for CssDurationUnit {
fn format_as_rust_code(&self, _tabs: usize) -> String {
match self {
Self::Milliseconds => "CssDurationUnit::Milliseconds".to_string(),
Self::Ticks => "CssDurationUnit::Ticks".to_string(),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub struct CssDuration {
pub inner: u32,
pub unit: CssDurationUnit,
}
impl CssDuration {
#[must_use]
pub const fn from_millis(ms: u32) -> Self {
Self {
inner: ms,
unit: CssDurationUnit::Milliseconds,
}
}
#[must_use]
pub const fn from_ticks(ticks: u32) -> Self {
Self {
inner: ticks,
unit: CssDurationUnit::Ticks,
}
}
#[allow(clippy::cast_lossless, clippy::cast_possible_truncation)]
#[must_use]
pub const fn millis(&self) -> u32 {
match self.unit {
CssDurationUnit::Milliseconds => self.inner,
CssDurationUnit::Ticks => {
let ms = (self.inner as u64) * 1000 / TICKS_PER_SECOND;
if ms > u32::MAX as u64 {
u32::MAX
} else {
ms as u32
}
}
}
}
#[allow(clippy::cast_lossless, clippy::cast_possible_truncation)]
#[must_use]
pub const fn ticks(&self) -> u32 {
match self.unit {
CssDurationUnit::Ticks => self.inner,
CssDurationUnit::Milliseconds => {
((self.inner as u64) * TICKS_PER_SECOND / 1000) as u32
}
}
}
}
impl PrintAsCssValue for CssDuration {
fn print_as_css_value(&self) -> String {
format!("{}{}", self.inner, self.unit.print_as_css_value())
}
}
impl crate::codegen::format::FormatAsRustCode for CssDuration {
fn format_as_rust_code(&self, _tabs: usize) -> String {
use crate::codegen::format::FormatAsRustCode;
format!(
"CssDuration {{ inner: {}, unit: {} }}",
self.inner,
self.unit.format_as_rust_code(0)
)
}
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum DurationParseError<'a> {
InvalidValue(&'a str),
ParseFloat(core::num::ParseFloatError),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(DurationParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { DurationParseError<'a>, {
InvalidValue(v) => format!("Invalid time value: \"{}\"", v),
ParseFloat(e) => format!("Invalid number for time value: {}", e),
}}
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum DurationParseErrorOwned {
InvalidValue(AzString),
ParseFloat(AzString),
}
#[cfg(feature = "parser")]
impl DurationParseError<'_> {
#[must_use] pub fn to_contained(&self) -> DurationParseErrorOwned {
match self {
Self::InvalidValue(s) => DurationParseErrorOwned::InvalidValue((*s).to_string().into()),
Self::ParseFloat(e) => DurationParseErrorOwned::ParseFloat(e.to_string().into()),
}
}
}
#[cfg(feature = "parser")]
impl DurationParseErrorOwned {
#[must_use] pub fn to_shared(&self) -> DurationParseError<'_> {
match self {
Self::InvalidValue(s) => DurationParseError::InvalidValue(s),
Self::ParseFloat(s) => DurationParseError::InvalidValue(s.as_str()),
}
}
}
#[cfg(feature = "parser")]
pub fn parse_duration(input: &str) -> Result<CssDuration, DurationParseError<'_>> {
let trimmed = input.trim().to_lowercase();
if trimmed == "0" {
return Ok(CssDuration::from_millis(0));
}
if let Some(num_str) = trimmed.strip_suffix("ms") {
let ms = num_str
.parse::<f32>()
.map_err(DurationParseError::ParseFloat)?;
if ms < 0.0 {
return Err(DurationParseError::InvalidValue(input));
}
Ok(CssDuration::from_millis(crate::cast::f32_to_u32(ms)))
} else if let Some(num_str) = trimmed.strip_suffix('s') {
let s = num_str
.parse::<f32>()
.map_err(DurationParseError::ParseFloat)?;
if s < 0.0 {
return Err(DurationParseError::InvalidValue(input));
}
Ok(CssDuration::from_millis(crate::cast::f32_to_u32(s * 1000.0)))
} else if let Some(num_str) = trimmed.strip_suffix('t') {
let t = num_str
.parse::<f32>()
.map_err(DurationParseError::ParseFloat)?;
if t < 0.0 {
return Err(DurationParseError::InvalidValue(input));
}
Ok(CssDuration::from_ticks(crate::cast::f32_to_u32(t)))
} else {
Err(DurationParseError::InvalidValue(input))
}
}
#[cfg(test)]
#[allow(clippy::unreadable_literal)]
mod autotest_generated {
use super::*;
use crate::codegen::format::FormatAsRustCode;
use crate::props::formatter::PrintAsCssValue;
#[cfg(feature = "parser")]
const TWO_POW_24: u32 = 16_777_216;
#[cfg(feature = "parser")]
fn ms(input: &str) -> u32 {
let d = parse_duration(input)
.unwrap_or_else(|e| panic!("expected {input:?} to parse, got {e}"));
assert_eq!(
d.unit,
CssDurationUnit::Milliseconds,
"{input:?} parsed as {:?}, not milliseconds",
d.unit
);
d.inner
}
#[cfg(feature = "parser")]
fn ticks(input: &str) -> u32 {
let d = parse_duration(input)
.unwrap_or_else(|e| panic!("expected {input:?} to parse, got {e}"));
assert_eq!(
d.unit,
CssDurationUnit::Ticks,
"{input:?} parsed as {:?}, not ticks",
d.unit
);
d.inner
}
#[cfg(feature = "parser")]
#[test]
fn valid_minimal_inputs_parse_to_expected_values() {
assert_eq!(ms("0"), 0);
assert_eq!(ms("0ms"), 0);
assert_eq!(ms("0s"), 0);
assert_eq!(ms("200ms"), 200);
assert_eq!(ms("1s"), 1000);
assert_eq!(ms("1.5s"), 1500);
assert_eq!(ms("0.5s"), 500);
assert_eq!(ms(".25s"), 250);
assert_eq!(ms("5e2ms"), 500);
assert_eq!(ms("+5ms"), 5);
}
#[cfg(feature = "parser")]
#[test]
fn ms_suffix_wins_over_s_suffix() {
assert_eq!(ms("5ms"), 5);
assert_ne!(ms("5ms"), ms("5s"));
assert_eq!(ms("5s"), 5000);
}
#[cfg(feature = "parser")]
#[test]
fn units_are_case_insensitive() {
assert_eq!(ms("200MS"), 200);
assert_eq!(ms("200Ms"), 200);
assert_eq!(ms("1S"), 1000);
assert_eq!(ms("1.5E1S"), 15000);
assert_eq!(ticks("5T"), 5);
}
#[cfg(feature = "parser")]
#[test]
fn the_t_unit_parses_to_a_tick_count_and_is_not_normalised_to_millis() {
assert_eq!(parse_duration("5t"), Ok(CssDuration::from_ticks(5)));
assert_eq!(ticks("0t"), 0);
assert_eq!(ticks("1t"), 1);
assert_eq!(ticks("60t"), 60);
assert_eq!(ticks("4294967295t"), u32::MAX);
assert_ne!(parse_duration("5t"), parse_duration("5ms"));
assert_ne!(parse_duration("60t"), parse_duration("1s"));
}
#[cfg(feature = "parser")]
#[test]
fn the_t_unit_does_not_collide_with_the_other_units_or_swallow_garbage() {
assert_eq!(ms("5ms"), 5);
assert_eq!(ms("5s"), 5000);
for garbage in ["5pt", "5t5", "t", "5tt", "5mst", "5st", "5 t", "-5t"] {
assert!(
parse_duration(garbage).is_err(),
"expected {garbage:?} to be rejected"
);
}
}
#[test]
fn millis_and_ticks_convert_at_the_nominal_frame_rate() {
assert_eq!(TICKS_PER_SECOND, 60);
assert_eq!(CssDuration::from_ticks(60).millis(), 1000);
assert_eq!(CssDuration::from_ticks(30).millis(), 500);
assert_eq!(CssDuration::from_ticks(1).millis(), 16);
assert_eq!(CssDuration::from_ticks(0).millis(), 0);
assert_eq!(CssDuration::from_millis(1000).ticks(), 60);
assert_eq!(CssDuration::from_millis(500).ticks(), 30);
assert_eq!(CssDuration::from_millis(16).ticks(), 0, "sub-frame is 0 frames");
assert_eq!(CssDuration::from_millis(17).ticks(), 1);
assert_eq!(CssDuration::from_millis(0).ticks(), 0);
assert_eq!(CssDuration::from_millis(7).millis(), 7);
assert_eq!(CssDuration::from_ticks(7).ticks(), 7);
}
#[test]
fn tick_to_milli_conversion_saturates_instead_of_wrapping() {
assert_eq!(CssDuration::from_ticks(u32::MAX).millis(), u32::MAX);
let last_exact = (u64::from(u32::MAX) * TICKS_PER_SECOND / 1000) as u32;
assert!(CssDuration::from_ticks(last_exact).millis() < u32::MAX);
assert_eq!(
CssDuration::from_millis(u32::MAX).ticks(),
(u64::from(u32::MAX) * TICKS_PER_SECOND / 1000) as u32
);
}
#[cfg(feature = "parser")]
#[test]
fn sub_millisecond_values_truncate_toward_zero() {
assert_eq!(ms("5.9ms"), 5);
assert_eq!(ms("0.9ms"), 0);
assert_eq!(ms("0.0009s"), 0); assert_eq!(ms("0.0015s"), 1); }
#[cfg(feature = "parser")]
#[test]
fn empty_input_is_rejected_without_panicking() {
assert_eq!(parse_duration(""), Err(DurationParseError::InvalidValue("")));
}
#[cfg(feature = "parser")]
#[test]
fn whitespace_only_input_is_rejected_and_error_keeps_the_raw_input() {
assert_eq!(
parse_duration(" "),
Err(DurationParseError::InvalidValue(" "))
);
assert_eq!(
parse_duration("\t\n"),
Err(DurationParseError::InvalidValue("\t\n"))
);
}
#[cfg(feature = "parser")]
#[test]
fn a_bare_unit_with_no_number_is_a_parse_float_error_not_a_panic() {
assert!(matches!(
parse_duration("ms"),
Err(DurationParseError::ParseFloat(_))
));
assert!(matches!(
parse_duration("s"),
Err(DurationParseError::ParseFloat(_))
));
}
#[cfg(feature = "parser")]
#[test]
fn unitless_numbers_other_than_literal_zero_are_rejected() {
assert_eq!(ms("0"), 0);
assert_eq!(
parse_duration("200"),
Err(DurationParseError::InvalidValue("200"))
);
assert_eq!(
parse_duration("1.5"),
Err(DurationParseError::InvalidValue("1.5"))
);
assert_eq!(
parse_duration("0.0"),
Err(DurationParseError::InvalidValue("0.0"))
);
assert_eq!(
parse_duration("00"),
Err(DurationParseError::InvalidValue("00"))
);
assert_eq!(
parse_duration("-0"),
Err(DurationParseError::InvalidValue("-0"))
);
}
#[cfg(feature = "parser")]
#[test]
fn garbage_and_junk_never_panic() {
for garbage in [
"abc", "!!!", "\0\0\0", "ms ms", "1,5s", "1 ms", "--5ms", "5mss", "5sms", "0x10ms",
"1e", "1e+", ".s", "-.ms", "s1", "ms200", "200ms;garbage", "200ms !important",
] {
assert!(
parse_duration(garbage).is_err(),
"expected {garbage:?} to be rejected"
);
}
}
#[cfg(feature = "parser")]
#[test]
fn leading_and_trailing_whitespace_is_trimmed_but_interior_space_is_not() {
assert_eq!(ms(" 200ms "), 200);
assert_eq!(ms("\t\n1.5s\r\n"), 1500);
assert!(matches!(
parse_duration("200 ms"),
Err(DurationParseError::ParseFloat(_))
));
assert!(matches!(
parse_duration("2 0 0ms"),
Err(DurationParseError::ParseFloat(_))
));
}
#[cfg(feature = "parser")]
#[test]
fn trailing_junk_after_a_valid_value_is_rejected_not_silently_accepted() {
assert!(parse_duration("200ms;").is_err());
assert!(parse_duration("200msx").is_err());
assert!(matches!(
parse_duration("200msms"),
Err(DurationParseError::ParseFloat(_))
));
}
#[cfg(feature = "parser")]
#[test]
fn negative_durations_are_rejected_in_both_units() {
assert_eq!(
parse_duration("-1ms"),
Err(DurationParseError::InvalidValue("-1ms"))
);
assert_eq!(
parse_duration("-0.5s"),
Err(DurationParseError::InvalidValue("-0.5s"))
);
assert_eq!(
parse_duration("-1e-30s"),
Err(DurationParseError::InvalidValue("-1e-30s"))
);
}
#[cfg(feature = "parser")]
#[test]
fn the_invalid_value_error_reports_the_original_untrimmed_uncased_input() {
assert_eq!(
parse_duration(" -1MS "),
Err(DurationParseError::InvalidValue(" -1MS "))
);
}
#[cfg(feature = "parser")]
#[test]
fn negative_zero_is_accepted_and_clamps_to_zero() {
assert_eq!(ms("-0ms"), 0);
assert_eq!(ms("-0.0s"), 0);
assert_eq!(ms("-0e10ms"), 0);
}
#[cfg(feature = "parser")]
#[test]
fn values_beyond_u32_max_saturate_instead_of_wrapping_or_panicking() {
assert_eq!(ms("4294967296ms"), u32::MAX); assert_eq!(ms("99999999999ms"), u32::MAX);
assert_eq!(ms("1e30s"), u32::MAX);
assert_eq!(ms("5000000s"), u32::MAX); }
#[cfg(feature = "parser")]
#[test]
fn float_overflow_to_infinity_saturates_to_u32_max() {
assert_eq!(ms("1e39ms"), u32::MAX); assert_eq!(ms("1e999999ms"), u32::MAX);
assert_eq!(ms("infms"), u32::MAX);
assert_eq!(ms("infinityms"), u32::MAX);
assert_eq!(ms("INFms"), u32::MAX);
assert_eq!(ms("infs"), u32::MAX);
}
#[cfg(feature = "parser")]
#[test]
fn negative_infinity_is_rejected_as_a_negative_duration() {
assert_eq!(
parse_duration("-infms"),
Err(DurationParseError::InvalidValue("-infms"))
);
assert_eq!(
parse_duration("-infinitys"),
Err(DurationParseError::InvalidValue("-infinitys"))
);
}
#[cfg(feature = "parser")]
#[test]
fn nan_is_accepted_and_degrades_to_zero_rather_than_panicking() {
assert_eq!(ms("nanms"), 0);
assert_eq!(ms("NaNms"), 0);
assert_eq!(ms("-nanms"), 0);
assert_eq!(ms("nans"), 0); }
#[cfg(feature = "parser")]
#[test]
fn underflow_to_zero_is_not_an_error() {
assert_eq!(ms("1e-30ms"), 0);
assert_eq!(ms("1e-999999s"), 0);
}
#[cfg(feature = "parser")]
#[test]
fn u32_max_and_f32_max_boundary_strings_are_handled() {
assert_eq!(ms("4294967295ms"), u32::MAX); assert_eq!(ms("4294967040ms"), 4294967040);
let f32_max = format!("{}ms", f32::MAX);
assert_eq!(ms(&f32_max), u32::MAX);
let i64_max = format!("{}ms", i64::MAX);
assert_eq!(ms(&i64_max), u32::MAX);
}
#[cfg(feature = "parser")]
#[test]
fn extremely_long_digit_string_saturates_without_hanging() {
let mut input = "9".repeat(100_000);
input.push_str("ms");
assert_eq!(ms(&input), u32::MAX);
}
#[cfg(feature = "parser")]
#[test]
fn extremely_long_run_of_leading_zeros_still_parses_exactly() {
let mut input = "0".repeat(100_000);
input.push_str("1ms");
assert_eq!(ms(&input), 1);
}
#[cfg(feature = "parser")]
#[test]
fn extremely_long_garbage_is_rejected_without_hanging() {
let input = "x".repeat(100_000);
assert!(parse_duration(&input).is_err());
let padded = format!("{}200ms{}", " ".repeat(50_000), " ".repeat(50_000));
assert_eq!(ms(&padded), 200);
}
#[cfg(feature = "parser")]
#[test]
fn deeply_nested_brackets_do_not_stack_overflow() {
let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
assert!(parse_duration(&nested).is_err());
let nested_with_unit = format!("{nested}s");
assert!(parse_duration(&nested_with_unit).is_err());
}
#[cfg(feature = "parser")]
#[test]
fn non_ascii_input_is_rejected_without_panicking() {
for input in [
"\u{1F600}", "\u{1F600}ms", "1\u{FF53}", "1s\u{0301}", "200ms", "\u{202E}200ms", "1\u{00A0}s", ] {
assert!(
parse_duration(input).is_err(),
"expected {input:?} to be rejected"
);
}
}
#[cfg(feature = "parser")]
#[test]
fn unicode_whitespace_around_a_valid_value_is_trimmed() {
assert_eq!(ms("\u{00A0}200ms\u{00A0}"), 200); assert_eq!(ms("\u{3000}1.5s\u{3000}"), 1500); }
#[cfg(feature = "parser")]
#[test]
fn lowercasing_that_changes_the_byte_length_does_not_panic() {
assert!(parse_duration("\u{0130}ms").is_err()); assert!(parse_duration("1\u{0130}s").is_err());
}
#[cfg(feature = "parser")]
#[test]
fn print_as_css_value_round_trips_through_parse_duration() {
for inner in [
0,
1,
2,
17,
999,
1000,
65_535,
1_000_000,
TWO_POW_24, 4_294_967_040, u32::MAX, ] {
let duration = CssDuration::from_millis(inner);
let printed = duration.print_as_css_value();
assert_eq!(
parse_duration(&printed),
Ok(duration),
"round-trip failed for {inner}ms (printed as {printed:?})"
);
}
}
#[cfg(feature = "parser")]
#[test]
fn print_as_css_value_round_trips_tick_durations_as_ticks() {
for inner in [0, 1, 5, 60, 999, TWO_POW_24, u32::MAX] {
let duration = CssDuration::from_ticks(inner);
let printed = duration.print_as_css_value();
assert_eq!(printed, format!("{inner}t"));
assert_eq!(
parse_duration(&printed),
Ok(duration),
"round-trip failed for {inner}t (printed as {printed:?})"
);
}
}
#[test]
fn print_as_css_value_always_emits_the_unit_it_was_built_with() {
for inner in [0, 1, u32::MAX] {
let printed = CssDuration::from_millis(inner).print_as_css_value();
assert!(printed.ends_with("ms"), "{printed:?} lacks a unit");
assert_eq!(printed, format!("{inner}ms"));
let printed = CssDuration::from_ticks(inner).print_as_css_value();
assert!(printed.ends_with('t'), "{printed:?} lacks a unit");
assert!(!printed.ends_with("ms"), "{printed:?} lost the tick unit");
assert_eq!(printed, format!("{inner}t"));
}
}
#[cfg(feature = "parser")]
#[test]
fn round_trip_above_two_pow_24_is_lossy_but_bounded() {
let duration = CssDuration::from_millis(TWO_POW_24 + 1);
let reparsed = parse_duration(&duration.print_as_css_value()).unwrap();
assert_ne!(reparsed.inner, duration.inner);
assert_eq!(reparsed.inner, TWO_POW_24);
assert!(reparsed.inner.abs_diff(duration.inner) <= 1);
}
#[cfg(feature = "parser")]
#[test]
fn seconds_and_milliseconds_agree_for_the_same_duration() {
assert_eq!(ms("2s"), ms("2000ms"));
assert_eq!(ms("0.001s"), ms("1ms"));
assert_eq!(ms("0s"), ms("0ms"));
}
#[test]
fn default_duration_is_zero_milliseconds() {
assert_eq!(CssDuration::default(), CssDuration::from_millis(0));
assert_eq!(CssDuration::default().inner, 0);
assert_eq!(CssDuration::default().unit, CssDurationUnit::Milliseconds);
assert_eq!(CssDurationUnit::default(), CssDurationUnit::Milliseconds);
}
#[test]
fn ordering_and_equality_follow_the_inner_count_within_one_unit() {
let a = CssDuration::from_millis(1);
let b = CssDuration::from_millis(2);
let max = CssDuration::from_millis(u32::MAX);
assert!(a < b);
assert!(b < max);
assert_eq!(a, CssDuration::from_millis(1));
assert_eq!(a.max(b), b);
assert_eq!(CssDuration::default(), CssDuration::from_millis(0));
assert_ne!(CssDuration::from_millis(5), CssDuration::from_ticks(5));
}
#[test]
fn format_as_rust_code_emits_a_constructor_and_ignores_indentation() {
let d = CssDuration::from_millis(42);
assert_eq!(
d.format_as_rust_code(0),
"CssDuration { inner: 42, unit: CssDurationUnit::Milliseconds }"
);
assert_eq!(d.format_as_rust_code(7), d.format_as_rust_code(0));
assert_eq!(
CssDuration::from_millis(u32::MAX).format_as_rust_code(0),
"CssDuration { inner: 4294967295, unit: CssDurationUnit::Milliseconds }"
);
assert_eq!(
CssDuration::from_ticks(5).format_as_rust_code(0),
"CssDuration { inner: 5, unit: CssDurationUnit::Ticks }"
);
assert_eq!(
CssDurationUnit::Ticks.format_as_rust_code(0),
"CssDurationUnit::Ticks"
);
}
#[cfg(feature = "parser")]
fn parse_float_error() -> core::num::ParseFloatError {
"not-a-float".parse::<f32>().unwrap_err()
}
#[cfg(feature = "parser")]
#[test]
fn to_contained_preserves_an_invalid_value_payload() {
let owned = DurationParseError::InvalidValue("10px").to_contained();
match owned {
DurationParseErrorOwned::InvalidValue(s) => assert_eq!(s.as_str(), "10px"),
DurationParseErrorOwned::ParseFloat(_) => panic!("variant changed"),
}
}
#[cfg(feature = "parser")]
#[test]
fn to_contained_stringifies_the_float_error() {
let owned = DurationParseError::ParseFloat(parse_float_error()).to_contained();
match owned {
DurationParseErrorOwned::ParseFloat(s) => {
assert!(!s.as_str().is_empty(), "float error message was empty");
assert_eq!(s.as_str(), parse_float_error().to_string());
}
DurationParseErrorOwned::InvalidValue(_) => panic!("variant changed"),
}
}
#[cfg(feature = "parser")]
#[test]
fn to_contained_handles_empty_and_extreme_payloads() {
assert_eq!(
DurationParseError::InvalidValue("").to_contained(),
DurationParseErrorOwned::InvalidValue(String::new().into())
);
let huge = "x".repeat(100_000);
let owned = DurationParseError::InvalidValue(&huge).to_contained();
match owned {
DurationParseErrorOwned::InvalidValue(s) => assert_eq!(s.as_str().len(), 100_000),
DurationParseErrorOwned::ParseFloat(_) => panic!("variant changed"),
}
let unicode = "\u{1F600}\u{0301}";
assert_eq!(
DurationParseError::InvalidValue(unicode).to_contained(),
DurationParseErrorOwned::InvalidValue(unicode.to_string().into())
);
}
#[cfg(feature = "parser")]
#[test]
fn to_shared_preserves_an_invalid_value_payload() {
let owned = DurationParseErrorOwned::InvalidValue("garbage".to_string().into());
assert_eq!(owned.to_shared(), DurationParseError::InvalidValue("garbage"));
}
#[cfg(feature = "parser")]
#[test]
fn to_shared_downgrades_parse_float_to_invalid_value() {
let msg = parse_float_error().to_string();
let owned = DurationParseErrorOwned::ParseFloat(msg.clone().into());
let shared = owned.to_shared();
assert!(!matches!(shared, DurationParseError::ParseFloat(_)));
assert_eq!(shared, DurationParseError::InvalidValue(msg.as_str()));
}
#[cfg(feature = "parser")]
#[test]
fn to_shared_does_not_panic_on_empty_or_extreme_payloads() {
assert_eq!(
DurationParseErrorOwned::InvalidValue(String::new().into()).to_shared(),
DurationParseError::InvalidValue("")
);
let huge = "y".repeat(100_000);
let owned = DurationParseErrorOwned::InvalidValue(huge.clone().into());
assert_eq!(owned.to_shared(), DurationParseError::InvalidValue(&huge));
let empty_float = DurationParseErrorOwned::ParseFloat(String::new().into());
assert_eq!(empty_float.to_shared(), DurationParseError::InvalidValue(""));
}
#[cfg(feature = "parser")]
#[test]
fn invalid_value_survives_a_full_shared_owned_shared_round_trip() {
let input = "10px";
let err = parse_duration(input).unwrap_err();
assert_eq!(err, DurationParseError::InvalidValue(input));
let owned = err.to_contained();
assert_eq!(owned.to_shared(), DurationParseError::InvalidValue(input));
}
#[cfg(feature = "parser")]
#[test]
fn a_word_ending_in_s_is_treated_as_a_seconds_value() {
assert!(matches!(
parse_duration("200 nanoseconds"),
Err(DurationParseError::ParseFloat(_))
));
assert!(matches!(
parse_duration("always"),
Err(DurationParseError::ParseFloat(_))
));
assert_eq!(
parse_duration("200 nanosecond"),
Err(DurationParseError::InvalidValue("200 nanosecond"))
);
}
#[cfg(feature = "parser")]
#[test]
fn error_display_never_panics_and_mentions_the_offender() {
let invalid = DurationParseError::InvalidValue("\u{1F600}");
let printed = format!("{invalid}");
assert!(printed.contains('\u{1F600}'), "{printed:?}");
let float = DurationParseError::ParseFloat(parse_float_error());
assert!(!format!("{float}").is_empty());
assert!(!format!("{invalid:?}").is_empty());
assert!(!format!("{float:?}").is_empty());
}
}