sim_time/
lib.rs

1#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
2#![cfg_attr(feature = "fail-on-warnings", deny(clippy::all))]
3#![forbid(unsafe_code)]
4
5mod config;
6
7use chrono::{DateTime, Utc};
8pub use config::*;
9use std::{
10    sync::{
11        Arc, OnceLock,
12        atomic::{AtomicU64, Ordering},
13    },
14    time::Duration,
15};
16
17static INSTANCE: OnceLock<Time> = OnceLock::new();
18
19#[derive(Clone)]
20struct Time {
21    config: TimeConfig,
22    elapsed_ms: Arc<AtomicU64>,
23    ticker_task: Arc<OnceLock<()>>,
24}
25
26impl Time {
27    fn new(config: TimeConfig) -> Self {
28        let time = Self {
29            config,
30            elapsed_ms: Arc::new(AtomicU64::new(0)),
31            ticker_task: Arc::new(OnceLock::new()),
32        };
33        if !time.config.realtime {
34            time.spawn_ticker();
35        }
36        time
37    }
38
39    fn spawn_ticker(&self) {
40        let elapsed_ms = self.elapsed_ms.clone();
41        let sim_config = self
42            .config
43            .simulation
44            .as_ref()
45            .expect("sim_time required when realtime is false");
46        let tick_interval_ms = sim_config.tick_interval_ms;
47        let tick_duration = sim_config.tick_duration_secs;
48        self.ticker_task.get_or_init(|| {
49            tokio::spawn(async move {
50                let mut interval =
51                    tokio::time::interval(tokio::time::Duration::from_millis(tick_interval_ms));
52                loop {
53                    interval.tick().await;
54                    elapsed_ms.fetch_add(tick_duration.as_millis() as u64, Ordering::Relaxed);
55                }
56            });
57        });
58    }
59
60    fn now(&self) -> DateTime<Utc> {
61        if self.config.realtime {
62            Utc::now()
63        } else {
64            let sim_config = self
65                .config
66                .simulation
67                .as_ref()
68                .expect("sim_time required when realtime is false");
69            let elapsed_ms = self.elapsed_ms.load(Ordering::Relaxed);
70
71            let simulated_time =
72                sim_config.start_at + chrono::Duration::milliseconds(elapsed_ms as i64);
73
74            if sim_config.transform_to_realtime && simulated_time >= Utc::now() {
75                Utc::now()
76            } else {
77                simulated_time
78            }
79        }
80    }
81
82    fn real_ms(&self, duration: Duration) -> Duration {
83        if self.config.realtime {
84            duration
85        } else {
86            let sim_config = self
87                .config
88                .simulation
89                .as_ref()
90                .expect("sim_time required when realtime is false");
91
92            let current_time = self.now();
93            let real_now = Utc::now();
94
95            if sim_config.transform_to_realtime && current_time >= real_now {
96                return duration;
97            }
98
99            let sim_ms_per_real_ms = sim_config.tick_duration_secs.as_millis() as f64
100                / sim_config.tick_interval_ms as f64;
101
102            Duration::from_millis((duration.as_millis() as f64 / sim_ms_per_real_ms).ceil() as u64)
103        }
104    }
105
106    fn sleep(&self, duration: Duration) -> tokio::time::Sleep {
107        tokio::time::sleep(self.real_ms(duration))
108    }
109
110    fn timeout<F>(&self, duration: Duration, future: F) -> tokio::time::Timeout<F::IntoFuture>
111    where
112        F: core::future::IntoFuture,
113    {
114        tokio::time::timeout(self.real_ms(duration), future)
115    }
116
117    pub async fn wait_until_realtime(&self) {
118        if self.config.realtime {
119            return;
120        }
121
122        let current = self.now();
123        let real_now = Utc::now();
124
125        if current >= real_now {
126            return;
127        }
128
129        let wait_duration =
130            std::time::Duration::from_millis((real_now - current).num_milliseconds() as u64);
131
132        self.sleep(wait_duration).await;
133    }
134}
135
136/// Returns a future that will return when the simulation has caught up to the current time.
137///
138/// Assumes that the simulation was configured to start in the past and has
139/// [`SimulationConfig::transform_to_realtime`](`config::SimulationConfig::transform_to_realtime`) set to `true`.
140pub async fn wait_until_realtime() {
141    INSTANCE
142        .get_or_init(|| Time::new(TimeConfig::default()))
143        .wait_until_realtime()
144        .await
145}
146
147/// Pass the [`TimeConfig`] to configure `sim-time` globally.
148/// Must be called before any other `fn`s otherwise `sim-time` will initialize with defaults.
149pub fn init(config: TimeConfig) {
150    INSTANCE.get_or_init(|| Time::new(config));
151}
152
153/// Returns the current time in the simulation
154pub fn now() -> DateTime<Utc> {
155    INSTANCE
156        .get_or_init(|| Time::new(TimeConfig::default()))
157        .now()
158}
159
160/// Will sleep for the simulated duration.
161pub fn sleep(duration: Duration) -> tokio::time::Sleep {
162    INSTANCE
163        .get_or_init(|| Time::new(TimeConfig::default()))
164        .sleep(duration)
165}
166
167/// Will timeout for the simulated duration.
168pub fn timeout<F>(duration: Duration, future: F) -> tokio::time::Timeout<F::IntoFuture>
169where
170    F: core::future::IntoFuture,
171{
172    INSTANCE
173        .get_or_init(|| Time::new(TimeConfig::default()))
174        .timeout(duration, future)
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use chrono::Duration as ChronoDuration;
181    use std::time::Duration as StdDuration;
182
183    #[tokio::test]
184    async fn test_simulated_time() {
185        // Configure time where 10ms = 10 days of simulated time
186        let config = TimeConfig {
187            realtime: false,
188            simulation: Some(SimulationConfig {
189                start_at: Utc::now(),
190                tick_interval_ms: 10,
191                tick_duration_secs: StdDuration::from_secs(10 * 24 * 60 * 60), // 10 days in seconds
192                transform_to_realtime: false,
193            }),
194        };
195
196        init(config);
197        let start = now();
198        tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
199        let end = now();
200        let elapsed = end - start;
201
202        assert!(
203            elapsed >= ChronoDuration::days(19) && elapsed <= ChronoDuration::days(21),
204            "Expected ~20 days to pass, but got {} days",
205            elapsed.num_days()
206        );
207    }
208
209    #[test]
210    fn test_default_realtime() {
211        let t1 = now();
212        let t2 = Utc::now();
213        assert!(t2 - t1 < ChronoDuration::seconds(1));
214    }
215}