epics_libcom_rs/runtime/env.rs
1//! EPICS environment parameters — C's `envSubr.c`.
2//!
3//! The **only** declaration of an EPICS environment default in this workspace is
4//! the generated [`super::env_table`], which `env-codegen` derives from the
5//! vendored `configure/CONFIG_ENV` + `CONFIG_SITE_ENV` the same way C's
6//! `bldEnvData.pl` derives `envData.c`. Everything else resolves *through* it.
7//!
8//! That is enforced by shape, not by convention: [`EnvParam`] has no public
9//! constructor (only the generated table, inside this crate, can mint one), and
10//! none of its accessors takes a `default` argument. A caller that could supply
11//! a default would be a second declaration source, so no caller can.
12//!
13//! [`get`] remains for the handful of variables that are NOT EPICS Base
14//! `ENV_PARAM`s — C reads those with a plain `getenv` and its caller owns the
15//! fallback, so this port does too.
16
17use super::stdlib::epics_parse_double;
18
19/// One row of C's `ENV_PARAM` table (`envDefs.h:41-45`): a parameter name and
20/// the default compiled in from `configure/CONFIG_ENV`.
21///
22/// Values of this type exist only in the generated [`super::env_table`].
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct EnvParam {
25 name: &'static str,
26 default: &'static str,
27}
28
29impl EnvParam {
30 /// Crate-private: the generated table is the only declaration source.
31 pub(crate) const fn new(name: &'static str, default: &'static str) -> Self {
32 Self { name, default }
33 }
34
35 pub const fn name(&self) -> &'static str {
36 self.name
37 }
38
39 /// The compiled-in default string, exactly as it appears in C's
40 /// `envData.c`.
41 pub const fn default_str(&self) -> &'static str {
42 self.default
43 }
44
45 /// C `envGetConfigParamPtr` (`envSubr.c:81-100`): the environment string,
46 /// falling back to the compiled default — with an **empty** result mapped to
47 /// `None`.
48 ///
49 /// The `None`/`Some` answer is a load-bearing *presence* test, not just an
50 /// absence check: `caservertask.c:491-508` uses it to pick *which* of two
51 /// parameters to resolve, so `FOO=` must read as "not configured".
52 pub fn get(&self) -> Option<String> {
53 let v = std::env::var(self.name).unwrap_or_else(|_| self.default.to_string());
54 if v.is_empty() { None } else { Some(v) }
55 }
56
57 /// C `envGetLongConfigParam` (`envSubr.c:303-320`) — `sscanf("%ld")` over the
58 /// resolved value, out of the first 127 bytes (all C's `char text[128]`
59 /// keeps).
60 ///
61 /// `None` is C's non-zero status. A set-but-unparseable value also prints
62 /// C's stderr line first; an *unresolvable* one (empty value and empty
63 /// default) prints nothing, because `envGetConfigParam` returned NULL before
64 /// `sscanf` ever ran.
65 pub fn long(&self) -> Option<i64> {
66 let text = self.get()?;
67 let text = truncate_to_c_buffer(&text);
68 match sscanf_long(text) {
69 Some(v) => Some(v),
70 None => {
71 eprintln!("Unable to find an integer in {}={text}", self.name);
72 None
73 }
74 }
75 }
76
77 /// The C caller idiom for a long-valued parameter:
78 ///
79 /// ```c
80 /// long i = 50; /* == the table default */
81 /// envGetLongConfigParam(&IOCSH_HISTSIZE, &i);
82 /// ```
83 ///
84 /// i.e. a rejected value leaves the compiled default standing. The `50` in
85 /// C's source is a hand-copy of the table; here it comes from the table.
86 pub fn long_or_default(&self) -> i64 {
87 self.long()
88 .or_else(|| sscanf_long(self.default))
89 .unwrap_or(0)
90 }
91
92 /// C `envGetDoubleConfigParam` (`envSubr.c:191-211`) — `epicsScanDouble` over
93 /// the resolved value.
94 ///
95 /// An unset parameter resolves to its compiled default and parses, so
96 /// `Ok(30.0)` for `EPICS_CA_CONN_TMO` on a clean shell. `Err` is C's non-zero
97 /// status: the value (or the default) is empty, or it did not scan — and in
98 /// the latter case C's stderr line has already been printed. C's callers then
99 /// print their own named diagnostic on top of it.
100 pub fn double(&self) -> Result<f64, EnvDoubleError> {
101 let raw = self.get().ok_or(EnvDoubleError::Unresolvable)?;
102 epics_parse_double(&raw).map_err(|_| {
103 eprintln!("Unable to find a real number in {}={raw}", self.name);
104 EnvDoubleError::Invalid
105 })
106 }
107
108 /// C `envGetBoolConfigParam` (`envSubr.c:324-333`):
109 /// `*pBool = epicsStrCaseCmp(text, "yes") == 0`, i.e. true **iff** the
110 /// resolved value is the word `yes`, any case. `"1"`, `"true"`, `"on"`,
111 /// `"yes "` are all false.
112 ///
113 /// `None` is C's -1 status — nothing to compare, so C leaves the caller's
114 /// variable untouched. The caller's `unwrap_or(..)` is then C's own
115 /// initializer, not a second copy of the table default.
116 pub fn bool(&self) -> Option<bool> {
117 Some(self.get()?.eq_ignore_ascii_case("yes"))
118 }
119
120 /// C `envGetInetPortConfigParam` (`envSubr.c:397-424`).
121 ///
122 /// `fallback` is the *compiled* port the C caller passes as `defaultPort`
123 /// — for the `EPICS_CAS_*` overrides that is a **different** parameter's
124 /// default (`caservertask.c:491-508` passes `CA_SERVER_PORT`), which is why
125 /// it is an argument rather than `self.default`. Derive it with
126 /// [`EnvParam::default_port`] so it still comes from the table.
127 ///
128 /// - Not resolvable, or no integer found: the "integer fetch failed"
129 /// diagnostics, then `fallback`.
130 /// - `<= IPPORT_USERRESERVED` (5000) or `> USHRT_MAX`: the "out of range"
131 /// diagnostics, then `fallback`.
132 ///
133 /// The diagnostics are byte-identical to compiled C, once per resolution —
134 /// there is no dedup in C:
135 ///
136 /// ```text
137 /// Unable to find an integer in EPICS_CA_SERVER_PORT=abc
138 /// EPICS Environment "EPICS_CA_SERVER_PORT" integer fetch failed
139 /// setting "EPICS_CA_SERVER_PORT" = 5064
140 /// ```
141 /// ```text
142 /// EPICS Environment "EPICS_CA_SERVER_PORT" out of range
143 /// Setting "EPICS_CA_SERVER_PORT" = 5064
144 /// ```
145 pub fn inet_port(&self, fallback: u16) -> u16 {
146 let mut port = match self.long() {
147 Some(v) => v,
148 None => {
149 eprintln!("EPICS Environment \"{}\" integer fetch failed", self.name);
150 eprintln!("setting \"{}\" = {fallback}", self.name);
151 i64::from(fallback)
152 }
153 };
154
155 if port <= i64::from(IPPORT_USERRESERVED) || port > i64::from(u16::MAX) {
156 eprintln!("EPICS Environment \"{}\" out of range", self.name);
157 port = i64::from(fallback);
158 eprintln!("Setting \"{}\" = {fallback}", self.name);
159 }
160
161 // Range-checked above, so the cast cannot truncate.
162 port as u16
163 }
164
165 /// The compiled default read as a port, at compile time.
166 ///
167 /// This is how a `pub const CA_SERVER_PORT: u16` is derived from the table
168 /// instead of hand-written next to it: a default that is not a valid port
169 /// fails const-evaluation, so the constant cannot drift from `CONFIG_ENV`.
170 pub const fn default_port(&self) -> u16 {
171 let bytes = self.default.as_bytes();
172 assert!(!bytes.is_empty(), "parameter has no compiled default port");
173 let mut i = 0;
174 let mut value: u32 = 0;
175 while i < bytes.len() {
176 let d = bytes[i];
177 assert!(d.is_ascii_digit(), "compiled default is not a port number");
178 value = value * 10 + (d - b'0') as u32;
179 assert!(
180 value <= u16::MAX as u32,
181 "compiled default exceeds USHRT_MAX"
182 );
183 i += 1;
184 }
185 value as u16
186 }
187
188 /// C `envPrtConfigParam` (`envSubr.c:353-364`) — one line of
189 /// `epicsPrtEnvParams`.
190 pub fn describe(&self) -> String {
191 match self.get() {
192 Some(v) => format!("{}: {v}", self.name),
193 None => format!("{} is undefined", self.name),
194 }
195 }
196}
197
198/// Why [`EnvParam::double`] did not yield a value — C's non-zero status.
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum EnvDoubleError {
201 /// The value and the compiled default are both empty, so
202 /// `envGetConfigParam` returned NULL before `epicsScanDouble` ran. No
203 /// diagnostic.
204 Unresolvable,
205 /// `epicsScanDouble` rejected the value. C's stderr line is already printed.
206 Invalid,
207}
208
209/// A plain `getenv`, no default and no `ENV_PARAM` behind it.
210///
211/// Only for variables that are NOT in C's table (`EPICS_CA_USE_SHELL_VARS`, the
212/// pvxs `EPICS_PVA*` set, the port's own `EPICS_RS_*`); C reads those with
213/// `getenv` too and its caller owns the fallback. Every EPICS Base `ENV_PARAM`
214/// must go through [`super::env_table`].
215pub fn get(key: &str) -> Option<String> {
216 std::env::var(key).ok()
217}
218
219/// Lowest port number a configurable EPICS port may take.
220///
221/// C parity: `IPPORT_USERRESERVED` is `#define`d to `5000` on every
222/// supported platform (`osi/os/*/osdSock.h`). `envGetInetPortConfigParam`
223/// rejects any port `<= IPPORT_USERRESERVED` or `> USHRT_MAX`.
224pub const IPPORT_USERRESERVED: u32 = 5000;
225
226/// C `envGetConfigParam` copies into `char text[128]` with `strncpy`, so only
227/// the first 127 bytes ever reach `sscanf`.
228fn truncate_to_c_buffer(raw: &str) -> &str {
229 let mut end = raw.len().min(127);
230 while !raw.is_char_boundary(end) {
231 end -= 1;
232 }
233 &raw[..end]
234}
235
236/// Parse the leading integer out of a string the way C `sscanf("%ld")`
237/// does: skip leading whitespace, accept an optional `+`/`-` sign, read
238/// the run of decimal digits, and ignore any trailing garbage suffix.
239///
240/// Returns `None` when no digit is found (C `sscanf` returns 0 matches).
241fn sscanf_long(text: &str) -> Option<i64> {
242 let bytes = text.as_bytes();
243 let mut i = 0;
244 // Skip leading whitespace (C `isspace`: space, \t, \n, \v, \f, \r).
245 while i < bytes.len() && crate::runtime::stdlib::c_isspace(bytes[i] as char) {
246 i += 1;
247 }
248 let mut neg = false;
249 if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
250 neg = bytes[i] == b'-';
251 i += 1;
252 }
253 let start = i;
254 let mut value: i64 = 0;
255 while i < bytes.len() && bytes[i].is_ascii_digit() {
256 value = value
257 .saturating_mul(10)
258 .saturating_add((bytes[i] - b'0') as i64);
259 i += 1;
260 }
261 if i == start {
262 // No digits — sscanf matched nothing.
263 return None;
264 }
265 Some(if neg { -value } else { value })
266}
267
268/// C `epicsPrtEnvParams` (`envSubr.c:383-392`) — every parameter EPICS Base
269/// knows, with its effective value, in `env_param_list[]` order.
270pub fn prt_env_params() -> impl Iterator<Item = String> {
271 super::env_table::ENV_PARAM_LIST
272 .iter()
273 .map(EnvParam::describe)
274}
275
276/// C `iocshRegisterCommon` (`iocshRegisterCommon.c:54-67`) — the environment
277/// variables an IOC shell publishes about *itself*, so a `.db`, a `.substitutions`
278/// or an `st.cmd` can expand `$(EPICS_VERSION_FULL)` or `$(ARCH)`.
279///
280/// These are outputs, not inputs: they are not `ENV_PARAM`s, they have no
281/// compiled default to fall back to, and C sets them with `epicsEnvSet`, which
282/// overwrites whatever the shell had. So does this — but only once per process,
283/// because a re-entrant `set_var` is what makes it unsound.
284///
285/// `ARCH` is C's `envGetConfigParam(&EPICS_BUILD_TARGET_ARCH)`, which is the
286/// build that produced the binary; here that is [`super::build_info`], reached
287/// through the generated table like every other parameter.
288pub fn register_iocsh_env_vars() {
289 use super::version as v;
290 static ONCE: std::sync::Once = std::sync::Once::new();
291 ONCE.call_once(|| {
292 // C: `if (targetArch) epicsEnvSet("ARCH", targetArch)` — an unresolvable
293 // parameter is a NULL, and NULL is not published as an empty `ARCH`.
294 let mut vars: Vec<(&str, String)> = super::env_table::EPICS_BUILD_TARGET_ARCH
295 .get()
296 .map(|arch| ("ARCH", arch))
297 .into_iter()
298 .collect();
299 vars.extend([
300 ("EPICS_VERSION_MAJOR", v::EPICS_VERSION.to_string()),
301 ("EPICS_VERSION_MIDDLE", v::EPICS_REVISION.to_string()),
302 ("EPICS_VERSION_MINOR", v::EPICS_MODIFICATION.to_string()),
303 ("EPICS_VERSION_PATCH", v::EPICS_PATCH_LEVEL.to_string()),
304 ("EPICS_VERSION_SNAPSHOT", v::EPICS_DEV_SNAPSHOT.to_string()),
305 ("EPICS_VERSION_SITE", v::EPICS_SITE_VERSION.to_string()),
306 ("EPICS_VERSION_SHORT", v::EPICS_VERSION_SHORT.to_string()),
307 ("EPICS_VERSION_FULL", v::EPICS_VERSION_FULL.to_string()),
308 ]);
309 for (name, value) in vars {
310 // SAFETY: `Once`, and called from `IocShell::new` — C registers these
311 // at the same point, before any script or record can read them.
312 unsafe { std::env::set_var(name, value) };
313 }
314 });
315}
316
317/// Set an environment variable only if it is not already set.
318///
319/// # Safety
320/// Uses `std::env::set_var` which is unsafe in multi-threaded programs.
321/// Call this early in main(), before spawning threads.
322pub fn set_default(name: &str, value: &str) {
323 if std::env::var_os(name).is_none() {
324 // SAFETY: called during IOC startup, before multi-threaded operation.
325 unsafe { std::env::set_var(name, value) };
326 }
327}
328
329/// Set an environment variable to a path relative to a crate's `CARGO_MANIFEST_DIR`.
330///
331/// `manifest_dir` must be the manifest dir of the crate that *owns* the assets,
332/// and `relative` a path beneath it. Reaching out of the crate (`../other-crate`)
333/// produces a path that only exists in a sibling-checkout layout: under a
334/// registry checkout the sibling is version-suffixed (`ad-core-rs-0.22.1`) and
335/// the path never resolves. To point at another crate's assets, use the dir that
336/// crate exports (e.g. `ad_core_rs::AD_CORE_DIR`, `motor_rs::MOTOR_IOC_DIR`).
337///
338/// Usage:
339/// ```ignore
340/// // In a binary crate, expose that crate's own db/ directory:
341/// epics_base_rs::runtime::env::set_crate_path("MYIOC", env!("CARGO_MANIFEST_DIR"), "db");
342/// ```
343pub fn set_crate_path(name: &str, manifest_dir: &str, relative: &str) {
344 set_default(name, &format!("{manifest_dir}/{relative}"));
345}
346
347pub fn hostname() -> String {
348 hostname::get()
349 .ok()
350 .and_then(|s| s.into_string().ok())
351 .unwrap_or_else(|| "localhost".to_string())
352}
353
354#[cfg(test)]
355mod tests {
356 use super::super::env_table::*;
357 use super::*;
358 use serial_test::serial;
359
360 /// A scratch parameter: the accessors are the unit under test, and the
361 /// generated table is not the place to add fixtures.
362 const SCRATCH: EnvParam = EnvParam::new("_EPICS_RT_TEST_VAR", "");
363 const SCRATCH_PORT: EnvParam = EnvParam::new("_EPICS_RT_TEST_PORT", "5064");
364
365 fn clear(p: EnvParam) {
366 // SAFETY: every test that mutates env is `#[serial(epics_env)]`.
367 unsafe { std::env::remove_var(p.name()) };
368 }
369 fn set(p: EnvParam, v: &str) {
370 // SAFETY: see `clear`.
371 unsafe { std::env::set_var(p.name(), v) };
372 }
373
374 /// The state every compiled-default assertion needs: nothing the generated
375 /// table knows about is set in the process environment. Restored on drop.
376 ///
377 /// Hand-picked `clear` calls cannot establish it, because the list that is
378 /// cleared and the list that is asserted on are then maintained apart:
379 /// `unset_resolves_to_the_compiled_default` cleared three parameters and
380 /// asserted on five, so a developer shell exporting
381 /// `EPICS_CA_AUTO_ADDR_LIST=NO` — which a shell that sets
382 /// `EPICS_CA_ADDR_LIST` normally does — failed it, on a table the test
383 /// never doubted. Walking `ENV_PARAM_LIST` deletes the second list:
384 /// whatever a test asserts on, the table has already covered.
385 #[must_use]
386 struct SilentEnv(Vec<(&'static str, String)>);
387
388 impl SilentEnv {
389 fn take() -> Self {
390 let saved = ENV_PARAM_LIST
391 .iter()
392 .filter_map(|p| std::env::var(p.name()).ok().map(|v| (p.name(), v)))
393 .collect();
394 for p in ENV_PARAM_LIST {
395 clear(*p);
396 }
397 Self(saved)
398 }
399 }
400
401 impl Drop for SilentEnv {
402 fn drop(&mut self) {
403 for (name, value) in &self.0 {
404 // SAFETY: see `clear`.
405 unsafe { std::env::set_var(name, value) };
406 }
407 }
408 }
409
410 #[test]
411 fn test_get_missing() {
412 assert_eq!(get("_EPICS_RT_NONEXISTENT_VAR_12345"), None);
413 }
414
415 #[test]
416 fn test_sscanf_long_lenient_parsing() {
417 // C `sscanf("%ld")` semantics: leading whitespace skipped,
418 // sign accepted, trailing garbage tolerated.
419 assert_eq!(sscanf_long("5064"), Some(5064));
420 assert_eq!(sscanf_long(" 6064"), Some(6064));
421 assert_eq!(sscanf_long("\t6064\n"), Some(6064));
422 assert_eq!(sscanf_long("5064abc"), Some(5064));
423 assert_eq!(sscanf_long("+6064"), Some(6064));
424 assert_eq!(sscanf_long("-1"), Some(-1));
425 assert_eq!(sscanf_long("not_a_number"), None);
426 assert_eq!(sscanf_long(""), None);
427 assert_eq!(sscanf_long(" "), None);
428 }
429
430 /// C `sscanf` skips leading whitespace by `isspace`, which includes the
431 /// vertical tab (`envSubr.c:315`). `EPICS_CA_SERVER_PORT=$'\v5064'`
432 /// therefore resolves to 5064 on a C IOC; reading no integer here would
433 /// discard the operator's port and silently bind the compiled default.
434 #[test]
435 fn a_leading_vertical_tab_is_whitespace_as_it_is_to_c() {
436 assert_eq!(sscanf_long("\u{0b}5064"), Some(5064));
437 assert_eq!(sscanf_long("\u{0b}\t \u{0c}-1"), Some(-1));
438 assert_eq!(sscanf_long("\u{0b}"), None);
439 }
440
441 /// The compiled default resolves when the environment is silent — the whole
442 /// point of the table.
443 #[test]
444 #[serial(epics_env)]
445 fn unset_resolves_to_the_compiled_default() {
446 let _silent = SilentEnv::take();
447 assert_eq!(EPICS_CA_CONN_TMO.double(), Ok(30.0));
448 assert_eq!(EPICS_CA_SERVER_PORT.long(), Some(5064));
449 assert_eq!(EPICS_CA_AUTO_ADDR_LIST.bool(), Some(true));
450 // Empty compiled default: `envGetConfigParamPtr` answers NULL.
451 assert_eq!(EPICS_CA_ADDR_LIST.get(), None);
452 assert_eq!(EPICS_CAS_SERVER_PORT.get(), None);
453 }
454
455 /// `default_port()` is const-evaluated, so this is really a compile-time
456 /// assertion that the table still holds C's ports.
457 #[test]
458 fn default_port_is_const_derived_from_the_table() {
459 const SERVER: u16 = EPICS_CA_SERVER_PORT.default_port();
460 const REPEATER: u16 = EPICS_CA_REPEATER_PORT.default_port();
461 const LOG: u16 = EPICS_IOC_LOG_PORT.default_port();
462 assert_eq!((SERVER, REPEATER, LOG), (5064, 5065, 7004));
463 }
464
465 #[test]
466 #[serial(epics_env)]
467 fn test_env_inet_port_valid() {
468 set(SCRATCH_PORT, "8080");
469 assert_eq!(SCRATCH_PORT.inet_port(5064), 8080);
470 clear(SCRATCH_PORT);
471 }
472
473 #[test]
474 #[serial(epics_env)]
475 fn test_env_inet_port_invalid_and_missing() {
476 set(SCRATCH_PORT, "not_a_number");
477 assert_eq!(SCRATCH_PORT.inet_port(5064), 5064);
478 clear(SCRATCH_PORT);
479 // Unset: the compiled "5064" default parses, silently.
480 assert_eq!(SCRATCH_PORT.inet_port(5064), 5064);
481 }
482
483 /// An empty value is "not configured" for `envGetConfigParamPtr`, so
484 /// `envGetLongConfigParam` fails and the port falls back to `fallback`.
485 #[test]
486 #[serial(epics_env)]
487 fn test_env_inet_port_empty_value_is_a_failed_fetch() {
488 set(SCRATCH, "");
489 assert_eq!(SCRATCH.get(), None);
490 assert_eq!(SCRATCH.inet_port(5064), 5064);
491 clear(SCRATCH);
492 }
493
494 #[test]
495 #[serial(epics_env)]
496 fn test_env_inet_port_lenient_whitespace_and_suffix() {
497 // C `sscanf("%ld")` accepts leading whitespace + trailing junk;
498 // `u16::parse` would reject both.
499 set(SCRATCH_PORT, " 6064");
500 assert_eq!(SCRATCH_PORT.inet_port(5064), 6064);
501 set(SCRATCH_PORT, "6064abc");
502 assert_eq!(SCRATCH_PORT.inet_port(5064), 6064);
503 clear(SCRATCH_PORT);
504 }
505
506 /// C copies the value into `char text[128]`, so a digit run that only
507 /// starts past byte 127 is invisible to `sscanf`.
508 #[test]
509 #[serial(epics_env)]
510 fn test_env_inet_port_truncates_at_the_c_buffer_length() {
511 set(SCRATCH_PORT, &format!("{}6064", " ".repeat(130)));
512 assert_eq!(SCRATCH_PORT.inet_port(5064), 5064);
513 // The same digits inside the first 127 bytes are seen.
514 set(SCRATCH_PORT, &format!("{}6064", " ".repeat(100)));
515 assert_eq!(SCRATCH_PORT.inet_port(5064), 6064);
516 clear(SCRATCH_PORT);
517 }
518
519 #[test]
520 #[serial(epics_env)]
521 fn test_env_inet_port_rejects_reserved_and_out_of_range_ports() {
522 // C `envGetInetPortConfigParam` rejects port <= IPPORT_USERRESERVED
523 // (5000) or > USHRT_MAX, falling back to the compiled default.
524 for bad in [
525 "0", "1", "80", "443", "3000", "5000", "-1", "65536", "70000", "99999",
526 ] {
527 set(SCRATCH_PORT, bad);
528 assert_eq!(
529 SCRATCH_PORT.inet_port(5064),
530 5064,
531 "out-of-range port {bad:?} must fall back to default"
532 );
533 }
534 // 5001 is the first acceptable port (> IPPORT_USERRESERVED).
535 set(SCRATCH_PORT, "5001");
536 assert_eq!(SCRATCH_PORT.inet_port(5064), 5001);
537 set(SCRATCH_PORT, "65535");
538 assert_eq!(SCRATCH_PORT.inet_port(5064), 65535);
539 clear(SCRATCH_PORT);
540 }
541
542 #[test]
543 #[serial(epics_env)]
544 fn test_get_bool_only_yes_case_insensitive() {
545 // C `envGetBoolConfigParam`: true iff epicsStrCaseCmp(text,"yes")==0.
546 for truthy in &["yes", "YES", "Yes", "yEs", "yeS"] {
547 set(SCRATCH, truthy);
548 assert_eq!(SCRATCH.bool(), Some(true), "{truthy:?} must be true");
549 }
550 for falsy in &[
551 "1", "true", "TRUE", "on", "yes ", " yes", "yes\n", "no", "0",
552 ] {
553 set(SCRATCH, falsy);
554 assert_eq!(SCRATCH.bool(), Some(false), "{falsy:?} must be false");
555 }
556 // Empty value AND empty compiled default: C's -1 status, caller's
557 // initializer survives.
558 set(SCRATCH, "");
559 assert_eq!(SCRATCH.bool(), None);
560 clear(SCRATCH);
561 assert_eq!(SCRATCH.bool(), None);
562 }
563
564 /// `long_or_default` is C's `long i = <default>; envGetLongConfigParam(...)`
565 /// — a rejected value leaves the compiled default standing.
566 #[test]
567 #[serial(epics_env)]
568 fn test_long_or_default_keeps_the_table_default_on_garbage() {
569 clear(IOCSH_HISTSIZE);
570 assert_eq!(IOCSH_HISTSIZE.long_or_default(), 50);
571 set(IOCSH_HISTSIZE, "garbage");
572 assert_eq!(IOCSH_HISTSIZE.long_or_default(), 50);
573 set(IOCSH_HISTSIZE, "1000");
574 assert_eq!(IOCSH_HISTSIZE.long_or_default(), 1000);
575 clear(IOCSH_HISTSIZE);
576 }
577
578 /// `envPrtConfigParam`: `NAME: value`, or `NAME is undefined` when the
579 /// resolved value is empty. Both forms verified against compiled C
580 /// (`env -i softIoc <<< epicsPrtEnvParams`).
581 #[test]
582 #[serial(epics_env)]
583 fn describe_matches_env_prt_config_param() {
584 let _silent = SilentEnv::take();
585 assert_eq!(
586 EPICS_CA_ADDR_LIST.describe(),
587 "EPICS_CA_ADDR_LIST is undefined"
588 );
589 assert_eq!(
590 EPICS_CA_SERVER_PORT.describe(),
591 "EPICS_CA_SERVER_PORT: 5064"
592 );
593 set(EPICS_CA_ADDR_LIST, "10.0.0.1");
594 assert_eq!(
595 EPICS_CA_ADDR_LIST.describe(),
596 "EPICS_CA_ADDR_LIST: 10.0.0.1"
597 );
598 }
599
600 #[test]
601 fn test_hostname() {
602 let h = hostname();
603 assert!(!h.is_empty());
604 }
605
606 /// The row-by-row diff against the BUILT C.
607 ///
608 /// The fixture is the verbatim stdout of
609 ///
610 /// ```text
611 /// printf 'epicsPrtEnvParams\nexit\n' | env -i epics-base/bin/linux-x86_64/softIoc
612 /// ```
613 ///
614 /// i.e. C walking its generated `env_param_list[]` with nothing in the
615 /// environment. Every name, every position, and every compiled default is
616 /// pinned by it — a dropped parameter, a reordered one, or a default that
617 /// drifts from `CONFIG_ENV` all fail here.
618 ///
619 /// This reads the TABLE, not the process environment, so it is independent
620 /// of whatever the developer's shell exports (and needs no `#[serial]`).
621 /// `describe()`'s env-override path is covered by
622 /// `describe_matches_env_prt_config_param`.
623 #[test]
624 #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))]
625 fn env_param_list_matches_compiled_c() {
626 let expected = include_str!("testdata/epicsPrtEnvParams.txt");
627 let ours: Vec<String> = ENV_PARAM_LIST
628 .iter()
629 .map(|p| {
630 if p.default_str().is_empty() {
631 format!("{} is undefined", p.name())
632 } else {
633 format!("{}: {}", p.name(), p.default_str())
634 }
635 })
636 .collect();
637 let theirs: Vec<&str> = expected.lines().collect();
638 assert_eq!(
639 ours.len(),
640 theirs.len(),
641 "C's env_param_list[] has {} rows, ours has {}",
642 theirs.len(),
643 ours.len()
644 );
645 for (i, (a, b)) in ours.iter().zip(&theirs).enumerate() {
646 assert_eq!(a, b, "row {i} differs from compiled C");
647 }
648 }
649
650 /// The IOC shell's own environment, diffed against compiled C.
651 ///
652 /// Fixture: `printf 'epicsEnvShow\nexit\n' | env -i softIoc` (7.0.10.1-DEV,
653 /// linux-x86_64), keeping the variables `iocshRegisterCommon` sets and
654 /// dropping the two the OS supplies (`PWD`) — nine names, nine values.
655 ///
656 /// A `.db` or an `st.cmd` that expands `$(EPICS_VERSION_FULL)` or `$(ARCH)`
657 /// resolves under C; before this it silently did not resolve here. Note
658 /// `EPICS_VERSION_SITE` is *set and empty*, not absent — an empty
659 /// `epicsEnvSet` value is still a variable, and `$(EPICS_VERSION_SITE)`
660 /// expands to nothing rather than failing.
661 #[test]
662 #[serial(epics_env)]
663 #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))]
664 fn iocsh_registers_the_version_env_vars_c_does() {
665 let expected = include_str!("testdata/iocshRegisterCommon.txt");
666 for name in expected.lines().filter_map(|l| l.split_once('=')) {
667 // SAFETY: `#[serial(epics_env)]`.
668 unsafe { std::env::remove_var(name.0) };
669 }
670
671 register_iocsh_env_vars();
672
673 for (i, line) in expected.lines().enumerate() {
674 let (name, value) = line.split_once('=').expect("NAME=value");
675 assert_eq!(
676 std::env::var(name).ok().as_deref(),
677 Some(value),
678 "line {i}: C's iocshRegisterCommon publishes `{line}`"
679 );
680 }
681 }
682}