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}