1use alloc::string::{String, ToString};
4use crate::corety::AzString;
5
6use crate::props::formatter::PrintAsCssValue;
7
8pub const TICKS_PER_SECOND: u64 = 60;
26
27#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
35#[repr(C)]
36pub enum CssDurationUnit {
37 #[default]
39 Milliseconds,
40 Ticks,
42}
43
44impl PrintAsCssValue for CssDurationUnit {
45 fn print_as_css_value(&self) -> String {
46 match self {
47 Self::Milliseconds => "ms".to_string(),
48 Self::Ticks => "t".to_string(),
49 }
50 }
51}
52
53impl crate::codegen::format::FormatAsRustCode for CssDurationUnit {
54 fn format_as_rust_code(&self, _tabs: usize) -> String {
55 match self {
56 Self::Milliseconds => "CssDurationUnit::Milliseconds".to_string(),
57 Self::Ticks => "CssDurationUnit::Ticks".to_string(),
58 }
59 }
60}
61
62#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
73#[repr(C)]
74#[derive(Default)]
75pub struct CssDuration {
76 pub inner: u32,
78 pub unit: CssDurationUnit,
80}
81
82impl CssDuration {
83 #[must_use]
85 pub const fn from_millis(ms: u32) -> Self {
86 Self {
87 inner: ms,
88 unit: CssDurationUnit::Milliseconds,
89 }
90 }
91
92 #[must_use]
94 pub const fn from_ticks(ticks: u32) -> Self {
95 Self {
96 inner: ticks,
97 unit: CssDurationUnit::Ticks,
98 }
99 }
100
101 #[allow(clippy::cast_lossless, clippy::cast_possible_truncation)]
109 #[must_use]
110 pub const fn millis(&self) -> u32 {
111 match self.unit {
112 CssDurationUnit::Milliseconds => self.inner,
113 CssDurationUnit::Ticks => {
114 let ms = (self.inner as u64) * 1000 / TICKS_PER_SECOND;
116 if ms > u32::MAX as u64 {
117 u32::MAX
118 } else {
119 ms as u32
120 }
121 }
122 }
123 }
124
125 #[allow(clippy::cast_lossless, clippy::cast_possible_truncation)]
133 #[must_use]
134 pub const fn ticks(&self) -> u32 {
135 match self.unit {
136 CssDurationUnit::Ticks => self.inner,
137 CssDurationUnit::Milliseconds => {
138 ((self.inner as u64) * TICKS_PER_SECOND / 1000) as u32
140 }
141 }
142 }
143}
144
145impl PrintAsCssValue for CssDuration {
146 fn print_as_css_value(&self) -> String {
147 format!("{}{}", self.inner, self.unit.print_as_css_value())
148 }
149}
150
151impl crate::codegen::format::FormatAsRustCode for CssDuration {
152 fn format_as_rust_code(&self, _tabs: usize) -> String {
153 use crate::codegen::format::FormatAsRustCode;
154 format!(
155 "CssDuration {{ inner: {}, unit: {} }}",
156 self.inner,
157 self.unit.format_as_rust_code(0)
158 )
159 }
160}
161
162#[cfg(feature = "parser")]
164#[derive(Clone, PartialEq, Eq)]
165pub enum DurationParseError<'a> {
166 InvalidValue(&'a str),
167 ParseFloat(core::num::ParseFloatError),
168}
169
170#[cfg(feature = "parser")]
171impl_debug_as_display!(DurationParseError<'a>);
172#[cfg(feature = "parser")]
173impl_display! { DurationParseError<'a>, {
174 InvalidValue(v) => format!("Invalid time value: \"{}\"", v),
175 ParseFloat(e) => format!("Invalid number for time value: {}", e),
176}}
177
178#[cfg(feature = "parser")]
180#[derive(Debug, Clone, PartialEq, Eq)]
181#[repr(C, u8)]
182pub enum DurationParseErrorOwned {
183 InvalidValue(AzString),
184 ParseFloat(AzString),
185}
186
187#[cfg(feature = "parser")]
188impl DurationParseError<'_> {
189 #[must_use] pub fn to_contained(&self) -> DurationParseErrorOwned {
190 match self {
191 Self::InvalidValue(s) => DurationParseErrorOwned::InvalidValue((*s).to_string().into()),
192 Self::ParseFloat(e) => DurationParseErrorOwned::ParseFloat(e.to_string().into()),
193 }
194 }
195}
196
197#[cfg(feature = "parser")]
198impl DurationParseErrorOwned {
199 #[must_use] pub fn to_shared(&self) -> DurationParseError<'_> {
200 match self {
201 Self::InvalidValue(s) => DurationParseError::InvalidValue(s),
202 Self::ParseFloat(s) => DurationParseError::InvalidValue(s.as_str()),
203 }
204 }
205}
206
207#[cfg(feature = "parser")]
222pub fn parse_duration(input: &str) -> Result<CssDuration, DurationParseError<'_>> {
226 let trimmed = input.trim().to_lowercase();
227 if trimmed == "0" {
228 return Ok(CssDuration::from_millis(0));
229 }
230 if let Some(num_str) = trimmed.strip_suffix("ms") {
234 let ms = num_str
235 .parse::<f32>()
236 .map_err(DurationParseError::ParseFloat)?;
237 if ms < 0.0 {
238 return Err(DurationParseError::InvalidValue(input));
239 }
240 Ok(CssDuration::from_millis(crate::cast::f32_to_u32(ms)))
241 } else if let Some(num_str) = trimmed.strip_suffix('s') {
242 let s = num_str
243 .parse::<f32>()
244 .map_err(DurationParseError::ParseFloat)?;
245 if s < 0.0 {
246 return Err(DurationParseError::InvalidValue(input));
247 }
248 Ok(CssDuration::from_millis(crate::cast::f32_to_u32(s * 1000.0)))
249 } else if let Some(num_str) = trimmed.strip_suffix('t') {
250 let t = num_str
251 .parse::<f32>()
252 .map_err(DurationParseError::ParseFloat)?;
253 if t < 0.0 {
254 return Err(DurationParseError::InvalidValue(input));
255 }
256 Ok(CssDuration::from_ticks(crate::cast::f32_to_u32(t)))
257 } else {
258 Err(DurationParseError::InvalidValue(input))
259 }
260}
261
262#[cfg(test)]
263#[allow(clippy::unreadable_literal)]
264mod autotest_generated {
265 use super::*;
266 use crate::codegen::format::FormatAsRustCode;
267 use crate::props::formatter::PrintAsCssValue;
268
269 #[cfg(feature = "parser")]
273 const TWO_POW_24: u32 = 16_777_216;
274
275 #[cfg(feature = "parser")]
278 fn ms(input: &str) -> u32 {
279 let d = parse_duration(input)
280 .unwrap_or_else(|e| panic!("expected {input:?} to parse, got {e}"));
281 assert_eq!(
282 d.unit,
283 CssDurationUnit::Milliseconds,
284 "{input:?} parsed as {:?}, not milliseconds",
285 d.unit
286 );
287 d.inner
288 }
289
290 #[cfg(feature = "parser")]
293 fn ticks(input: &str) -> u32 {
294 let d = parse_duration(input)
295 .unwrap_or_else(|e| panic!("expected {input:?} to parse, got {e}"));
296 assert_eq!(
297 d.unit,
298 CssDurationUnit::Ticks,
299 "{input:?} parsed as {:?}, not ticks",
300 d.unit
301 );
302 d.inner
303 }
304
305 #[cfg(feature = "parser")]
308 #[test]
309 fn valid_minimal_inputs_parse_to_expected_values() {
310 assert_eq!(ms("0"), 0);
311 assert_eq!(ms("0ms"), 0);
312 assert_eq!(ms("0s"), 0);
313 assert_eq!(ms("200ms"), 200);
314 assert_eq!(ms("1s"), 1000);
315 assert_eq!(ms("1.5s"), 1500);
316 assert_eq!(ms("0.5s"), 500);
317 assert_eq!(ms(".25s"), 250);
318 assert_eq!(ms("5e2ms"), 500);
319 assert_eq!(ms("+5ms"), 5);
320 }
321
322 #[cfg(feature = "parser")]
325 #[test]
326 fn ms_suffix_wins_over_s_suffix() {
327 assert_eq!(ms("5ms"), 5);
328 assert_ne!(ms("5ms"), ms("5s"));
329 assert_eq!(ms("5s"), 5000);
330 }
331
332 #[cfg(feature = "parser")]
333 #[test]
334 fn units_are_case_insensitive() {
335 assert_eq!(ms("200MS"), 200);
336 assert_eq!(ms("200Ms"), 200);
337 assert_eq!(ms("1S"), 1000);
338 assert_eq!(ms("1.5E1S"), 15000);
339 assert_eq!(ticks("5T"), 5);
340 }
341
342 #[cfg(feature = "parser")]
348 #[test]
349 fn the_t_unit_parses_to_a_tick_count_and_is_not_normalised_to_millis() {
350 assert_eq!(parse_duration("5t"), Ok(CssDuration::from_ticks(5)));
351 assert_eq!(ticks("0t"), 0);
352 assert_eq!(ticks("1t"), 1);
353 assert_eq!(ticks("60t"), 60);
354 assert_eq!(ticks("4294967295t"), u32::MAX);
355 assert_ne!(parse_duration("5t"), parse_duration("5ms"));
357 assert_ne!(parse_duration("60t"), parse_duration("1s"));
358 }
359
360 #[cfg(feature = "parser")]
363 #[test]
364 fn the_t_unit_does_not_collide_with_the_other_units_or_swallow_garbage() {
365 assert_eq!(ms("5ms"), 5);
366 assert_eq!(ms("5s"), 5000);
367 for garbage in ["5pt", "5t5", "t", "5tt", "5mst", "5st", "5 t", "-5t"] {
369 assert!(
370 parse_duration(garbage).is_err(),
371 "expected {garbage:?} to be rejected"
372 );
373 }
374 }
375
376 #[test]
380 fn millis_and_ticks_convert_at_the_nominal_frame_rate() {
381 assert_eq!(TICKS_PER_SECOND, 60);
382
383 assert_eq!(CssDuration::from_ticks(60).millis(), 1000);
384 assert_eq!(CssDuration::from_ticks(30).millis(), 500);
385 assert_eq!(CssDuration::from_ticks(1).millis(), 16);
386 assert_eq!(CssDuration::from_ticks(0).millis(), 0);
387
388 assert_eq!(CssDuration::from_millis(1000).ticks(), 60);
389 assert_eq!(CssDuration::from_millis(500).ticks(), 30);
390 assert_eq!(CssDuration::from_millis(16).ticks(), 0, "sub-frame is 0 frames");
391 assert_eq!(CssDuration::from_millis(17).ticks(), 1);
392 assert_eq!(CssDuration::from_millis(0).ticks(), 0);
393
394 assert_eq!(CssDuration::from_millis(7).millis(), 7);
397 assert_eq!(CssDuration::from_ticks(7).ticks(), 7);
398 }
399
400 #[test]
403 fn tick_to_milli_conversion_saturates_instead_of_wrapping() {
404 assert_eq!(CssDuration::from_ticks(u32::MAX).millis(), u32::MAX);
405 let last_exact = (u64::from(u32::MAX) * TICKS_PER_SECOND / 1000) as u32;
407 assert!(CssDuration::from_ticks(last_exact).millis() < u32::MAX);
408 assert_eq!(
410 CssDuration::from_millis(u32::MAX).ticks(),
411 (u64::from(u32::MAX) * TICKS_PER_SECOND / 1000) as u32
412 );
413 }
414
415 #[cfg(feature = "parser")]
419 #[test]
420 fn sub_millisecond_values_truncate_toward_zero() {
421 assert_eq!(ms("5.9ms"), 5);
422 assert_eq!(ms("0.9ms"), 0);
423 assert_eq!(ms("0.0009s"), 0); assert_eq!(ms("0.0015s"), 1); }
426
427 #[cfg(feature = "parser")]
430 #[test]
431 fn empty_input_is_rejected_without_panicking() {
432 assert_eq!(parse_duration(""), Err(DurationParseError::InvalidValue("")));
433 }
434
435 #[cfg(feature = "parser")]
436 #[test]
437 fn whitespace_only_input_is_rejected_and_error_keeps_the_raw_input() {
438 assert_eq!(
441 parse_duration(" "),
442 Err(DurationParseError::InvalidValue(" "))
443 );
444 assert_eq!(
445 parse_duration("\t\n"),
446 Err(DurationParseError::InvalidValue("\t\n"))
447 );
448 }
449
450 #[cfg(feature = "parser")]
453 #[test]
454 fn a_bare_unit_with_no_number_is_a_parse_float_error_not_a_panic() {
455 assert!(matches!(
456 parse_duration("ms"),
457 Err(DurationParseError::ParseFloat(_))
458 ));
459 assert!(matches!(
460 parse_duration("s"),
461 Err(DurationParseError::ParseFloat(_))
462 ));
463 }
464
465 #[cfg(feature = "parser")]
466 #[test]
467 fn unitless_numbers_other_than_literal_zero_are_rejected() {
468 assert_eq!(ms("0"), 0);
470 assert_eq!(
471 parse_duration("200"),
472 Err(DurationParseError::InvalidValue("200"))
473 );
474 assert_eq!(
475 parse_duration("1.5"),
476 Err(DurationParseError::InvalidValue("1.5"))
477 );
478 assert_eq!(
479 parse_duration("0.0"),
480 Err(DurationParseError::InvalidValue("0.0"))
481 );
482 assert_eq!(
483 parse_duration("00"),
484 Err(DurationParseError::InvalidValue("00"))
485 );
486 assert_eq!(
487 parse_duration("-0"),
488 Err(DurationParseError::InvalidValue("-0"))
489 );
490 }
491
492 #[cfg(feature = "parser")]
493 #[test]
494 fn garbage_and_junk_never_panic() {
495 for garbage in [
496 "abc", "!!!", "\0\0\0", "ms ms", "1,5s", "1 ms", "--5ms", "5mss", "5sms", "0x10ms",
497 "1e", "1e+", ".s", "-.ms", "s1", "ms200", "200ms;garbage", "200ms !important",
498 ] {
499 assert!(
502 parse_duration(garbage).is_err(),
503 "expected {garbage:?} to be rejected"
504 );
505 }
506 }
507
508 #[cfg(feature = "parser")]
509 #[test]
510 fn leading_and_trailing_whitespace_is_trimmed_but_interior_space_is_not() {
511 assert_eq!(ms(" 200ms "), 200);
512 assert_eq!(ms("\t\n1.5s\r\n"), 1500);
513 assert!(matches!(
515 parse_duration("200 ms"),
516 Err(DurationParseError::ParseFloat(_))
517 ));
518 assert!(matches!(
519 parse_duration("2 0 0ms"),
520 Err(DurationParseError::ParseFloat(_))
521 ));
522 }
523
524 #[cfg(feature = "parser")]
525 #[test]
526 fn trailing_junk_after_a_valid_value_is_rejected_not_silently_accepted() {
527 assert!(parse_duration("200ms;").is_err());
528 assert!(parse_duration("200msx").is_err());
529 assert!(matches!(
531 parse_duration("200msms"),
532 Err(DurationParseError::ParseFloat(_))
533 ));
534 }
535
536 #[cfg(feature = "parser")]
539 #[test]
540 fn negative_durations_are_rejected_in_both_units() {
541 assert_eq!(
542 parse_duration("-1ms"),
543 Err(DurationParseError::InvalidValue("-1ms"))
544 );
545 assert_eq!(
546 parse_duration("-0.5s"),
547 Err(DurationParseError::InvalidValue("-0.5s"))
548 );
549 assert_eq!(
550 parse_duration("-1e-30s"),
551 Err(DurationParseError::InvalidValue("-1e-30s"))
552 );
553 }
554
555 #[cfg(feature = "parser")]
556 #[test]
557 fn the_invalid_value_error_reports_the_original_untrimmed_uncased_input() {
558 assert_eq!(
560 parse_duration(" -1MS "),
561 Err(DurationParseError::InvalidValue(" -1MS "))
562 );
563 }
564
565 #[cfg(feature = "parser")]
568 #[test]
569 fn negative_zero_is_accepted_and_clamps_to_zero() {
570 assert_eq!(ms("-0ms"), 0);
571 assert_eq!(ms("-0.0s"), 0);
572 assert_eq!(ms("-0e10ms"), 0);
573 }
574
575 #[cfg(feature = "parser")]
578 #[test]
579 fn values_beyond_u32_max_saturate_instead_of_wrapping_or_panicking() {
580 assert_eq!(ms("4294967296ms"), u32::MAX); assert_eq!(ms("99999999999ms"), u32::MAX);
582 assert_eq!(ms("1e30s"), u32::MAX);
583 assert_eq!(ms("5000000s"), u32::MAX); }
585
586 #[cfg(feature = "parser")]
589 #[test]
590 fn float_overflow_to_infinity_saturates_to_u32_max() {
591 assert_eq!(ms("1e39ms"), u32::MAX); assert_eq!(ms("1e999999ms"), u32::MAX);
593 assert_eq!(ms("infms"), u32::MAX);
594 assert_eq!(ms("infinityms"), u32::MAX);
595 assert_eq!(ms("INFms"), u32::MAX);
596 assert_eq!(ms("infs"), u32::MAX);
597 }
598
599 #[cfg(feature = "parser")]
600 #[test]
601 fn negative_infinity_is_rejected_as_a_negative_duration() {
602 assert_eq!(
603 parse_duration("-infms"),
604 Err(DurationParseError::InvalidValue("-infms"))
605 );
606 assert_eq!(
607 parse_duration("-infinitys"),
608 Err(DurationParseError::InvalidValue("-infinitys"))
609 );
610 }
611
612 #[cfg(feature = "parser")]
616 #[test]
617 fn nan_is_accepted_and_degrades_to_zero_rather_than_panicking() {
618 assert_eq!(ms("nanms"), 0);
619 assert_eq!(ms("NaNms"), 0);
620 assert_eq!(ms("-nanms"), 0);
621 assert_eq!(ms("nans"), 0); }
623
624 #[cfg(feature = "parser")]
625 #[test]
626 fn underflow_to_zero_is_not_an_error() {
627 assert_eq!(ms("1e-30ms"), 0);
628 assert_eq!(ms("1e-999999s"), 0);
629 }
630
631 #[cfg(feature = "parser")]
632 #[test]
633 fn u32_max_and_f32_max_boundary_strings_are_handled() {
634 assert_eq!(ms("4294967295ms"), u32::MAX); assert_eq!(ms("4294967040ms"), 4294967040); let f32_max = format!("{}ms", f32::MAX);
638 assert_eq!(ms(&f32_max), u32::MAX);
639
640 let i64_max = format!("{}ms", i64::MAX);
641 assert_eq!(ms(&i64_max), u32::MAX);
642 }
643
644 #[cfg(feature = "parser")]
647 #[test]
648 fn extremely_long_digit_string_saturates_without_hanging() {
649 let mut input = "9".repeat(100_000);
650 input.push_str("ms");
651 assert_eq!(ms(&input), u32::MAX);
652 }
653
654 #[cfg(feature = "parser")]
655 #[test]
656 fn extremely_long_run_of_leading_zeros_still_parses_exactly() {
657 let mut input = "0".repeat(100_000);
658 input.push_str("1ms");
659 assert_eq!(ms(&input), 1);
660 }
661
662 #[cfg(feature = "parser")]
663 #[test]
664 fn extremely_long_garbage_is_rejected_without_hanging() {
665 let input = "x".repeat(100_000);
666 assert!(parse_duration(&input).is_err());
667
668 let padded = format!("{}200ms{}", " ".repeat(50_000), " ".repeat(50_000));
670 assert_eq!(ms(&padded), 200);
671 }
672
673 #[cfg(feature = "parser")]
674 #[test]
675 fn deeply_nested_brackets_do_not_stack_overflow() {
676 let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
678 assert!(parse_duration(&nested).is_err());
679
680 let nested_with_unit = format!("{nested}s");
681 assert!(parse_duration(&nested_with_unit).is_err());
682 }
683
684 #[cfg(feature = "parser")]
687 #[test]
688 fn non_ascii_input_is_rejected_without_panicking() {
689 for input in [
690 "\u{1F600}", "\u{1F600}ms", "1\u{FF53}", "1s\u{0301}", "200ms", "\u{202E}200ms", "1\u{00A0}s", ] {
698 assert!(
699 parse_duration(input).is_err(),
700 "expected {input:?} to be rejected"
701 );
702 }
703 }
704
705 #[cfg(feature = "parser")]
707 #[test]
708 fn unicode_whitespace_around_a_valid_value_is_trimmed() {
709 assert_eq!(ms("\u{00A0}200ms\u{00A0}"), 200); assert_eq!(ms("\u{3000}1.5s\u{3000}"), 1500); }
712
713 #[cfg(feature = "parser")]
717 #[test]
718 fn lowercasing_that_changes_the_byte_length_does_not_panic() {
719 assert!(parse_duration("\u{0130}ms").is_err()); assert!(parse_duration("1\u{0130}s").is_err());
721 }
722
723 #[cfg(feature = "parser")]
726 #[test]
727 fn print_as_css_value_round_trips_through_parse_duration() {
728 for inner in [
729 0,
730 1,
731 2,
732 17,
733 999,
734 1000,
735 65_535,
736 1_000_000,
737 TWO_POW_24, 4_294_967_040, u32::MAX, ] {
741 let duration = CssDuration::from_millis(inner);
742 let printed = duration.print_as_css_value();
743 assert_eq!(
744 parse_duration(&printed),
745 Ok(duration),
746 "round-trip failed for {inner}ms (printed as {printed:?})"
747 );
748 }
749 }
750
751 #[cfg(feature = "parser")]
755 #[test]
756 fn print_as_css_value_round_trips_tick_durations_as_ticks() {
757 for inner in [0, 1, 5, 60, 999, TWO_POW_24, u32::MAX] {
758 let duration = CssDuration::from_ticks(inner);
759 let printed = duration.print_as_css_value();
760 assert_eq!(printed, format!("{inner}t"));
761 assert_eq!(
762 parse_duration(&printed),
763 Ok(duration),
764 "round-trip failed for {inner}t (printed as {printed:?})"
765 );
766 }
767 }
768
769 #[test]
770 fn print_as_css_value_always_emits_the_unit_it_was_built_with() {
771 for inner in [0, 1, u32::MAX] {
772 let printed = CssDuration::from_millis(inner).print_as_css_value();
773 assert!(printed.ends_with("ms"), "{printed:?} lacks a unit");
774 assert_eq!(printed, format!("{inner}ms"));
775
776 let printed = CssDuration::from_ticks(inner).print_as_css_value();
777 assert!(printed.ends_with('t'), "{printed:?} lacks a unit");
778 assert!(!printed.ends_with("ms"), "{printed:?} lost the tick unit");
779 assert_eq!(printed, format!("{inner}t"));
780 }
781 }
782
783 #[cfg(feature = "parser")]
787 #[test]
788 fn round_trip_above_two_pow_24_is_lossy_but_bounded() {
789 let duration = CssDuration::from_millis(TWO_POW_24 + 1);
790 let reparsed = parse_duration(&duration.print_as_css_value()).unwrap();
791 assert_ne!(reparsed.inner, duration.inner);
792 assert_eq!(reparsed.inner, TWO_POW_24);
793 assert!(reparsed.inner.abs_diff(duration.inner) <= 1);
794 }
795
796 #[cfg(feature = "parser")]
797 #[test]
798 fn seconds_and_milliseconds_agree_for_the_same_duration() {
799 assert_eq!(ms("2s"), ms("2000ms"));
800 assert_eq!(ms("0.001s"), ms("1ms"));
801 assert_eq!(ms("0s"), ms("0ms"));
802 }
803
804 #[test]
810 fn default_duration_is_zero_milliseconds() {
811 assert_eq!(CssDuration::default(), CssDuration::from_millis(0));
812 assert_eq!(CssDuration::default().inner, 0);
813 assert_eq!(CssDuration::default().unit, CssDurationUnit::Milliseconds);
814 assert_eq!(CssDurationUnit::default(), CssDurationUnit::Milliseconds);
815 }
816
817 #[test]
818 fn ordering_and_equality_follow_the_inner_count_within_one_unit() {
819 let a = CssDuration::from_millis(1);
820 let b = CssDuration::from_millis(2);
821 let max = CssDuration::from_millis(u32::MAX);
822 assert!(a < b);
823 assert!(b < max);
824 assert_eq!(a, CssDuration::from_millis(1));
825 assert_eq!(a.max(b), b);
826 assert_eq!(CssDuration::default(), CssDuration::from_millis(0));
827
828 assert_ne!(CssDuration::from_millis(5), CssDuration::from_ticks(5));
832 }
833
834 #[test]
835 fn format_as_rust_code_emits_a_constructor_and_ignores_indentation() {
836 let d = CssDuration::from_millis(42);
837 assert_eq!(
838 d.format_as_rust_code(0),
839 "CssDuration { inner: 42, unit: CssDurationUnit::Milliseconds }"
840 );
841 assert_eq!(d.format_as_rust_code(7), d.format_as_rust_code(0));
842 assert_eq!(
843 CssDuration::from_millis(u32::MAX).format_as_rust_code(0),
844 "CssDuration { inner: 4294967295, unit: CssDurationUnit::Milliseconds }"
845 );
846 assert_eq!(
847 CssDuration::from_ticks(5).format_as_rust_code(0),
848 "CssDuration { inner: 5, unit: CssDurationUnit::Ticks }"
849 );
850 assert_eq!(
851 CssDurationUnit::Ticks.format_as_rust_code(0),
852 "CssDurationUnit::Ticks"
853 );
854 }
855
856 #[cfg(feature = "parser")]
859 fn parse_float_error() -> core::num::ParseFloatError {
860 "not-a-float".parse::<f32>().unwrap_err()
861 }
862
863 #[cfg(feature = "parser")]
864 #[test]
865 fn to_contained_preserves_an_invalid_value_payload() {
866 let owned = DurationParseError::InvalidValue("10px").to_contained();
867 match owned {
868 DurationParseErrorOwned::InvalidValue(s) => assert_eq!(s.as_str(), "10px"),
869 DurationParseErrorOwned::ParseFloat(_) => panic!("variant changed"),
870 }
871 }
872
873 #[cfg(feature = "parser")]
874 #[test]
875 fn to_contained_stringifies_the_float_error() {
876 let owned = DurationParseError::ParseFloat(parse_float_error()).to_contained();
877 match owned {
878 DurationParseErrorOwned::ParseFloat(s) => {
879 assert!(!s.as_str().is_empty(), "float error message was empty");
880 assert_eq!(s.as_str(), parse_float_error().to_string());
881 }
882 DurationParseErrorOwned::InvalidValue(_) => panic!("variant changed"),
883 }
884 }
885
886 #[cfg(feature = "parser")]
887 #[test]
888 fn to_contained_handles_empty_and_extreme_payloads() {
889 assert_eq!(
890 DurationParseError::InvalidValue("").to_contained(),
891 DurationParseErrorOwned::InvalidValue(String::new().into())
892 );
893
894 let huge = "x".repeat(100_000);
895 let owned = DurationParseError::InvalidValue(&huge).to_contained();
896 match owned {
897 DurationParseErrorOwned::InvalidValue(s) => assert_eq!(s.as_str().len(), 100_000),
898 DurationParseErrorOwned::ParseFloat(_) => panic!("variant changed"),
899 }
900
901 let unicode = "\u{1F600}\u{0301}";
903 assert_eq!(
904 DurationParseError::InvalidValue(unicode).to_contained(),
905 DurationParseErrorOwned::InvalidValue(unicode.to_string().into())
906 );
907 }
908
909 #[cfg(feature = "parser")]
910 #[test]
911 fn to_shared_preserves_an_invalid_value_payload() {
912 let owned = DurationParseErrorOwned::InvalidValue("garbage".to_string().into());
913 assert_eq!(owned.to_shared(), DurationParseError::InvalidValue("garbage"));
914 }
915
916 #[cfg(feature = "parser")]
922 #[test]
923 fn to_shared_downgrades_parse_float_to_invalid_value() {
924 let msg = parse_float_error().to_string();
925 let owned = DurationParseErrorOwned::ParseFloat(msg.clone().into());
926 let shared = owned.to_shared();
927
928 assert!(!matches!(shared, DurationParseError::ParseFloat(_)));
929 assert_eq!(shared, DurationParseError::InvalidValue(msg.as_str()));
930 }
931
932 #[cfg(feature = "parser")]
933 #[test]
934 fn to_shared_does_not_panic_on_empty_or_extreme_payloads() {
935 assert_eq!(
936 DurationParseErrorOwned::InvalidValue(String::new().into()).to_shared(),
937 DurationParseError::InvalidValue("")
938 );
939
940 let huge = "y".repeat(100_000);
941 let owned = DurationParseErrorOwned::InvalidValue(huge.clone().into());
942 assert_eq!(owned.to_shared(), DurationParseError::InvalidValue(&huge));
943
944 let empty_float = DurationParseErrorOwned::ParseFloat(String::new().into());
945 assert_eq!(empty_float.to_shared(), DurationParseError::InvalidValue(""));
946 }
947
948 #[cfg(feature = "parser")]
951 #[test]
952 fn invalid_value_survives_a_full_shared_owned_shared_round_trip() {
953 let input = "10px";
954 let err = parse_duration(input).unwrap_err();
955 assert_eq!(err, DurationParseError::InvalidValue(input));
956
957 let owned = err.to_contained();
958 assert_eq!(owned.to_shared(), DurationParseError::InvalidValue(input));
959 }
960
961 #[cfg(feature = "parser")]
965 #[test]
966 fn a_word_ending_in_s_is_treated_as_a_seconds_value() {
967 assert!(matches!(
968 parse_duration("200 nanoseconds"),
969 Err(DurationParseError::ParseFloat(_))
970 ));
971 assert!(matches!(
972 parse_duration("always"),
973 Err(DurationParseError::ParseFloat(_))
974 ));
975 assert_eq!(
977 parse_duration("200 nanosecond"),
978 Err(DurationParseError::InvalidValue("200 nanosecond"))
979 );
980 }
981
982 #[cfg(feature = "parser")]
983 #[test]
984 fn error_display_never_panics_and_mentions_the_offender() {
985 let invalid = DurationParseError::InvalidValue("\u{1F600}");
986 let printed = format!("{invalid}");
987 assert!(printed.contains('\u{1F600}'), "{printed:?}");
988
989 let float = DurationParseError::ParseFloat(parse_float_error());
990 assert!(!format!("{float}").is_empty());
991
992 assert!(!format!("{invalid:?}").is_empty());
994 assert!(!format!("{float:?}").is_empty());
995 }
996}