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}