Skip to main content

sentry_options/
testing.rs

1//! Testing utilities for overriding option values in tests.
2//!
3//! # Example
4//!
5//! ```rust,ignore
6//! use sentry_options::Options;
7//! use sentry_options::testing::override_options;
8//! use serde_json::json;
9//!
10//! #[test]
11//! fn test_feature() {
12//!     let opts = Options::from_directory(test_dir).unwrap();
13//!
14//!     let _guard = override_options(&[("seer", "feature.enabled", json!(true))]).unwrap();
15//!     assert_eq!(opts.get("seer", "feature.enabled").unwrap(), json!(true));
16//! }
17//! ```
18
19use std::cell::RefCell;
20use std::collections::HashMap;
21
22use serde_json::Value;
23
24use crate::{GLOBAL_OPTIONS, OptionsError, Result};
25
26/// Initialize options, ignoring "already initialized" errors.
27///
28/// This is useful in tests where multiple test functions may call this,
29/// but only the first one actually initializes.
30///
31/// # Errors
32///
33/// Returns an error if initialization fails for reasons other than
34/// already being initialized (e.g., schema loading errors).
35///
36/// # Example
37///
38/// ```rust,ignore
39/// use sentry_options::testing::ensure_initialized;
40///
41/// #[test]
42/// fn test_something() {
43///     ensure_initialized().unwrap();
44///     // ... test code
45/// }
46/// ```
47pub fn ensure_initialized() -> Result<()> {
48    match crate::init() {
49        Ok(()) => Ok(()),
50        Err(OptionsError::AlreadyInitialized) => Ok(()),
51        Err(e) => Err(e),
52    }
53}
54
55thread_local! {
56    static OVERRIDES: RefCell<HashMap<String, HashMap<String, Value>>> = RefCell::new(HashMap::new());
57}
58
59/// Set an override value for a specific namespace and key.
60pub fn set_override(namespace: &str, key: &str, value: Value) {
61    OVERRIDES.with(|o| {
62        o.borrow_mut()
63            .entry(namespace.to_string())
64            .or_default()
65            .insert(key.to_string(), value);
66    });
67}
68
69/// Get an override value if one exists.
70pub fn get_override(namespace: &str, key: &str) -> Option<Value> {
71    OVERRIDES.with(|o| {
72        o.borrow()
73            .get(namespace)
74            .and_then(|ns| ns.get(key).cloned())
75    })
76}
77
78/// Clear an override for a specific namespace and key.
79pub fn clear_override(namespace: &str, key: &str) {
80    OVERRIDES.with(|o| {
81        if let Some(ns_map) = o.borrow_mut().get_mut(namespace) {
82            ns_map.remove(key);
83        }
84    });
85}
86
87/// Guard that restores overrides when dropped.
88pub struct OverrideGuard {
89    previous: Vec<(String, String, Option<Value>)>,
90}
91
92impl Drop for OverrideGuard {
93    fn drop(&mut self) {
94        OVERRIDES.with(|o| {
95            let mut map = o.borrow_mut();
96            for (ns, key, prev_value) in self.previous.drain(..) {
97                match prev_value {
98                    Some(v) => {
99                        map.entry(ns).or_default().insert(key, v);
100                    }
101                    None => {
102                        if let Some(ns_map) = map.get_mut(&ns) {
103                            ns_map.remove(&key);
104                        }
105                    }
106                }
107            }
108        });
109    }
110}
111
112/// Set overrides for the lifetime of the returned guard.
113///
114/// Requires that options have been initialized via `init()` or `ensure_initialized()`.
115/// Validates that each key exists in the schema and the value matches the expected type.
116/// When the guard is dropped (goes out of scope), the overrides are restored
117/// to their previous values.
118///
119/// # Panics
120///
121/// Panics if options have not been initialized.
122///
123/// # Note
124///
125/// Overrides are thread-local. They won't apply to spawned threads.
126///
127/// # Errors
128///
129/// Returns an error if:
130/// - Any namespace doesn't exist
131/// - Any key doesn't exist in the schema
132/// - Any value doesn't match the expected type
133///
134/// # Example
135///
136/// ```rust,ignore
137/// use sentry_options::testing::{ensure_initialized, override_options};
138/// use serde_json::json;
139///
140/// ensure_initialized().unwrap();
141/// let _guard = override_options(&[
142///     ("namespace", "key1", json!(true)),
143///     ("namespace", "key2", json!(42)),
144/// ]).unwrap();
145/// // overrides are active here
146/// // when _guard goes out of scope, overrides are restored
147/// ```
148pub fn override_options(overrides: &[(&str, &str, Value)]) -> Result<OverrideGuard> {
149    // Validate all overrides before applying any
150    let opts = GLOBAL_OPTIONS
151        .get()
152        .expect("options not initialized - call init() or ensure_initialized() first");
153    for (ns, key, value) in overrides {
154        opts.validate_override(ns, key, value)?;
155    }
156
157    let mut previous = Vec::with_capacity(overrides.len());
158
159    OVERRIDES.with(|o| {
160        let mut map = o.borrow_mut();
161        for (ns, key, value) in overrides {
162            let prev = map.get(*ns).and_then(|m| m.get(*key).cloned());
163            previous.push((ns.to_string(), key.to_string(), prev));
164            map.entry(ns.to_string())
165                .or_default()
166                .insert(key.to_string(), value.clone());
167        }
168    });
169
170    Ok(OverrideGuard { previous })
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use serde_json::json;
177
178    #[test]
179    fn test_set_get_clear_override() {
180        set_override("ns", "key", json!(true));
181        assert_eq!(get_override("ns", "key"), Some(json!(true)));
182        clear_override("ns", "key");
183        assert_eq!(get_override("ns", "key"), None);
184    }
185
186    #[test]
187    fn test_override_guard_restores() {
188        ensure_initialized().unwrap();
189        set_override("sentry-options-testing", "int-option", json!(1));
190
191        {
192            let _guard =
193                override_options(&[("sentry-options-testing", "int-option", json!(2))]).unwrap();
194            assert_eq!(
195                get_override("sentry-options-testing", "int-option"),
196                Some(json!(2))
197            );
198        }
199
200        assert_eq!(
201            get_override("sentry-options-testing", "int-option"),
202            Some(json!(1))
203        );
204        clear_override("sentry-options-testing", "int-option");
205    }
206
207    #[test]
208    fn test_override_guard_clears_new_key() {
209        ensure_initialized().unwrap();
210        assert_eq!(get_override("sentry-options-testing", "bool-option"), None);
211
212        {
213            let _guard =
214                override_options(&[("sentry-options-testing", "bool-option", json!(true))])
215                    .unwrap();
216            assert_eq!(
217                get_override("sentry-options-testing", "bool-option"),
218                Some(json!(true))
219            );
220        }
221
222        assert_eq!(get_override("sentry-options-testing", "bool-option"), None);
223    }
224
225    #[test]
226    fn test_nested_overrides() {
227        ensure_initialized().unwrap();
228        {
229            let _outer =
230                override_options(&[("sentry-options-testing", "int-option", json!(100))]).unwrap();
231            assert_eq!(
232                get_override("sentry-options-testing", "int-option"),
233                Some(json!(100))
234            );
235
236            {
237                let _inner =
238                    override_options(&[("sentry-options-testing", "int-option", json!(200))])
239                        .unwrap();
240                assert_eq!(
241                    get_override("sentry-options-testing", "int-option"),
242                    Some(json!(200))
243                );
244            }
245
246            assert_eq!(
247                get_override("sentry-options-testing", "int-option"),
248                Some(json!(100))
249            );
250        }
251
252        assert_eq!(get_override("sentry-options-testing", "int-option"), None);
253    }
254}