use super::*;
use crate::server::event_notifications::CommittedIntrinsicTransition;
use bacnet_objects::analog::{AnalogInputObject, AnalogOutputObject, AnalogValueObject};
use bacnet_objects::binary::{BinaryInputObject, BinaryOutputObject, BinaryValueObject};
use bacnet_objects::event::TransitionOutcome;
use bacnet_objects::multistate::{
MultiStateInputObject, MultiStateOutputObject, MultiStateValueObject,
};
use bacnet_objects::traits::BACnetObject;
use bacnet_types::enums::{EngineeringUnits, EventState, EventType};
fn builtin_intrinsic_objects() -> Vec<Box<dyn BACnetObject>> {
vec![
Box::new(
AnalogInputObject::new(
41,
"localized analog input",
EngineeringUnits::SQUARE_METERS,
)
.unwrap(),
),
Box::new(
AnalogOutputObject::new(
41,
"localized analog output",
EngineeringUnits::SQUARE_METERS,
)
.unwrap(),
),
Box::new(
AnalogValueObject::new(
41,
"localized analog value",
EngineeringUnits::SQUARE_METERS,
)
.unwrap(),
),
Box::new(BinaryInputObject::new(41, "localized binary input").unwrap()),
Box::new(BinaryOutputObject::new(41, "localized binary output").unwrap()),
Box::new(BinaryValueObject::new(41, "localized binary value").unwrap()),
Box::new(MultiStateInputObject::new(41, "localized multistate input", 3).unwrap()),
Box::new(MultiStateOutputObject::new(41, "localized multistate output", 3).unwrap()),
Box::new(MultiStateValueObject::new(41, "localized multistate value", 3).unwrap()),
Box::new(
bacnet_objects::access_control::AccessZoneObject::new(41, "localized access zone")
.unwrap(),
),
Box::new(
bacnet_objects::access_control::AccessDoorObject::new(41, "localized access door")
.unwrap(),
),
]
}
fn commit(
db: &mut ObjectDatabase,
oid: ObjectIdentifier,
from: EventState,
to: EventState,
distribute: bool,
) -> Option<CommittedIntrinsicTransition> {
BACnetServer::<TestTransport>::commit_intrinsic_transition(
db,
&oid,
TransitionOutcome {
change: EventStateChange { from, to },
event_type: EventType::OUT_OF_RANGE,
distribute,
},
)
}
fn message_slots(db: &ObjectDatabase, oid: ObjectIdentifier) -> [String; 3] {
std::array::from_fn(|index| {
let PropertyValue::CharacterString(text) = db
.get(&oid)
.unwrap()
.read_property(
PropertyIdentifier::EVENT_MESSAGE_TEXTS,
Some(index as u32 + 1),
)
.unwrap()
else {
panic!("Event_Message_Texts coordinate must be a character string");
};
text
})
}
fn committed_properties(db: &ObjectDatabase, oid: ObjectIdentifier) -> Vec<PropertyValue> {
let object = db.get(&oid).unwrap();
let mut values = vec![
object
.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap(),
object
.read_property(PropertyIdentifier::ACKED_TRANSITIONS, None)
.unwrap(),
];
for property in [
PropertyIdentifier::EVENT_TIME_STAMPS,
PropertyIdentifier::EVENT_MESSAGE_TEXTS,
] {
values.extend((1..=3).map(|index| object.read_property(property, Some(index)).unwrap()));
}
values
}
#[test]
fn all_eleven_builtin_families_store_each_policy_message_in_only_its_coordinate() {
let objects = builtin_intrinsic_objects();
assert_eq!(objects.len(), 11);
for object in objects {
let oid = object.object_identifier();
let mut db = ObjectDatabase::new();
db.add(object).unwrap();
let cases = [
(EventState::NORMAL, EventState::OFFNORMAL),
(EventState::OFFNORMAL, EventState::FAULT),
(EventState::FAULT, EventState::NORMAL),
];
let mut expected = std::array::from_fn(|_| String::new());
for (index, (from, to)) in cases.into_iter().enumerate() {
assert!(
commit(&mut db, oid, from, to, false).is_some(),
"{oid} must use the built-in atomic commit path"
);
expected[index] = format!("{oid}: {from} -> {to}");
assert_eq!(
message_slots(&db, oid),
expected,
"{oid} must update only transition coordinate {index}"
);
}
}
}
#[test]
fn policy_format_uses_object_and_state_display_including_unknown_state_numbers() {
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 7).unwrap();
let unknown = EventState::from_raw(65_535);
let mut db = ObjectDatabase::new();
db.add(Box::new(
AnalogInputObject::new(7, "name is not policy", EngineeringUnits::SQUARE_METERS).unwrap(),
))
.unwrap();
let committed = commit(&mut db, oid, EventState::NORMAL, unknown, true)
.expect("the local transition and message commit independently of wire projection");
assert!(
committed.event_values.is_none(),
"an unsupported structured projection suppresses only the outbound frame"
);
assert_eq!(
message_slots(&db, oid),
[
"ANALOG_INPUT,7: NORMAL -> 65535".into(),
String::new(),
String::new(),
]
);
}
#[test]
fn stale_commit_does_not_mutate_committed_event_properties() {
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 8).unwrap();
let mut db = ObjectDatabase::new();
db.add(Box::new(
AnalogInputObject::new(8, "AI-8", EngineeringUnits::SQUARE_METERS).unwrap(),
))
.unwrap();
assert!(
commit(
&mut db,
oid,
EventState::NORMAL,
EventState::HIGH_LIMIT,
true,
)
.is_some()
);
let before = committed_properties(&db, oid);
assert!(commit(&mut db, oid, EventState::NORMAL, EventState::FAULT, true,).is_none());
assert_eq!(committed_properties(&db, oid), before);
}
#[tokio::test]
async fn outbound_message_text_equals_the_committed_history_coordinate() {
let db = db_with_high_limit_transition(0x80);
let sent = broadcasts_from_per_write_path(&db, DccState::Enable).await;
let notification = decode_broadcast_notification(&sent);
let expected = "ANALOG_INPUT,1: NORMAL -> HIGH_LIMIT";
let history = message_slots(
&*db.read().await,
ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
);
assert_eq!(history, [expected.into(), String::new(), String::new()]);
assert_eq!(notification.message_text, Some(expected.into()));
}
#[tokio::test]
async fn event_enable_and_dcc_suppression_still_commit_the_policy_message() {
for (event_enable, dcc, label) in [
(0x00, DccState::Enable, "Event_Enable"),
(
0x80,
DccState::DisableInitiation,
"device communication control",
),
] {
let db = db_with_high_limit_transition(event_enable);
let sent = broadcasts_from_per_write_path(&db, dcc).await;
assert!(sent.is_empty(), "{label} must suppress distribution");
assert_eq!(
message_slots(
&*db.read().await,
ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
),
[
"ANALOG_INPUT,1: NORMAL -> HIGH_LIMIT".into(),
String::new(),
String::new(),
],
"{label} must not suppress the local message-history commit"
);
}
}