Skip to main content

shipper_duration/
lib.rs

1//! Duration parsing and serde codecs for shipper.
2//!
3//! This crate centralizes duration handling so CLI parsing and config/state
4//! serde use one implementation.
5
6use std::time::Duration;
7
8use serde::{Deserialize, Deserializer, Serializer};
9
10/// Parse a human-readable duration string (for example `2s`, `500ms`, `1m`).
11///
12/// # Examples
13///
14/// ```
15/// use std::time::Duration;
16/// use shipper_duration::parse_duration;
17///
18/// assert_eq!(parse_duration("2s").unwrap(), Duration::from_secs(2));
19/// assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
20/// assert_eq!(parse_duration("1m").unwrap(), Duration::from_secs(60));
21/// ```
22pub fn parse_duration(input: &str) -> Result<Duration, humantime::DurationError> {
23    humantime::parse_duration(input)
24}
25
26/// Deserialize a [`Duration`] from either a human-readable string or a millisecond integer.
27pub 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
45/// Serialize a [`Duration`] as milliseconds (`u64`) for stable round-tripping.
46pub 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        /// Human-readable formatting always produces a non-empty string.
163        #[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        /// Formatting the same duration twice always yields the same string.
171        #[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        /// format → parse round-trip preserves the original duration.
180        #[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        /// Sub-second durations mention "ms" in the formatted output.
189        #[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        /// Whole-second durations (< 1 min) mention "s" in the formatted output.
197        #[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        /// Whole-minute durations mention "m" in the formatted output.
205        #[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        /// Whole-hour durations mention "h" in the formatted output.
213        #[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        /// Serde JSON round-trip via integer millisecond representation.
221        #[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        /// Serde TOML round-trip via human-readable string representation.
229        #[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        /// Arbitrary UTF-8 strings never cause parse_duration to panic.
239        #[test]
240        fn arbitrary_strings_never_panic(s in "\\PC{0,64}") {
241            let _ = parse_duration(&s);
242        }
243
244        /// Adding two durations then formatting and reparsing produces the sum.
245        #[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    // -- Zero duration --
263
264    #[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    // -- Large durations --
297
298    #[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); // 1 year
312        let json = serde_json::to_value(H { v: large }).unwrap();
313        assert_eq!(json["v"], 365 * 24 * 3600 * 1000_u64);
314    }
315
316    // -- Sub-millisecond precision --
317
318    #[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        // Duration with only microseconds → as_millis() == 0
333        #[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    // -- Parsing edge cases --
344
345    #[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    // -- Comparison / ordering --
368
369    #[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    // -- Arithmetic --
381
382    #[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    // -- Combined units without spaces --
403
404    #[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    // -- Zero forms equivalence --
417
418    #[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    // -- Invalid input patterns --
428
429    #[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    // -- Format-parse roundtrips --
450
451    #[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    // -- Complex multi-unit combinations --
468
469    #[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    // -- deserialize_duration: unexpected JSON types must error, not silently coerce --
490
491    #[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    // -- deserialize_duration: unexpected TOML types must error --
530
531    #[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    // -- serialize_duration: sub-millisecond inputs truncate toward zero, never round up --
556
557    #[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        // 1500us is 1.5ms; truncation (not rounding) yields 1ms.
578        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    // -- serialize_duration: very large values must not panic --
586
587    #[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        // Duration::MAX.as_millis() exceeds u64::MAX; the `as u64` cast wraps
599        // (returns the low 64 bits of the u128). Pinning: must not panic.
600        let h = Holder {
601            value: Duration::MAX,
602        };
603        let _ = serde_json::to_value(&h).expect("serialize");
604    }
605
606    // -- parse_duration: whitespace handling --
607
608    #[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        // humantime allows whitespace between the number and its unit suffix.
621        assert_eq!(parse_duration("2 s").unwrap(), Duration::from_secs(2));
622    }
623
624    // -- parse_duration: error path coverage --
625
626    #[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    // -- Round-trip across JSON and TOML for cross-format consistency --
646
647    #[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        // serialize_duration emits a u64 millisecond count; TOML accepts that as
658        // an integer for the deserializer's u64 arm.
659        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}