#![allow(clippy::approx_constant)]
use epics_base_rs::server::recgbl::alarm_status;
use epics_base_rs::server::record::{AlarmSeverity, CommonFields, ProcessAction, Record};
use epics_base_rs::server::records::bi::BiRecord;
use epics_base_rs::server::records::bo::BoRecord;
use epics_base_rs::server::records::dfanout::DfanoutRecord;
use epics_base_rs::server::records::mbbi::MbbiRecord;
use epics_base_rs::server::records::mbbi_direct::MbbiDirectRecord;
use epics_base_rs::server::records::mbbo::MbboRecord;
use epics_base_rs::server::records::mbbo_direct::MbboDirectRecord;
use epics_base_rs::server::records::sel::SelRecord;
use epics_base_rs::types::EpicsValue;
#[test]
fn mbbi_direct_exposes_upper_16_bits() {
let mut rec = MbbiDirectRecord::default();
rec.rval = 1 << 26;
rec.process().unwrap();
assert!(matches!(rec.get_field("B1A"), Some(EpicsValue::UChar(1))));
assert!(matches!(
rec.get_field("VAL"),
Some(EpicsValue::Long(v)) if v == (1 << 26)
));
}
#[test]
fn mbbi_direct_bit_put_reverts_on_process() {
let mut rec = MbbiDirectRecord::default();
rec.put_field("B0", EpicsValue::Char(1)).unwrap();
assert_eq!(rec.val, 0, "a Bx put must not fold into VAL (mbbiDirect)");
rec.set_device_did_compute(true);
rec.process().unwrap();
assert_eq!(rec.bits[0], 0, "process re-derives B0 from VAL=0");
assert_eq!(rec.val, 0, "VAL unchanged across the bit put + process");
assert!(matches!(rec.get_field("B0"), Some(EpicsValue::UChar(0))));
}
#[test]
fn mbbi_direct_process_rederives_bits_from_val() {
let mut rec = MbbiDirectRecord::default();
rec.rval = 5;
rec.process().unwrap();
assert_eq!(rec.val, 5);
assert_eq!(rec.bits[0], 1);
assert_eq!(rec.bits[1], 0);
assert_eq!(rec.bits[2], 1);
assert!(rec.bits[3..].iter().all(|&b| b == 0));
}
#[test]
fn mbbi_direct_process_folds_32_bits() {
let mut rec = MbbiDirectRecord::default();
rec.rval = 1 << 31; rec.process().unwrap();
assert_eq!(rec.val, 1u32 << 31);
assert_eq!(rec.bits[31], 1);
assert_eq!(rec.bits[30], 0);
}
#[test]
fn mbbo_direct_exposes_upper_16_bits() {
let mut rec = MbboDirectRecord::default();
rec.put_field("B1F", EpicsValue::Char(1)).unwrap();
assert!(matches!(
rec.get_field("VAL"),
Some(EpicsValue::Long(v)) if v as u32 == (1u32 << 31)
));
}
#[test]
fn mbbi_direct_nobt_above_16_sets_mask() {
let mut rec = MbbiDirectRecord::default();
rec.nobt = 24;
rec.init_record(0).unwrap();
assert_eq!(rec.mask, (1u32 << 24) - 1);
}
#[test]
fn mbbo_direct_process_keeps_device_rbv() {
let mut rec = MbboDirectRecord::default();
rec.rbv = 999;
rec.put_field("VAL", EpicsValue::Long(5)).unwrap();
rec.process().unwrap();
assert_eq!(rec.rval, 5, "RVAL is the commanded value");
assert_eq!(
rec.rbv, 999,
"RBV must keep the device read-back, not mirror RVAL"
);
}
#[test]
fn bo_state_alarm_osv() {
let mut rec = BoRecord::new(1);
rec.osv = AlarmSeverity::Major as i16;
let mut common = CommonFields {
udf: 0,
..Default::default()
};
rec.check_alarms(&mut common);
assert_eq!(common.nsev, AlarmSeverity::Major);
assert_eq!(common.nsta, alarm_status::STATE_ALARM);
}
#[test]
fn bo_high_one_shot_resets_val() {
let mut rec = BoRecord::new(1);
rec.high = 0.5;
let outcome = rec.process().unwrap();
assert!(
outcome
.actions
.iter()
.any(|a| matches!(a, ProcessAction::DelayedCallbackAfter(_))),
"HIGH>0 with VAL=1 arms C's `callbackRequestDelayed` (boRecord.c:257-262)"
);
rec.process().unwrap();
assert_eq!(rec.val, 1, "a process cycle cannot consume the one-shot");
rec.delayed_callback_fire(false);
assert_eq!(rec.val, 0, "momentary bo returns to Done after HIGH");
}
#[test]
fn bi_state_alarm_zsv() {
let mut rec = BiRecord::new(0);
rec.zsv = AlarmSeverity::Minor as i16;
let mut common = CommonFields {
udf: 0,
..Default::default()
};
rec.check_alarms(&mut common);
assert_eq!(common.nsev, AlarmSeverity::Minor);
assert_eq!(common.nsta, alarm_status::STATE_ALARM);
}
#[test]
fn bi_cos_alarm_fires_on_change() {
let mut rec = BiRecord::new(1);
rec.cosv = AlarmSeverity::Major as i16;
rec.lalm = 0; let mut common = CommonFields {
udf: 0,
..Default::default()
};
rec.check_alarms(&mut common);
assert_eq!(common.nsev, AlarmSeverity::Major);
assert_eq!(common.nsta, alarm_status::COS_ALARM);
let mut common2 = CommonFields {
udf: 0,
..Default::default()
};
rec.check_alarms(&mut common2);
assert_eq!(common2.nsev, AlarmSeverity::NoAlarm);
}
#[test]
fn mbbi_state_alarm_per_state() {
let mut rec = MbbiRecord::new(2);
rec.twsv = AlarmSeverity::Major as i16; let mut common = CommonFields {
udf: 0,
..Default::default()
};
rec.check_alarms(&mut common);
assert_eq!(common.nsev, AlarmSeverity::Major);
assert_eq!(common.nsta, alarm_status::STATE_ALARM);
}
#[test]
fn mbbo_soft_alarm_on_illegal_val() {
let mut rec = MbboRecord::new(20); rec.zrvl = 1; rec.init_record(0).unwrap();
rec.process().unwrap();
let mut common = CommonFields {
udf: 0,
..Default::default()
};
rec.check_alarms(&mut common);
assert_eq!(common.nsev, AlarmSeverity::Invalid);
assert_eq!(common.nsta, alarm_status::SOFT_ALARM);
}
#[test]
fn mbbo_state_alarm_per_state() {
let mut rec = MbboRecord::new(3);
rec.thsv = AlarmSeverity::Minor as i16;
let mut common = CommonFields {
udf: 0,
..Default::default()
};
rec.check_alarms(&mut common);
assert_eq!(common.nsev, AlarmSeverity::Minor);
assert_eq!(common.nsta, alarm_status::STATE_ALARM);
}
#[test]
fn sel_specified_out_of_range_raises_soft_alarm() {
let mut rec = SelRecord::default();
rec.selm = 0; rec.seln = 50; rec.val = 7.0; rec.process().unwrap();
assert_eq!(rec.val, 7.0, "VAL unchanged on out-of-range SELN");
let mut common = CommonFields::default();
rec.check_alarms(&mut common);
assert_eq!(common.nsev, AlarmSeverity::Invalid);
assert_eq!(common.nsta, alarm_status::SOFT_ALARM);
}
#[test]
fn sel_specified_negative_seln_treated_out_of_range() {
let mut rec = SelRecord::default();
rec.selm = 0;
rec.seln = 0xFFFF; rec.process().unwrap();
let mut common = CommonFields::default();
rec.check_alarms(&mut common);
assert_eq!(common.nsev, AlarmSeverity::Invalid);
assert_eq!(common.nsta, alarm_status::SOFT_ALARM);
}
#[test]
fn sel_specified_selects_input_and_nan_marks_undefined() {
let mut rec = SelRecord::default();
rec.selm = 0;
rec.seln = 1; rec.b = 3.5;
rec.process().unwrap();
assert_eq!(rec.val, 3.5);
assert!(!rec.value_is_undefined());
rec.seln = 2; rec.process().unwrap();
assert!(rec.val.is_nan());
assert!(rec.value_is_undefined());
}
#[test]
fn sel_high_signal_updates_seln() {
let mut rec = SelRecord::default();
rec.selm = 1; rec.a = 1.0;
rec.b = 9.0;
rec.c = 4.0;
rec.process().unwrap();
assert_eq!(rec.val, 9.0);
assert_eq!(rec.seln, 1, "SELN reflects winning index (B)");
}
#[test]
fn sel_low_signal_updates_seln() {
let mut rec = SelRecord::default();
rec.selm = 2; rec.a = 5.0;
rec.b = 2.0;
rec.c = 8.0;
rec.process().unwrap();
assert_eq!(rec.val, 2.0);
assert_eq!(rec.seln, 1);
}
#[test]
fn sel_median_sets_seln_to_count() {
let mut rec = SelRecord::default();
rec.selm = 3; rec.a = 1.0;
rec.b = 5.0;
rec.c = 3.0;
rec.process().unwrap();
assert_eq!(rec.val, 3.0, "median of [1,3,5]");
assert_eq!(rec.seln, 3, "SELN = number of valid inputs");
}
#[test]
fn sel_median_empty_yields_undefined() {
let mut rec = SelRecord::default();
rec.selm = 3;
rec.process().unwrap();
assert!(rec.val.is_nan());
assert_eq!(rec.seln, 0);
assert!(rec.value_is_undefined());
}
#[test]
fn sel_high_empty_yields_neg_inf_udf_clear() {
let mut rec = SelRecord::default();
rec.selm = 1; rec.process().unwrap();
assert!(
rec.val == f64::NEG_INFINITY,
"all-NaN High selection must yield VAL = -inf (C selRecord.c:362), got {}",
rec.val
);
assert!(
!rec.value_is_undefined(),
"all-NaN High: isnan(-inf) is FALSE so UDF must stay clear"
);
}
#[test]
fn sel_low_empty_yields_pos_inf_udf_clear() {
let mut rec = SelRecord::default();
rec.selm = 2; rec.process().unwrap();
assert!(
rec.val == f64::INFINITY,
"all-NaN Low selection must yield VAL = +inf (C selRecord.c:371), got {}",
rec.val
);
assert!(
!rec.value_is_undefined(),
"all-NaN Low: isnan(+inf) is FALSE so UDF must stay clear"
);
}
#[test]
fn sel_high_limit_alarm() {
let mut rec = SelRecord::default();
rec.selm = 0;
rec.seln = 0;
rec.a = 100.0;
rec.high = 50.0;
rec.hsv = AlarmSeverity::Minor as i16;
rec.process().unwrap();
assert_eq!(rec.val, 100.0);
let mut common = CommonFields {
udf: rec.value_is_undefined() as u8,
..Default::default()
};
rec.check_alarms(&mut common);
assert_eq!(common.nsev, AlarmSeverity::Minor);
assert_eq!(common.nsta, alarm_status::HIGH_ALARM);
}
#[test]
fn sel_hihi_limit_alarm_takes_priority() {
let mut rec = SelRecord::default();
rec.selm = 0;
rec.seln = 0;
rec.a = 100.0;
rec.high = 50.0;
rec.hsv = AlarmSeverity::Minor as i16;
rec.hihi = 90.0;
rec.hhsv = AlarmSeverity::Major as i16;
rec.process().unwrap();
let mut common = CommonFields {
udf: rec.value_is_undefined() as u8,
..Default::default()
};
rec.check_alarms(&mut common);
assert_eq!(common.nsev, AlarmSeverity::Major);
assert_eq!(common.nsta, alarm_status::HIHI_ALARM);
}
#[test]
fn dfanout_low_limit_alarm() {
let mut rec = DfanoutRecord::new(-10.0);
rec.low = 0.0;
rec.lsv = AlarmSeverity::Minor as i16;
rec.process().unwrap();
let mut common = CommonFields {
udf: rec.value_is_undefined() as u8,
..Default::default()
};
rec.check_alarms(&mut common);
assert_eq!(common.nsev, AlarmSeverity::Minor);
assert_eq!(common.nsta, alarm_status::LOW_ALARM);
}
#[test]
fn dfanout_lolo_limit_alarm() {
let mut rec = DfanoutRecord::new(-100.0);
rec.low = 0.0;
rec.lsv = AlarmSeverity::Minor as i16;
rec.lolo = -50.0;
rec.llsv = AlarmSeverity::Major as i16;
rec.process().unwrap();
let mut common = CommonFields {
udf: rec.value_is_undefined() as u8,
..Default::default()
};
rec.check_alarms(&mut common);
assert_eq!(common.nsev, AlarmSeverity::Major);
assert_eq!(common.nsta, alarm_status::LOLO_ALARM);
}
#[test]
fn dfanout_no_alarm_when_in_range() {
let mut rec = DfanoutRecord::new(5.0);
rec.high = 50.0;
rec.hsv = AlarmSeverity::Major as i16;
rec.low = -50.0;
rec.lsv = AlarmSeverity::Major as i16;
rec.process().unwrap();
let mut common = CommonFields {
udf: rec.value_is_undefined() as u8,
..Default::default()
};
rec.check_alarms(&mut common);
assert_eq!(common.nsev, AlarmSeverity::NoAlarm);
}
#[test]
fn dfanout_udf_alarm_on_nan_val() {
let mut rec = DfanoutRecord::new(f64::NAN);
assert!(rec.value_is_undefined());
let mut common = CommonFields {
udf: 1,
..Default::default()
};
rec.check_alarms(&mut common);
assert_eq!(common.nsev, AlarmSeverity::from_u16(common.udfs as u16));
assert_eq!(common.nsta, alarm_status::UDF_ALARM);
}
#[test]
fn dfanout_output_links_preserved() {
let mut rec = DfanoutRecord::default();
rec.put_field("OUTA", EpicsValue::String("REC_A".into()))
.unwrap();
rec.put_field("OUTP", EpicsValue::String("REC_P".into()))
.unwrap();
let links = rec.output_links();
assert_eq!(links, vec!["REC_A", "REC_P"]);
}
#[test]
fn dfanout_exposes_deadband_fields() {
let mut rec = DfanoutRecord::default();
rec.put_field("MDEL", EpicsValue::Double(2.0)).unwrap();
rec.put_field("ADEL", EpicsValue::Double(5.0)).unwrap();
assert!(matches!(
rec.get_field("MDEL"),
Some(EpicsValue::Double(v)) if (v - 2.0).abs() < 1e-9
));
assert!(matches!(
rec.get_field("ADEL"),
Some(EpicsValue::Double(v)) if (v - 5.0).abs() < 1e-9
));
assert!(rec.uses_monitor_deadband());
}