cranpose_render_common/
debug_toggles.rs1use std::{
2 collections::BTreeMap,
3 ffi::{OsStr, OsString},
4 str::FromStr,
5 sync::{
6 Mutex, OnceLock, PoisonError,
7 atomic::{AtomicU64, Ordering},
8 },
9};
10
11static GENERATION: AtomicU64 = AtomicU64::new(0);
12
13pub struct DebugToggle {
16 name: &'static str,
17 cached: Mutex<Option<(u64, Option<String>)>>,
18 absent_generation: AtomicU64,
19}
20
21impl DebugToggle {
22 pub const fn new(name: &'static str) -> Self {
23 Self {
24 name,
25 cached: Mutex::new(None),
26 absent_generation: AtomicU64::new(u64::MAX),
27 }
28 }
29
30 pub fn with<R>(&self, read: impl FnOnce(Option<&str>) -> R) -> R {
32 let generation = GENERATION.load(Ordering::Acquire);
33 if self.absent_generation.load(Ordering::Acquire) == generation {
34 return read(None);
35 }
36 let mut cached = self.cached.lock().unwrap_or_else(PoisonError::into_inner);
37 if cached.as_ref().is_none_or(|(seen, _)| *seen != generation) {
38 *cached = Some((generation, debug_toggle(self.name)));
39 }
40 let value = cached.as_ref().and_then(|(_, value)| value.as_deref());
41 if value.is_none() {
42 self.absent_generation.store(generation, Ordering::Release);
43 }
44 read(value)
45 }
46
47 pub fn is_set(&self) -> bool {
49 self.with(|value| value.is_some())
50 }
51
52 pub fn flag(&self) -> bool {
54 self.with(|value| matches!(value, Some("1" | "true" | "yes")))
55 }
56
57 pub fn equals(&self, expected: &str) -> bool {
59 self.with(|value| value == Some(expected))
60 }
61
62 pub fn parse<T: FromStr>(&self) -> Option<T> {
64 self.with(|value| value.and_then(|value| value.parse().ok()))
65 }
66}
67
68fn overrides() -> &'static Mutex<BTreeMap<&'static str, OsString>> {
69 static OVERRIDES: OnceLock<Mutex<BTreeMap<&'static str, OsString>>> = OnceLock::new();
70 OVERRIDES.get_or_init(|| Mutex::new(BTreeMap::new()))
71}
72
73#[doc(hidden)]
74pub fn debug_toggle(name: &'static str) -> Option<String> {
75 let map = overrides().lock().unwrap_or_else(PoisonError::into_inner);
76 if let Some(value) = map.get(name) {
77 return value.to_str().map(str::to_owned);
78 }
79 drop(map);
80 std::env::var(name).ok()
81}
82
83#[doc(hidden)]
84pub fn debug_toggle_os(name: &'static str) -> Option<OsString> {
85 let map = overrides().lock().unwrap_or_else(PoisonError::into_inner);
86 if let Some(value) = map.get(name) {
87 return Some(value.clone());
88 }
89 drop(map);
90 std::env::var_os(name)
91}
92
93#[doc(hidden)]
94pub fn set_debug_toggle(name: &'static str, value: Option<&str>) {
95 set_debug_toggle_os(name, value.map(OsStr::new));
96}
97
98#[doc(hidden)]
99pub fn set_debug_toggle_os(name: &'static str, value: Option<&OsStr>) {
100 let mut map = overrides().lock().unwrap_or_else(PoisonError::into_inner);
101 match value {
102 Some(value) => map.insert(name, value.to_owned()),
103 None => map.remove(name),
104 };
105 GENERATION.fetch_add(1, Ordering::Release);
106}
107
108#[cfg(all(test, unix))]
109mod tests {
110 use std::os::unix::ffi::OsStrExt;
111
112 use super::*;
113
114 #[test]
115 fn a_non_utf8_path_override_survives_byte_for_byte() {
116 let raw = OsStr::from_bytes(b"/cranpose/\xff\xfe/cache.bin");
117 set_debug_toggle_os("CRANPOSE_TOGGLE_OS_TEST", Some(raw));
118 assert_eq!(
119 debug_toggle_os("CRANPOSE_TOGGLE_OS_TEST").as_deref(),
120 Some(raw)
121 );
122 assert_eq!(debug_toggle("CRANPOSE_TOGGLE_OS_TEST"), None);
123 set_debug_toggle_os("CRANPOSE_TOGGLE_OS_TEST", None);
124 assert_eq!(debug_toggle_os("CRANPOSE_TOGGLE_OS_TEST"), None);
125 }
126
127 #[test]
128 fn an_unset_toggle_observes_overrides_from_another_thread() {
129 let toggle = std::sync::Arc::new(DebugToggle::new("CRANPOSE_TOGGLE_THREAD_TEST"));
130 assert!(!toggle.is_set());
131 let reader = std::sync::Arc::clone(&toggle);
132 std::thread::spawn(move || {
133 set_debug_toggle("CRANPOSE_TOGGLE_THREAD_TEST", Some("1"));
134 assert!(reader.flag());
135 assert_eq!(reader.parse::<u32>(), Some(1));
136 })
137 .join()
138 .expect("override thread");
139 assert!(toggle.equals("1"));
140 set_debug_toggle("CRANPOSE_TOGGLE_THREAD_TEST", None);
141 assert!(!toggle.is_set());
142 assert_eq!(toggle.parse::<u32>(), None);
143 }
144
145 #[test]
146 fn a_cached_toggle_follows_every_override() {
147 static TOGGLE: DebugToggle = DebugToggle::new("CRANPOSE_TOGGLE_CACHE_TEST");
148 assert!(!TOGGLE.is_set());
149 set_debug_toggle("CRANPOSE_TOGGLE_CACHE_TEST", Some("1"));
150 assert!(TOGGLE.flag());
151 assert!(TOGGLE.equals("1"));
152 assert_eq!(TOGGLE.parse::<u32>(), Some(1));
153 set_debug_toggle("CRANPOSE_TOGGLE_CACHE_TEST", Some("frame"));
154 assert!(!TOGGLE.flag());
155 assert!(TOGGLE.equals("frame"));
156 assert_eq!(TOGGLE.parse::<u32>(), None);
157 set_debug_toggle("CRANPOSE_TOGGLE_CACHE_TEST", None);
158 assert!(!TOGGLE.is_set());
159 }
160}