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/// 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}