1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//! End-to-end wiring for the `getenv` built-in device support (base
//! `getenvDevSup` / `devLsiEnviron` / `devSiEnviron`): a `stringin`/`lsi`
//! record with `DTYP="getenv"` must route through the dynamic builtin factory
//! (`builtin_dynamic_factory`), get the device wired with its INST_IO `INP`,
//! and have its `VAL` written from the named environment variable — the
//! factory→`wire_device_to_record`→`read` path.
//!
//! This test caught a real latent bug: getenv was originally registered as a
//! context-free *static* factory and read the env-var name from
//! `record.get_field("INP")` — but the inner typed record handed to the device
//! does NOT expose `INP` (it lives on the `RecordInstance` common header), so
//! `INP` resolved to empty and every getenv record came up INVALID through
//! `IocBuilder`. The fix moves getenv to the dynamic factory so it receives the
//! `INP` from `DeviceSupportContext`, like its INP/OUT-needing siblings (stdio /
//! Db State / Soft Timestamp). getenv was the only one of the four base builtins
//! whose siblings all had an IOC-build e2e test while it had only direct-call
//! unit tests that bypass `IocBuilder`; this closes that asymmetry.
//!
//! `std::env::set_var` is race-free here: nextest runs each `#[test]` in its own
//! process, so the variable set below is private to this test's process.
use std::collections::{HashMap, HashSet};
use epics_base_rs::server::ioc_builder::IocBuilder;
use epics_base_rs::server::recgbl::alarm_status;
use epics_base_rs::server::record::AlarmSeverity;
use epics_base_rs::types::EpicsValue;
/// A processed `stringin` with `DTYP="getenv"` and `INP="@VAR"` reads the
/// environment variable into `VAL` — proving the static builtin factory wired
/// the device and its read ran end-to-end (the pre-wiring default `VAL` is an
/// empty string, so a non-empty match proves the device ran).
#[epics_macros_rs::epics_test]
async fn getenv_stringin_reads_env_var_through_iocbuilder() {
// SAFETY: nextest isolates each test in its own process (see module doc),
// so this set_var cannot race another test.
unsafe {
std::env::set_var("GETENV_E2E_TEST_VAR", "hello-from-env");
}
let (db, _) = IocBuilder::new()
.db_string(
r#"
record(stringin, "GETENV_SI") {
field(DTYP, "getenv")
field(INP, "@GETENV_E2E_TEST_VAR")
}
"#,
&HashMap::new(),
)
.unwrap()
.build()
.await
.unwrap();
let mut visited = HashSet::new();
db.process_record_with_links("GETENV_SI", &mut visited, 0)
.await
.unwrap();
let rec = db.get_record("GETENV_SI").expect("record exists");
let inst = rec.read();
assert_ne!(
inst.common.sevr,
AlarmSeverity::Invalid,
"stringin/getenv with a set env var must not be INVALID"
);
assert_eq!(
inst.record.get_field("VAL"),
Some(EpicsValue::String("hello-from-env".into())),
"VAL must be the environment variable value read by the wired device"
);
}
/// C registers a getenv dset only for `stringin`/`lsi`; the device's
/// record-type gate Errs in `init()`, so `wire_device_to_record` flags an
/// `ai` with `DTYP="getenv"` INVALID at build time — proving the device
/// attached and gated rather than being silently accepted.
#[epics_macros_rs::epics_test]
async fn getenv_wrong_record_type_is_invalid() {
let (db, _) = IocBuilder::new()
.db_string(
r#"
record(ai, "GETENV_AI") {
field(DTYP, "getenv")
field(INP, "@PATH")
}
"#,
&HashMap::new(),
)
.unwrap()
.build()
.await
.unwrap();
let ai = db.get_record("GETENV_AI").expect("ai exists");
let inst = ai.read();
assert_eq!(
inst.common.sevr,
AlarmSeverity::Invalid,
"ai with DTYP=getenv must be INVALID (no getenv device support for ai)"
);
}
/// An unset env var: C raises UDF_ALARM at UDFS (default INVALID) and clears
/// VAL, returning success (read_lsi devEnviron.c:62-67 / read_stringin
/// :114-118). base-rs surfaces the SAME UDF_ALARM via the device `last_alarm()`
/// channel — not READ_ALARM (which the earlier soft-Err path produced) and with
/// no per-cycle stderr spam.
#[epics_macros_rs::epics_test]
async fn getenv_unset_var_raises_udf_alarm_not_read_alarm() {
// SAFETY: nextest isolates each test in its own process (see module doc), so
// removing this variable cannot race another test.
unsafe {
std::env::remove_var("GETENV_E2E_DEFINITELY_UNSET_VAR");
}
let (db, _) = IocBuilder::new()
.db_string(
r#"
record(stringin, "GETENV_UNSET") {
field(DTYP, "getenv")
field(INP, "@GETENV_E2E_DEFINITELY_UNSET_VAR")
}
"#,
&HashMap::new(),
)
.unwrap()
.build()
.await
.unwrap();
let mut visited = HashSet::new();
db.process_record_with_links("GETENV_UNSET", &mut visited, 0)
.await
.unwrap();
let rec = db.get_record("GETENV_UNSET").expect("record exists");
let inst = rec.read();
assert_eq!(
inst.common.stat,
alarm_status::UDF_ALARM,
"unset env var must raise UDF_ALARM (C recGblSetSevrMsg UDF_ALARM), not \
READ_ALARM"
);
assert_eq!(
inst.common.sevr,
AlarmSeverity::Invalid,
"unset-var severity must be UDFS (default INVALID)"
);
assert_eq!(
inst.record.get_field("VAL"),
Some(EpicsValue::String(String::new().into())),
"unset env var clears VAL to empty (C val[0]=0)"
);
}
/// The unset-var alarm fires at the record's *configured* UDFS, not a hardcoded
/// INVALID — getenv captures `prec->udfs` in set_process_context, matching C
/// `recGblSetSevrMsg(prec, UDF_ALARM, prec->udfs, …)`. A `field(UDFS,"MINOR")`
/// record with an unset var must raise UDF_ALARM at MINOR. This guards the
/// reason `set_process_context` exists: drop the `ctx.udfs` capture and the
/// severity reverts to the INVALID default, failing this assertion (the
/// default-UDFS test above would still pass, so it cannot catch that regression).
#[epics_macros_rs::epics_test]
async fn getenv_unset_var_honors_user_lowered_udfs() {
// SAFETY: nextest isolates each test in its own process (see module doc), so
// removing this variable cannot race another test.
unsafe {
std::env::remove_var("GETENV_E2E_UDFS_MINOR_UNSET");
}
let (db, _) = IocBuilder::new()
.db_string(
r#"
record(stringin, "GETENV_UDFS") {
field(DTYP, "getenv")
field(INP, "@GETENV_E2E_UDFS_MINOR_UNSET")
field(UDFS, "MINOR")
}
"#,
&HashMap::new(),
)
.unwrap()
.build()
.await
.unwrap();
let mut visited = HashSet::new();
db.process_record_with_links("GETENV_UDFS", &mut visited, 0)
.await
.unwrap();
let rec = db.get_record("GETENV_UDFS").expect("record exists");
let inst = rec.read();
assert_eq!(
inst.common.stat,
alarm_status::UDF_ALARM,
"unset env var must raise UDF_ALARM regardless of the UDFS severity"
);
assert_eq!(
inst.common.sevr,
AlarmSeverity::Minor,
"unset-var severity must follow the record's UDFS (MINOR), not a hardcoded \
INVALID"
);
}