_diffctx/config/
env_overrides.rs1pub fn parse_f64_or_default(raw: Option<String>, default: f64) -> f64 {
10 raw.and_then(|s| s.parse::<f64>().ok())
11 .filter(|v| v.is_finite() && *v >= 0.0)
12 .unwrap_or(default)
13}
14
15pub fn parse_fraction_or_default(raw: Option<String>, default: f64) -> f64 {
16 parse_f64_or_default(raw, default).clamp(0.0, 1.0)
17}
18
19pub fn parse_open_fraction_or_default(raw: Option<String>, default: f64) -> f64 {
23 const EPS: f64 = 1e-4;
24 parse_f64_or_default(raw, default).clamp(EPS, 1.0 - EPS)
25}
26
27pub fn parse_usize_or_default(raw: Option<String>, default: usize) -> usize {
28 raw.and_then(|s| s.parse::<usize>().ok()).unwrap_or(default)
29}
30
31pub fn parse_u32_or_default(raw: Option<String>, default: u32) -> u32 {
32 raw.and_then(|s| s.parse::<u32>().ok()).unwrap_or(default)
33}
34
35pub fn read_env_f64(name: &str, default: f64) -> f64 {
36 parse_f64_or_default(std::env::var(name).ok(), default)
37}
38
39pub fn read_env_fraction(name: &str, default: f64) -> f64 {
40 parse_fraction_or_default(std::env::var(name).ok(), default)
41}
42
43pub fn read_env_open_fraction(name: &str, default: f64) -> f64 {
44 parse_open_fraction_or_default(std::env::var(name).ok(), default)
45}
46
47pub fn read_env_usize(name: &str, default: usize) -> usize {
48 parse_usize_or_default(std::env::var(name).ok(), default)
49}
50
51pub fn read_env_u32(name: &str, default: u32) -> u32 {
52 parse_u32_or_default(std::env::var(name).ok(), default)
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn f64_accepts_finite_nonneg() {
61 assert_eq!(parse_f64_or_default(Some("0.42".into()), 1.0), 0.42);
62 assert_eq!(parse_f64_or_default(Some("0".into()), 1.0), 0.0);
63 }
64
65 #[test]
66 fn f64_rejects_negative_and_nonfinite() {
67 assert_eq!(parse_f64_or_default(Some("-0.5".into()), 1.0), 1.0);
68 assert_eq!(parse_f64_or_default(Some("nan".into()), 1.0), 1.0);
69 assert_eq!(parse_f64_or_default(Some("inf".into()), 1.0), 1.0);
70 }
71
72 #[test]
73 fn fraction_clamps_into_unit_interval() {
74 assert_eq!(parse_fraction_or_default(Some("0.5".into()), 0.7), 0.5);
75 assert_eq!(parse_fraction_or_default(Some("1.05".into()), 0.7), 1.0);
76 assert_eq!(parse_fraction_or_default(Some("42".into()), 0.7), 1.0);
77 assert_eq!(parse_fraction_or_default(Some("-0.5".into()), 0.7), 0.7);
78 assert_eq!(parse_fraction_or_default(Some("nan".into()), 0.7), 0.7);
79 assert_eq!(parse_fraction_or_default(None, 0.7), 0.7);
80 }
81
82 #[test]
83 fn open_fraction_clamps_to_open_interval() {
84 let v_one = parse_open_fraction_or_default(Some("1.0".into()), 0.6);
86 assert!(
87 v_one < 1.0,
88 "open fraction must clamp 1.0 below 1; got {v_one}"
89 );
90 assert!(
91 v_one > 0.99,
92 "clamp must stay near 1.0, not collapse to default"
93 );
94 let v_zero = parse_open_fraction_or_default(Some("0.0".into()), 0.6);
96 assert!(
97 v_zero > 0.0,
98 "open fraction must clamp 0.0 above 0; got {v_zero}"
99 );
100 assert_eq!(parse_open_fraction_or_default(Some("0.6".into()), 0.0), 0.6);
102 assert!(parse_open_fraction_or_default(Some("42".into()), 0.6) < 1.0);
104 }
105
106 #[test]
107 fn f64_falls_back_on_missing_or_unparseable() {
108 assert_eq!(parse_f64_or_default(None, 0.7), 0.7);
109 assert_eq!(parse_f64_or_default(Some("hello".into()), 0.7), 0.7);
110 assert_eq!(parse_f64_or_default(Some("".into()), 0.7), 0.7);
111 }
112
113 #[test]
114 fn usize_parses_or_falls_back() {
115 assert_eq!(parse_usize_or_default(Some("42".into()), 7), 42);
116 assert_eq!(parse_usize_or_default(Some("-1".into()), 7), 7);
117 assert_eq!(parse_usize_or_default(None, 7), 7);
118 }
119
120 #[test]
121 fn u32_parses_or_falls_back() {
122 assert_eq!(parse_u32_or_default(Some("24".into()), 8), 24);
123 assert_eq!(parse_u32_or_default(Some("nope".into()), 8), 8);
124 }
125}
126
127#[cfg(test)]
135mod override_name_consistency {
136 use std::collections::BTreeSet;
137 use std::path::{Path, PathBuf};
138
139 use regex::Regex;
140
141 fn crate_root() -> PathBuf {
142 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
143 }
144
145 fn extract(text: &str, re: &Regex) -> BTreeSet<String> {
146 re.captures_iter(text).map(|c| c[1].to_string()).collect()
147 }
148
149 fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
150 let Ok(entries) = std::fs::read_dir(dir) else {
151 return;
152 };
153 for entry in entries.flatten() {
154 let path = entry.path();
155 if path.is_dir() {
156 collect_rs_files(&path, out);
157 } else if path.extension().is_some_and(|e| e == "rs") {
158 out.push(path);
159 }
160 }
161 }
162
163 fn code_literal_set() -> BTreeSet<String> {
175 let re = Regex::new(
176 r#"read_env_(?:f64|fraction|open_fraction|usize|u32)\(\s*"(DIFFCTX_[A-Z0-9_]+)""#,
177 )
178 .unwrap();
179 let mut files = Vec::new();
180 collect_rs_files(&crate_root().join("src"), &mut files);
181 let mut set = BTreeSet::new();
182 for file in files {
183 if file.file_name().and_then(|n| n.to_str()) == Some("env_overrides.rs") {
184 continue;
185 }
186 let text =
187 std::fs::read_to_string(&file).unwrap_or_else(|e| panic!("reading {file:?}: {e}"));
188 set.extend(extract(&text, &re));
189 }
190 set
191 }
192
193 fn script_param_set() -> BTreeSet<String> {
194 let re = Regex::new(r#"\("(DIFFCTX_[A-Z0-9_]+)","#).unwrap();
195 let path = crate_root().join("../../scripts/sensitivity_check.py");
196 let text =
197 std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("reading {path:?}: {e}"));
198 extract(&text, &re)
199 }
200
201 fn documented_tier3_set() -> BTreeSet<String> {
207 let re = Regex::new(r"`(DIFFCTX_[A-Z0-9_]+)`").unwrap();
208 let path = crate_root().join("../../docs/engineering/parameter-strategy.md");
209 let text =
210 std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("reading {path:?}: {e}"));
211 let start = text
212 .find("### Tier 3")
213 .expect("parameter-strategy.md must have a '### Tier 3' section header");
214 let end = text[start..]
215 .find("The Tier-1 pair")
216 .map(|i| start + i)
217 .unwrap_or(text.len());
218 let table_rows: String = text[start..end]
219 .lines()
220 .filter(|line| line.trim_start().starts_with('|'))
221 .collect::<Vec<_>>()
222 .join("\n");
223 extract(&table_rows, &re)
224 }
225
226 const TIER1_EXTRAS_READ_BUT_NOT_TABLED: &[&str] =
232 &["DIFFCTX_OP_SELECTION_CORE_BUDGET_FRACTION"];
233
234 #[test]
235 fn every_script_swept_name_is_actually_read_in_code() {
236 let code = code_literal_set();
237 let script = script_param_set();
238 assert!(
239 script.len() >= 10,
240 "sensitivity_check.py's OPERATIONAL_PARAMS parsed too small ({}) \
241 -- the extraction regex likely drifted from the source format",
242 script.len()
243 );
244 let phantom: Vec<&String> = script.iter().filter(|n| !code.contains(*n)).collect();
245 assert!(
246 phantom.is_empty(),
247 "scripts/sensitivity_check.py sweeps names never read by any \
248 config module (dead sweep points, measuring the default at \
249 every perturbation): {phantom:?}"
250 );
251 }
252
253 #[test]
254 fn documented_tier3_names_match_the_code_reads_that_back_them() {
255 let code = code_literal_set();
256 let tier3_code: BTreeSet<String> = code
257 .into_iter()
258 .filter(|n| !TIER1_EXTRAS_READ_BUT_NOT_TABLED.contains(&n.as_str()))
259 .collect();
260 let documented = documented_tier3_set();
261 assert!(
262 documented.len() >= 10,
263 "parameter-strategy.md's Tier-3 table parsed too small ({}) \
264 -- the section markers or extraction regex likely drifted",
265 documented.len()
266 );
267 assert_eq!(
268 tier3_code, documented,
269 "crate's Tier-3 env-read set and parameter-strategy.md's table \
270 disagree -- a var was added/renamed/removed on one side only"
271 );
272 }
273}