Skip to main content

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