mas_storage/
clock.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7//! A [`Clock`] is a way to get the current date and time.
8//!
9//! This module defines two implemetation of the [`Clock`] trait:
10//! [`SystemClock`] which uses the system time, and a [`MockClock`], which can
11//! be used and freely manipulated in tests.
12
13use std::sync::{Arc, atomic::AtomicI64};
14
15use chrono::{DateTime, TimeZone, Utc};
16
17/// Represents a clock which can give the current date and time
18pub trait Clock: Sync {
19    /// Get the current date and time
20    fn now(&self) -> DateTime<Utc>;
21}
22
23impl<C: Clock + Send + ?Sized> Clock for Arc<C> {
24    fn now(&self) -> DateTime<Utc> {
25        (**self).now()
26    }
27}
28
29impl<C: Clock + ?Sized> Clock for Box<C> {
30    fn now(&self) -> DateTime<Utc> {
31        (**self).now()
32    }
33}
34
35/// A clock which uses the system time
36#[derive(Clone, Default)]
37pub struct SystemClock {
38    _private: (),
39}
40
41impl Clock for SystemClock {
42    fn now(&self) -> DateTime<Utc> {
43        // This is the clock used elsewhere, it's fine to call Utc::now here
44        #[allow(clippy::disallowed_methods)]
45        Utc::now()
46    }
47}
48
49/// A fake clock, which uses a fixed timestamp, and can be advanced with the
50/// [`MockClock::advance`] method.
51pub struct MockClock {
52    timestamp: AtomicI64,
53}
54
55impl Default for MockClock {
56    fn default() -> Self {
57        let datetime = Utc.with_ymd_and_hms(2022, 1, 16, 14, 40, 0).unwrap();
58        Self::new(datetime)
59    }
60}
61
62impl MockClock {
63    /// Create a new clock which starts at the given datetime
64    #[must_use]
65    pub fn new(datetime: DateTime<Utc>) -> Self {
66        let timestamp = AtomicI64::new(datetime.timestamp());
67        Self { timestamp }
68    }
69
70    /// Move the clock forward by the given amount of time
71    pub fn advance(&self, duration: chrono::Duration) {
72        self.timestamp
73            .fetch_add(duration.num_seconds(), std::sync::atomic::Ordering::Relaxed);
74    }
75}
76
77impl Clock for MockClock {
78    fn now(&self) -> DateTime<Utc> {
79        let timestamp = self.timestamp.load(std::sync::atomic::Ordering::Relaxed);
80        chrono::TimeZone::timestamp_opt(&Utc, timestamp, 0).unwrap()
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use chrono::Duration;
87
88    use super::*;
89
90    #[test]
91    fn test_mocked_clock() {
92        let clock = MockClock::default();
93
94        // Time should be frozen, and give out the same timestamp on each call
95        let first = clock.now();
96        std::thread::sleep(std::time::Duration::from_millis(10));
97        let second = clock.now();
98
99        assert_eq!(first, second);
100
101        // Clock can be advanced by a fixed duration
102        clock.advance(Duration::microseconds(10 * 1000 * 1000));
103        let third = clock.now();
104        assert_eq!(first + Duration::microseconds(10 * 1000 * 1000), third);
105    }
106
107    #[test]
108    fn test_real_clock() {
109        let clock = SystemClock::default();
110
111        // Time should not be frozen
112        let first = clock.now();
113        std::thread::sleep(std::time::Duration::from_millis(10));
114        let second = clock.now();
115
116        assert_ne!(first, second);
117        assert!(first < second);
118    }
119}