Skip to main content

ironflow_core/
dry_run.rs

1//! Global dry-run mode control.
2//!
3//! When dry-run mode is active, operations log their intent without executing.
4//! Shell commands are not spawned, HTTP requests are not sent, and Agent calls
5//! return a placeholder response with zero cost.
6//!
7//! # Levels of control
8//!
9//! 1. **Per-operation** - call `.dry_run(true)` on any builder (`Shell`, `Agent`, `Http`).
10//! 2. **Global** - call [`set_dry_run`]`(true)` to enable dry-run for all operations
11//!    that don't have an explicit per-operation setting.
12//!
13//! # Examples
14//!
15//! ```no_run
16//! use ironflow_core::prelude::*;
17//!
18//! # async fn example() -> Result<(), OperationError> {
19//! // Global dry-run
20//! set_dry_run(true);
21//! let output = Shell::new("rm -rf /").await?; // not executed
22//! assert_eq!(output.stdout(), "");
23//!
24//! // Per-operation dry-run (overrides global)
25//! set_dry_run(false);
26//! let output = Shell::new("echo hello").dry_run(true).await?;
27//! assert_eq!(output.stdout(), "");
28//! # Ok(())
29//! # }
30//! ```
31
32use std::sync::atomic::{AtomicBool, Ordering};
33
34static DRY_RUN: AtomicBool = AtomicBool::new(false);
35
36/// Enable or disable global dry-run mode.
37///
38/// When enabled, all operations that do not have an explicit per-operation
39/// dry-run setting will use dry-run behavior.
40///
41/// # Thread safety
42///
43/// This sets a **process-wide** flag. If you run multiple workflows
44/// concurrently, prefer the per-operation `.dry_run(true)` builder method
45/// instead, which does not affect other workflows.
46///
47/// # Examples
48///
49/// ```no_run
50/// use ironflow_core::dry_run::set_dry_run;
51///
52/// set_dry_run(true);  // all operations skip execution
53/// set_dry_run(false); // back to normal
54/// ```
55pub fn set_dry_run(enabled: bool) {
56    DRY_RUN.store(enabled, Ordering::Release);
57}
58
59/// Check whether global dry-run mode is currently enabled.
60pub fn is_dry_run() -> bool {
61    DRY_RUN.load(Ordering::Acquire)
62}
63
64/// RAII guard that sets global dry-run on creation and restores the previous
65/// value on drop. Useful in tests to avoid leaking state.
66///
67/// # Examples
68///
69/// ```no_run
70/// use ironflow_core::dry_run::DryRunGuard;
71///
72/// {
73///     let _guard = DryRunGuard::new(true);
74///     // operations here run in dry-run mode
75/// }
76/// // previous dry-run state restored
77/// ```
78pub struct DryRunGuard {
79    previous: bool,
80}
81
82impl DryRunGuard {
83    /// Enable or disable global dry-run for the lifetime of this guard.
84    pub fn new(enabled: bool) -> Self {
85        let previous = is_dry_run();
86        set_dry_run(enabled);
87        Self { previous }
88    }
89}
90
91impl Drop for DryRunGuard {
92    fn drop(&mut self) {
93        set_dry_run(self.previous);
94    }
95}
96
97/// Resolve the effective dry-run state for an operation.
98///
99/// If the operation has an explicit per-operation setting, use that.
100/// Otherwise, fall back to the global setting.
101pub(crate) fn effective_dry_run(per_operation: Option<bool>) -> bool {
102    per_operation.unwrap_or_else(is_dry_run)
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use serial_test::serial;
109
110    #[test]
111    #[serial]
112    fn set_and_get() {
113        set_dry_run(true);
114        assert!(is_dry_run());
115        set_dry_run(false);
116        assert!(!is_dry_run());
117    }
118
119    #[test]
120    #[serial]
121    fn effective_uses_per_operation_when_set() {
122        set_dry_run(false);
123        assert!(effective_dry_run(Some(true)));
124
125        set_dry_run(true);
126        assert!(!effective_dry_run(Some(false)));
127        set_dry_run(false);
128    }
129
130    #[test]
131    #[serial]
132    fn effective_falls_back_to_global() {
133        set_dry_run(true);
134        assert!(effective_dry_run(None));
135
136        set_dry_run(false);
137        assert!(!effective_dry_run(None));
138    }
139
140    #[test]
141    #[serial]
142    fn guard_restores_previous_value() {
143        set_dry_run(false);
144        {
145            let _guard = DryRunGuard::new(true);
146            assert!(is_dry_run());
147        }
148        assert!(!is_dry_run());
149    }
150
151    #[test]
152    #[serial]
153    fn guard_nested_restores_correctly() {
154        set_dry_run(false);
155        {
156            let _outer = DryRunGuard::new(true);
157            assert!(is_dry_run());
158            {
159                let _inner = DryRunGuard::new(false);
160                assert!(!is_dry_run());
161            }
162            assert!(is_dry_run());
163        }
164        assert!(!is_dry_run());
165    }
166}