use super::*;
use bacnet_objects::elevator::EscalatorObject;
use bacnet_objects::multistate::MultiStateInputObject;
fn request(oid: ObjectIdentifier, property: PropertyIdentifier, elements: &[u8]) -> Vec<u8> {
request_indexed(oid, property, None, elements)
}
fn request_indexed(
oid: ObjectIdentifier,
property: PropertyIdentifier,
index: Option<u32>,
elements: &[u8],
) -> Vec<u8> {
let mut encoded = BytesMut::new();
bacnet_encoding::primitives::encode_ctx_object_id(&mut encoded, 0, &oid);
bacnet_encoding::primitives::encode_ctx_enumerated(&mut encoded, 1, property.to_raw());
if let Some(index) = index {
bacnet_encoding::primitives::encode_ctx_unsigned(&mut encoded, 2, u64::from(index));
}
encoded.extend_from_slice(&[0x3e]);
encoded.extend_from_slice(elements);
encoded.extend_from_slice(&[0x3f]);
encoded.to_vec()
}
fn unsigned(values: &[u8]) -> Vec<u8> {
values.iter().flat_map(|value| [0x21, *value]).collect()
}
fn msi_db(alarm_values: Vec<u32>) -> (ObjectDatabase, ObjectIdentifier) {
msi_db_with_states(3, alarm_values)
}
fn msi_db_with_states(
number_of_states: u32,
alarm_values: Vec<u32>,
) -> (ObjectDatabase, ObjectIdentifier) {
let mut msi = MultiStateInputObject::new(1, "MSI-1", number_of_states).unwrap();
msi.set_alarm_values(alarm_values);
let oid = msi.object_identifier();
let mut db = ObjectDatabase::new();
db.add(Box::new(msi)).unwrap();
(db, oid)
}
fn alarm_values(db: &ObjectDatabase, oid: ObjectIdentifier) -> PropertyValue {
db.get(&oid)
.unwrap()
.read_property(PropertyIdentifier::ALARM_VALUES, None)
.unwrap()
}
fn list_of(values: impl IntoIterator<Item = u64>) -> PropertyValue {
PropertyValue::List(values.into_iter().map(PropertyValue::Unsigned).collect())
}
fn add(db: &mut ObjectDatabase, oid: ObjectIdentifier, elements: &[u8]) -> Result<(), Error> {
handle_add_list_element(
db,
&request(oid, PropertyIdentifier::ALARM_VALUES, elements),
)
}
fn remove(db: &mut ObjectDatabase, oid: ObjectIdentifier, elements: &[u8]) -> Result<(), Error> {
handle_remove_list_element(
db,
&request(oid, PropertyIdentifier::ALARM_VALUES, elements),
)
}
#[test]
fn add_and_remove_list_element_mutate_msi_alarm_values() {
let (mut db, oid) = msi_db(vec![]);
add(&mut db, oid, &unsigned(&[2])).unwrap();
assert_eq!(alarm_values(&db, oid), list_of([2]));
remove(&mut db, oid, &unsigned(&[2])).unwrap();
assert_eq!(alarm_values(&db, oid), list_of([]));
}
#[test]
fn add_list_element_leaves_elements_already_present() {
let (mut db, oid) = msi_db(vec![1, 2]);
add(&mut db, oid, &unsigned(&[2, 3, 3, 1])).unwrap();
assert_eq!(alarm_values(&db, oid), list_of([1, 2, 3]));
add(&mut db, oid, &unsigned(&[3, 1])).unwrap();
assert_eq!(alarm_values(&db, oid), list_of([1, 2, 3]));
add(&mut db, oid, &[0x22, 0, 2]).unwrap();
assert_eq!(alarm_values(&db, oid), list_of([1, 2, 3]));
}
#[test]
fn remove_list_element_refuses_an_absent_element_and_removes_nothing() {
let (mut db, oid) = msi_db(vec![1, 2, 3]);
for (elements, position) in [(&[5][..], 1), (&[1, 5, 2], 2), (&[1, 2, 3, 4], 4)] {
assert_eq!(
list_refusal(remove(&mut db, oid, &unsigned(elements))),
(
ErrorClass::SERVICES,
ErrorCode::LIST_ELEMENT_NOT_FOUND,
position
),
"{elements:?}"
);
assert_eq!(alarm_values(&db, oid), list_of([1, 2, 3]), "{elements:?}");
}
remove(&mut db, oid, &unsigned(&[3, 1, 3])).unwrap();
assert_eq!(alarm_values(&db, oid), list_of([2]));
}
#[test]
fn remove_list_element_checks_the_element_datatype_before_presence() {
let (mut db, oid) = msi_db(vec![1, 2]);
let wrong_type = (ErrorClass::PROPERTY, ErrorCode::INVALID_DATA_TYPE);
let absent = (ErrorClass::SERVICES, ErrorCode::LIST_ELEMENT_NOT_FOUND);
for (elements, (class, code), position) in [
(vec![0x11], wrong_type, 1),
(vec![0x21, 1, 0x91, 2], wrong_type, 2),
(vec![0x21, 1, 0x21, 9, 0x11], absent, 2),
] {
assert_eq!(
list_refusal(remove(&mut db, oid, &elements)),
(class, code, position),
"{elements:02x?}"
);
assert_eq!(alarm_values(&db, oid), list_of([1, 2]));
}
let (mut db, oid) = msi_db(vec![]);
assert_eq!(
list_refusal(remove(&mut db, oid, &[0x11])),
(ErrorClass::SERVICES, ErrorCode::LIST_ELEMENT_NOT_FOUND, 1)
);
}
#[test]
fn add_list_element_malformed_tail_errors_without_partial_commit() {
let (mut db, oid) = msi_db(vec![1]);
assert_eq!(
list_refusal(add(&mut db, oid, &[0x21, 2, 0x21, 3, 0xD1, 0])),
(ErrorClass::PROPERTY, ErrorCode::INVALID_DATA_TYPE, 3)
);
assert_eq!(alarm_values(&db, oid), list_of([1]));
}
#[test]
fn remove_list_element_malformed_tail_errors_without_partial_commit() {
let (mut db, oid) = msi_db(vec![2, 3]);
assert_eq!(
list_refusal(remove(&mut db, oid, &[0x21, 2, 0xD1, 0])),
(ErrorClass::PROPERTY, ErrorCode::INVALID_DATA_TYPE, 2)
);
assert_eq!(alarm_values(&db, oid), list_of([2, 3]));
}
#[test]
fn add_list_element_over_cap_returns_the_clause_15_1_error() {
let (mut db, oid) = msi_db_with_states(2048, (1..=1024).collect());
const NEW: [u8; 3] = [0x22, 0x07, 0xD0];
assert_eq!(
list_refusal(add(&mut db, oid, &NEW)),
(
ErrorClass::RESOURCES,
ErrorCode::NO_SPACE_TO_ADD_LIST_ELEMENT,
1
)
);
assert_eq!(
list_refusal(add(&mut db, oid, &[&unsigned(&[7, 9])[..], &NEW].concat())),
(
ErrorClass::RESOURCES,
ErrorCode::NO_SPACE_TO_ADD_LIST_ELEMENT,
3
)
);
add(&mut db, oid, &unsigned(&[7, 9])).unwrap();
assert_eq!(alarm_values(&db, oid), list_of(1..=1024));
}
#[test]
fn add_list_element_names_the_new_element_the_object_refuses() {
let mut db = ObjectDatabase::new();
let escalator = EscalatorObject::new(1, "ESC-1").unwrap();
let oid = escalator.object_identifier();
db.add(Box::new(escalator)).unwrap();
let fault_signals = |db: &ObjectDatabase| {
db.get(&oid)
.unwrap()
.read_property(PropertyIdentifier::FAULT_SIGNALS, None)
.unwrap()
};
handle_add_list_element(
&mut db,
&request(oid, PropertyIdentifier::FAULT_SIGNALS, &[0x91, 1]),
)
.unwrap();
assert_eq!(
list_refusal(handle_add_list_element(
&mut db,
&request(oid, PropertyIdentifier::FAULT_SIGNALS, &[0x91, 1, 0x91, 20]),
)),
(ErrorClass::PROPERTY, ErrorCode::VALUE_OUT_OF_RANGE, 2)
);
assert_eq!(
list_refusal(handle_add_list_element(
&mut db,
&request(
oid,
PropertyIdentifier::FAULT_SIGNALS,
&[0x91, 1, 0x44, 0x3F, 0x80, 0, 0],
),
)),
(ErrorClass::PROPERTY, ErrorCode::INVALID_DATA_TYPE, 2)
);
handle_add_list_element(
&mut db,
&request(oid, PropertyIdentifier::FAULT_SIGNALS, &[0x91, 1, 0x91, 1]),
)
.unwrap();
assert_eq!(
fault_signals(&db),
PropertyValue::List(vec![PropertyValue::Enumerated(1)])
);
}
#[test]
fn add_list_element_names_an_alarm_state_past_the_count() {
let (mut db, oid) = msi_db(vec![1]);
assert_eq!(
list_refusal(add(&mut db, oid, &unsigned(&[1, 2, 4]))),
(ErrorClass::PROPERTY, ErrorCode::VALUE_OUT_OF_RANGE, 3)
);
assert_eq!(alarm_values(&db, oid), list_of([1]));
}
#[test]
fn add_list_element_rejects_array_index_on_alarm_values() {
let (mut db, oid) = msi_db(vec![]);
let request = request_indexed(oid, PropertyIdentifier::ALARM_VALUES, Some(1), &[0x21, 2]);
assert_eq!(
list_refusal(handle_add_list_element(&mut db, &request)),
(ErrorClass::PROPERTY, ErrorCode::PROPERTY_IS_NOT_AN_ARRAY, 0)
);
assert_eq!(alarm_values(&db, oid), list_of([]));
}
#[test]
fn whole_list_write_property_decodes_all_elements() {
let (mut db, oid) = msi_db(vec![]);
let write = |db: &mut ObjectDatabase, property_value: Vec<u8>| {
let mut encoded = BytesMut::new();
WritePropertyRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::ALARM_VALUES,
property_array_index: None,
property_value,
priority: None,
}
.encode(&mut encoded)
.unwrap();
handle_write_property(db, &encoded)
};
write(&mut db, unsigned(&[2, 3])).unwrap();
assert_eq!(alarm_values(&db, oid), list_of([2, 3]));
assert_eq!(
list_refusal(write(&mut db, vec![0x21, 3, 0x11]).map(|_| ())),
(ErrorClass::PROPERTY, ErrorCode::INVALID_DATA_TYPE, 2)
);
assert_eq!(
alarm_values(&db, oid),
list_of([2, 3]),
"refused write leaves the list unchanged"
);
assert_eq!(
list_refusal(write(&mut db, unsigned(&[1, 7])).map(|_| ())),
(ErrorClass::PROPERTY, ErrorCode::VALUE_OUT_OF_RANGE, 2)
);
assert_eq!(alarm_values(&db, oid), list_of([2, 3]));
}