durable_execution_sdk_testing/time_control.rs
1//! Time control utilities for durable execution testing.
2//!
3//! This module provides the `TimeControl` struct for managing fake time during tests,
4//! enabling instant advancement of wait operations without blocking.
5//!
6//! # Overview
7//!
8//! When testing durable functions that include wait operations, you typically don't want
9//! to wait for actual time to pass. The `TimeControl` utilities integrate with Tokio's
10//! time manipulation features to enable instant time advancement.
11//!
12//! # Examples
13//!
14//! ```ignore
15//! use durable_execution_sdk_testing::TimeControl;
16//! use std::time::Duration;
17//!
18//! #[tokio::test]
19//! async fn test_with_time_control() {
20//! // Enable time skipping
21//! TimeControl::enable().await.unwrap();
22//!
23//! // Advance time by 5 seconds instantly
24//! TimeControl::advance(Duration::from_secs(5)).await;
25//!
26//! // Disable time skipping when done
27//! TimeControl::disable().await.unwrap();
28//! }
29//! ```
30//!
31//! # Requirements
32//!
33//! - 2.1: WHEN time skipping is enabled, THE Local_Test_Runner SHALL use Tokio's
34//! time manipulation to skip wait durations
35//! - 2.2: WHEN a wait operation is encountered with time skipping enabled,
36//! THE Local_Test_Runner SHALL advance time instantly without blocking
37//! - 2.3: WHEN time skipping is disabled, THE Local_Test_Runner SHALL execute
38//! wait operations with real timing
39//! - 2.4: WHEN teardown_test_environment() is called, THE Local_Test_Runner
40//! SHALL restore normal time behavior
41
42use std::sync::atomic::{AtomicBool, Ordering};
43use std::time::Duration;
44
45use crate::error::TestError;
46
47/// Safely resume tokio's clock, catching any panic (e.g. if time wasn't paused
48/// or if called during stack unwinding).
49fn resume_time_safely() {
50 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
51 tokio::time::resume();
52 }));
53}
54
55/// Global flag indicating whether time control is active.
56static TIME_CONTROL_ACTIVE: AtomicBool = AtomicBool::new(false);
57
58/// Checks if Tokio's time is currently paused.
59///
60/// This is a helper function that can be used to check if time is paused
61/// before calling `tokio::time::resume()`, which panics if time is not paused.
62///
63/// # Returns
64///
65/// `true` if time is currently paused, `false` otherwise.
66pub fn is_time_paused() -> bool {
67 TimeControl::is_time_paused_internal()
68}
69
70/// Utilities for controlling time during durable execution tests.
71///
72/// `TimeControl` provides methods to pause, resume, and advance time using
73/// Tokio's time manipulation features. This enables tests to skip wait
74/// operations instantly without blocking.
75///
76/// # Thread Safety
77///
78/// Time control is global to the Tokio runtime. Only one test should use
79/// time control at a time, or tests should be run sequentially when using
80/// time manipulation.
81///
82/// # Examples
83///
84/// ```ignore
85/// use durable_execution_sdk_testing::TimeControl;
86/// use std::time::Duration;
87///
88/// #[tokio::test]
89/// async fn test_workflow_with_waits() {
90/// // Enable time skipping for fast test execution
91/// TimeControl::enable().await.unwrap();
92///
93/// // Run your workflow - wait operations complete instantly
94/// // ...
95///
96/// // Optionally advance time by a specific duration
97/// TimeControl::advance(Duration::from_secs(60)).await;
98///
99/// // Restore normal time behavior
100/// TimeControl::disable().await.unwrap();
101/// }
102/// ```
103#[derive(Debug, Clone, Copy)]
104pub struct TimeControl;
105
106impl TimeControl {
107 /// Enables time skipping by pausing Tokio's internal clock.
108 ///
109 /// When time is paused, `tokio::time::sleep` and other time-based operations
110 /// will not block. Instead, time advances only when explicitly advanced or
111 /// when there are no other tasks to run.
112 /// # Errors
113 ///
114 /// Returns `Ok(())` on success. This operation is idempotent - calling it
115 /// multiple times has no additional effect.
116 ///
117 /// # Examples
118 ///
119 /// ```ignore
120 /// use durable_execution_sdk_testing::TimeControl;
121 ///
122 /// #[tokio::test]
123 /// async fn test_enable_time_control() {
124 /// TimeControl::enable().await.unwrap();
125 /// assert!(TimeControl::is_enabled());
126 /// TimeControl::disable().await.unwrap();
127 /// }
128 /// ```
129 pub async fn enable() -> Result<(), TestError> {
130 if TIME_CONTROL_ACTIVE.load(Ordering::SeqCst) {
131 // Already enabled, nothing to do
132 return Ok(());
133 }
134
135 // Pause Tokio's internal clock
136 // Note: tokio::time::pause() will panic if:
137 // 1. Called when time is already paused (e.g., from a previous test)
138 // 2. Called from a multi-threaded runtime (requires current_thread)
139 // We handle both cases by checking if time is already paused first,
140 // and by using catch_unwind to handle the multi-threaded runtime case.
141 if !Self::is_time_paused_internal() {
142 // Try to pause time, but handle the case where we're not in current_thread runtime
143 use std::panic;
144 let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
145 tokio::time::pause();
146 }));
147
148 if let Err(panic_info) = result {
149 // Check if the panic was because we're not in a current_thread runtime
150 let is_runtime_error = panic_info
151 .downcast_ref::<&str>()
152 .map(|msg| msg.contains("current_thread"))
153 .unwrap_or(false)
154 || panic_info
155 .downcast_ref::<String>()
156 .map(|msg| msg.contains("current_thread"))
157 .unwrap_or(false);
158
159 if is_runtime_error {
160 tracing::warn!(
161 "Time control requires current_thread Tokio runtime. \
162 Time skipping may not work correctly."
163 );
164 return Err(TestError::invalid_configuration(
165 "Time control requires current_thread Tokio runtime",
166 ));
167 }
168
169 // For other unexpected panics, do not set the flag
170 return Err(TestError::invalid_configuration(
171 "Failed to pause Tokio time: unexpected panic",
172 ));
173 }
174 }
175 TIME_CONTROL_ACTIVE.store(true, Ordering::SeqCst);
176
177 Ok(())
178 }
179
180 /// Checks if Tokio's time is currently paused (regardless of our tracking flag).
181 ///
182 /// This is useful for detecting if time was paused by another test or code path.
183 ///
184 /// Note: This function uses catch_unwind to detect if time is paused, which
185 /// requires the current_thread runtime. If called from a multi-threaded runtime,
186 /// it will return false (assuming time is not paused).
187 pub(crate) fn is_time_paused_internal() -> bool {
188 // Try to detect if time is paused by checking if we can call pause without panicking
189 // Unfortunately, there's no direct API for this, so we use a workaround:
190 // We check if the auto-advance behavior is active (which only happens when paused)
191 use std::panic;
192
193 // Use catch_unwind to safely check if pause would panic
194 let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
195 // This will panic if:
196 // 1. Time is already paused (the case we want to detect)
197 // 2. We're not in a current_thread runtime (which we should treat as "not paused")
198 tokio::time::pause();
199 }));
200
201 match result {
202 Ok(()) => {
203 // Pause succeeded, so time wasn't paused before. Resume to restore state.
204 tokio::time::resume();
205 false
206 }
207 Err(panic_info) => {
208 // Check if the panic was because time was already paused
209 // vs because we're not in a current_thread runtime
210 if let Some(msg) = panic_info.downcast_ref::<&str>() {
211 if msg.contains("current_thread") {
212 // Not in current_thread runtime - treat as not paused
213 return false;
214 }
215 }
216 if let Some(msg) = panic_info.downcast_ref::<String>() {
217 if msg.contains("current_thread") {
218 // Not in current_thread runtime - treat as not paused
219 return false;
220 }
221 }
222 // Pause panicked for another reason (likely already paused)
223 true
224 }
225 }
226 }
227
228 /// Disables time skipping by resuming Tokio's internal clock.
229 ///
230 /// After calling this method, time-based operations will use real time again.
231 /// # Errors
232 ///
233 /// Returns `Ok(())` on success. This operation is idempotent - calling it
234 /// multiple times has no additional effect.
235 ///
236 /// # Examples
237 ///
238 /// ```ignore
239 /// use durable_execution_sdk_testing::TimeControl;
240 ///
241 /// #[tokio::test]
242 /// async fn test_disable_time_control() {
243 /// TimeControl::enable().await.unwrap();
244 /// TimeControl::disable().await.unwrap();
245 /// assert!(!TimeControl::is_enabled());
246 /// }
247 /// ```
248 pub async fn disable() -> Result<(), TestError> {
249 if !TIME_CONTROL_ACTIVE.load(Ordering::SeqCst) {
250 // Already disabled, nothing to do
251 return Ok(());
252 }
253
254 // Resume Tokio's internal clock safely (handles case where time isn't paused)
255 resume_time_safely();
256 TIME_CONTROL_ACTIVE.store(false, Ordering::SeqCst);
257
258 Ok(())
259 }
260
261 /// Returns whether time control is currently enabled.
262 ///
263 /// # Examples
264 ///
265 /// ```ignore
266 /// use durable_execution_sdk_testing::TimeControl;
267 ///
268 /// #[tokio::test]
269 /// async fn test_is_enabled() {
270 /// assert!(!TimeControl::is_enabled());
271 /// TimeControl::enable().await.unwrap();
272 /// assert!(TimeControl::is_enabled());
273 /// TimeControl::disable().await.unwrap();
274 /// assert!(!TimeControl::is_enabled());
275 /// }
276 /// ```
277 pub fn is_enabled() -> bool {
278 TIME_CONTROL_ACTIVE.load(Ordering::SeqCst)
279 }
280
281 /// Advances the paused clock by the specified duration.
282 ///
283 /// This method instantly advances Tokio's internal clock by the given duration,
284 /// causing any pending timers that would expire within that duration to fire.
285 /// # Arguments
286 ///
287 /// * `duration` - The amount of time to advance the clock by
288 ///
289 /// # Panics
290 ///
291 /// This method will panic if time control is not enabled (i.e., if `enable()`
292 /// has not been called or `disable()` has been called).
293 ///
294 /// # Examples
295 ///
296 /// ```ignore
297 /// use durable_execution_sdk_testing::TimeControl;
298 /// use std::time::Duration;
299 ///
300 /// #[tokio::test]
301 /// async fn test_advance_time() {
302 /// TimeControl::enable().await.unwrap();
303 ///
304 /// // Advance time by 1 hour instantly
305 /// TimeControl::advance(Duration::from_secs(3600)).await;
306 ///
307 /// TimeControl::disable().await.unwrap();
308 /// }
309 /// ```
310 pub async fn advance(duration: Duration) {
311 tokio::time::advance(duration).await;
312 }
313
314 /// Advances the paused clock by the specified number of seconds.
315 ///
316 /// This is a convenience method equivalent to `advance(Duration::from_secs(seconds))`.
317 ///
318 /// # Arguments
319 ///
320 /// * `seconds` - The number of seconds to advance the clock by
321 ///
322 /// # Examples
323 ///
324 /// ```ignore
325 /// use durable_execution_sdk_testing::TimeControl;
326 ///
327 /// #[tokio::test]
328 /// async fn test_advance_seconds() {
329 /// TimeControl::enable().await.unwrap();
330 ///
331 /// // Advance time by 60 seconds
332 /// TimeControl::advance_secs(60).await;
333 ///
334 /// TimeControl::disable().await.unwrap();
335 /// }
336 /// ```
337 pub async fn advance_secs(seconds: u64) {
338 Self::advance(Duration::from_secs(seconds)).await;
339 }
340
341 /// Advances the paused clock by the specified number of milliseconds.
342 ///
343 /// This is a convenience method equivalent to `advance(Duration::from_millis(millis))`.
344 ///
345 /// # Arguments
346 ///
347 /// * `millis` - The number of milliseconds to advance the clock by
348 ///
349 /// # Examples
350 ///
351 /// ```ignore
352 /// use durable_execution_sdk_testing::TimeControl;
353 ///
354 /// #[tokio::test]
355 /// async fn test_advance_millis() {
356 /// TimeControl::enable().await.unwrap();
357 ///
358 /// // Advance time by 500 milliseconds
359 /// TimeControl::advance_millis(500).await;
360 ///
361 /// TimeControl::disable().await.unwrap();
362 /// }
363 /// ```
364 pub async fn advance_millis(millis: u64) {
365 Self::advance(Duration::from_millis(millis)).await;
366 }
367
368 /// Resets time control state.
369 ///
370 /// This method disables time control if it's enabled and resets the internal
371 /// state. It's useful for cleanup between tests.
372 ///
373 /// # Examples
374 ///
375 /// ```ignore
376 /// use durable_execution_sdk_testing::TimeControl;
377 ///
378 /// #[tokio::test]
379 /// async fn test_reset() {
380 /// TimeControl::enable().await.unwrap();
381 /// TimeControl::reset().await.unwrap();
382 /// assert!(!TimeControl::is_enabled());
383 /// }
384 /// ```
385 pub async fn reset() -> Result<(), TestError> {
386 Self::disable().await
387 }
388}
389
390/// A guard that automatically disables time control when dropped.
391///
392/// This is useful for ensuring time control is properly cleaned up even if
393/// a test panics or returns early.
394///
395/// # Examples
396///
397/// ```ignore
398/// use durable_execution_sdk_testing::TimeControlGuard;
399///
400/// #[tokio::test]
401/// async fn test_with_guard() {
402/// let _guard = TimeControlGuard::new().await.unwrap();
403/// // Time control is now enabled
404///
405/// // ... run tests ...
406///
407/// // Time control is automatically disabled when _guard is dropped
408/// }
409/// ```
410pub struct TimeControlGuard {
411 _private: (),
412}
413
414impl TimeControlGuard {
415 /// Creates a new `TimeControlGuard` and enables time control.
416 ///
417 /// # Errors
418 ///
419 /// Returns an error if time control cannot be enabled.
420 ///
421 /// # Examples
422 ///
423 /// ```ignore
424 /// use durable_execution_sdk_testing::TimeControlGuard;
425 ///
426 /// #[tokio::test]
427 /// async fn test_guard_creation() {
428 /// let guard = TimeControlGuard::new().await.unwrap();
429 /// assert!(TimeControl::is_enabled());
430 /// drop(guard);
431 /// }
432 /// ```
433 pub async fn new() -> Result<Self, TestError> {
434 TimeControl::enable().await?;
435 Ok(Self { _private: () })
436 }
437}
438
439impl Drop for TimeControlGuard {
440 fn drop(&mut self) {
441 // We can't call async functions in Drop, so we just update the flag
442 // and let the runtime handle the resume on the next opportunity.
443 // Note: This is a best-effort cleanup. For proper cleanup, users should
444 // explicitly call TimeControl::disable() or use the guard in an async context.
445 if TIME_CONTROL_ACTIVE.load(Ordering::SeqCst) {
446 // Resume time synchronously, catching panics to avoid
447 // double-panic if called during stack unwinding.
448 resume_time_safely();
449 TIME_CONTROL_ACTIVE.store(false, Ordering::SeqCst);
450 }
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use std::time::Instant;
458
459 #[tokio::test]
460 async fn test_enable_disable() {
461 // Ensure clean state
462 TimeControl::disable().await.unwrap();
463 assert!(!TimeControl::is_enabled());
464
465 // Enable
466 TimeControl::enable().await.unwrap();
467 assert!(TimeControl::is_enabled());
468
469 // Disable
470 TimeControl::disable().await.unwrap();
471 assert!(!TimeControl::is_enabled());
472 }
473
474 #[tokio::test]
475 async fn test_enable_is_idempotent() {
476 // Ensure clean state - first check if time is paused and resume if so
477 // Note: We need to be careful here because is_time_paused_internal()
478 // uses catch_unwind which may not work well in all test scenarios
479 if TIME_CONTROL_ACTIVE.load(Ordering::SeqCst) {
480 tokio::time::resume();
481 TIME_CONTROL_ACTIVE.store(false, Ordering::SeqCst);
482 }
483
484 // Enable multiple times
485 TimeControl::enable().await.unwrap();
486 TimeControl::enable().await.unwrap();
487 TimeControl::enable().await.unwrap();
488
489 assert!(TimeControl::is_enabled());
490
491 // Cleanup
492 TimeControl::disable().await.unwrap();
493 }
494
495 #[tokio::test]
496 async fn test_disable_is_idempotent() {
497 // Ensure clean state
498 TimeControl::disable().await.unwrap();
499
500 // Disable multiple times (should not panic)
501 TimeControl::disable().await.unwrap();
502 TimeControl::disable().await.unwrap();
503
504 assert!(!TimeControl::is_enabled());
505 }
506
507 #[tokio::test(start_paused = true)]
508 async fn test_advance_time() {
509 let start = Instant::now();
510
511 // Advance by 1 second (clock is already paused by the runtime)
512 TimeControl::advance_secs(1).await;
513
514 // The wall clock time should be very small (not 1 second)
515 let elapsed = start.elapsed();
516 assert!(
517 elapsed < Duration::from_millis(100),
518 "Time advance should be instant, but took {:?}",
519 elapsed
520 );
521 }
522
523 #[tokio::test]
524 async fn test_advance_millis() {
525 // Ensure clean state
526 TimeControl::disable().await.unwrap();
527
528 // Enable time control
529 TimeControl::enable().await.unwrap();
530
531 let start = Instant::now();
532
533 // Advance by 5000 milliseconds (5 seconds)
534 TimeControl::advance_millis(5000).await;
535
536 // The wall clock time should be very small
537 let elapsed = start.elapsed();
538 assert!(
539 elapsed < Duration::from_millis(100),
540 "Time advance should be instant, but took {:?}",
541 elapsed
542 );
543
544 // Cleanup
545 TimeControl::disable().await.unwrap();
546 }
547
548 #[tokio::test]
549 async fn test_sleep_with_time_control() {
550 // Ensure clean state
551 TimeControl::disable().await.unwrap();
552
553 // Enable time control
554 TimeControl::enable().await.unwrap();
555
556 let start = Instant::now();
557
558 // Sleep for 10 seconds (should be instant with time control)
559 tokio::time::sleep(Duration::from_secs(10)).await;
560
561 // The wall clock time should be very small
562 let elapsed = start.elapsed();
563 assert!(
564 elapsed < Duration::from_millis(100),
565 "Sleep should be instant with time control, but took {:?}",
566 elapsed
567 );
568
569 // Cleanup
570 TimeControl::disable().await.unwrap();
571 }
572
573 #[tokio::test]
574 async fn test_reset() {
575 // Enable time control
576 TimeControl::enable().await.unwrap();
577 assert!(TimeControl::is_enabled());
578
579 // Reset
580 TimeControl::reset().await.unwrap();
581 assert!(!TimeControl::is_enabled());
582 }
583
584 #[tokio::test]
585 async fn test_guard_enables_time_control() {
586 // Ensure clean state
587 TimeControl::disable().await.unwrap();
588 assert!(!TimeControl::is_enabled());
589
590 {
591 let _guard = TimeControlGuard::new().await.unwrap();
592 assert!(TimeControl::is_enabled());
593 }
594
595 // Guard should have disabled time control on drop
596 assert!(!TimeControl::is_enabled());
597 }
598
599 #[tokio::test]
600 async fn test_guard_cleanup_on_drop() {
601 // Ensure clean state
602 TimeControl::disable().await.unwrap();
603
604 // Create and immediately drop guard
605 let guard = TimeControlGuard::new().await.unwrap();
606 assert!(TimeControl::is_enabled());
607 drop(guard);
608
609 // Time control should be disabled
610 assert!(!TimeControl::is_enabled());
611 }
612}