use super::*;
const NO_FAULT: Reliability = Reliability::NO_FAULT_DETECTED;
const FAULTED: Reliability = Reliability::OVER_RANGE;
fn make_delayed_detector(time_delay: u32) -> OutOfRangeDetector {
OutOfRangeDetector {
high_limit: 80.0,
low_limit: 20.0,
deadband: 2.0,
limit_enable: LimitEnable::all(),
event_enable: EventTransitionBits::all(),
time_delay,
..Default::default()
}
}
fn make_asymmetric_detector(time_delay: u32, time_delay_normal: u32) -> OutOfRangeDetector {
let mut det = make_delayed_detector(time_delay);
det.time_delay_normal = Some(time_delay_normal);
det
}
#[test]
fn time_delay_normal_selects_the_return_to_normal_delay() {
let mut det = make_asymmetric_detector(5, 10);
assert!(det.probe(81.0, NO_FAULT).is_none());
assert_eq!(
det.pending.as_ref().expect("pending seeded").remaining,
5,
"the offnormal direction is seeded from Time_Delay, not Time_Delay_Normal"
);
for _ in 0..4 {
assert!(det.tick(81.0, NO_FAULT).is_none());
assert_eq!(det.event_state, EventState::NORMAL);
}
let change = det.tick(81.0, NO_FAULT).unwrap().change; assert_eq!(change.to, EventState::HIGH_LIMIT);
assert!(det.probe(50.0, NO_FAULT).is_none());
assert_eq!(
det.pending
.as_ref()
.expect("return-to-normal pending seeded")
.remaining,
10,
"the NORMAL direction is seeded from Time_Delay_Normal"
);
for _ in 0..9 {
let outcome = det.tick(50.0, NO_FAULT);
assert!(outcome.is_none(), "NORMAL must wait out pTimeDelayNormal");
assert_eq!(det.event_state, EventState::HIGH_LIMIT);
}
let change = det.tick(50.0, NO_FAULT).unwrap().change; assert_eq!(change.from, EventState::HIGH_LIMIT);
assert_eq!(change.to, EventState::NORMAL);
assert!(det.pending.is_none());
}
#[test]
fn time_delay_normal_absent_falls_back_to_time_delay() {
let mut det = make_delayed_detector(4);
assert!(det.time_delay_normal.is_none());
assert!(det.probe(81.0, NO_FAULT).is_none());
for _ in 0..3 {
assert!(det.tick(81.0, NO_FAULT).is_none());
}
assert_eq!(
det.tick(81.0, NO_FAULT).unwrap().change.to,
EventState::HIGH_LIMIT
);
assert!(det.probe(50.0, NO_FAULT).is_none());
assert_eq!(
det.pending
.as_ref()
.expect("return pending seeded")
.remaining,
4,
"absent Time_Delay_Normal takes on Time_Delay's value"
);
for _ in 0..3 {
assert!(det.tick(50.0, NO_FAULT).is_none());
}
assert_eq!(
det.tick(50.0, NO_FAULT).unwrap().change.to,
EventState::NORMAL
);
}
#[test]
fn time_delay_normal_zero_fires_the_return_to_normal_immediately() {
let mut det = make_asymmetric_detector(5, 0);
det.event_state = EventState::HIGH_LIMIT;
let change = det.probe(50.0, NO_FAULT).unwrap().change;
assert_eq!(change.to, EventState::NORMAL);
assert!(det.pending.is_none());
assert!(det.probe(81.0, NO_FAULT).is_none());
for _ in 0..4 {
assert!(det.tick(81.0, NO_FAULT).is_none());
}
assert_eq!(
det.tick(81.0, NO_FAULT).unwrap().change.to,
EventState::HIGH_LIMIT
);
}
#[test]
fn time_delay_normal_does_not_govern_offnormal_to_offnormal() {
let mut det = make_asymmetric_detector(5, 30);
det.event_state = EventState::HIGH_LIMIT;
assert!(det.probe(19.0, NO_FAULT).is_none());
let pending = det
.pending
.as_ref()
.expect("direct re-cross seeded pending");
assert_eq!(pending.state, EventState::LOW_LIMIT);
assert_eq!(
pending.remaining, 5,
"offnormal→offnormal uses pTimeDelay, not pTimeDelayNormal"
);
for _ in 0..4 {
assert!(det.tick(19.0, NO_FAULT).is_none());
}
assert_eq!(
det.tick(19.0, NO_FAULT).unwrap().change.to,
EventState::LOW_LIMIT
);
}
#[test]
fn time_delay_normal_mid_delay_reseed_uses_the_current_targets_delay() {
let mut det = make_asymmetric_detector(5, 10);
det.event_state = EventState::HIGH_LIMIT;
assert!(det.probe(19.0, NO_FAULT).is_none());
let pending = det.pending.as_ref().expect("re-cross pending seeded");
assert_eq!(pending.state, EventState::LOW_LIMIT);
assert_eq!(pending.remaining, 5);
assert!(det.probe(50.0, NO_FAULT).is_none());
let pending = det
.pending
.as_ref()
.expect("re-seeded toward NORMAL with pTimeDelayNormal");
assert_eq!(pending.state, EventState::NORMAL);
assert_eq!(pending.remaining, 10);
for _ in 0..9 {
assert!(det.tick(50.0, NO_FAULT).is_none());
}
assert_eq!(
det.tick(50.0, NO_FAULT).unwrap().change.to,
EventState::NORMAL
);
}
#[test]
fn fault_transitions_ignore_both_delays() {
let mut det = make_asymmetric_detector(30, 30);
assert!(det.probe(81.0, NO_FAULT).is_none());
assert!(det.pending.is_some());
let outcome = det.tick(81.0, FAULTED).expect("FAULT fires immediately");
assert_eq!(outcome.change.to, EventState::FAULT);
assert_eq!(
outcome.event_type,
bacnet_types::enums::EventType::CHANGE_OF_RELIABILITY
);
assert!(det.pending.is_none());
assert_eq!(det.event_state, EventState::FAULT);
}
#[test]
fn time_delay_normal_change_of_state_asymmetric_round_trip() {
let mut det = ChangeOfStateDetector {
alarm_values: vec![1],
event_enable: EventTransitionBits::all(),
time_delay: 2,
time_delay_normal: Some(4),
..Default::default()
};
assert!(det.probe(1, NO_FAULT).is_none());
assert_eq!(det.pending.as_ref().unwrap().remaining, 2);
assert!(det.tick(1, NO_FAULT).is_none());
assert_eq!(
det.tick(1, NO_FAULT).unwrap().change.to,
EventState::OFFNORMAL
);
assert!(det.probe(0, NO_FAULT).is_none());
assert_eq!(
det.pending.as_ref().unwrap().remaining,
4,
"Clause 13.3.2 (b) waits pTimeDelayNormal"
);
for _ in 0..3 {
assert!(det.tick(0, NO_FAULT).is_none());
}
assert_eq!(det.tick(0, NO_FAULT).unwrap().change.to, EventState::NORMAL);
}
#[test]
fn time_delay_normal_command_failure_asymmetric_round_trip() {
let mut det = CommandFailureDetector {
event_enable: EventTransitionBits::all(),
time_delay: 1,
time_delay_normal: Some(3),
..Default::default()
};
assert!(det.probe(1, 0, NO_FAULT).is_none()); assert_eq!(det.pending.as_ref().unwrap().remaining, 1);
assert_eq!(
det.tick(1, 0, NO_FAULT).unwrap().change.to,
EventState::OFFNORMAL
);
assert!(det.probe(1, 1, NO_FAULT).is_none()); assert_eq!(
det.pending.as_ref().unwrap().remaining,
3,
"Clause 13.3.4 (b) waits pTimeDelayNormal"
);
for _ in 0..2 {
assert!(det.tick(1, 1, NO_FAULT).is_none());
}
assert_eq!(
det.tick(1, 1, NO_FAULT).unwrap().change.to,
EventState::NORMAL
);
}
#[test]
fn time_delay_normal_delayed_return_still_cancels_on_revert() {
let mut det = make_asymmetric_detector(5, 10);
det.event_state = EventState::HIGH_LIMIT;
assert!(det.probe(50.0, NO_FAULT).is_none());
for _ in 0..5 {
assert!(det.tick(50.0, NO_FAULT).is_none());
}
assert!(
det.tick(81.0, NO_FAULT).is_none(),
"revert cancels, no fire"
);
assert_eq!(det.event_state, EventState::HIGH_LIMIT);
assert!(det.pending.is_none(), "cancelled, not re-armed");
}