1use crate::corety::AzString;
4use alloc::string::{String, ToString};
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]
190 pub fn to_contained(&self) -> DurationParseErrorOwned {
191 match self {
192 Self::InvalidValue(s) => DurationParseErrorOwned::InvalidValue((*s).to_string().into()),
193 Self::ParseFloat(e) => DurationParseErrorOwned::ParseFloat(e.to_string().into()),
194 }
195 }
196}
197
198#[cfg(feature = "parser")]
199impl DurationParseErrorOwned {
200 #[must_use]
201 pub fn to_shared(&self) -> DurationParseError<'_> {
202 match self {
203 Self::InvalidValue(s) => DurationParseError::InvalidValue(s),
204 Self::ParseFloat(s) => DurationParseError::InvalidValue(s.as_str()),
205 }
206 }
207}
208
209#[cfg(feature = "parser")]
224pub fn parse_duration(input: &str) -> Result<CssDuration, DurationParseError<'_>> {
228 let trimmed = input.trim().to_lowercase();
229 if trimmed == "0" {
230 return Ok(CssDuration::from_millis(0));
231 }
232 if let Some(num_str) = trimmed.strip_suffix("ms") {
236 let ms = num_str
237 .parse::<f32>()
238 .map_err(DurationParseError::ParseFloat)?;
239 if ms < 0.0 {
240 return Err(DurationParseError::InvalidValue(input));
241 }
242 Ok(CssDuration::from_millis(crate::cast::f32_to_u32(ms)))
243 } else if let Some(num_str) = trimmed.strip_suffix('s') {
244 let s = num_str
245 .parse::<f32>()
246 .map_err(DurationParseError::ParseFloat)?;
247 if s < 0.0 {
248 return Err(DurationParseError::InvalidValue(input));
249 }
250 Ok(CssDuration::from_millis(crate::cast::f32_to_u32(
251 s * 1000.0,
252 )))
253 } else if let Some(num_str) = trimmed.strip_suffix('t') {
254 let t = num_str
255 .parse::<f32>()
256 .map_err(DurationParseError::ParseFloat)?;
257 if t < 0.0 {
258 return Err(DurationParseError::InvalidValue(input));
259 }
260 Ok(CssDuration::from_ticks(crate::cast::f32_to_u32(t)))
261 } else {
262 Err(DurationParseError::InvalidValue(input))
263 }
264}
265
266#[cfg(test)]
267#[allow(clippy::unreadable_literal)]
268mod autotest_generated {
269 use super::*;
270 use crate::codegen::format::FormatAsRustCode;
271 use crate::props::formatter::PrintAsCssValue;
272
273 #[cfg(feature = "parser")]
277 const TWO_POW_24: u32 = 16_777_216;
278
279 #[cfg(feature = "parser")]
282 fn ms(input: &str) -> u32 {
283 let d = parse_duration(input)
284 .unwrap_or_else(|e| panic!("expected {input:?} to parse, got {e}"));
285 assert_eq!(
286 d.unit,
287 CssDurationUnit::Milliseconds,
288 "{input:?} parsed as {:?}, not milliseconds",
289 d.unit
290 );
291 d.inner
292 }
293
294 #[cfg(feature = "parser")]
297 fn ticks(input: &str) -> u32 {
298 let d = parse_duration(input)
299 .unwrap_or_else(|e| panic!("expected {input:?} to parse, got {e}"));
300 assert_eq!(
301 d.unit,
302 CssDurationUnit::Ticks,
303 "{input:?} parsed as {:?}, not ticks",
304 d.unit
305 );
306 d.inner
307 }
308
309 #[cfg(feature = "parser")]
312 #[test]
313 fn valid_minimal_inputs_parse_to_expected_values() {
314 assert_eq!(ms("0"), 0);
315 assert_eq!(ms("0ms"), 0);
316 assert_eq!(ms("0s"), 0);
317 assert_eq!(ms("200ms"), 200);
318 assert_eq!(ms("1s"), 1000);
319 assert_eq!(ms("1.5s"), 1500);
320 assert_eq!(ms("0.5s"), 500);
321 assert_eq!(ms(".25s"), 250);
322 assert_eq!(ms("5e2ms"), 500);
323 assert_eq!(ms("+5ms"), 5);
324 }
325
326 #[cfg(feature = "parser")]
329 #[test]
330 fn ms_suffix_wins_over_s_suffix() {
331 assert_eq!(ms("5ms"), 5);
332 assert_ne!(ms("5ms"), ms("5s"));
333 assert_eq!(ms("5s"), 5000);
334 }
335
336 #[cfg(feature = "parser")]
337 #[test]
338 fn units_are_case_insensitive() {
339 assert_eq!(ms("200MS"), 200);
340 assert_eq!(ms("200Ms"), 200);
341 assert_eq!(ms("1S"), 1000);
342 assert_eq!(ms("1.5E1S"), 15000);
343 assert_eq!(ticks("5T"), 5);
344 }
345
346 #[cfg(feature = "parser")]
352 #[test]
353 fn the_t_unit_parses_to_a_tick_count_and_is_not_normalised_to_millis() {
354 assert_eq!(parse_duration("5t"), Ok(CssDuration::from_ticks(5)));
355 assert_eq!(ticks("0t"), 0);
356 assert_eq!(ticks("1t"), 1);
357 assert_eq!(ticks("60t"), 60);
358 assert_eq!(ticks("4294967295t"), u32::MAX);
359 assert_ne!(parse_duration("5t"), parse_duration("5ms"));
361 assert_ne!(parse_duration("60t"), parse_duration("1s"));
362 }
363
364 #[cfg(feature = "parser")]
367 #[test]
368 fn the_t_unit_does_not_collide_with_the_other_units_or_swallow_garbage() {
369 assert_eq!(ms("5ms"), 5);
370 assert_eq!(ms("5s"), 5000);
371 for garbage in ["5pt", "5t5", "t", "5tt", "5mst", "5st", "5 t", "-5t"] {
373 assert!(
374 parse_duration(garbage).is_err(),
375 "expected {garbage:?} to be rejected"
376 );
377 }
378 }
379
380 #[test]
384 fn millis_and_ticks_convert_at_the_nominal_frame_rate() {
385 assert_eq!(TICKS_PER_SECOND, 60);
386
387 assert_eq!(CssDuration::from_ticks(60).millis(), 1000);
388 assert_eq!(CssDuration::from_ticks(30).millis(), 500);
389 assert_eq!(CssDuration::from_ticks(1).millis(), 16);
390 assert_eq!(CssDuration::from_ticks(0).millis(), 0);
391
392 assert_eq!(CssDuration::from_millis(1000).ticks(), 60);
393 assert_eq!(CssDuration::from_millis(500).ticks(), 30);
394 assert_eq!(
395 CssDuration::from_millis(16).ticks(),
396 0,
397 "sub-frame is 0 frames"
398 );
399 assert_eq!(CssDuration::from_millis(17).ticks(), 1);
400 assert_eq!(CssDuration::from_millis(0).ticks(), 0);
401
402 assert_eq!(CssDuration::from_millis(7).millis(), 7);
405 assert_eq!(CssDuration::from_ticks(7).ticks(), 7);
406 }
407
408 #[test]
411 fn tick_to_milli_conversion_saturates_instead_of_wrapping() {
412 assert_eq!(CssDuration::from_ticks(u32::MAX).millis(), u32::MAX);
413 let last_exact = (u64::from(u32::MAX) * TICKS_PER_SECOND / 1000) as u32;
415 assert!(CssDuration::from_ticks(last_exact).millis() < u32::MAX);
416 assert_eq!(
418 CssDuration::from_millis(u32::MAX).ticks(),
419 (u64::from(u32::MAX) * TICKS_PER_SECOND / 1000) as u32
420 );
421 }
422
423 #[cfg(feature = "parser")]
427 #[test]
428 fn sub_millisecond_values_truncate_toward_zero() {
429 assert_eq!(ms("5.9ms"), 5);
430 assert_eq!(ms("0.9ms"), 0);
431 assert_eq!(ms("0.0009s"), 0); assert_eq!(ms("0.0015s"), 1); }
434
435 #[cfg(feature = "parser")]
438 #[test]
439 fn empty_input_is_rejected_without_panicking() {
440 assert_eq!(
441 parse_duration(""),
442 Err(DurationParseError::InvalidValue(""))
443 );
444 }
445
446 #[cfg(feature = "parser")]
447 #[test]
448 fn whitespace_only_input_is_rejected_and_error_keeps_the_raw_input() {
449 assert_eq!(
452 parse_duration(" "),
453 Err(DurationParseError::InvalidValue(" "))
454 );
455 assert_eq!(
456 parse_duration("\t\n"),
457 Err(DurationParseError::InvalidValue("\t\n"))
458 );
459 }
460
461 #[cfg(feature = "parser")]
464 #[test]
465 fn a_bare_unit_with_no_number_is_a_parse_float_error_not_a_panic() {
466 assert!(matches!(
467 parse_duration("ms"),
468 Err(DurationParseError::ParseFloat(_))
469 ));
470 assert!(matches!(
471 parse_duration("s"),
472 Err(DurationParseError::ParseFloat(_))
473 ));
474 }
475
476 #[cfg(feature = "parser")]
477 #[test]
478 fn unitless_numbers_other_than_literal_zero_are_rejected() {
479 assert_eq!(ms("0"), 0);
481 assert_eq!(
482 parse_duration("200"),
483 Err(DurationParseError::InvalidValue("200"))
484 );
485 assert_eq!(
486 parse_duration("1.5"),
487 Err(DurationParseError::InvalidValue("1.5"))
488 );
489 assert_eq!(
490 parse_duration("0.0"),
491 Err(DurationParseError::InvalidValue("0.0"))
492 );
493 assert_eq!(
494 parse_duration("00"),
495 Err(DurationParseError::InvalidValue("00"))
496 );
497 assert_eq!(
498 parse_duration("-0"),
499 Err(DurationParseError::InvalidValue("-0"))
500 );
501 }
502
503 #[cfg(feature = "parser")]
504 #[test]
505 fn garbage_and_junk_never_panic() {
506 for garbage in [
507 "abc",
508 "!!!",
509 "\0\0\0",
510 "ms ms",
511 "1,5s",
512 "1 ms",
513 "--5ms",
514 "5mss",
515 "5sms",
516 "0x10ms",
517 "1e",
518 "1e+",
519 ".s",
520 "-.ms",
521 "s1",
522 "ms200",
523 "200ms;garbage",
524 "200ms !important",
525 ] {
526 assert!(
529 parse_duration(garbage).is_err(),
530 "expected {garbage:?} to be rejected"
531 );
532 }
533 }
534
535 #[cfg(feature = "parser")]
536 #[test]
537 fn leading_and_trailing_whitespace_is_trimmed_but_interior_space_is_not() {
538 assert_eq!(ms(" 200ms "), 200);
539 assert_eq!(ms("\t\n1.5s\r\n"), 1500);
540 assert!(matches!(
542 parse_duration("200 ms"),
543 Err(DurationParseError::ParseFloat(_))
544 ));
545 assert!(matches!(
546 parse_duration("2 0 0ms"),
547 Err(DurationParseError::ParseFloat(_))
548 ));
549 }
550
551 #[cfg(feature = "parser")]
552 #[test]
553 fn trailing_junk_after_a_valid_value_is_rejected_not_silently_accepted() {
554 assert!(parse_duration("200ms;").is_err());
555 assert!(parse_duration("200msx").is_err());
556 assert!(matches!(
558 parse_duration("200msms"),
559 Err(DurationParseError::ParseFloat(_))
560 ));
561 }
562
563 #[cfg(feature = "parser")]
566 #[test]
567 fn negative_durations_are_rejected_in_both_units() {
568 assert_eq!(
569 parse_duration("-1ms"),
570 Err(DurationParseError::InvalidValue("-1ms"))
571 );
572 assert_eq!(
573 parse_duration("-0.5s"),
574 Err(DurationParseError::InvalidValue("-0.5s"))
575 );
576 assert_eq!(
577 parse_duration("-1e-30s"),
578 Err(DurationParseError::InvalidValue("-1e-30s"))
579 );
580 }
581
582 #[cfg(feature = "parser")]
583 #[test]
584 fn the_invalid_value_error_reports_the_original_untrimmed_uncased_input() {
585 assert_eq!(
587 parse_duration(" -1MS "),
588 Err(DurationParseError::InvalidValue(" -1MS "))
589 );
590 }
591
592 #[cfg(feature = "parser")]
595 #[test]
596 fn negative_zero_is_accepted_and_clamps_to_zero() {
597 assert_eq!(ms("-0ms"), 0);
598 assert_eq!(ms("-0.0s"), 0);
599 assert_eq!(ms("-0e10ms"), 0);
600 }
601
602 #[cfg(feature = "parser")]
605 #[test]
606 fn values_beyond_u32_max_saturate_instead_of_wrapping_or_panicking() {
607 assert_eq!(ms("4294967296ms"), u32::MAX); assert_eq!(ms("99999999999ms"), u32::MAX);
609 assert_eq!(ms("1e30s"), u32::MAX);
610 assert_eq!(ms("5000000s"), u32::MAX); }
612
613 #[cfg(feature = "parser")]
616 #[test]
617 fn float_overflow_to_infinity_saturates_to_u32_max() {
618 assert_eq!(ms("1e39ms"), u32::MAX); assert_eq!(ms("1e999999ms"), u32::MAX);
620 assert_eq!(ms("infms"), u32::MAX);
621 assert_eq!(ms("infinityms"), u32::MAX);
622 assert_eq!(ms("INFms"), u32::MAX);
623 assert_eq!(ms("infs"), u32::MAX);
624 }
625
626 #[cfg(feature = "parser")]
627 #[test]
628 fn negative_infinity_is_rejected_as_a_negative_duration() {
629 assert_eq!(
630 parse_duration("-infms"),
631 Err(DurationParseError::InvalidValue("-infms"))
632 );
633 assert_eq!(
634 parse_duration("-infinitys"),
635 Err(DurationParseError::InvalidValue("-infinitys"))
636 );
637 }
638
639 #[cfg(feature = "parser")]
643 #[test]
644 fn nan_is_accepted_and_degrades_to_zero_rather_than_panicking() {
645 assert_eq!(ms("nanms"), 0);
646 assert_eq!(ms("NaNms"), 0);
647 assert_eq!(ms("-nanms"), 0);
648 assert_eq!(ms("nans"), 0); }
650
651 #[cfg(feature = "parser")]
652 #[test]
653 fn underflow_to_zero_is_not_an_error() {
654 assert_eq!(ms("1e-30ms"), 0);
655 assert_eq!(ms("1e-999999s"), 0);
656 }
657
658 #[cfg(feature = "parser")]
659 #[test]
660 fn u32_max_and_f32_max_boundary_strings_are_handled() {
661 assert_eq!(ms("4294967295ms"), u32::MAX); assert_eq!(ms("4294967040ms"), 4294967040); let f32_max = format!("{}ms", f32::MAX);
665 assert_eq!(ms(&f32_max), u32::MAX);
666
667 let i64_max = format!("{}ms", i64::MAX);
668 assert_eq!(ms(&i64_max), u32::MAX);
669 }
670
671 #[cfg(feature = "parser")]
674 #[test]
675 fn extremely_long_digit_string_saturates_without_hanging() {
676 let mut input = "9".repeat(100_000);
677 input.push_str("ms");
678 assert_eq!(ms(&input), u32::MAX);
679 }
680
681 #[cfg(feature = "parser")]
682 #[test]
683 fn extremely_long_run_of_leading_zeros_still_parses_exactly() {
684 let mut input = "0".repeat(100_000);
685 input.push_str("1ms");
686 assert_eq!(ms(&input), 1);
687 }
688
689 #[cfg(feature = "parser")]
690 #[test]
691 fn extremely_long_garbage_is_rejected_without_hanging() {
692 let input = "x".repeat(100_000);
693 assert!(parse_duration(&input).is_err());
694
695 let padded = format!("{}200ms{}", " ".repeat(50_000), " ".repeat(50_000));
697 assert_eq!(ms(&padded), 200);
698 }
699
700 #[cfg(feature = "parser")]
701 #[test]
702 fn deeply_nested_brackets_do_not_stack_overflow() {
703 let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
705 assert!(parse_duration(&nested).is_err());
706
707 let nested_with_unit = format!("{nested}s");
708 assert!(parse_duration(&nested_with_unit).is_err());
709 }
710
711 #[cfg(feature = "parser")]
714 #[test]
715 fn non_ascii_input_is_rejected_without_panicking() {
716 for input in [
717 "\u{1F600}", "\u{1F600}ms", "1\u{FF53}", "1s\u{0301}", "200ms", "\u{202E}200ms", "1\u{00A0}s", ] {
725 assert!(
726 parse_duration(input).is_err(),
727 "expected {input:?} to be rejected"
728 );
729 }
730 }
731
732 #[cfg(feature = "parser")]
734 #[test]
735 fn unicode_whitespace_around_a_valid_value_is_trimmed() {
736 assert_eq!(ms("\u{00A0}200ms\u{00A0}"), 200); assert_eq!(ms("\u{3000}1.5s\u{3000}"), 1500); }
739
740 #[cfg(feature = "parser")]
744 #[test]
745 fn lowercasing_that_changes_the_byte_length_does_not_panic() {
746 assert!(parse_duration("\u{0130}ms").is_err()); assert!(parse_duration("1\u{0130}s").is_err());
748 }
749
750 #[cfg(feature = "parser")]
753 #[test]
754 fn print_as_css_value_round_trips_through_parse_duration() {
755 for inner in [
756 0,
757 1,
758 2,
759 17,
760 999,
761 1000,
762 65_535,
763 1_000_000,
764 TWO_POW_24, 4_294_967_040, u32::MAX, ] {
768 let duration = CssDuration::from_millis(inner);
769 let printed = duration.print_as_css_value();
770 assert_eq!(
771 parse_duration(&printed),
772 Ok(duration),
773 "round-trip failed for {inner}ms (printed as {printed:?})"
774 );
775 }
776 }
777
778 #[cfg(feature = "parser")]
782 #[test]
783 fn print_as_css_value_round_trips_tick_durations_as_ticks() {
784 for inner in [0, 1, 5, 60, 999, TWO_POW_24, u32::MAX] {
785 let duration = CssDuration::from_ticks(inner);
786 let printed = duration.print_as_css_value();
787 assert_eq!(printed, format!("{inner}t"));
788 assert_eq!(
789 parse_duration(&printed),
790 Ok(duration),
791 "round-trip failed for {inner}t (printed as {printed:?})"
792 );
793 }
794 }
795
796 #[test]
797 fn print_as_css_value_always_emits_the_unit_it_was_built_with() {
798 for inner in [0, 1, u32::MAX] {
799 let printed = CssDuration::from_millis(inner).print_as_css_value();
800 assert!(printed.ends_with("ms"), "{printed:?} lacks a unit");
801 assert_eq!(printed, format!("{inner}ms"));
802
803 let printed = CssDuration::from_ticks(inner).print_as_css_value();
804 assert!(printed.ends_with('t'), "{printed:?} lacks a unit");
805 assert!(!printed.ends_with("ms"), "{printed:?} lost the tick unit");
806 assert_eq!(printed, format!("{inner}t"));
807 }
808 }
809
810 #[cfg(feature = "parser")]
814 #[test]
815 fn round_trip_above_two_pow_24_is_lossy_but_bounded() {
816 let duration = CssDuration::from_millis(TWO_POW_24 + 1);
817 let reparsed = parse_duration(&duration.print_as_css_value()).unwrap();
818 assert_ne!(reparsed.inner, duration.inner);
819 assert_eq!(reparsed.inner, TWO_POW_24);
820 assert!(reparsed.inner.abs_diff(duration.inner) <= 1);
821 }
822
823 #[cfg(feature = "parser")]
824 #[test]
825 fn seconds_and_milliseconds_agree_for_the_same_duration() {
826 assert_eq!(ms("2s"), ms("2000ms"));
827 assert_eq!(ms("0.001s"), ms("1ms"));
828 assert_eq!(ms("0s"), ms("0ms"));
829 }
830
831 #[test]
837 fn default_duration_is_zero_milliseconds() {
838 assert_eq!(CssDuration::default(), CssDuration::from_millis(0));
839 assert_eq!(CssDuration::default().inner, 0);
840 assert_eq!(CssDuration::default().unit, CssDurationUnit::Milliseconds);
841 assert_eq!(CssDurationUnit::default(), CssDurationUnit::Milliseconds);
842 }
843
844 #[test]
845 fn ordering_and_equality_follow_the_inner_count_within_one_unit() {
846 let a = CssDuration::from_millis(1);
847 let b = CssDuration::from_millis(2);
848 let max = CssDuration::from_millis(u32::MAX);
849 assert!(a < b);
850 assert!(b < max);
851 assert_eq!(a, CssDuration::from_millis(1));
852 assert_eq!(a.max(b), b);
853 assert_eq!(CssDuration::default(), CssDuration::from_millis(0));
854
855 assert_ne!(CssDuration::from_millis(5), CssDuration::from_ticks(5));
859 }
860
861 #[test]
862 fn format_as_rust_code_emits_a_constructor_and_ignores_indentation() {
863 let d = CssDuration::from_millis(42);
864 assert_eq!(
865 d.format_as_rust_code(0),
866 "CssDuration { inner: 42, unit: CssDurationUnit::Milliseconds }"
867 );
868 assert_eq!(d.format_as_rust_code(7), d.format_as_rust_code(0));
869 assert_eq!(
870 CssDuration::from_millis(u32::MAX).format_as_rust_code(0),
871 "CssDuration { inner: 4294967295, unit: CssDurationUnit::Milliseconds }"
872 );
873 assert_eq!(
874 CssDuration::from_ticks(5).format_as_rust_code(0),
875 "CssDuration { inner: 5, unit: CssDurationUnit::Ticks }"
876 );
877 assert_eq!(
878 CssDurationUnit::Ticks.format_as_rust_code(0),
879 "CssDurationUnit::Ticks"
880 );
881 }
882
883 #[cfg(feature = "parser")]
886 fn parse_float_error() -> core::num::ParseFloatError {
887 "not-a-float".parse::<f32>().unwrap_err()
888 }
889
890 #[cfg(feature = "parser")]
891 #[test]
892 fn to_contained_preserves_an_invalid_value_payload() {
893 let owned = DurationParseError::InvalidValue("10px").to_contained();
894 match owned {
895 DurationParseErrorOwned::InvalidValue(s) => assert_eq!(s.as_str(), "10px"),
896 DurationParseErrorOwned::ParseFloat(_) => panic!("variant changed"),
897 }
898 }
899
900 #[cfg(feature = "parser")]
901 #[test]
902 fn to_contained_stringifies_the_float_error() {
903 let owned = DurationParseError::ParseFloat(parse_float_error()).to_contained();
904 match owned {
905 DurationParseErrorOwned::ParseFloat(s) => {
906 assert!(!s.as_str().is_empty(), "float error message was empty");
907 assert_eq!(s.as_str(), parse_float_error().to_string());
908 }
909 DurationParseErrorOwned::InvalidValue(_) => panic!("variant changed"),
910 }
911 }
912
913 #[cfg(feature = "parser")]
914 #[test]
915 fn to_contained_handles_empty_and_extreme_payloads() {
916 assert_eq!(
917 DurationParseError::InvalidValue("").to_contained(),
918 DurationParseErrorOwned::InvalidValue(String::new().into())
919 );
920
921 let huge = "x".repeat(100_000);
922 let owned = DurationParseError::InvalidValue(&huge).to_contained();
923 match owned {
924 DurationParseErrorOwned::InvalidValue(s) => assert_eq!(s.as_str().len(), 100_000),
925 DurationParseErrorOwned::ParseFloat(_) => panic!("variant changed"),
926 }
927
928 let unicode = "\u{1F600}\u{0301}";
930 assert_eq!(
931 DurationParseError::InvalidValue(unicode).to_contained(),
932 DurationParseErrorOwned::InvalidValue(unicode.to_string().into())
933 );
934 }
935
936 #[cfg(feature = "parser")]
937 #[test]
938 fn to_shared_preserves_an_invalid_value_payload() {
939 let owned = DurationParseErrorOwned::InvalidValue("garbage".to_string().into());
940 assert_eq!(
941 owned.to_shared(),
942 DurationParseError::InvalidValue("garbage")
943 );
944 }
945
946 #[cfg(feature = "parser")]
952 #[test]
953 fn to_shared_downgrades_parse_float_to_invalid_value() {
954 let msg = parse_float_error().to_string();
955 let owned = DurationParseErrorOwned::ParseFloat(msg.clone().into());
956 let shared = owned.to_shared();
957
958 assert!(!matches!(shared, DurationParseError::ParseFloat(_)));
959 assert_eq!(shared, DurationParseError::InvalidValue(msg.as_str()));
960 }
961
962 #[cfg(feature = "parser")]
963 #[test]
964 fn to_shared_does_not_panic_on_empty_or_extreme_payloads() {
965 assert_eq!(
966 DurationParseErrorOwned::InvalidValue(String::new().into()).to_shared(),
967 DurationParseError::InvalidValue("")
968 );
969
970 let huge = "y".repeat(100_000);
971 let owned = DurationParseErrorOwned::InvalidValue(huge.clone().into());
972 assert_eq!(owned.to_shared(), DurationParseError::InvalidValue(&huge));
973
974 let empty_float = DurationParseErrorOwned::ParseFloat(String::new().into());
975 assert_eq!(
976 empty_float.to_shared(),
977 DurationParseError::InvalidValue("")
978 );
979 }
980
981 #[cfg(feature = "parser")]
984 #[test]
985 fn invalid_value_survives_a_full_shared_owned_shared_round_trip() {
986 let input = "10px";
987 let err = parse_duration(input).unwrap_err();
988 assert_eq!(err, DurationParseError::InvalidValue(input));
989
990 let owned = err.to_contained();
991 assert_eq!(owned.to_shared(), DurationParseError::InvalidValue(input));
992 }
993
994 #[cfg(feature = "parser")]
998 #[test]
999 fn a_word_ending_in_s_is_treated_as_a_seconds_value() {
1000 assert!(matches!(
1001 parse_duration("200 nanoseconds"),
1002 Err(DurationParseError::ParseFloat(_))
1003 ));
1004 assert!(matches!(
1005 parse_duration("always"),
1006 Err(DurationParseError::ParseFloat(_))
1007 ));
1008 assert_eq!(
1010 parse_duration("200 nanosecond"),
1011 Err(DurationParseError::InvalidValue("200 nanosecond"))
1012 );
1013 }
1014
1015 #[cfg(feature = "parser")]
1016 #[test]
1017 fn error_display_never_panics_and_mentions_the_offender() {
1018 let invalid = DurationParseError::InvalidValue("\u{1F600}");
1019 let printed = format!("{invalid}");
1020 assert!(printed.contains('\u{1F600}'), "{printed:?}");
1021
1022 let float = DurationParseError::ParseFloat(parse_float_error());
1023 assert!(!format!("{float}").is_empty());
1024
1025 assert!(!format!("{invalid:?}").is_empty());
1027 assert!(!format!("{float:?}").is_empty());
1028 }
1029}