use super::*;
const NO_FAULT: Reliability = Reliability::NO_FAULT_DETECTED;
fn make_detector() -> OutOfRangeDetector {
OutOfRangeDetector {
high_limit: 80.0,
low_limit: 20.0,
deadband: 2.0,
limit_enable: LimitEnable::all(),
notification_class: 1,
notify_type: NotifyType::ALARM,
event_enable: EventTransitionBits::all(),
time_delay: 0,
time_delay_normal: None,
event_state: EventState::NORMAL,
acked_transitions: EventTransitionBits::all(),
pending: None,
fault_reliability: None,
}
}
#[test]
fn normal_stays_normal_within_limits() {
let mut det = make_detector();
assert!(det.evaluate(50.0, NO_FAULT).is_none());
assert_eq!(det.event_state, EventState::NORMAL);
}
#[test]
fn normal_to_high_limit() {
let mut det = make_detector();
let change = det.evaluate(81.0, NO_FAULT).unwrap().change;
assert_eq!(change.from, EventState::NORMAL);
assert_eq!(change.to, EventState::HIGH_LIMIT);
assert_eq!(det.event_state, EventState::HIGH_LIMIT);
}
#[test]
fn normal_to_low_limit() {
let mut det = make_detector();
let change = det.evaluate(19.0, NO_FAULT).unwrap().change;
assert_eq!(change.from, EventState::NORMAL);
assert_eq!(change.to, EventState::LOW_LIMIT);
assert_eq!(det.event_state, EventState::LOW_LIMIT);
}
#[test]
fn at_boundary_no_transition() {
let mut det = make_detector();
assert!(det.evaluate(80.0, NO_FAULT).is_none());
assert!(det.evaluate(20.0, NO_FAULT).is_none());
}
#[test]
fn high_limit_to_normal_with_deadband() {
let mut det = make_detector();
det.evaluate(81.0, NO_FAULT);
assert!(det.evaluate(79.0, NO_FAULT).is_none());
let change = det.evaluate(77.0, NO_FAULT).unwrap().change;
assert_eq!(change.from, EventState::HIGH_LIMIT);
assert_eq!(change.to, EventState::NORMAL);
}
#[test]
fn low_limit_to_normal_with_deadband() {
let mut det = make_detector();
det.evaluate(19.0, NO_FAULT);
assert!(det.evaluate(21.0, NO_FAULT).is_none());
let change = det.evaluate(23.0, NO_FAULT).unwrap().change;
assert_eq!(change.from, EventState::LOW_LIMIT);
assert_eq!(change.to, EventState::NORMAL);
}
#[test]
fn high_limit_to_low_limit_direct() {
let mut det = make_detector();
det.evaluate(81.0, NO_FAULT);
let change = det.evaluate(19.0, NO_FAULT).unwrap().change;
assert_eq!(change.from, EventState::HIGH_LIMIT);
assert_eq!(change.to, EventState::LOW_LIMIT);
}
#[test]
fn low_limit_to_high_limit_direct() {
let mut det = make_detector();
det.evaluate(19.0, NO_FAULT);
let change = det.evaluate(81.0, NO_FAULT).unwrap().change;
assert_eq!(change.from, EventState::LOW_LIMIT);
assert_eq!(change.to, EventState::HIGH_LIMIT);
}
#[test]
fn high_limit_disabled_no_transition() {
let mut det = make_detector();
det.limit_enable.remove(LimitEnable::HIGH_LIMIT_ENABLE);
assert!(det.evaluate(100.0, NO_FAULT).is_none());
}
#[test]
fn low_limit_disabled_no_transition() {
let mut det = make_detector();
det.limit_enable.remove(LimitEnable::LOW_LIMIT_ENABLE);
assert!(det.evaluate(0.0, NO_FAULT).is_none());
}
#[test]
fn both_limits_disabled() {
let mut det = make_detector();
det.limit_enable = LimitEnable::empty();
assert!(det.evaluate(100.0, NO_FAULT).is_none());
assert!(det.evaluate(0.0, NO_FAULT).is_none());
}
#[test]
fn deadband_at_exact_boundary() {
let mut det = make_detector();
det.evaluate(81.0, NO_FAULT);
assert!(det.evaluate(78.0, NO_FAULT).is_none());
let change = det.evaluate(77.99, NO_FAULT).unwrap().change;
assert_eq!(change.to, EventState::NORMAL);
}
#[test]
fn event_state_change_uses_supplied_algorithm() {
use bacnet_types::enums::EventType;
let change = EventStateChange {
from: EventState::NORMAL,
to: EventState::HIGH_LIMIT,
};
assert_eq!(
change.event_type(OutOfRangeDetector::ALGORITHM),
EventType::OUT_OF_RANGE
);
}
#[test]
fn fault_transitions_are_change_of_reliability() {
use bacnet_types::enums::EventType;
for (from, to) in [
(EventState::NORMAL, EventState::FAULT),
(EventState::FAULT, EventState::NORMAL),
(EventState::OFFNORMAL, EventState::FAULT),
(EventState::FAULT, EventState::OFFNORMAL),
(EventState::FAULT, EventState::FAULT),
] {
let change = EventStateChange { from, to };
assert_eq!(
change.event_type(OutOfRangeDetector::ALGORITHM),
EventType::CHANGE_OF_RELIABILITY,
"{from:?} -> {to:?} has FAULT at one end"
);
}
}
#[test]
fn fault_takes_precedence_over_limit_states() {
use bacnet_types::enums::EventType;
for (from, to) in [
(EventState::HIGH_LIMIT, EventState::FAULT),
(EventState::LOW_LIMIT, EventState::FAULT),
(EventState::FAULT, EventState::HIGH_LIMIT),
(EventState::FAULT, EventState::LOW_LIMIT),
] {
let change = EventStateChange { from, to };
assert_eq!(
change.event_type(OutOfRangeDetector::ALGORITHM),
EventType::CHANGE_OF_RELIABILITY,
"{from:?} -> {to:?}: FAULT outranks the limit states"
);
}
}
#[test]
fn non_fault_transitions_are_unchanged() {
use bacnet_types::enums::EventType;
for (from, to, expected) in [
(
EventState::NORMAL,
EventState::HIGH_LIMIT,
EventType::OUT_OF_RANGE,
),
(
EventState::LOW_LIMIT,
EventState::NORMAL,
EventType::OUT_OF_RANGE,
),
(
EventState::NORMAL,
EventState::OFFNORMAL,
EventType::CHANGE_OF_STATE,
),
(
EventState::OFFNORMAL,
EventState::NORMAL,
EventType::CHANGE_OF_STATE,
),
] {
let change = EventStateChange { from, to };
assert_eq!(change.event_type(expected), expected, "{from:?} -> {to:?}");
}
}
#[test]
fn event_state_change_to_normal_from_high() {
use bacnet_types::enums::EventType;
let change = EventStateChange {
from: EventState::HIGH_LIMIT,
to: EventState::NORMAL,
};
assert_eq!(
change.event_type(OutOfRangeDetector::ALGORITHM),
EventType::OUT_OF_RANGE
);
}
#[test]
fn event_enable_zero_suppresses_distribution_not_the_transition() {
let mut det = make_detector();
det.event_enable = EventTransitionBits::empty();
for (pv, expected) in [
(81.0, EventState::HIGH_LIMIT),
(50.0, EventState::NORMAL),
(19.0, EventState::LOW_LIMIT),
] {
let outcome = det
.evaluate(pv, NO_FAULT)
.expect("transition is reported even when suppressed");
assert!(!outcome.distribute);
assert_eq!(outcome.change.to, expected);
assert_eq!(det.event_state, expected);
}
}
#[test]
fn event_enable_to_normal_only() {
let mut det = make_detector();
det.event_enable = EventTransitionBits::TO_NORMAL;
let outcome = det.evaluate(81.0, NO_FAULT).unwrap();
assert!(!outcome.distribute);
assert_eq!(outcome.change.to, EventState::HIGH_LIMIT);
assert_eq!(det.event_state, EventState::HIGH_LIMIT);
let change = det.evaluate(50.0, NO_FAULT).unwrap().change;
assert_eq!(change.from, EventState::HIGH_LIMIT);
assert_eq!(change.to, EventState::NORMAL);
let outcome = det.evaluate(19.0, NO_FAULT).unwrap();
assert!(!outcome.distribute);
assert_eq!(outcome.change.to, EventState::LOW_LIMIT);
assert_eq!(det.event_state, EventState::LOW_LIMIT);
let change = det.evaluate(50.0, NO_FAULT).unwrap().change;
assert_eq!(change.from, EventState::LOW_LIMIT);
assert_eq!(change.to, EventState::NORMAL);
}
#[test]
fn event_enable_to_offnormal_only() {
let mut det = make_detector();
det.event_enable = EventTransitionBits::TO_OFFNORMAL;
let change = det.evaluate(81.0, NO_FAULT).unwrap().change;
assert_eq!(change.to, EventState::HIGH_LIMIT);
let outcome = det.evaluate(50.0, NO_FAULT).unwrap();
assert!(!outcome.distribute);
assert_eq!(outcome.change.to, EventState::NORMAL);
assert_eq!(det.event_state, EventState::NORMAL);
}
#[test]
fn event_state_change_generic() {
use bacnet_types::enums::EventType;
let change = EventStateChange {
from: EventState::NORMAL,
to: EventState::NORMAL,
};
assert_eq!(
change.event_type(ChangeOfStateDetector::ALGORITHM),
EventType::CHANGE_OF_STATE
);
}
#[test]
fn cos_normal_when_no_alarm_values() {
let mut det = ChangeOfStateDetector {
event_enable: EventTransitionBits::all(),
..Default::default()
};
assert!(det.evaluate(0, NO_FAULT).is_none()); }
#[test]
fn cos_normal_to_offnormal() {
let mut det = ChangeOfStateDetector {
alarm_values: vec![1], event_enable: EventTransitionBits::all(),
..Default::default()
};
let change = det.evaluate(1, NO_FAULT).unwrap().change;
assert_eq!(change.from, EventState::NORMAL);
assert_eq!(change.to, EventState::OFFNORMAL);
}
#[test]
fn cos_offnormal_to_normal() {
let mut det = ChangeOfStateDetector {
alarm_values: vec![1],
event_enable: EventTransitionBits::all(),
..Default::default()
};
det.evaluate(1, NO_FAULT); let change = det.evaluate(0, NO_FAULT).unwrap().change; assert_eq!(change.from, EventState::OFFNORMAL);
assert_eq!(change.to, EventState::NORMAL);
}
#[test]
fn cos_stays_offnormal_while_in_alarm() {
let mut det = ChangeOfStateDetector {
alarm_values: vec![1],
event_enable: EventTransitionBits::all(),
..Default::default()
};
det.evaluate(1, NO_FAULT); assert!(det.evaluate(1, NO_FAULT).is_none()); }
#[test]
fn cos_multistate_alarm_values() {
let mut det = ChangeOfStateDetector {
alarm_values: vec![3, 5, 7], event_enable: EventTransitionBits::all(),
..Default::default()
};
assert!(det.evaluate(1, NO_FAULT).is_none()); let change = det.evaluate(5, NO_FAULT).unwrap().change;
assert_eq!(change.to, EventState::OFFNORMAL);
assert!(det.evaluate(3, NO_FAULT).is_none()); let change = det.evaluate(2, NO_FAULT).unwrap().change;
assert_eq!(change.to, EventState::NORMAL);
}
#[test]
fn cmdfail_matching_stays_normal() {
let mut det = CommandFailureDetector {
event_enable: EventTransitionBits::all(),
..Default::default()
};
assert!(det.evaluate(1, 1, NO_FAULT).is_none()); }
#[test]
fn cmdfail_mismatch_goes_offnormal() {
let mut det = CommandFailureDetector {
event_enable: EventTransitionBits::all(),
..Default::default()
};
let change = det.evaluate(1, 0, NO_FAULT).unwrap().change; assert_eq!(change.to, EventState::OFFNORMAL);
}
#[test]
fn cmdfail_match_restores_normal() {
let mut det = CommandFailureDetector {
event_enable: EventTransitionBits::all(),
..Default::default()
};
det.evaluate(1, 0, NO_FAULT); let change = det.evaluate(1, 1, NO_FAULT).unwrap().change; assert_eq!(change.to, EventState::NORMAL);
}
fn make_delayed_detector(time_delay: u32) -> OutOfRangeDetector {
let mut det = make_detector();
det.time_delay = time_delay;
det
}
#[test]
fn time_delay_zero_fires_immediately_on_probe() {
let mut det = make_delayed_detector(0);
let change = det.probe(81.0, NO_FAULT).unwrap().change;
assert_eq!(change.to, EventState::HIGH_LIMIT);
assert_eq!(det.event_state, EventState::HIGH_LIMIT);
assert!(det.pending.is_none());
}
#[test]
fn time_delay_nonzero_probe_seeds_pending_without_firing() {
let mut det = make_delayed_detector(3);
assert!(det.probe(81.0, NO_FAULT).is_none());
assert_eq!(det.event_state, EventState::NORMAL);
let pending = det.pending.as_ref().expect("pending seeded");
assert_eq!(pending.state, EventState::HIGH_LIMIT);
assert_eq!(pending.remaining, 3);
}
#[test]
fn time_delay_repeated_probes_do_not_accelerate_countdown() {
let mut det = make_delayed_detector(3);
for _ in 0..6 {
assert!(det.probe(81.0, NO_FAULT).is_none());
}
assert_eq!(det.event_state, EventState::NORMAL);
let pending = det.pending.as_ref().expect("pending still seeded");
assert_eq!(
pending.remaining, 3,
"writes must not advance the countdown"
);
}
#[test]
fn time_delay_redundant_probe_does_not_reset_countdown() {
let mut det = make_delayed_detector(3);
det.probe(81.0, NO_FAULT); assert!(det.tick(81.0, NO_FAULT).is_none());
assert!(det.probe(81.0, NO_FAULT).is_none());
assert_eq!(
det.pending.as_ref().unwrap().remaining,
2,
"redundant probe must not re-seed the countdown"
);
assert!(det.tick(81.0, NO_FAULT).is_none()); let change = det.tick(81.0, NO_FAULT).unwrap().change; assert_eq!(change.to, EventState::HIGH_LIMIT);
assert_eq!(det.event_state, EventState::HIGH_LIMIT);
}
#[test]
fn time_delay_chattering_input_still_fires() {
let mut det = make_delayed_detector(3);
det.probe(81.0, NO_FAULT); for _ in 0..3 {
assert!(det.probe(81.0, NO_FAULT).is_none());
let r = det.tick(81.0, NO_FAULT);
if det.pending.is_some() {
assert!(r.is_none());
} else {
assert!(r.is_some(), "transition must fire on the 3rd tick");
}
}
assert_eq!(det.event_state, EventState::HIGH_LIMIT);
assert!(det.pending.is_none());
}
#[test]
fn time_delay_fires_after_exact_tick_count() {
let mut det = make_delayed_detector(3);
det.probe(81.0, NO_FAULT); assert_eq!(det.event_state, EventState::NORMAL);
assert!(det.tick(81.0, NO_FAULT).is_none()); assert!(det.tick(81.0, NO_FAULT).is_none()); let change = det.tick(81.0, NO_FAULT).unwrap().change; assert_eq!(change.from, EventState::NORMAL);
assert_eq!(change.to, EventState::HIGH_LIMIT);
assert_eq!(det.event_state, EventState::HIGH_LIMIT);
assert!(det.pending.is_none());
}
#[test]
fn time_delay_cancelled_on_revert_before_expiry() {
let mut det = make_delayed_detector(3);
det.probe(81.0, NO_FAULT); assert!(det.tick(81.0, NO_FAULT).is_none());
assert!(det.tick(50.0, NO_FAULT).is_none());
assert!(det.pending.is_none());
assert_eq!(det.event_state, EventState::NORMAL);
assert!(det.tick(50.0, NO_FAULT).is_none());
assert_eq!(det.event_state, EventState::NORMAL);
}
#[test]
fn time_delay_reseeded_when_target_changes_mid_delay() {
let mut det = make_delayed_detector(2);
det.probe(81.0, NO_FAULT); assert!(det.tick(81.0, NO_FAULT).is_none());
assert!(det.tick(19.0, NO_FAULT).is_none());
let pending = det.pending.as_ref().expect("re-seeded for new target");
assert_eq!(pending.state, EventState::LOW_LIMIT);
assert_eq!(pending.remaining, 2);
assert_eq!(det.event_state, EventState::NORMAL);
}
#[test]
fn time_delay_change_of_state_seeds_and_fires() {
let mut det = ChangeOfStateDetector {
alarm_values: vec![1],
event_enable: EventTransitionBits::all(),
time_delay: 2,
..Default::default()
};
assert!(det.probe(1, NO_FAULT).is_none());
assert_eq!(det.event_state, EventState::NORMAL);
assert!(det.tick(1, NO_FAULT).is_none()); let change = det.tick(1, NO_FAULT).unwrap().change; assert_eq!(change.to, EventState::OFFNORMAL);
assert_eq!(det.event_state, EventState::OFFNORMAL);
}
#[test]
fn time_delay_command_failure_seeds_and_fires() {
let mut det = CommandFailureDetector {
event_enable: EventTransitionBits::all(),
time_delay: 1,
..Default::default()
};
assert!(det.probe(1, 0, NO_FAULT).is_none()); assert_eq!(det.event_state, EventState::NORMAL);
let change = det.tick(1, 0, NO_FAULT).unwrap().change; assert_eq!(change.to, EventState::OFFNORMAL);
}
#[test]
fn time_delay_event_enable_gates_distribution_not_state_during_delay() {
let mut det = make_delayed_detector(1);
det.event_enable = EventTransitionBits::empty();
assert!(
det.probe(81.0, NO_FAULT).is_none(),
"delay seeded, nothing fired yet"
);
let outcome = det
.tick(81.0, NO_FAULT)
.expect("delay elapsed: transition is reported");
assert!(!outcome.distribute);
assert_eq!(outcome.change.to, EventState::HIGH_LIMIT);
assert_eq!(det.event_state, EventState::HIGH_LIMIT);
}