use std::collections::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;
const DB: &str = r#"
record(ao, "GOOD:SRC") { field(VAL, "3") }
record(ao, "AO:DEAD") {
field(OMSL, "closed_loop")
field(DOL, "NO:SUCH:RECORD")
field(VAL, "2")
}
record(ao, "AO:LIVE") {
field(OMSL, "closed_loop")
field(DOL, "GOOD:SRC")
field(VAL, "2")
}
record(ao, "AO:CONST") {
field(OMSL, "closed_loop")
field(DOL, "9")
field(VAL, "2")
}
record(ao, "AO:DEAD:INC") {
field(OMSL, "closed_loop")
field(OIF, "Incremental")
field(DOL, "NO:SUCH:RECORD")
field(VAL, "2")
}
record(ao, "AO:ROC") {
field(OMSL, "closed_loop")
field(DOL, "NO:SUCH:RECORD")
field(OROC, "1")
field(VAL, "0")
}
record(mbbo, "MBBO:DEAD") {
field(OMSL, "closed_loop")
field(DOL, "NO:SUCH:RECORD")
field(ZRVL, "10") field(ONVL, "11")
}
record(calc, "CALC:SDIS") {
field(CALC, "1")
field(SDIS, "NO:SUCH:RECORD.VAL")
field(DISV, "1")
}
record(calc, "CALC:TSEL") {
field(CALC, "1")
field(TSEL, "NO:SUCH:RECORD.VAL")
}
record(calc, "CALC:TSEL:TIME") {
field(CALC, "1")
field(TSEL, "NO:SUCH:RECORD.TIME")
}
record(dfanout, "DFO:SELL") {
field(SELM, "Specified")
field(SELL, "NO:SUCH:RECORD.VAL")
}
record(ao, "SEQ:TGT") { field(VAL, "0") }
record(seq, "SEQ:DEAD") {
field(SELM, "All")
field(DOL0, "NO:SUCH:RECORD.VAL")
field(LNK0, "SEQ:TGT")
field(DO0, "7")
}
record(seq, "SEQ:LIVE") {
field(SELM, "All")
field(DOL0, "GOOD:SRC")
field(LNK0, "SEQ:TGT")
}
record(scalcout, "SC:DEAD") {
field(CALC, "1")
field(INAA, "NO:SUCH:RECORD.VAL")
}
"#;
async fn build() -> std::sync::Arc<epics_base_rs::server::database::PvDatabase> {
IocBuilder::new()
.db_string(DB, &std::collections::HashMap::new())
.unwrap()
.build()
.await
.unwrap()
.0
}
async fn process(db: &epics_base_rs::server::database::PvDatabase, rec: &str) {
let mut visited = HashSet::new();
db.process_record_with_links(rec, &mut visited, 0)
.await
.unwrap();
}
fn field(
db: &epics_base_rs::server::database::PvDatabase,
rec: &str,
f: &str,
) -> Option<EpicsValue> {
db.get_record(rec).unwrap().read().record.get_field(f)
}
fn alarm(
db: &epics_base_rs::server::database::PvDatabase,
rec: &str,
) -> (AlarmSeverity, u16, String) {
let r = db.get_record(rec).unwrap();
let c = &r.read().common;
(c.sevr, c.stat, c.amsg.to_string())
}
#[epics_macros_rs::epics_test]
async fn dead_dol_reverts_ao_val_to_pval() {
let db = build().await;
db.put_pv("AO:DEAD", EpicsValue::Double(7.0)).await.unwrap();
process(&db, "AO:DEAD").await;
assert_eq!(
field(&db, "AO:DEAD", "VAL"),
Some(EpicsValue::Double(2.0)),
"C `fetch_value`: `prec->val = prec->pval` runs BEFORE dbGetLink"
);
assert_eq!(
field(&db, "AO:DEAD", "PVAL"),
Some(EpicsValue::Double(2.0)),
"`if(!status) convert(prec,value)` is skipped, so PVAL never took the put"
);
}
#[epics_macros_rs::epics_test]
async fn dead_dol_raises_link_invalid_named_dol() {
let db = build().await;
process(&db, "AO:DEAD").await;
assert_eq!(
alarm(&db, "AO:DEAD"),
(
AlarmSeverity::Invalid,
alarm_status::LINK_ALARM,
"field DOL".into()
),
);
}
#[epics_macros_rs::epics_test]
async fn live_dol_drives_val_and_raises_no_link_alarm() {
let db = build().await;
process(&db, "AO:LIVE").await;
assert_eq!(field(&db, "AO:LIVE", "VAL"), Some(EpicsValue::Double(3.0)));
let (sevr, stat, _) = alarm(&db, "AO:LIVE");
assert_eq!(
(sevr, stat),
(AlarmSeverity::NoAlarm, alarm_status::NO_ALARM)
);
}
#[epics_macros_rs::epics_test]
async fn constant_dol_is_not_a_failed_read() {
let db = build().await;
assert_eq!(
field(&db, "AO:CONST", "VAL"),
Some(EpicsValue::Double(9.0)),
"the constant seeds VAL once, at init"
);
db.put_pv("AO:CONST", EpicsValue::Double(7.0))
.await
.unwrap();
process(&db, "AO:CONST").await;
assert_eq!(field(&db, "AO:CONST", "VAL"), Some(EpicsValue::Double(7.0)));
let (sevr, stat, _) = alarm(&db, "AO:CONST");
assert_eq!(
(sevr, stat),
(AlarmSeverity::NoAlarm, alarm_status::NO_ALARM)
);
}
#[epics_macros_rs::epics_test]
async fn dead_dol_incremental_takes_the_same_failure_arm() {
let db = build().await;
db.put_pv("AO:DEAD:INC", EpicsValue::Double(7.0))
.await
.unwrap();
process(&db, "AO:DEAD:INC").await;
assert_eq!(
field(&db, "AO:DEAD:INC", "VAL"),
Some(EpicsValue::Double(2.0))
);
assert_eq!(
alarm(&db, "AO:DEAD:INC"),
(
AlarmSeverity::Invalid,
alarm_status::LINK_ALARM,
"field DOL".into()
),
);
}
#[epics_macros_rs::epics_test]
async fn dead_dol_freezes_the_oroc_ramp() {
let db = build().await;
db.put_pv("AO:ROC", EpicsValue::Double(10.0)).await.unwrap();
process(&db, "AO:ROC").await;
assert_eq!(
field(&db, "AO:ROC", "OVAL"),
Some(EpicsValue::Double(0.0)),
"C skips convert(), so the OROC ramp does not advance"
);
}
#[epics_macros_rs::epics_test]
async fn dead_dol_holds_a_client_rval_on_mbbo() {
let db = build().await;
db.put_pv("MBBO:DEAD", EpicsValue::Enum(1)).await.unwrap();
db.put_pv("MBBO:DEAD.RVAL", EpicsValue::ULong(99))
.await
.unwrap();
process(&db, "MBBO:DEAD").await;
assert_eq!(
field(&db, "MBBO:DEAD", "RVAL"),
Some(EpicsValue::ULong(99)),
"C `mbboRecord.c:205` jumps past convert(prec)"
);
}
#[epics_macros_rs::epics_test]
async fn dead_sdis_raises_link_invalid_named_sdis() {
let db = build().await;
process(&db, "CALC:SDIS").await;
assert_eq!(
alarm(&db, "CALC:SDIS"),
(
AlarmSeverity::Invalid,
alarm_status::LINK_ALARM,
"field SDIS".into()
),
);
}
#[epics_macros_rs::epics_test]
async fn dead_tsel_raises_link_invalid_named_tsel() {
let db = build().await;
process(&db, "CALC:TSEL").await;
assert_eq!(
alarm(&db, "CALC:TSEL"),
(
AlarmSeverity::Invalid,
alarm_status::LINK_ALARM,
"field TSEL".into()
),
);
}
#[epics_macros_rs::epics_test]
async fn dead_tsel_dot_time_raises_no_link_alarm() {
let db = build().await;
process(&db, "CALC:TSEL:TIME").await;
let (sevr, stat, _) = alarm(&db, "CALC:TSEL:TIME");
assert_eq!(
(sevr, stat),
(AlarmSeverity::NoAlarm, alarm_status::NO_ALARM)
);
}
#[epics_macros_rs::epics_test]
async fn dead_sell_raises_link_invalid_named_sell() {
let db = build().await;
db.put_pv("DFO:SELL", EpicsValue::Double(1.0))
.await
.unwrap();
process(&db, "DFO:SELL").await;
assert_eq!(
alarm(&db, "DFO:SELL"),
(
AlarmSeverity::Invalid,
alarm_status::LINK_ALARM,
"field SELL".into()
),
);
}
#[epics_macros_rs::epics_test]
async fn dead_seq_dol_raises_link_invalid_named_dol0() {
let db = build().await;
db.put_pv("SEQ:DEAD", EpicsValue::Double(0.0))
.await
.unwrap();
process(&db, "SEQ:DEAD").await;
assert_eq!(
alarm(&db, "SEQ:DEAD"),
(
AlarmSeverity::Invalid,
epics_base_rs::server::recgbl::alarm_status::LINK_ALARM,
"field DOL0".to_string()
),
"seqRecord.c:259 is a dbGetLink, so dbLink.c:339 setLinkAlarm applies"
);
assert_eq!(
field(&db, "SEQ:DEAD", "DO0"),
Some(EpicsValue::Double(7.0)),
"a failed read leaves the DOn value field alone"
);
}
#[epics_macros_rs::epics_test]
async fn live_seq_dol_raises_no_link_alarm() {
let db = build().await;
db.put_pv("SEQ:LIVE", EpicsValue::Double(0.0))
.await
.unwrap();
process(&db, "SEQ:LIVE").await;
let (sevr, stat, _) = alarm(&db, "SEQ:LIVE");
assert_eq!(
(sevr, stat),
(AlarmSeverity::NoAlarm, 0),
"a healthy dbGetLink raises nothing"
);
assert_eq!(field(&db, "SEQ:LIVE", "DO0"), Some(EpicsValue::Double(3.0)));
assert_eq!(field(&db, "SEQ:TGT", "VAL"), Some(EpicsValue::Double(3.0)));
}
#[epics_macros_rs::epics_test]
async fn dead_scalcout_string_input_raises_link_invalid_named_inaa() {
let db = build().await;
db.put_pv("SC:DEAD", EpicsValue::Double(0.0)).await.unwrap();
process(&db, "SC:DEAD").await;
assert_eq!(
alarm(&db, "SC:DEAD"),
(
AlarmSeverity::Invalid,
epics_base_rs::server::recgbl::alarm_status::LINK_ALARM,
"field INAA".to_string()
),
);
assert_eq!(
field(&db, "SC:DEAD", "VAL"),
Some(EpicsValue::Double(1.0)),
"and the cycle still ran: fetch_values returns 0 for the string loop"
);
}