1use std::time::Duration;
7
8use serde::{Deserialize, Deserializer, Serializer};
9
10pub fn parse_duration(input: &str) -> Result<Duration, humantime::DurationError> {
23 humantime::parse_duration(input)
24}
25
26pub fn deserialize_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error>
28where
29 D: Deserializer<'de>,
30{
31 #[derive(Deserialize)]
32 #[serde(untagged)]
33 enum DurationHelper {
34 String(String),
35 U64(u64),
36 }
37
38 match DurationHelper::deserialize(deserializer)? {
39 DurationHelper::String(s) => parse_duration(&s)
40 .map_err(|e| serde::de::Error::custom(format!("invalid duration: {e}"))),
41 DurationHelper::U64(ms) => Ok(Duration::from_millis(ms)),
42 }
43}
44
45pub fn serialize_duration<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
47where
48 S: Serializer,
49{
50 serializer.serialize_u64(duration.as_millis() as u64)
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use proptest::prelude::*;
57
58 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
59 struct DurationHolder {
60 #[serde(
61 deserialize_with = "deserialize_duration",
62 serialize_with = "serialize_duration"
63 )]
64 value: Duration,
65 }
66
67 #[test]
68 fn parse_duration_accepts_human_readable_values() {
69 assert_eq!(
70 parse_duration("250ms").expect("parse"),
71 Duration::from_millis(250)
72 );
73 assert_eq!(parse_duration("2s").expect("parse"), Duration::from_secs(2));
74 }
75
76 #[test]
77 fn deserialize_accepts_number_and_string() {
78 let from_num: DurationHolder = serde_json::from_str(r#"{"value":1500}"#).expect("json");
79 assert_eq!(from_num.value, Duration::from_millis(1500));
80
81 let from_str: DurationHolder = serde_json::from_str(r#"{"value":"1500ms"}"#).expect("json");
82 assert_eq!(from_str.value, Duration::from_millis(1500));
83 }
84
85 #[test]
86 fn serialize_writes_milliseconds() {
87 let value = DurationHolder {
88 value: Duration::from_millis(4321),
89 };
90 let json = serde_json::to_value(&value).expect("json");
91 assert_eq!(json["value"], 4321);
92 }
93
94 #[test]
95 fn deserialize_rejects_invalid_duration_string() {
96 let err = serde_json::from_str::<DurationHolder>(r#"{"value":"not-a-duration"}"#)
97 .expect_err("must fail");
98 assert!(err.to_string().contains("invalid duration"));
99 }
100
101 proptest! {
102 #[test]
103 fn duration_roundtrips_as_milliseconds(ms in 0_u64..10_000_000_000) {
104 let holder = DurationHolder {
105 value: Duration::from_millis(ms),
106 };
107
108 let json = serde_json::to_string(&holder).expect("serialize");
109 let reparsed: DurationHolder = serde_json::from_str(&json).expect("deserialize");
110
111 prop_assert_eq!(reparsed, holder);
112 }
113 }
114
115 #[test]
116 fn serde_json_full_roundtrip() {
117 let holder = DurationHolder {
118 value: Duration::from_secs(3661),
119 };
120 let json = serde_json::to_string(&holder).unwrap();
121 let reparsed: DurationHolder = serde_json::from_str(&json).unwrap();
122 assert_eq!(reparsed, holder);
123 }
124
125 #[test]
126 fn serde_toml_string_deserialization() {
127 let toml_str = r#"value = "2m 30s""#;
128 let holder: DurationHolder = toml::from_str(toml_str).unwrap();
129 assert_eq!(holder.value, Duration::from_secs(150));
130 }
131
132 #[test]
133 fn deserialize_rejects_boolean() {
134 let err =
135 serde_json::from_str::<DurationHolder>(r#"{"value":true}"#).expect_err("must fail");
136 assert!(!err.to_string().is_empty());
137 }
138
139 #[test]
140 fn deserialize_rejects_float() {
141 let err =
142 serde_json::from_str::<DurationHolder>(r#"{"value":1.5}"#).expect_err("must fail");
143 assert!(!err.to_string().is_empty());
144 }
145}
146
147#[cfg(test)]
148mod proptests {
149 use super::*;
150 use proptest::prelude::*;
151
152 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
153 struct DurationHolder {
154 #[serde(
155 deserialize_with = "deserialize_duration",
156 serialize_with = "serialize_duration"
157 )]
158 value: Duration,
159 }
160
161 proptest! {
162 #[test]
164 fn format_is_never_empty(ms in 0u64..10_000_000_000u64) {
165 let d = Duration::from_millis(ms);
166 let formatted = humantime::format_duration(d).to_string();
167 prop_assert!(!formatted.is_empty(), "formatted duration was empty for {ms}ms");
168 }
169
170 #[test]
172 fn format_consistency(ms in 0u64..10_000_000_000u64) {
173 let d = Duration::from_millis(ms);
174 let first = humantime::format_duration(d).to_string();
175 let second = humantime::format_duration(d).to_string();
176 prop_assert_eq!(first, second);
177 }
178
179 #[test]
181 fn parse_format_roundtrip(ms in 0u64..10_000_000u64) {
182 let d = Duration::from_millis(ms);
183 let formatted = humantime::format_duration(d).to_string();
184 let parsed = parse_duration(&formatted).expect("should parse formatted duration");
185 prop_assert_eq!(parsed, d);
186 }
187
188 #[test]
190 fn millisecond_range_contains_ms(ms in 1u64..1000u64) {
191 let d = Duration::from_millis(ms);
192 let formatted = humantime::format_duration(d).to_string();
193 prop_assert!(formatted.contains("ms"), "expected 'ms' in \"{formatted}\"");
194 }
195
196 #[test]
198 fn seconds_range_contains_s(secs in 1u64..60u64) {
199 let d = Duration::from_secs(secs);
200 let formatted = humantime::format_duration(d).to_string();
201 prop_assert!(formatted.contains('s'), "expected 's' in \"{formatted}\"");
202 }
203
204 #[test]
206 fn minutes_range_contains_m(mins in 1u64..60u64) {
207 let d = Duration::from_secs(mins * 60);
208 let formatted = humantime::format_duration(d).to_string();
209 prop_assert!(formatted.contains('m'), "expected 'm' in \"{formatted}\"");
210 }
211
212 #[test]
214 fn hours_range_contains_h(hours in 1u64..24u64) {
215 let d = Duration::from_secs(hours * 3600);
216 let formatted = humantime::format_duration(d).to_string();
217 prop_assert!(formatted.contains('h'), "expected 'h' in \"{formatted}\"");
218 }
219
220 #[test]
222 fn serde_json_u64_roundtrip(ms in 0u64..10_000_000_000u64) {
223 let json = format!(r#"{{"value":{ms}}}"#);
224 let holder: DurationHolder = serde_json::from_str(&json).expect("deserialize");
225 prop_assert_eq!(holder.value, Duration::from_millis(ms));
226 }
227
228 #[test]
230 fn serde_toml_string_roundtrip(ms in 1u64..10_000_000u64) {
231 let d = Duration::from_millis(ms);
232 let formatted = humantime::format_duration(d).to_string();
233 let toml_str = format!("value = \"{formatted}\"");
234 let holder: DurationHolder = toml::from_str(&toml_str).expect("toml deserialize");
235 prop_assert_eq!(holder.value, d);
236 }
237
238 #[test]
240 fn arbitrary_strings_never_panic(s in "\\PC{0,64}") {
241 let _ = parse_duration(&s);
242 }
243
244 #[test]
246 fn combined_duration_format_roundtrip(
247 a_ms in 0u64..1_000_000u64,
248 b_ms in 0u64..1_000_000u64,
249 ) {
250 let combined = Duration::from_millis(a_ms) + Duration::from_millis(b_ms);
251 let formatted = humantime::format_duration(combined).to_string();
252 let parsed = parse_duration(&formatted).expect("should parse formatted combined duration");
253 prop_assert_eq!(parsed, combined);
254 }
255 }
256}
257
258#[cfg(test)]
259mod edge_case_tests {
260 use super::*;
261
262 #[test]
265 fn parse_zero_ms() {
266 assert_eq!(parse_duration("0ms").unwrap(), Duration::ZERO);
267 }
268
269 #[test]
270 fn parse_zero_seconds() {
271 assert_eq!(parse_duration("0s").unwrap(), Duration::ZERO);
272 }
273
274 #[test]
275 fn deserialize_zero_from_integer() {
276 #[derive(serde::Deserialize)]
277 struct H {
278 #[serde(deserialize_with = "deserialize_duration")]
279 v: Duration,
280 }
281 let h: H = serde_json::from_str(r#"{"v":0}"#).unwrap();
282 assert_eq!(h.v, Duration::ZERO);
283 }
284
285 #[test]
286 fn serialize_zero_is_zero() {
287 #[derive(serde::Serialize)]
288 struct H {
289 #[serde(serialize_with = "serialize_duration")]
290 v: Duration,
291 }
292 let json = serde_json::to_value(H { v: Duration::ZERO }).unwrap();
293 assert_eq!(json["v"], 0);
294 }
295
296 #[test]
299 fn parse_large_hours() {
300 let d = parse_duration("9999h").unwrap();
301 assert_eq!(d, Duration::from_secs(9999 * 3600));
302 }
303
304 #[test]
305 fn serialize_large_millis() {
306 #[derive(serde::Serialize)]
307 struct H {
308 #[serde(serialize_with = "serialize_duration")]
309 v: Duration,
310 }
311 let large = Duration::from_secs(365 * 24 * 3600); let json = serde_json::to_value(H { v: large }).unwrap();
313 assert_eq!(json["v"], 365 * 24 * 3600 * 1000_u64);
314 }
315
316 #[test]
319 fn parse_microseconds() {
320 let d = parse_duration("500us").unwrap();
321 assert_eq!(d, Duration::from_micros(500));
322 }
323
324 #[test]
325 fn parse_nanoseconds() {
326 let d = parse_duration("100ns").unwrap();
327 assert_eq!(d, Duration::from_nanos(100));
328 }
329
330 #[test]
331 fn serialize_truncates_sub_millis_to_zero() {
332 #[derive(serde::Serialize)]
334 struct H {
335 #[serde(serialize_with = "serialize_duration")]
336 v: Duration,
337 }
338 let d = Duration::from_micros(999);
339 let json = serde_json::to_value(H { v: d }).unwrap();
340 assert_eq!(json["v"], 0);
341 }
342
343 #[test]
346 fn parse_empty_string_is_error() {
347 assert!(parse_duration("").is_err());
348 }
349
350 #[test]
351 fn parse_whitespace_only_is_error() {
352 assert!(parse_duration(" ").is_err());
353 }
354
355 #[test]
356 fn parse_combined_units() {
357 let d = parse_duration("1h 30m 15s").unwrap();
358 assert_eq!(d, Duration::from_secs(3600 + 30 * 60 + 15));
359 }
360
361 #[test]
362 fn parse_day_unit() {
363 let d = parse_duration("2days").unwrap();
364 assert_eq!(d, Duration::from_secs(2 * 86400));
365 }
366
367 #[test]
370 fn parsed_durations_maintain_ordering() {
371 let a = parse_duration("500ms").unwrap();
372 let b = parse_duration("1s").unwrap();
373 let c = parse_duration("1m").unwrap();
374 let d = parse_duration("1h").unwrap();
375 assert!(a < b);
376 assert!(b < c);
377 assert!(c < d);
378 }
379
380 #[test]
383 fn parsed_durations_support_addition() {
384 let a = parse_duration("30s").unwrap();
385 let b = parse_duration("30s").unwrap();
386 assert_eq!(a + b, Duration::from_secs(60));
387 }
388
389 #[test]
390 fn parsed_durations_support_subtraction() {
391 let a = parse_duration("2m").unwrap();
392 let b = parse_duration("30s").unwrap();
393 assert_eq!(a - b, Duration::from_secs(90));
394 }
395
396 #[test]
397 fn parsed_duration_supports_multiplication() {
398 let a = parse_duration("500ms").unwrap();
399 assert_eq!(a * 4, Duration::from_secs(2));
400 }
401
402 #[test]
405 fn parse_combined_no_spaces() {
406 assert_eq!(
407 parse_duration("1h30m").unwrap(),
408 Duration::from_secs(3600 + 30 * 60)
409 );
410 assert_eq!(
411 parse_duration("2m30s").unwrap(),
412 Duration::from_secs(2 * 60 + 30)
413 );
414 }
415
416 #[test]
419 fn parse_zero_forms_are_equivalent() {
420 let zero = Duration::ZERO;
421 assert_eq!(parse_duration("0s").unwrap(), zero);
422 assert_eq!(parse_duration("0ms").unwrap(), zero);
423 assert_eq!(parse_duration("0ns").unwrap(), zero);
424 assert_eq!(parse_duration("0us").unwrap(), zero);
425 }
426
427 #[test]
430 fn parse_number_without_unit_is_error() {
431 assert!(parse_duration("42").is_err());
432 }
433
434 #[test]
435 fn parse_unknown_unit_is_error() {
436 assert!(parse_duration("5xyz").is_err());
437 }
438
439 #[test]
440 fn parse_negative_is_error() {
441 assert!(parse_duration("-5s").is_err());
442 }
443
444 #[test]
445 fn parse_overflow_is_error() {
446 assert!(parse_duration("99999999999999999999999999999s").is_err());
447 }
448
449 #[test]
452 fn format_then_parse_roundtrip_deterministic() {
453 let cases = [
454 Duration::ZERO,
455 Duration::from_millis(250),
456 Duration::from_secs(42),
457 Duration::from_secs(3661),
458 Duration::from_secs(90061),
459 ];
460 for d in cases {
461 let formatted = humantime::format_duration(d).to_string();
462 let parsed = parse_duration(&formatted).unwrap();
463 assert_eq!(parsed, d, "roundtrip failed for {formatted}");
464 }
465 }
466
467 #[test]
470 fn parse_days_hours_minutes_combined() {
471 let d = parse_duration("1day 2h 30m").unwrap();
472 assert_eq!(d, Duration::from_secs(86400 + 2 * 3600 + 30 * 60));
473 }
474}
475
476#[cfg(test)]
477mod coverage_gaps {
478 use super::*;
479
480 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
481 struct Holder {
482 #[serde(
483 deserialize_with = "deserialize_duration",
484 serialize_with = "serialize_duration"
485 )]
486 value: Duration,
487 }
488
489 #[test]
492 fn deserialize_json_null_is_error() {
493 let err = serde_json::from_str::<Holder>(r#"{"value":null}"#).expect_err("null must fail");
494 assert!(!err.to_string().is_empty());
495 }
496
497 #[test]
498 fn deserialize_json_array_is_error() {
499 let err = serde_json::from_str::<Holder>(r#"{"value":[]}"#).expect_err("array must fail");
500 assert!(!err.to_string().is_empty());
501 }
502
503 #[test]
504 fn deserialize_json_true_is_error() {
505 let err = serde_json::from_str::<Holder>(r#"{"value":true}"#).expect_err("true must fail");
506 assert!(!err.to_string().is_empty());
507 }
508
509 #[test]
510 fn deserialize_json_false_is_error() {
511 let err =
512 serde_json::from_str::<Holder>(r#"{"value":false}"#).expect_err("false must fail");
513 assert!(!err.to_string().is_empty());
514 }
515
516 #[test]
517 fn deserialize_json_float_is_error() {
518 let err = serde_json::from_str::<Holder>(r#"{"value":1.5}"#).expect_err("float must fail");
519 assert!(!err.to_string().is_empty());
520 }
521
522 #[test]
523 fn deserialize_json_negative_integer_is_error() {
524 let err =
525 serde_json::from_str::<Holder>(r#"{"value":-1}"#).expect_err("negative must fail");
526 assert!(!err.to_string().is_empty());
527 }
528
529 #[test]
532 fn deserialize_toml_bool_is_error() {
533 let err = toml::from_str::<Holder>(r#"value = true"#).expect_err("bool must fail");
534 assert!(!err.to_string().is_empty());
535 }
536
537 #[test]
538 fn deserialize_toml_float_is_error() {
539 let err = toml::from_str::<Holder>(r#"value = 1.5"#).expect_err("float must fail");
540 assert!(!err.to_string().is_empty());
541 }
542
543 #[test]
544 fn deserialize_toml_array_is_error() {
545 let err = toml::from_str::<Holder>(r#"value = []"#).expect_err("array must fail");
546 assert!(!err.to_string().is_empty());
547 }
548
549 #[test]
550 fn deserialize_toml_negative_integer_is_error() {
551 let err = toml::from_str::<Holder>(r#"value = -1"#).expect_err("negative must fail");
552 assert!(!err.to_string().is_empty());
553 }
554
555 #[test]
558 fn serialize_500us_truncates_to_zero() {
559 let h = Holder {
560 value: Duration::from_nanos(500_000),
561 };
562 let json = serde_json::to_value(&h).unwrap();
563 assert_eq!(json["value"], 0);
564 }
565
566 #[test]
567 fn serialize_1us_truncates_to_zero() {
568 let h = Holder {
569 value: Duration::from_micros(1),
570 };
571 let json = serde_json::to_value(&h).unwrap();
572 assert_eq!(json["value"], 0);
573 }
574
575 #[test]
576 fn serialize_1500us_truncates_to_1ms() {
577 let h = Holder {
579 value: Duration::from_micros(1500),
580 };
581 let json = serde_json::to_value(&h).unwrap();
582 assert_eq!(json["value"], 1);
583 }
584
585 #[test]
588 fn serialize_u32_max_seconds_does_not_panic() {
589 let h = Holder {
590 value: Duration::from_secs(u32::MAX as u64),
591 };
592 let json = serde_json::to_value(&h).expect("serialize");
593 assert_eq!(json["value"], (u32::MAX as u64) * 1000);
594 }
595
596 #[test]
597 fn serialize_duration_max_does_not_panic() {
598 let h = Holder {
601 value: Duration::MAX,
602 };
603 let _ = serde_json::to_value(&h).expect("serialize");
604 }
605
606 #[test]
609 fn parse_leading_and_trailing_whitespace_is_accepted() {
610 assert_eq!(parse_duration(" 2s ").unwrap(), Duration::from_secs(2));
611 }
612
613 #[test]
614 fn parse_trailing_newline_is_accepted() {
615 assert_eq!(parse_duration("2s\n").unwrap(), Duration::from_secs(2));
616 }
617
618 #[test]
619 fn parse_internal_space_between_digit_and_unit_is_accepted() {
620 assert_eq!(parse_duration("2 s").unwrap(), Duration::from_secs(2));
622 }
623
624 #[test]
627 fn parse_empty_string_error_is_duration_error() {
628 let err: humantime::DurationError = parse_duration("").expect_err("empty must fail");
629 assert!(!err.to_string().is_empty());
630 }
631
632 #[test]
633 fn parse_garbage_string_error_is_duration_error() {
634 let err: humantime::DurationError =
635 parse_duration("not a duration").expect_err("garbage must fail");
636 assert!(!err.to_string().is_empty());
637 }
638
639 #[test]
640 fn parse_negative_string_error_is_duration_error() {
641 let err: humantime::DurationError = parse_duration("-1s").expect_err("negative must fail");
642 assert!(!err.to_string().is_empty());
643 }
644
645 #[test]
648 fn json_then_toml_roundtrip_yields_identical_duration() {
649 let original = Holder {
650 value: Duration::from_millis(3_750),
651 };
652
653 let json = serde_json::to_string(&original).expect("json ser");
654 let via_json: Holder = serde_json::from_str(&json).expect("json de");
655 assert_eq!(via_json, original);
656
657 let toml_str = toml::to_string(&original).expect("toml ser");
660 let via_toml: Holder = toml::from_str(&toml_str).expect("toml de");
661 assert_eq!(via_toml, original);
662
663 assert_eq!(via_json, via_toml);
664 }
665
666 #[test]
667 fn json_integer_and_toml_string_yield_same_duration() {
668 let json: Holder = serde_json::from_str(r#"{"value":150000}"#).expect("json");
669 let toml_val: Holder = toml::from_str(r#"value = "2m 30s""#).expect("toml");
670 assert_eq!(json, toml_val);
671 assert_eq!(json.value, Duration::from_secs(150));
672 }
673}
674
675#[cfg(test)]
676mod snapshot_tests {
677 use super::*;
678 use insta::assert_debug_snapshot;
679
680 #[test]
681 fn snapshot_parsed_zero() {
682 assert_debug_snapshot!(parse_duration("0s").unwrap());
683 }
684
685 #[test]
686 fn snapshot_parsed_millis() {
687 assert_debug_snapshot!(parse_duration("250ms").unwrap());
688 }
689
690 #[test]
691 fn snapshot_parsed_seconds() {
692 assert_debug_snapshot!(parse_duration("42s").unwrap());
693 }
694
695 #[test]
696 fn snapshot_parsed_minutes() {
697 assert_debug_snapshot!(parse_duration("5m").unwrap());
698 }
699
700 #[test]
701 fn snapshot_parsed_hours() {
702 assert_debug_snapshot!(parse_duration("2h").unwrap());
703 }
704
705 #[test]
706 fn snapshot_parsed_combined() {
707 assert_debug_snapshot!(parse_duration("1h 30m 15s 200ms").unwrap());
708 }
709
710 #[test]
711 fn snapshot_parse_error() {
712 assert_debug_snapshot!(parse_duration("not-valid"));
713 }
714
715 #[test]
716 fn snapshot_deserialized_from_integer() {
717 #[derive(Debug, serde::Deserialize)]
718 #[allow(dead_code)]
719 struct H {
720 #[serde(deserialize_with = "deserialize_duration")]
721 v: Duration,
722 }
723 let h: H = serde_json::from_str(r#"{"v":3661000}"#).unwrap();
724 assert_debug_snapshot!(h);
725 }
726
727 #[test]
728 fn snapshot_deserialized_from_string() {
729 #[derive(Debug, serde::Deserialize)]
730 #[allow(dead_code)]
731 struct H {
732 #[serde(deserialize_with = "deserialize_duration")]
733 v: Duration,
734 }
735 let h: H = serde_json::from_str(r#"{"v":"1h 1m 1s"}"#).unwrap();
736 assert_debug_snapshot!(h);
737 }
738
739 #[test]
740 fn snapshot_formatted_common_durations() {
741 let formatted: Vec<(&str, String)> = vec![
742 ("zero", Duration::ZERO),
743 ("half_second", Duration::from_millis(500)),
744 ("one_minute", Duration::from_secs(60)),
745 ("one_hour_one_min_one_sec", Duration::from_secs(3661)),
746 ("one_day", Duration::from_secs(86400)),
747 ]
748 .into_iter()
749 .map(|(name, d)| (name, humantime::format_duration(d).to_string()))
750 .collect();
751 assert_debug_snapshot!(formatted);
752 }
753
754 #[test]
755 fn snapshot_parse_errors_various() {
756 let results: Vec<(&str, String)> = vec!["", " ", "42", "-5s", "5xyz"]
757 .into_iter()
758 .map(|input| (input, parse_duration(input).unwrap_err().to_string()))
759 .collect();
760 assert_debug_snapshot!(results);
761 }
762}