use super::*;
const NO_FAULT: Reliability = Reliability::NO_FAULT_DETECTED;
const OVER_RANGE: Reliability = Reliability::OVER_RANGE;
const NO_SENSOR: Reliability = Reliability::NO_SENSOR;
const SHORTED_LOOP: Reliability = Reliability::SHORTED_LOOP;
const TO_OFFNORMAL: EventTransitionBits = EventTransitionBits::TO_OFFNORMAL;
const TO_FAULT: EventTransitionBits = EventTransitionBits::TO_FAULT;
const TO_NORMAL: EventTransitionBits = EventTransitionBits::TO_NORMAL;
#[test]
fn event_transition_classifier_covers_every_event_state_and_bit() {
let cases = [
(EventState::NORMAL, EventTransition::ToNormal),
(EventState::FAULT, EventTransition::ToFault),
(EventState::OFFNORMAL, EventTransition::ToOffnormal),
(EventState::HIGH_LIMIT, EventTransition::ToOffnormal),
(EventState::LOW_LIMIT, EventTransition::ToOffnormal),
(EventState::LIFE_SAFETY_ALARM, EventTransition::ToOffnormal),
];
for (state, expected_transition) in cases {
let transition = EventTransition::for_target_state(state);
assert_eq!(transition, expected_transition, "state {}", state.to_raw());
}
let bits = [
(EventTransition::ToOffnormal, TO_OFFNORMAL),
(EventTransition::ToFault, TO_FAULT),
(EventTransition::ToNormal, TO_NORMAL),
];
for (transition, expected_bit) in bits {
assert_eq!(transition.bit_mask(), expected_bit, "{transition:?}");
}
}
fn 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 fault_precedence_truth_table() {
assert_eq!(
fault_precedence(OVER_RANGE, None, EventState::NORMAL),
FaultPrecedence::EnterFault
);
assert_eq!(
fault_precedence(OVER_RANGE, None, EventState::HIGH_LIMIT),
FaultPrecedence::EnterFault
);
assert_eq!(
fault_precedence(OVER_RANGE, Some(OVER_RANGE), EventState::FAULT),
FaultPrecedence::HoldFault
);
assert_eq!(
fault_precedence(NO_SENSOR, Some(OVER_RANGE), EventState::FAULT),
FaultPrecedence::ReenterFault
);
assert_eq!(
fault_precedence(OVER_RANGE, None, EventState::FAULT),
FaultPrecedence::ReenterFault
);
assert_eq!(
fault_precedence(NO_FAULT, Some(OVER_RANGE), EventState::FAULT),
FaultPrecedence::RecoverToNormal
);
assert_eq!(
fault_precedence(NO_FAULT, None, EventState::NORMAL),
FaultPrecedence::RunAlgorithm
);
assert_eq!(
fault_precedence(NO_FAULT, None, EventState::HIGH_LIMIT),
FaultPrecedence::RunAlgorithm
);
}
#[test]
fn any_non_zero_reliability_faults_not_just_a_known_one() {
for reliability in [OVER_RANGE, SHORTED_LOOP, Reliability::from_raw(9999)] {
let mut det = detector();
assert_eq!(
det.probe(50.0, reliability).unwrap().change.to,
EventState::FAULT
);
}
}
#[test]
fn bad_reliability_drives_event_state_to_fault() {
let mut det = detector();
let outcome = det.probe(50.0, OVER_RANGE).expect("fault transition");
assert_eq!(outcome.change.from, EventState::NORMAL);
assert_eq!(outcome.change.to, EventState::FAULT);
assert_eq!(det.event_state, EventState::FAULT);
}
#[test]
fn fault_takes_precedence_over_the_algorithm() {
let mut det = detector();
assert_eq!(
det.probe(99.0, OVER_RANGE).unwrap().change.to,
EventState::FAULT
);
}
#[test]
fn fault_entry_ignores_time_delay() {
let mut det = detector();
det.time_delay = 10;
let outcome = det
.probe(50.0, OVER_RANGE)
.expect("fault fires immediately");
assert_eq!(outcome.change.to, EventState::FAULT);
assert!(det.pending.is_none());
}
#[test]
fn fault_entry_cancels_an_in_flight_countdown() {
let mut det = detector();
det.time_delay = 5;
assert!(det.probe(99.0, NO_FAULT).is_none(), "countdown seeded");
assert_eq!(det.pending.as_ref().unwrap().state, EventState::HIGH_LIMIT);
assert_eq!(
det.probe(99.0, OVER_RANGE).unwrap().change.to,
EventState::FAULT
);
assert!(
det.pending.is_none(),
"pre-fault countdown must be discarded"
);
}
#[test]
fn holding_fault_blocks_a_state_independent_algorithm() {
let mut det = ChangeOfStateDetector {
alarm_values: vec![1],
event_enable: EventTransitionBits::all(),
time_delay: 0,
event_state: EventState::NORMAL,
..Default::default()
};
assert_eq!(
det.probe(1, SHORTED_LOOP).unwrap().change.to,
EventState::FAULT
);
assert!(
det.probe(0, SHORTED_LOOP).is_none(),
"a non-alarm value must not recover the object while Reliability is bad"
);
assert_eq!(det.event_state, EventState::FAULT);
assert!(det.tick(0, SHORTED_LOOP).is_none());
assert_eq!(det.event_state, EventState::FAULT);
}
#[test]
fn holding_fault_reports_no_further_transitions() {
let mut det = detector();
assert!(det.probe(50.0, OVER_RANGE).is_some());
assert!(det.probe(50.0, OVER_RANGE).is_none());
assert!(det.tick(50.0, OVER_RANGE).is_none());
assert!(
det.probe(99.0, OVER_RANGE).is_none(),
"the standing fault must not report another transition"
);
assert_eq!(det.event_state, EventState::FAULT);
}
#[test]
fn out_of_range_reenters_fault_only_when_reliability_changes() {
let mut det = detector();
let mut outcomes = usize::from(det.probe(50.0, OVER_RANGE).is_some());
for _ in 0..4 {
outcomes += usize::from(det.tick(50.0, OVER_RANGE).is_some());
}
assert_eq!(outcomes, 1, "an unchanged fault must not flood");
assert_eq!(det.fault_reliability, Some(OVER_RANGE));
let reentry = det
.probe(50.0, NO_SENSOR)
.expect("changed reliability re-enters FAULT");
assert_eq!(reentry.change.from, EventState::FAULT);
assert_eq!(reentry.change.to, EventState::FAULT);
assert_eq!(reentry.event_type, EventType::CHANGE_OF_RELIABILITY);
assert_eq!(
EventTransition::for_target_state(reentry.change.to),
EventTransition::ToFault
);
assert_eq!(det.fault_reliability, Some(NO_SENSOR));
assert!(det.tick(50.0, NO_SENSOR).is_none());
assert_eq!(
det.probe(50.0, NO_FAULT).unwrap().change.to,
EventState::NORMAL
);
assert_eq!(det.fault_reliability, None);
assert_eq!(
det.probe(50.0, NO_SENSOR).unwrap().change.to,
EventState::FAULT
);
}
#[test]
fn change_of_state_reenters_fault_only_when_reliability_changes() {
let mut det = ChangeOfStateDetector {
alarm_values: vec![1],
event_enable: EventTransitionBits::all(),
..Default::default()
};
let mut outcomes = usize::from(det.probe(0, OVER_RANGE).is_some());
for _ in 0..4 {
outcomes += usize::from(det.tick(0, OVER_RANGE).is_some());
}
assert_eq!(outcomes, 1, "an unchanged fault must not flood");
assert_eq!(det.fault_reliability, Some(OVER_RANGE));
let reentry = det
.probe(0, NO_SENSOR)
.expect("changed reliability re-enters FAULT");
assert_eq!(reentry.change.from, EventState::FAULT);
assert_eq!(reentry.change.to, EventState::FAULT);
assert_eq!(reentry.event_type, EventType::CHANGE_OF_RELIABILITY);
assert_eq!(
EventTransition::for_target_state(reentry.change.to),
EventTransition::ToFault
);
assert_eq!(det.fault_reliability, Some(NO_SENSOR));
assert!(det.tick(0, NO_SENSOR).is_none());
assert_eq!(
det.probe(0, NO_FAULT).unwrap().change.to,
EventState::NORMAL
);
assert_eq!(det.fault_reliability, None);
assert_eq!(
det.probe(0, NO_SENSOR).unwrap().change.to,
EventState::FAULT
);
}
#[test]
fn command_failure_reenters_fault_only_when_reliability_changes() {
let mut det = CommandFailureDetector {
event_enable: EventTransitionBits::all(),
..Default::default()
};
let mut outcomes = usize::from(det.probe(0, 0, OVER_RANGE).is_some());
for _ in 0..4 {
outcomes += usize::from(det.tick(0, 0, OVER_RANGE).is_some());
}
assert_eq!(outcomes, 1, "an unchanged fault must not flood");
assert_eq!(det.fault_reliability, Some(OVER_RANGE));
let reentry = det
.probe(0, 0, NO_SENSOR)
.expect("changed reliability re-enters FAULT");
assert_eq!(reentry.change.from, EventState::FAULT);
assert_eq!(reentry.change.to, EventState::FAULT);
assert_eq!(reentry.event_type, EventType::CHANGE_OF_RELIABILITY);
assert_eq!(
EventTransition::for_target_state(reentry.change.to),
EventTransition::ToFault
);
assert_eq!(det.fault_reliability, Some(NO_SENSOR));
assert!(det.tick(0, 0, NO_SENSOR).is_none());
assert_eq!(
det.probe(0, 0, NO_FAULT).unwrap().change.to,
EventState::NORMAL
);
assert_eq!(det.fault_reliability, None);
assert_eq!(
det.probe(0, 0, NO_SENSOR).unwrap().change.to,
EventState::FAULT
);
}
#[test]
fn recovery_from_fault_enters_normal_not_the_algorithm_state() {
let mut det = detector();
assert_eq!(
det.probe(99.0, OVER_RANGE).unwrap().change.to,
EventState::FAULT
);
let outcome = det.probe(99.0, NO_FAULT).expect("recovery transition");
assert_eq!(outcome.change.from, EventState::FAULT);
assert_eq!(
outcome.change.to,
EventState::NORMAL,
"recovery must enter NORMAL even though present_value is still out of range"
);
}
#[test]
fn algorithm_reasserts_offnormal_after_recovery() {
let mut det = detector();
det.probe(99.0, OVER_RANGE);
det.probe(99.0, NO_FAULT);
assert_eq!(det.event_state, EventState::NORMAL);
let outcome = det.probe(99.0, NO_FAULT).expect("algorithm re-detects");
assert_eq!(outcome.change.from, EventState::NORMAL);
assert_eq!(outcome.change.to, EventState::HIGH_LIMIT);
}
#[test]
fn recovery_honors_time_delay_on_the_subsequent_offnormal_transition() {
let mut det = detector();
det.time_delay = 3;
det.probe(99.0, OVER_RANGE);
assert_eq!(
det.probe(99.0, NO_FAULT).unwrap().change.to,
EventState::NORMAL,
"recovery itself is not delayed"
);
assert!(det.probe(99.0, NO_FAULT).is_none(), "offnormal is delayed");
assert_eq!(det.pending.as_ref().unwrap().remaining, 3);
}
#[test]
fn fault_transitions_report_change_of_reliability() {
let mut det = detector();
let into = det.probe(50.0, OVER_RANGE).unwrap().change;
assert_eq!(
into.event_type(OutOfRangeDetector::ALGORITHM),
EventType::CHANGE_OF_RELIABILITY
);
assert_eq!(into.transition(), EventTransition::ToFault);
let out = det.probe(50.0, NO_FAULT).unwrap().change;
assert_eq!(
out.event_type(OutOfRangeDetector::ALGORITHM),
EventType::CHANGE_OF_RELIABILITY
);
assert_eq!(out.transition(), EventTransition::ToNormal);
}
#[test]
fn fault_override_wins_in_both_directions_for_every_detector_algorithm() {
let mut out_of_range = detector();
let into = out_of_range.probe(50.0, OVER_RANGE).unwrap();
let out = out_of_range.probe(50.0, NO_FAULT).unwrap();
assert_eq!(into.event_type, EventType::CHANGE_OF_RELIABILITY);
assert_eq!(out.event_type, EventType::CHANGE_OF_RELIABILITY);
let mut change_of_state = ChangeOfStateDetector {
alarm_values: vec![1],
event_enable: EventTransitionBits::all(),
..Default::default()
};
let into = change_of_state.probe(0, OVER_RANGE).unwrap();
let out = change_of_state.probe(0, NO_FAULT).unwrap();
assert_eq!(into.event_type, EventType::CHANGE_OF_RELIABILITY);
assert_eq!(out.event_type, EventType::CHANGE_OF_RELIABILITY);
let mut command_failure = CommandFailureDetector {
event_enable: EventTransitionBits::all(),
..Default::default()
};
let into = command_failure.probe(0, 0, OVER_RANGE).unwrap();
let out = command_failure.probe(0, 0, NO_FAULT).unwrap();
assert_eq!(into.event_type, EventType::CHANGE_OF_RELIABILITY);
assert_eq!(out.event_type, EventType::CHANGE_OF_RELIABILITY);
}
#[test]
fn out_of_range_detector_reports_its_algorithm_for_non_fault_transition() {
let outcome = detector().probe(99.0, NO_FAULT).unwrap();
assert_eq!(outcome.change.to, EventState::HIGH_LIMIT);
assert_eq!(outcome.event_type, EventType::OUT_OF_RANGE);
}
#[test]
fn change_of_state_detector_reports_its_algorithm_for_non_fault_transition() {
let mut detector = ChangeOfStateDetector {
alarm_values: vec![1],
event_enable: EventTransitionBits::all(),
..Default::default()
};
let outcome = detector.probe(1, NO_FAULT).unwrap();
assert_eq!(outcome.change.to, EventState::OFFNORMAL);
assert_eq!(outcome.event_type, EventType::CHANGE_OF_STATE);
}
#[test]
fn command_failure_offnormal_reports_command_failure_not_change_of_state() {
let mut detector = CommandFailureDetector {
event_enable: EventTransitionBits::all(),
..Default::default()
};
let outcome = detector.probe(1, 0, NO_FAULT).unwrap();
assert_eq!(outcome.change.to, EventState::OFFNORMAL);
assert_eq!(outcome.event_type, EventType::COMMAND_FAILURE);
}
#[test]
fn fault_distribution_honors_the_to_fault_event_enable_bit() {
let mut det = detector();
det.event_enable = TO_FAULT;
assert!(det.probe(50.0, OVER_RANGE).unwrap().distribute);
let mut det = detector();
det.event_enable = TO_OFFNORMAL | TO_NORMAL;
let outcome = det.probe(50.0, OVER_RANGE).expect("still detected");
assert!(!outcome.distribute);
assert_eq!(
det.event_state,
EventState::FAULT,
"a cleared bit must not suppress the Event_State write"
);
}
#[test]
fn out_of_range_distribution_selects_the_to_normal_bit() {
for (event_enable, expected) in [(TO_NORMAL, true), (TO_OFFNORMAL | TO_FAULT, false)] {
let mut det = detector();
det.event_state = EventState::HIGH_LIMIT;
det.event_enable = event_enable;
let outcome = det.probe(50.0, NO_FAULT).expect("TO_NORMAL transition");
assert_eq!(outcome.change.to, EventState::NORMAL);
assert_eq!(outcome.distribute, expected, "Event_Enable {event_enable}");
}
}
#[test]
fn out_of_range_distribution_selects_the_to_offnormal_bit() {
for (event_enable, expected) in [(TO_OFFNORMAL, true), (TO_FAULT | TO_NORMAL, false)] {
let mut det = detector();
det.event_enable = event_enable;
let outcome = det.probe(90.0, NO_FAULT).expect("TO_OFFNORMAL transition");
assert_eq!(outcome.change.to, EventState::HIGH_LIMIT);
assert_eq!(outcome.distribute, expected, "Event_Enable {event_enable}");
}
}
#[test]
fn change_of_state_distribution_selects_the_to_normal_bit() {
for (event_enable, expected) in [(TO_NORMAL, true), (TO_OFFNORMAL | TO_FAULT, false)] {
let mut det = ChangeOfStateDetector {
alarm_values: vec![1],
event_enable,
event_state: EventState::OFFNORMAL,
..Default::default()
};
let outcome = det.probe(0, NO_FAULT).expect("TO_NORMAL transition");
assert_eq!(outcome.change.to, EventState::NORMAL);
assert_eq!(outcome.distribute, expected, "Event_Enable {event_enable}");
}
}
#[test]
fn change_of_state_distribution_selects_the_to_offnormal_bit() {
for (event_enable, expected) in [(TO_OFFNORMAL, true), (TO_FAULT | TO_NORMAL, false)] {
let mut det = ChangeOfStateDetector {
alarm_values: vec![1],
event_enable,
..Default::default()
};
let outcome = det.probe(1, NO_FAULT).expect("TO_OFFNORMAL transition");
assert_eq!(outcome.change.to, EventState::OFFNORMAL);
assert_eq!(outcome.distribute, expected, "Event_Enable {event_enable}");
}
}
#[test]
fn command_failure_distribution_selects_the_to_normal_bit() {
for (event_enable, expected) in [(TO_NORMAL, true), (TO_OFFNORMAL | TO_FAULT, false)] {
let mut det = CommandFailureDetector {
event_enable,
event_state: EventState::OFFNORMAL,
..Default::default()
};
let outcome = det.probe(1, 1, NO_FAULT).expect("TO_NORMAL transition");
assert_eq!(outcome.change.to, EventState::NORMAL);
assert_eq!(outcome.distribute, expected, "Event_Enable {event_enable}");
}
}
#[test]
fn command_failure_distribution_selects_the_to_offnormal_bit() {
for (event_enable, expected) in [(TO_OFFNORMAL, true), (TO_FAULT | TO_NORMAL, false)] {
let mut det = CommandFailureDetector {
event_enable,
..Default::default()
};
let outcome = det.probe(1, 0, NO_FAULT).expect("TO_OFFNORMAL transition");
assert_eq!(outcome.change.to, EventState::OFFNORMAL);
assert_eq!(outcome.distribute, expected, "Event_Enable {event_enable}");
}
}
#[test]
fn change_of_state_detector_applies_fault_precedence() {
let mut det = ChangeOfStateDetector {
alarm_values: vec![1],
event_enable: EventTransitionBits::all(),
time_delay: 0,
event_state: EventState::NORMAL,
..Default::default()
};
assert_eq!(
det.probe(0, SHORTED_LOOP).unwrap().change.to,
EventState::FAULT
);
assert_eq!(
det.probe(1, NO_FAULT).unwrap().change.to,
EventState::NORMAL
);
}
#[test]
fn command_failure_detector_applies_fault_precedence() {
let mut det = CommandFailureDetector {
event_enable: EventTransitionBits::all(),
time_delay: 0,
event_state: EventState::NORMAL,
..Default::default()
};
assert_eq!(
det.probe(1, 1, SHORTED_LOOP).unwrap().change.to,
EventState::FAULT
);
}
#[test]
fn change_of_state_fault_entry_discards_an_in_flight_countdown() {
let mut det = ChangeOfStateDetector {
alarm_values: vec![1],
event_enable: EventTransitionBits::all(),
time_delay: 2,
event_state: EventState::NORMAL,
..Default::default()
};
assert!(det.probe(1, NO_FAULT).is_none(), "countdown seeded");
assert_eq!(det.pending.as_ref().unwrap().state, EventState::OFFNORMAL);
assert_eq!(
det.probe(1, SHORTED_LOOP).unwrap().change.to,
EventState::FAULT
);
assert!(det.pending.is_none(), "pre-fault countdown is discarded");
assert_eq!(
det.probe(1, NO_FAULT).unwrap().change.to,
EventState::NORMAL
);
assert!(det.tick(1, NO_FAULT).is_none(), "a fresh countdown starts");
assert_eq!(det.pending.as_ref().unwrap().remaining, 2);
}
#[test]
fn command_failure_holds_fault_while_reliability_remains_bad() {
let mut det = CommandFailureDetector {
event_enable: EventTransitionBits::all(),
event_state: EventState::NORMAL,
..Default::default()
};
assert_eq!(
det.probe(1, 1, SHORTED_LOOP).unwrap().change.to,
EventState::FAULT
);
assert!(det.probe(1, 1, SHORTED_LOOP).is_none());
assert!(det.tick(1, 1, SHORTED_LOOP).is_none());
assert_eq!(det.event_state, EventState::FAULT);
}
#[test]
fn command_failure_recovers_to_normal_before_rerunning_its_algorithm() {
let mut det = CommandFailureDetector {
event_enable: EventTransitionBits::all(),
event_state: EventState::NORMAL,
..Default::default()
};
assert_eq!(
det.probe(1, 1, SHORTED_LOOP).unwrap().change.to,
EventState::FAULT
);
let recovery = det
.tick(1, 0, NO_FAULT)
.expect("healthy reliability recovers through the periodic path");
assert_eq!(recovery.change.from, EventState::FAULT);
assert_eq!(recovery.change.to, EventState::NORMAL);
assert_eq!(
det.probe(1, 0, NO_FAULT).unwrap().change.to,
EventState::OFFNORMAL
);
}
#[test]
fn command_failure_fault_entry_discards_an_in_flight_countdown() {
let mut det = CommandFailureDetector {
event_enable: EventTransitionBits::all(),
time_delay: 2,
event_state: EventState::NORMAL,
..Default::default()
};
assert!(det.probe(1, 0, NO_FAULT).is_none(), "countdown seeded");
assert_eq!(det.pending.as_ref().unwrap().state, EventState::OFFNORMAL);
assert_eq!(
det.tick(1, 0, SHORTED_LOOP).unwrap().change.to,
EventState::FAULT
);
assert!(det.pending.is_none(), "pre-fault countdown is discarded");
}
#[test]
fn writing_reliability_on_an_object_drives_event_state_to_fault() {
use crate::analog::AnalogInputObject;
use crate::traits::BACnetObject;
use bacnet_types::enums::PropertyIdentifier;
use bacnet_types::primitives::PropertyValue;
let mut ai = AnalogInputObject::new(1, "ai-1", 62).expect("construct");
assert!(
ai.evaluate_intrinsic_reporting().is_none(),
"healthy object has no transition to report"
);
ai.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.expect("Out_Of_Service is writable");
ai.write_property(
PropertyIdentifier::RELIABILITY,
None,
PropertyValue::Enumerated(OVER_RANGE.to_raw()),
None,
)
.expect("reliability is writable");
let outcome = ai
.evaluate_intrinsic_reporting()
.expect("reliability must drive a transition");
assert_eq!(outcome.change.to, EventState::FAULT);
assert_eq!(
outcome.change.event_type(OutOfRangeDetector::ALGORITHM),
EventType::CHANGE_OF_RELIABILITY
);
}
#[test]
fn ticking_an_object_uses_reliability_for_fault_and_recovery() {
use crate::analog::AnalogInputObject;
use crate::traits::BACnetObject;
use bacnet_types::enums::PropertyIdentifier;
use bacnet_types::primitives::PropertyValue;
let mut ai = AnalogInputObject::new(3, "ai-3", 62).expect("construct");
ai.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.expect("Out_Of_Service is writable");
ai.write_property(
PropertyIdentifier::RELIABILITY,
None,
PropertyValue::Enumerated(OVER_RANGE.to_raw()),
None,
)
.expect("reliability is writable");
let proposal = ai
.tick_intrinsic_reporting()
.expect("periodic tick must observe bad reliability");
let fault = commit_test_proposal(&mut ai, proposal);
assert_eq!(fault.change.to, EventState::FAULT);
ai.write_property(
PropertyIdentifier::RELIABILITY,
None,
PropertyValue::Enumerated(NO_FAULT.to_raw()),
None,
)
.expect("reliability is writable");
let recovery = ai
.tick_intrinsic_reporting()
.expect("periodic tick must observe recovered reliability");
assert_eq!(recovery.change.from, EventState::FAULT);
assert_eq!(recovery.change.to, EventState::NORMAL);
}
#[test]
fn faulted_object_reports_both_fault_and_in_alarm_status_flags() {
use crate::analog::AnalogInputObject;
use crate::traits::BACnetObject;
use bacnet_types::enums::PropertyIdentifier;
use bacnet_types::primitives::{PropertyValue, StatusFlags};
let mut ai = AnalogInputObject::new(2, "ai-2", 62).expect("construct");
ai.set_reliability_internal(OVER_RANGE)
.expect("in-service reliability evaluation is supported");
let proposal = ai
.evaluate_intrinsic_reporting()
.expect("bad reliability must propose FAULT");
commit_test_proposal(&mut ai, proposal);
let flags = ai
.read_property(PropertyIdentifier::STATUS_FLAGS, None)
.expect("status flags readable");
let PropertyValue::BitString { data, .. } = flags else {
panic!("Status_Flags is a bit string");
};
let bits = data[0] >> 4;
assert_ne!(
bits & StatusFlags::FAULT.bits(),
0,
"FAULT follows Reliability"
);
assert_ne!(
bits & StatusFlags::IN_ALARM.bits(),
0,
"IN_ALARM follows Event_State, which Reliability now drives"
);
}
#[test]
fn command_failure_to_fault_distribution_is_no_longer_hardcoded_off() {
let mut det = CommandFailureDetector {
event_enable: TO_FAULT,
time_delay: 0,
event_state: EventState::NORMAL,
..Default::default()
};
assert!(
det.probe(1, 1, SHORTED_LOOP).unwrap().distribute,
"TO_FAULT distribution must follow Event_Enable like the other two directions"
);
let mut det = CommandFailureDetector {
event_enable: TO_OFFNORMAL | TO_NORMAL,
time_delay: 0,
event_state: EventState::NORMAL,
..Default::default()
};
assert!(!det.probe(1, 1, SHORTED_LOOP).unwrap().distribute);
}