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
//! R15-81: waveform/aai/aao clear UDF unconditionally and never raise
//! UDF_ALARM.
//!
//! Their C `process()` clears UDF itself, on the line after `readValue` returns
//! and whatever status it returned:
//!
//! status = readValue(prec);
//! if (!pact && prec->pact) return 0;
//! prec->pact = TRUE;
//! prec->udf = FALSE; /* waveformRecord.c:144, aaiRecord.c:174 */
//!
//! (aao: `if (!pact) { prec->udf = FALSE; ... }`, aaoRecord.c:164-165.) So a
//! FAILED simulation read still leaves the record DEFINED — the framework's
//! simulation tail was gating the clear on the SIOL fetch status, which is the
//! rule for the scalar records (`longinRecord.c:418` `if (status == 0)
//! prec->udf = FALSE;`) but not for these three.
//!
//! And none of the three has a `checkAlarms` or names UDF_ALARM at all — the
//! only severities they raise are SIMM_ALARM and SOFT_ALARM — so UDF must never
//! become an alarm on them, whatever it says.
//!
//! subArray is the counter-case and keeps the defaults: `prec->udf = !!status;
//! if (status) recGblSetSevr(prec, UDF_ALARM, prec->udfs);`
//! (subArrayRecord.c:148-150).
//!
//! Boundaries: SIOL read failed vs succeeded; UDF set vs UDF alarm raised; the
//! three unconditional kinds vs subArray.
use std::collections::HashSet;
use epics_base_rs::server::ioc_builder::IocBuilder;
use epics_base_rs::server::record::{AlarmSeverity, Record};
use epics_base_rs::server::records::waveform::{ArrayKind, WaveformRecord};
use epics_base_rs::types::{DbFieldType, EpicsValue};
/// SIMM=YES with a SIOL pointing at a record that does not exist: the
/// simulation read fails.
const DB: &str = r#"
record(aai, "SIM:AAI") {
field(FTVL, "DOUBLE")
field(NELM, "8")
field(SIMM, "YES")
field(SIOL, "NO:SUCH:RECORD")
}
record(aao, "SIM:AAO") {
field(FTVL, "DOUBLE")
field(NELM, "8")
field(SIMM, "YES")
field(SIOL, "NO:SUCH:RECORD")
}
record(waveform, "SIM:WF") {
field(FTVL, "DOUBLE")
field(NELM, "8")
field(SIMM, "YES")
field(SIOL, "NO:SUCH:RECORD")
}
"#;
/// A failed SIOL read must still clear UDF, and must not raise any alarm from
/// UDF (SIMM_ALARM at SIMS=NO_ALARM is the only thing C raises here).
#[epics_macros_rs::epics_test]
async fn failed_sim_read_still_clears_udf_on_the_array_kinds() {
let (db, _) = IocBuilder::new()
.db_string(DB, &std::collections::HashMap::new())
.unwrap()
.build()
.await
.unwrap();
for rec in ["SIM:AAI", "SIM:AAO", "SIM:WF"] {
let handle = db.get_record(rec).unwrap();
assert!(
handle.read().common.udf != 0,
"{rec}: undefined before the first process"
);
let mut visited = HashSet::new();
db.process_record_with_links(rec, &mut visited, 0)
.await
.unwrap();
let common = &handle.read().common;
assert!(
common.udf == 0,
"{rec}: C `process` clears UDF after readValue whatever its status"
);
assert_ne!(
common.stat,
epics_base_rs::server::recgbl::alarm_status::UDF_ALARM,
"{rec}: these records never raise UDF_ALARM (no checkAlarms in C)"
);
}
}
/// The declared rules, per kind. subArray is the exception on both.
#[test]
fn subarray_keeps_the_status_gated_udf_rules() {
for (kind, unconditional, raises) in [
(ArrayKind::Waveform, true, false),
(ArrayKind::Aai, true, false),
(ArrayKind::Aao, true, false),
(ArrayKind::SubArray, false, true),
] {
let mut rec = WaveformRecord::new(4, DbFieldType::Double);
rec.kind = kind;
assert_eq!(
rec.clears_udf_unconditionally(),
unconditional,
"{kind:?}: C `prec->udf = FALSE` in process (waveform/aai/aao) vs \
`prec->udf = !!status` (subArray)"
);
assert_eq!(
rec.raises_udf_alarm(),
raises,
"{kind:?}: only subArrayRecord.c:150 calls recGblSetSevr(UDF_ALARM)"
);
}
}
/// A never-processed waveform carries the INITIAL UDF severity — not a
/// process-time UDF alarm.
///
/// The two are different mechanisms and only the second is waveform-specific:
/// `iocInit.c:521-523` gives EVERY record `SEVR = UDFS` while `udf && stat ==
/// UDF_ALARM` (and STAT is born `UDF`), whereas `recGblCheckUdf` — the
/// process-time raise that `waveformRecord.c` never calls — is what this record
/// type genuinely lacks. softIoc, `record(waveform,"UDFWF")` with no INP, never
/// processed: `STAT=UDF SEVR=INVALID UDF=1`.
#[epics_macros_rs::epics_test]
async fn an_undefined_waveform_carries_the_initial_udf_severity() {
let (db, _) = IocBuilder::new()
.db_string(
r#"record(waveform, "UDF:WF") { field(FTVL, "DOUBLE") field(NELM, "4") }"#,
&std::collections::HashMap::new(),
)
.unwrap()
.build()
.await
.unwrap();
let handle = db.get_record("UDF:WF").unwrap();
assert!(handle.read().common.udf != 0, "never processed, so UDF");
assert_eq!(
db.get_pv("UDF:WF.SEVR").unwrap().to_f64(),
Some(AlarmSeverity::Invalid as i32 as f64),
"iocInit gives every never-processed record SEVR = UDFS (INVALID)"
);
assert_eq!(
db.get_pv("UDF:WF.STAT").unwrap().to_f64(),
Some(f64::from(
epics_base_rs::server::recgbl::alarm_status::UDF_ALARM
)),
"STAT is born UDF (dbCommon.dbd initial(\"UDF\"))"
);
assert_eq!(
db.get_pv("UDF:WF").unwrap(),
EpicsValue::DoubleArray(vec![])
);
}