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