Skip to main content

asupersync/types/
id.rs

1//! Identifier types for runtime entities.
2//!
3//! These types provide type-safe identifiers for the core runtime entities:
4//! regions, tasks, and obligations. They wrap arena indices with type safety.
5
6use crate::util::ArenaIndex;
7use core::fmt;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use std::ops::Add;
10use std::sync::atomic::{AtomicU32, Ordering};
11use std::time::Duration;
12
13static EPHEMERAL_REGION_COUNTER: AtomicU32 = AtomicU32::new(1);
14static EPHEMERAL_TASK_COUNTER: AtomicU32 = AtomicU32::new(1);
15
16/// A unique identifier for a region in the runtime.
17///
18/// Regions form a tree structure and own all work spawned within them.
19#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct RegionId(pub(crate) ArenaIndex);
21
22impl RegionId {
23    /// Creates a new region ID from an arena index (internal use).
24    #[inline]
25    #[must_use]
26    #[cfg_attr(feature = "test-internals", visibility::make(pub))]
27    pub(crate) const fn from_arena(index: ArenaIndex) -> Self {
28        Self(index)
29    }
30
31    /// Returns the underlying arena index (internal use).
32    #[inline]
33    #[must_use]
34    #[allow(dead_code)]
35    #[cfg(not(feature = "test-internals"))]
36    pub(crate) const fn arena_index(self) -> ArenaIndex {
37        self.0
38    }
39
40    /// Returns the underlying arena index (internal use).
41    #[inline]
42    #[must_use]
43    #[allow(dead_code)]
44    #[cfg(feature = "test-internals")]
45    pub const fn arena_index(self) -> ArenaIndex {
46        self.0
47    }
48
49    /// Creates a region ID for testing/benchmarking purposes.
50    #[doc(hidden)]
51    #[must_use]
52    pub const fn new_for_test(index: u32, generation: u32) -> Self {
53        Self(ArenaIndex::new(index, generation))
54    }
55
56    /// Creates a default region ID for testing purposes.
57    ///
58    /// This creates an ID with index 0 and generation 0, suitable for
59    /// unit tests that don't care about specific ID values.
60    #[doc(hidden)]
61    #[must_use]
62    pub const fn testing_default() -> Self {
63        Self(ArenaIndex::new(0, 0))
64    }
65
66    /// Creates a new ephemeral region ID for request-scoped contexts created
67    /// outside the runtime scheduler.
68    ///
69    /// This is intended for production request handling that needs unique
70    /// identifiers without full runtime region registration.
71    #[must_use]
72    pub fn new_ephemeral() -> Self {
73        let index = EPHEMERAL_REGION_COUNTER.fetch_add(1, Ordering::Relaxed);
74        Self(ArenaIndex::new(index, 1))
75    }
76}
77
78impl fmt::Debug for RegionId {
79    #[inline]
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        write!(f, "RegionId({}:{})", self.0.index(), self.0.generation())
82    }
83}
84
85impl fmt::Display for RegionId {
86    #[inline]
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        write!(f, "R{}", self.0.index())
89    }
90}
91
92#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
93struct SerdeArenaIndex {
94    index: u32,
95    generation: u32,
96}
97
98impl SerdeArenaIndex {
99    const fn to_arena(self) -> ArenaIndex {
100        ArenaIndex::new(self.index, self.generation)
101    }
102}
103
104impl From<ArenaIndex> for SerdeArenaIndex {
105    fn from(value: ArenaIndex) -> Self {
106        Self {
107            index: value.index(),
108            generation: value.generation(),
109        }
110    }
111}
112
113impl Serialize for RegionId {
114    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
115    where
116        S: Serializer,
117    {
118        SerdeArenaIndex::from(self.0).serialize(serializer)
119    }
120}
121
122impl<'de> Deserialize<'de> for RegionId {
123    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124    where
125        D: Deserializer<'de>,
126    {
127        let idx = SerdeArenaIndex::deserialize(deserializer)?;
128        Ok(Self(idx.to_arena()))
129    }
130}
131
132/// A unique identifier for a task in the runtime.
133///
134/// Tasks are units of concurrent execution owned by regions.
135#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
136pub struct TaskId(pub(crate) ArenaIndex);
137
138impl TaskId {
139    /// Creates a new task ID from an arena index (internal use).
140    #[inline]
141    #[must_use]
142    #[allow(dead_code)]
143    #[cfg_attr(feature = "test-internals", visibility::make(pub))]
144    pub(crate) const fn from_arena(index: ArenaIndex) -> Self {
145        Self(index)
146    }
147
148    /// Returns the underlying arena index (internal use).
149    #[inline]
150    #[must_use]
151    #[allow(dead_code)]
152    #[cfg(not(feature = "test-internals"))]
153    pub(crate) const fn arena_index(self) -> ArenaIndex {
154        self.0
155    }
156
157    /// Returns the underlying arena index (internal use).
158    #[inline]
159    #[must_use]
160    #[allow(dead_code)]
161    #[cfg(feature = "test-internals")]
162    pub const fn arena_index(self) -> ArenaIndex {
163        self.0
164    }
165
166    /// Creates a task ID for testing/benchmarking purposes.
167    #[doc(hidden)]
168    #[must_use]
169    pub const fn new_for_test(index: u32, generation: u32) -> Self {
170        Self(ArenaIndex::new(index, generation))
171    }
172
173    /// Creates a default task ID for testing purposes.
174    ///
175    /// This creates an ID with index 0 and generation 0, suitable for
176    /// unit tests that don't care about specific ID values.
177    #[doc(hidden)]
178    #[must_use]
179    pub const fn testing_default() -> Self {
180        Self(ArenaIndex::new(0, 0))
181    }
182
183    /// Creates a new ephemeral task ID for request-scoped contexts created
184    /// outside the runtime scheduler.
185    #[must_use]
186    pub fn new_ephemeral() -> Self {
187        let index = EPHEMERAL_TASK_COUNTER.fetch_add(1, Ordering::Relaxed);
188        Self(ArenaIndex::new(index, 1))
189    }
190}
191
192impl fmt::Debug for TaskId {
193    #[inline]
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        write!(f, "TaskId({}:{})", self.0.index(), self.0.generation())
196    }
197}
198
199impl fmt::Display for TaskId {
200    #[inline]
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        write!(f, "T{}", self.0.index())
203    }
204}
205
206impl Serialize for TaskId {
207    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
208    where
209        S: Serializer,
210    {
211        SerdeArenaIndex::from(self.0).serialize(serializer)
212    }
213}
214
215impl<'de> Deserialize<'de> for TaskId {
216    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
217    where
218        D: Deserializer<'de>,
219    {
220        let idx = SerdeArenaIndex::deserialize(deserializer)?;
221        Ok(Self(idx.to_arena()))
222    }
223}
224
225/// A unique identifier for an obligation in the runtime.
226///
227/// Obligations represent resources that must be resolved (commit, abort, ack, etc.)
228/// before their owning region can close.
229#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
230pub struct ObligationId(pub(crate) ArenaIndex);
231
232impl ObligationId {
233    /// Creates a new obligation ID from an arena index (internal use).
234    #[inline]
235    #[must_use]
236    #[allow(dead_code)]
237    pub(crate) const fn from_arena(index: ArenaIndex) -> Self {
238        Self(index)
239    }
240
241    /// Returns the underlying arena index (internal use).
242    #[inline]
243    #[must_use]
244    #[allow(dead_code)]
245    #[cfg(not(feature = "test-internals"))]
246    pub(crate) const fn arena_index(self) -> ArenaIndex {
247        self.0
248    }
249
250    /// Returns the underlying arena index (internal use).
251    #[inline]
252    #[must_use]
253    #[allow(dead_code)]
254    #[cfg(feature = "test-internals")]
255    pub const fn arena_index(self) -> ArenaIndex {
256        self.0
257    }
258
259    /// Creates an obligation ID for testing/benchmarking purposes.
260    #[doc(hidden)]
261    #[must_use]
262    pub const fn new_for_test(index: u32, generation: u32) -> Self {
263        Self(ArenaIndex::new(index, generation))
264    }
265}
266
267impl fmt::Debug for ObligationId {
268    #[inline]
269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270        write!(
271            f,
272            "ObligationId({}:{})",
273            self.0.index(),
274            self.0.generation()
275        )
276    }
277}
278
279impl fmt::Display for ObligationId {
280    #[inline]
281    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282        write!(f, "O{}", self.0.index())
283    }
284}
285
286impl Serialize for ObligationId {
287    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
288    where
289        S: Serializer,
290    {
291        SerdeArenaIndex::from(self.0).serialize(serializer)
292    }
293}
294
295impl<'de> Deserialize<'de> for ObligationId {
296    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
297    where
298        D: Deserializer<'de>,
299    {
300        let idx = SerdeArenaIndex::deserialize(deserializer)?;
301        Ok(Self(idx.to_arena()))
302    }
303}
304
305/// A logical timestamp for the runtime.
306///
307/// In the production runtime, this corresponds to wall-clock time.
308/// In the lab runtime, this is virtual time controlled by the scheduler.
309#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
310pub struct Time(u64);
311
312impl Time {
313    /// The zero instant (epoch).
314    pub const ZERO: Self = Self(0);
315
316    /// The maximum representable instant.
317    pub const MAX: Self = Self(u64::MAX);
318
319    /// Creates a new time from nanoseconds since epoch.
320    #[inline]
321    #[must_use]
322    pub const fn from_nanos(nanos: u64) -> Self {
323        Self(nanos)
324    }
325
326    /// Creates a new time from milliseconds since epoch.
327    #[inline]
328    #[must_use]
329    pub const fn from_millis(millis: u64) -> Self {
330        Self(millis.saturating_mul(1_000_000))
331    }
332
333    /// Creates a new time from seconds since epoch.
334    #[inline]
335    #[must_use]
336    pub const fn from_secs(secs: u64) -> Self {
337        Self(secs.saturating_mul(1_000_000_000))
338    }
339
340    /// Returns the time as nanoseconds since epoch.
341    #[inline]
342    #[must_use]
343    pub const fn as_nanos(self) -> u64 {
344        self.0
345    }
346
347    /// Returns the time as milliseconds since epoch (truncated).
348    #[inline]
349    #[must_use]
350    pub const fn as_millis(self) -> u64 {
351        self.0 / 1_000_000
352    }
353
354    /// Returns the time as seconds since epoch (truncated).
355    #[inline]
356    #[must_use]
357    pub const fn as_secs(self) -> u64 {
358        self.0 / 1_000_000_000
359    }
360
361    /// Adds a duration in nanoseconds, saturating on overflow.
362    #[inline]
363    #[must_use]
364    pub const fn saturating_add_nanos(self, nanos: u64) -> Self {
365        Self(self.0.saturating_add(nanos))
366    }
367
368    /// Subtracts a duration in nanoseconds, saturating at zero.
369    #[inline]
370    #[must_use]
371    pub const fn saturating_sub_nanos(self, nanos: u64) -> Self {
372        Self(self.0.saturating_sub(nanos))
373    }
374
375    /// Returns the duration between two times in nanoseconds.
376    ///
377    /// Returns 0 if `self` is before `earlier`.
378    #[inline]
379    #[must_use]
380    pub const fn duration_since(self, earlier: Self) -> u64 {
381        self.0.saturating_sub(earlier.0)
382    }
383}
384
385impl Add<Duration> for Time {
386    type Output = Self;
387
388    #[inline]
389    fn add(self, rhs: Duration) -> Self::Output {
390        let nanos: u64 = rhs.as_nanos().min(u128::from(u64::MAX)) as u64;
391        self.saturating_add_nanos(nanos)
392    }
393}
394
395impl fmt::Debug for Time {
396    #[inline]
397    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
398        write!(f, "Time({}ns)", self.0)
399    }
400}
401
402impl fmt::Display for Time {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        if self.0 >= 1_000_000_000 {
405            write!(
406                f,
407                "{}.{:03}s",
408                self.0 / 1_000_000_000,
409                (self.0 / 1_000_000) % 1000
410            )
411        } else if self.0 >= 1_000_000 {
412            write!(f, "{}ms", self.0 / 1_000_000)
413        } else if self.0 >= 1_000 {
414            write!(f, "{}us", self.0 / 1_000)
415        } else {
416            write!(f, "{}ns", self.0)
417        }
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    #[test]
426    fn time_conversions() {
427        assert_eq!(Time::from_secs(1).as_nanos(), 1_000_000_000);
428        assert_eq!(Time::from_millis(1).as_nanos(), 1_000_000);
429        assert_eq!(Time::from_nanos(1).as_nanos(), 1);
430
431        assert_eq!(Time::from_nanos(1_500_000_000).as_secs(), 1);
432        assert_eq!(Time::from_nanos(1_500_000_000).as_millis(), 1500);
433    }
434
435    #[test]
436    fn time_arithmetic() {
437        let t1 = Time::from_secs(1);
438        let t2 = t1.saturating_add_nanos(500_000_000);
439        assert_eq!(t2.as_millis(), 1500);
440
441        let t3 = t2.saturating_sub_nanos(2_000_000_000);
442        assert_eq!(t3, Time::ZERO);
443    }
444
445    #[test]
446    fn time_ordering() {
447        assert!(Time::from_secs(1) < Time::from_secs(2));
448        assert!(Time::from_millis(1000) == Time::from_secs(1));
449    }
450
451    // ---- RegionId ----
452
453    #[test]
454    fn region_id_debug_format() {
455        let id = RegionId::new_for_test(5, 3);
456        let dbg = format!("{id:?}");
457        assert!(dbg.contains("RegionId"), "{dbg}");
458        assert!(dbg.contains('5'), "{dbg}");
459        assert!(dbg.contains('3'), "{dbg}");
460    }
461
462    #[test]
463    fn region_id_display_format() {
464        let id = RegionId::new_for_test(42, 0);
465        assert_eq!(format!("{id}"), "R42");
466    }
467
468    #[test]
469    fn region_id_equality_and_hash() {
470        use crate::util::DetHasher;
471        use std::hash::{Hash, Hasher};
472
473        let a = RegionId::new_for_test(1, 2);
474        let b = RegionId::new_for_test(1, 2);
475        let c = RegionId::new_for_test(1, 3);
476
477        assert_eq!(a, b);
478        assert_ne!(a, c);
479
480        let mut ha = DetHasher::default();
481        let mut hb = DetHasher::default();
482        a.hash(&mut ha);
483        b.hash(&mut hb);
484        assert_eq!(ha.finish(), hb.finish());
485    }
486
487    #[test]
488    fn region_id_ordering() {
489        let a = RegionId::new_for_test(1, 0);
490        let b = RegionId::new_for_test(2, 0);
491        assert!(a < b);
492        assert!(a <= b);
493        assert!(b > a);
494    }
495
496    #[test]
497    fn region_id_copy_clone() {
498        let id = RegionId::new_for_test(1, 0);
499        let copied = id;
500        let cloned = id;
501        assert_eq!(id, copied);
502        assert_eq!(id, cloned);
503    }
504
505    #[test]
506    fn region_id_testing_default() {
507        let id = RegionId::testing_default();
508        assert_eq!(format!("{id}"), "R0");
509    }
510
511    #[test]
512    fn region_id_ephemeral_unique() {
513        let a = RegionId::new_ephemeral();
514        let b = RegionId::new_ephemeral();
515        assert_ne!(a, b);
516    }
517
518    #[test]
519    fn region_id_serde_roundtrip() {
520        let id = RegionId::new_for_test(99, 7);
521        let json = serde_json::to_string(&id).expect("serialize");
522        let deserialized: RegionId = serde_json::from_str(&json).expect("deserialize");
523        assert_eq!(id, deserialized);
524    }
525
526    // ---- TaskId ----
527
528    #[test]
529    fn task_id_debug_format() {
530        let id = TaskId::new_for_test(10, 2);
531        let dbg = format!("{id:?}");
532        assert!(dbg.contains("TaskId"), "{dbg}");
533        assert!(dbg.contains("10"), "{dbg}");
534        assert!(dbg.contains('2'), "{dbg}");
535    }
536
537    #[test]
538    fn task_id_display_format() {
539        let id = TaskId::new_for_test(7, 0);
540        assert_eq!(format!("{id}"), "T7");
541    }
542
543    #[test]
544    fn task_id_equality_and_hash() {
545        use crate::util::DetHasher;
546        use std::hash::{Hash, Hasher};
547
548        let a = TaskId::new_for_test(3, 1);
549        let b = TaskId::new_for_test(3, 1);
550        let c = TaskId::new_for_test(3, 2);
551
552        assert_eq!(a, b);
553        assert_ne!(a, c);
554
555        let mut ha = DetHasher::default();
556        let mut hb = DetHasher::default();
557        a.hash(&mut ha);
558        b.hash(&mut hb);
559        assert_eq!(ha.finish(), hb.finish());
560    }
561
562    #[test]
563    fn task_id_ordering() {
564        let a = TaskId::new_for_test(1, 0);
565        let b = TaskId::new_for_test(2, 0);
566        assert!(a < b);
567    }
568
569    #[test]
570    fn task_id_copy_clone() {
571        let id = TaskId::new_for_test(5, 1);
572        let copied = id;
573        let cloned = id;
574        assert_eq!(id, copied);
575        assert_eq!(id, cloned);
576    }
577
578    #[test]
579    fn task_id_testing_default() {
580        let id = TaskId::testing_default();
581        assert_eq!(format!("{id}"), "T0");
582    }
583
584    #[test]
585    fn task_id_ephemeral_unique() {
586        let a = TaskId::new_ephemeral();
587        let b = TaskId::new_ephemeral();
588        assert_ne!(a, b);
589    }
590
591    #[test]
592    fn task_id_serde_roundtrip() {
593        let id = TaskId::new_for_test(42, 5);
594        let json = serde_json::to_string(&id).expect("serialize");
595        let deserialized: TaskId = serde_json::from_str(&json).expect("deserialize");
596        assert_eq!(id, deserialized);
597    }
598
599    // ---- ObligationId ----
600
601    #[test]
602    fn obligation_id_debug_format() {
603        let id = ObligationId::new_for_test(8, 1);
604        let dbg = format!("{id:?}");
605        assert!(dbg.contains("ObligationId"), "{dbg}");
606        assert!(dbg.contains('8'), "{dbg}");
607    }
608
609    #[test]
610    fn obligation_id_display_format() {
611        let id = ObligationId::new_for_test(3, 0);
612        assert_eq!(format!("{id}"), "O3");
613    }
614
615    #[test]
616    fn obligation_id_equality_and_hash() {
617        use crate::util::DetHasher;
618        use std::hash::{Hash, Hasher};
619
620        let a = ObligationId::new_for_test(1, 1);
621        let b = ObligationId::new_for_test(1, 1);
622        let c = ObligationId::new_for_test(2, 1);
623
624        assert_eq!(a, b);
625        assert_ne!(a, c);
626
627        let mut ha = DetHasher::default();
628        let mut hb = DetHasher::default();
629        a.hash(&mut ha);
630        b.hash(&mut hb);
631        assert_eq!(ha.finish(), hb.finish());
632    }
633
634    #[test]
635    fn obligation_id_ordering() {
636        let a = ObligationId::new_for_test(1, 0);
637        let b = ObligationId::new_for_test(2, 0);
638        assert!(a < b);
639    }
640
641    #[test]
642    fn obligation_id_copy_clone() {
643        let id = ObligationId::new_for_test(1, 0);
644        let copied = id;
645        let cloned = id;
646        assert_eq!(id, copied);
647        assert_eq!(id, cloned);
648    }
649
650    #[test]
651    fn obligation_id_serde_roundtrip() {
652        let id = ObligationId::new_for_test(77, 3);
653        let json = serde_json::to_string(&id).expect("serialize");
654        let deserialized: ObligationId = serde_json::from_str(&json).expect("deserialize");
655        assert_eq!(id, deserialized);
656    }
657
658    // ---- Time Display ----
659
660    #[test]
661    fn time_display_seconds() {
662        let t = Time::from_secs(2);
663        let disp = format!("{t}");
664        assert_eq!(disp, "2.000s");
665    }
666
667    #[test]
668    fn time_display_seconds_with_millis() {
669        let t = Time::from_nanos(1_234_000_000);
670        let disp = format!("{t}");
671        assert_eq!(disp, "1.234s");
672    }
673
674    #[test]
675    fn time_display_milliseconds() {
676        let t = Time::from_millis(500);
677        let disp = format!("{t}");
678        assert_eq!(disp, "500ms");
679    }
680
681    #[test]
682    fn time_display_microseconds() {
683        let t = Time::from_nanos(5_000);
684        let disp = format!("{t}");
685        assert_eq!(disp, "5us");
686    }
687
688    #[test]
689    fn time_display_nanoseconds() {
690        let t = Time::from_nanos(42);
691        let disp = format!("{t}");
692        assert_eq!(disp, "42ns");
693    }
694
695    #[test]
696    fn time_display_zero() {
697        assert_eq!(format!("{}", Time::ZERO), "0ns");
698    }
699
700    // ---- Time edge cases ----
701
702    #[test]
703    fn time_debug_format() {
704        let t = Time::from_nanos(100);
705        let dbg = format!("{t:?}");
706        assert_eq!(dbg, "Time(100ns)");
707    }
708
709    #[test]
710    fn time_default_is_zero() {
711        assert_eq!(Time::default(), Time::ZERO);
712    }
713
714    #[test]
715    fn time_max_constant() {
716        assert_eq!(Time::MAX.as_nanos(), u64::MAX);
717    }
718
719    #[test]
720    fn time_saturating_add_overflow() {
721        let t = Time::MAX;
722        let result = t.saturating_add_nanos(1);
723        assert_eq!(result, Time::MAX);
724    }
725
726    #[test]
727    fn time_saturating_sub_underflow() {
728        let t = Time::ZERO;
729        let result = t.saturating_sub_nanos(100);
730        assert_eq!(result, Time::ZERO);
731    }
732
733    #[test]
734    fn time_duration_since() {
735        let t1 = Time::from_secs(5);
736        let t2 = Time::from_secs(3);
737        assert_eq!(t1.duration_since(t2), 2_000_000_000);
738        assert_eq!(t2.duration_since(t1), 0); // saturates at 0
739    }
740
741    #[test]
742    fn time_add_duration() {
743        let t = Time::from_secs(1);
744        let result = t + Duration::from_millis(500);
745        assert_eq!(result.as_millis(), 1500);
746    }
747
748    #[test]
749    fn time_from_millis_saturation() {
750        let t = Time::from_millis(u64::MAX);
751        // Should saturate, not overflow
752        assert_eq!(t, Time::MAX);
753    }
754
755    #[test]
756    fn time_from_secs_saturation() {
757        let t = Time::from_secs(u64::MAX);
758        assert_eq!(t, Time::MAX);
759    }
760
761    #[test]
762    fn time_serde_roundtrip() {
763        let t = Time::from_nanos(12345);
764        let json = serde_json::to_string(&t).expect("serialize");
765        let deserialized: Time = serde_json::from_str(&json).expect("deserialize");
766        assert_eq!(t, deserialized);
767    }
768
769    #[test]
770    fn time_hash_consistency() {
771        use crate::util::DetHasher;
772        use std::hash::{Hash, Hasher};
773
774        let a = Time::from_secs(1);
775        let b = Time::from_millis(1000);
776        assert_eq!(a, b);
777
778        let mut ha = DetHasher::default();
779        let mut hb = DetHasher::default();
780        a.hash(&mut ha);
781        b.hash(&mut hb);
782        assert_eq!(ha.finish(), hb.finish());
783    }
784}