use super::*;
use bacnet_objects::analog::{AnalogInputObject, AnalogOutputObject};
use bacnet_objects::command::CommandObject;
use bacnet_objects::device::{DeviceConfig, DeviceObject};
use bacnet_objects::event_log::EventLogObject;
use bacnet_objects::group::{GlobalGroupObject, GroupObject, StructuredViewObject};
use bacnet_objects::multistate::MultiStateInputObject;
use bacnet_objects::notification_class::NotificationClass;
use bacnet_objects::schedule::{CalendarObject, ScheduleObject};
use bacnet_objects::staging::{StagingConfig, StagingObject};
use bacnet_objects::value_types::CharacterStringValueObject;
use bacnet_services::common::BACnetPropertyValue;
use bacnet_services::wpm::WriteAccessSpecification;
use bacnet_types::constructed::{BACnetDeviceObjectReference, BACnetStageLimitValue};
use bacnet_types::enums::EngineeringUnits;
use bacnet_types::error::RemoteError;
fn oid(object_type: ObjectType, instance: u32) -> ObjectIdentifier {
ObjectIdentifier::new(object_type, instance).unwrap()
}
fn staging(instance: u32, name: &str) -> StagingObject {
StagingObject::new(
instance,
name,
StagingConfig {
present_value: 0.0,
min_present_value: -1.0,
units: EngineeringUnits::DEGREES_CELSIUS,
priority_for_writing: 8,
stages: vec![
BACnetStageLimitValue {
limit: 1.0,
values: vec![false],
deadband: 0.0,
},
BACnetStageLimitValue {
limit: 2.0,
values: vec![false],
deadband: 0.0,
},
BACnetStageLimitValue {
limit: 3.0,
values: vec![false],
deadband: 0.0,
},
],
target_references: vec![BACnetDeviceObjectReference {
device_identifier: None,
object_identifier: oid(ObjectType::BINARY_OUTPUT, ObjectIdentifier::MAX_INSTANCE),
}],
stage_names: Some(vec!["Low".into(), "Middle".into(), "High".into()]),
},
)
.unwrap()
}
fn gating_db() -> ObjectDatabase {
let mut db = ObjectDatabase::new();
let mut device = DeviceObject::new(DeviceConfig {
identity: bacnet_objects::DeviceIdentity::default().with_name("GateDevice"),
..Default::default()
})
.unwrap();
device.set_object_list(vec![
oid(ObjectType::DEVICE, 1),
oid(ObjectType::ANALOG_INPUT, 1),
]);
db.add(Box::new(device)).unwrap();
db.add(Box::new(
AnalogInputObject::new(1, "AI-1", EngineeringUnits::DEGREES_CELSIUS).unwrap(),
))
.unwrap();
db.add(Box::new(
AnalogOutputObject::new(1, "AO-1", EngineeringUnits::DEGREES_CELSIUS).unwrap(),
))
.unwrap();
db.add(Box::new(MultiStateInputObject::new(1, "MSI-1", 3).unwrap()))
.unwrap();
db.add(Box::new(CalendarObject::new(1, "CAL-1").unwrap()))
.unwrap();
db.add(Box::new(GroupObject::new(1, "GRP-1").unwrap()))
.unwrap();
db.add(Box::new(NotificationClass::new(1, "NC-1").unwrap()))
.unwrap();
db.add(Box::new(EventLogObject::new(1, "ELG-1", 16).unwrap()))
.unwrap();
db.add(Box::new(
ScheduleObject::new(1, "SCH-1", PropertyValue::Unsigned(0)).unwrap(),
))
.unwrap();
db.add(Box::new(
CharacterStringValueObject::new(1, "CSV-1").unwrap(),
))
.unwrap();
db.add(Box::new(GlobalGroupObject::new(1, "GG-1").unwrap()))
.unwrap();
db.add(Box::new(StructuredViewObject::new(1, "SV-1").unwrap()))
.unwrap();
let mut command = CommandObject::new(1, "CMD-1").unwrap();
command
.set_action(vec![bacnet_types::constructed::BACnetActionList {
commands: vec![bacnet_types::constructed::BACnetActionCommand {
device_identifier: None,
object_identifier: oid(ObjectType::ANALOG_OUTPUT, 1),
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
property_value: PropertyValue::Real(1.0),
priority: None,
post_delay: None,
quit_on_failure: false,
write_successful: true,
}],
}])
.unwrap();
db.add(Box::new(command)).unwrap();
db.add(Box::new(staging(1, "STG-1"))).unwrap();
db
}
const LIST_TARGETS: &[(ObjectType, PropertyIdentifier)] = &[
(ObjectType::CALENDAR, PropertyIdentifier::DATE_LIST),
(ObjectType::GROUP, PropertyIdentifier::LIST_OF_GROUP_MEMBERS),
(
ObjectType::NOTIFICATION_CLASS,
PropertyIdentifier::RECIPIENT_LIST,
),
(ObjectType::EVENT_LOG, PropertyIdentifier::LOG_BUFFER),
(
ObjectType::DEVICE,
PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS,
),
(
ObjectType::SCHEDULE,
PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES,
),
(
ObjectType::MULTI_STATE_INPUT,
PropertyIdentifier::ALARM_VALUES,
),
];
fn read_indexed(
db: &ObjectDatabase,
target_oid: ObjectIdentifier,
property: PropertyIdentifier,
index: u32,
) -> Result<(), Error> {
let request = ReadPropertyRequest {
object_identifier: target_oid,
property_identifier: property,
property_array_index: Some(index),
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_read_property(db, &buf, &mut ack_buf).map(|_| ())
}
fn assert_not_an_array(result: Result<(), Error>, context: &str) {
match result {
Err(Error::Remote(RemoteError::Error { class, code })) => {
assert_eq!(class, ErrorClass::PROPERTY, "{context}: wrong error class");
assert_eq!(
code,
ErrorCode::PROPERTY_IS_NOT_AN_ARRAY,
"{context}: wrong error code"
);
}
other => panic!("{context}: expected PROPERTY/PROPERTY_IS_NOT_AN_ARRAY, got {other:?}"),
}
}
fn assert_protocol_error(result: Result<(), Error>, expected_code: ErrorCode, context: &str) {
match result {
Err(Error::Remote(RemoteError::Error { class, code })) => {
assert_eq!(class, ErrorClass::PROPERTY, "{context}: wrong error class");
assert_eq!(code, expected_code, "{context}: wrong error code");
}
other => panic!("{context}: expected PROPERTY/{expected_code:?}, got {other:?}"),
}
}
fn encode_value(value: PropertyValue) -> Vec<u8> {
let mut buf = BytesMut::new();
encode_property_value(&mut buf, &value).unwrap();
buf.to_vec()
}
fn write_indexed(
db: &mut ObjectDatabase,
target_oid: ObjectIdentifier,
property: PropertyIdentifier,
index: Option<u32>,
value_bytes: Vec<u8>,
) -> Result<(), Error> {
let request = WritePropertyRequest {
object_identifier: target_oid,
property_identifier: property,
property_array_index: index,
property_value: value_bytes,
priority: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
handle_write_property(db, &buf).map(|_| ())
}
fn wpm_single(
db: &mut ObjectDatabase,
target_oid: ObjectIdentifier,
property: PropertyIdentifier,
index: Option<u32>,
value_bytes: Vec<u8>,
) -> Result<(), Error> {
let request = WritePropertyMultipleRequest {
list_of_write_access_specs: vec![WriteAccessSpecification {
object_identifier: target_oid,
list_of_properties: vec![BACnetPropertyValue {
property_identifier: property,
property_array_index: index,
value: value_bytes,
priority: None,
}],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
handle_write_property_multiple(db, &buf).map(|_| ())
}
#[test]
fn list_properties_reject_indexed_read_property() {
let db = gating_db();
for &(object_type, property) in LIST_TARGETS {
assert_not_an_array(
read_indexed(&db, oid(object_type, 1), property, 1),
&format!("ReadProperty {object_type:?}.{property:?}"),
);
}
}
#[test]
fn list_properties_reject_indexed_read_property_multiple_inline() {
use bacnet_types::constructed::{PropertyReference, ReadAccessSpecification};
let db = gating_db();
for &(object_type, property) in LIST_TARGETS {
let request = ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid(object_type, 1),
list_of_property_references: vec![
PropertyReference {
property_identifier: PropertyIdentifier::OBJECT_NAME,
property_array_index: None,
},
PropertyReference {
property_identifier: property,
property_array_index: Some(1),
},
],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let mut ack_buf = BytesMut::new();
handle_read_property_multiple(&db, &buf, &mut ack_buf).unwrap();
let ack = ReadPropertyMultipleACK::decode(&ack_buf).unwrap();
let results = &ack.list_of_read_access_results[0].list_of_results;
assert!(
results[0].property_value.is_some(),
"{object_type:?}.OBJECT_NAME: sibling read must succeed"
);
assert_eq!(
results[1].error,
Some((ErrorClass::PROPERTY, ErrorCode::PROPERTY_IS_NOT_AN_ARRAY)),
"{object_type:?}.{property:?}: indexed list read must fail inline"
);
assert_eq!(results[1].property_array_index, None);
}
}
#[test]
fn list_properties_reject_indexed_write_property_and_write_property_multiple() {
for &(object_type, property) in LIST_TARGETS {
let mut db = gating_db();
let value = encode_value(PropertyValue::OctetString(vec![0]));
assert_not_an_array(
write_indexed(
&mut db,
oid(object_type, 1),
property,
Some(1),
value.clone(),
),
&format!("WriteProperty {object_type:?}.{property:?}"),
);
assert_not_an_array(
wpm_single(&mut db, oid(object_type, 1), property, Some(1), value),
&format!("WritePropertyMultiple {object_type:?}.{property:?}"),
);
}
}
#[test]
fn true_arrays_pass_the_gate_on_read_property() {
let db = gating_db();
let mut buf = BytesMut::new();
ReadPropertyRequest {
object_identifier: oid(ObjectType::DEVICE, 1),
property_identifier: PropertyIdentifier::OBJECT_LIST,
property_array_index: Some(0),
}
.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_read_property(&db, &buf, &mut ack_buf).unwrap();
let ack = ReadPropertyACK::decode(&ack_buf).unwrap();
let (count, _) =
bacnet_encoding::primitives::decode_application_value(&ack.property_value, 0).unwrap();
assert_eq!(count, PropertyValue::Unsigned(2));
read_indexed(
&db,
oid(ObjectType::DEVICE, 1),
PropertyIdentifier::OBJECT_LIST,
1,
)
.unwrap();
read_indexed(
&db,
oid(ObjectType::DEVICE, 1),
PropertyIdentifier::OBJECT_LIST,
2,
)
.unwrap();
for index in [0, 1, 3] {
read_indexed(
&db,
oid(ObjectType::ANALOG_INPUT, 1),
PropertyIdentifier::PROPERTY_LIST,
index,
)
.unwrap();
}
for index in [0, 1, 3] {
read_indexed(
&db,
oid(ObjectType::MULTI_STATE_INPUT, 1),
PropertyIdentifier::STATE_TEXT,
index,
)
.unwrap();
}
read_indexed(
&db,
oid(ObjectType::SCHEDULE, 1),
PropertyIdentifier::WEEKLY_SCHEDULE,
0,
)
.unwrap();
read_indexed(
&db,
oid(ObjectType::SCHEDULE, 1),
PropertyIdentifier::WEEKLY_SCHEDULE,
1,
)
.unwrap();
read_indexed(
&db,
oid(ObjectType::SCHEDULE, 1),
PropertyIdentifier::EXCEPTION_SCHEDULE,
0,
)
.unwrap();
for index in [1, 8, 16] {
read_indexed(
&db,
oid(ObjectType::ANALOG_OUTPUT, 1),
PropertyIdentifier::PRIORITY_ARRAY,
index,
)
.unwrap();
}
}
#[test]
fn modeled_arrays_on_command_and_staging_pass_the_gate() {
let db = gating_db();
for &(property, object_type) in &[
(PropertyIdentifier::ACTION, ObjectType::COMMAND),
(PropertyIdentifier::STAGES, ObjectType::STAGING),
(PropertyIdentifier::STAGE_NAMES, ObjectType::STAGING),
(PropertyIdentifier::TARGET_REFERENCES, ObjectType::STAGING),
] {
for index in [0, 1] {
read_indexed(&db, oid(object_type, 1), property, index).unwrap_or_else(|e| {
panic!("{object_type:?}.{property:?} index {index} must pass the gate: {e:?}")
});
}
}
}
#[test]
fn event_time_stamps_accepts_index_range_via_event_history() {
let db = gating_db();
let msi = oid(ObjectType::MULTI_STATE_INPUT, 1);
for index in [None, Some(0), Some(1), Some(2), Some(3)] {
let request = ReadPropertyRequest {
object_identifier: msi,
property_identifier: PropertyIdentifier::EVENT_TIME_STAMPS,
property_array_index: index,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_read_property(&db, &buf, &mut ack_buf)
.unwrap_or_else(|e| panic!("EVENT_TIME_STAMPS index {index:?} must be served: {e:?}"));
}
assert_protocol_error(
read_indexed(&db, msi, PropertyIdentifier::EVENT_TIME_STAMPS, 4),
ErrorCode::INVALID_ARRAY_INDEX,
"EVENT_TIME_STAMPS index 4",
);
}
#[test]
fn alarm_values_gate_varies_by_object_type() {
let db = gating_db();
assert_not_an_array(
read_indexed(
&db,
oid(ObjectType::MULTI_STATE_INPUT, 1),
PropertyIdentifier::ALARM_VALUES,
1,
),
"ReadProperty MULTI_STATE_INPUT.ALARM_VALUES",
);
assert_protocol_error(
read_indexed(
&db,
oid(ObjectType::CHARACTERSTRING_VALUE, 1),
PropertyIdentifier::ALARM_VALUES,
1,
),
ErrorCode::UNKNOWN_PROPERTY,
"ReadProperty CHARACTERSTRING_VALUE.ALARM_VALUES must NOT be gated",
);
}
#[test]
fn schedule_list_of_object_property_references_rejects_indexed_read_and_write() {
let db = gating_db();
let sched = oid(ObjectType::SCHEDULE, 1);
assert_not_an_array(
read_indexed(
&db,
sched,
PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES,
1,
),
"ReadProperty SCHEDULE.LIST_OF_OBJECT_PROPERTY_REFERENCES",
);
let mut db = gating_db();
let value = encode_value(PropertyValue::OctetString(vec![0]));
assert_not_an_array(
write_indexed(
&mut db,
sched,
PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES,
Some(1),
value,
),
"WriteProperty SCHEDULE.LIST_OF_OBJECT_PROPERTY_REFERENCES",
);
}
#[test]
fn object_level_classification_matrix() {
use bacnet_objects::traits::BACnetObject;
use bacnet_types::enums::EngineeringUnits;
let nc = NotificationClass::new(9, "NC-9").unwrap();
assert!(nc.is_array_property(PropertyIdentifier::PRIORITY));
assert!(!nc.is_array_property(PropertyIdentifier::RECIPIENT_LIST));
assert!(!nc.is_array_property(PropertyIdentifier::ACK_REQUIRED));
let schedule = ScheduleObject::new(9, "SCH-9", PropertyValue::Unsigned(0)).unwrap();
assert!(!schedule.is_array_property(PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES));
assert!(schedule.is_array_property(PropertyIdentifier::WEEKLY_SCHEDULE));
assert!(schedule.is_array_property(PropertyIdentifier::EXCEPTION_SCHEDULE));
assert!(!schedule.is_array_property(PropertyIdentifier::PRESENT_VALUE));
let calendar = CalendarObject::new(9, "CAL-9").unwrap();
assert!(!calendar.is_array_property(PropertyIdentifier::DATE_LIST));
let event_log = EventLogObject::new(9, "ELG-9", 16).unwrap();
assert!(!event_log.is_array_property(PropertyIdentifier::LOG_BUFFER));
let gg = GlobalGroupObject::new(9, "GG-9").unwrap();
assert!(gg.is_array_property(PropertyIdentifier::GROUP_MEMBERS));
assert!(gg.is_array_property(PropertyIdentifier::GROUP_MEMBER_NAMES));
assert!(gg.is_array_property(PropertyIdentifier::PRESENT_VALUE));
let sv = StructuredViewObject::new(9, "SV-9").unwrap();
assert!(sv.is_array_property(PropertyIdentifier::SUBORDINATE_LIST));
assert!(sv.is_array_property(PropertyIdentifier::SUBORDINATE_ANNOTATIONS));
let ai = AnalogInputObject::new(9, "AI-9", EngineeringUnits::DEGREES_CELSIUS).unwrap();
assert!(!ai.is_array_property(PropertyIdentifier::PRESENT_VALUE));
assert!(ai.is_array_property(PropertyIdentifier::PROPERTY_LIST));
let csv = CharacterStringValueObject::new(9, "CSV-9").unwrap();
assert!(csv.is_array_property(PropertyIdentifier::ALARM_VALUES));
assert!(csv.is_array_property(PropertyIdentifier::FAULT_VALUES));
assert!(csv.is_array_property(PropertyIdentifier::PRIORITY_ARRAY));
let ao = AnalogOutputObject::new(9, "AO-9", EngineeringUnits::DEGREES_CELSIUS).unwrap();
assert!(ao.is_array_property(PropertyIdentifier::PRIORITY_ARRAY));
assert!(!ao.is_array_property(PropertyIdentifier::PRESENT_VALUE));
let cmd = CommandObject::new(9, "CMD-9").unwrap();
assert!(cmd.is_array_property(PropertyIdentifier::ACTION));
assert!(cmd.is_array_property(PropertyIdentifier::ACTION_TEXT));
assert!(!cmd.is_array_property(PropertyIdentifier::PRESENT_VALUE));
let stg = staging(9, "STG-9");
assert!(stg.is_array_property(PropertyIdentifier::STAGES));
assert!(stg.is_array_property(PropertyIdentifier::STAGE_NAMES));
assert!(stg.is_array_property(PropertyIdentifier::TARGET_REFERENCES));
assert!(!stg.is_array_property(PropertyIdentifier::PRESENT_STAGE));
}
#[test]
fn wpm_omitted_index_priority_array_write_is_write_access_denied() {
let mut db = gating_db();
let ao = oid(ObjectType::ANALOG_OUTPUT, 1);
let value = encode_value(PropertyValue::Real(1.0));
assert_protocol_error(
wpm_single(&mut db, ao, PropertyIdentifier::PRIORITY_ARRAY, None, value),
ErrorCode::WRITE_ACCESS_DENIED,
"WPM PRIORITY_ARRAY without index",
);
}
#[test]
fn write_property_omitted_index_priority_array_is_write_access_denied() {
let mut db = gating_db();
let ao = oid(ObjectType::ANALOG_OUTPUT, 1);
let value = encode_value(PropertyValue::Real(1.0));
assert_protocol_error(
write_indexed(&mut db, ao, PropertyIdentifier::PRIORITY_ARRAY, None, value),
ErrorCode::WRITE_ACCESS_DENIED,
"WriteProperty PRIORITY_ARRAY without index",
);
}
#[test]
fn priority_array_count_and_out_of_range_write_are_read_only() {
let mut db = gating_db();
let ao = oid(ObjectType::ANALOG_OUTPUT, 1);
for index in [Some(0), Some(17)] {
let value = encode_value(PropertyValue::Real(1.0));
assert_protocol_error(
wpm_single(
&mut db,
ao,
PropertyIdentifier::PRIORITY_ARRAY,
index,
value.clone(),
),
ErrorCode::WRITE_ACCESS_DENIED,
&format!("WPM PRIORITY_ARRAY index {index:?}"),
);
assert_protocol_error(
write_indexed(
&mut db,
ao,
PropertyIdentifier::PRIORITY_ARRAY,
index,
value,
),
ErrorCode::WRITE_ACCESS_DENIED,
&format!("WriteProperty PRIORITY_ARRAY index {index:?}"),
);
}
}
#[test]
fn indexed_access_to_recipient_list_rejected_with_not_an_array() {
let db = gating_db();
let nc = oid(ObjectType::NOTIFICATION_CLASS, 1);
assert_not_an_array(
read_indexed(&db, nc, PropertyIdentifier::RECIPIENT_LIST, 2),
"ReadProperty NOTIFICATION_CLASS.RECIPIENT_LIST",
);
let mut db = gating_db();
let value = encode_value(PropertyValue::OctetString(vec![0]));
assert_not_an_array(
write_indexed(
&mut db,
nc,
PropertyIdentifier::RECIPIENT_LIST,
Some(2),
value,
),
"WriteProperty NOTIFICATION_CLASS.RECIPIENT_LIST",
);
let mut framed = BytesMut::new();
bacnet_encoding::constructed::encode_destination_list(&mut framed, &[]).unwrap();
let mut db = gating_db();
write_indexed(
&mut db,
nc,
PropertyIdentifier::RECIPIENT_LIST,
None,
framed.to_vec(),
)
.unwrap();
}
#[test]
fn notification_class_priority_accepts_index_range() {
let db = gating_db();
let nc = oid(ObjectType::NOTIFICATION_CLASS, 1);
let mut buf = BytesMut::new();
ReadPropertyRequest {
object_identifier: nc,
property_identifier: PropertyIdentifier::PRIORITY,
property_array_index: Some(0),
}
.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_read_property(&db, &buf, &mut ack_buf).unwrap();
let ack = ReadPropertyACK::decode(&ack_buf).unwrap();
assert_eq!(ack.property_array_index, Some(0));
let (count, _) =
bacnet_encoding::primitives::decode_application_value(&ack.property_value, 0).unwrap();
assert_eq!(count, PropertyValue::Unsigned(3));
for index in 1..=3 {
read_indexed(&db, nc, PropertyIdentifier::PRIORITY, index).unwrap();
}
}
#[test]
fn wpm_gate_rejection_keeps_valid_prefix() {
let mut db = gating_db();
let nc = oid(ObjectType::NOTIFICATION_CLASS, 1);
let request = WritePropertyMultipleRequest {
list_of_write_access_specs: vec![WriteAccessSpecification {
object_identifier: nc,
list_of_properties: vec![
BACnetPropertyValue {
property_identifier: PropertyIdentifier::DESCRIPTION,
property_array_index: None,
value: encode_value(PropertyValue::CharacterString("MUTATED".into())),
priority: None,
},
BACnetPropertyValue {
property_identifier: PropertyIdentifier::RECIPIENT_LIST,
property_array_index: Some(1),
value: encode_value(PropertyValue::OctetString(vec![0])),
priority: None,
},
],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
assert_not_an_array(
handle_write_property_multiple(&mut db, &buf).map(|_| ()),
"WPM with one gated reference",
);
assert_eq!(
db.get(&nc)
.unwrap()
.read_property(PropertyIdentifier::DESCRIPTION, None)
.unwrap(),
PropertyValue::CharacterString("MUTATED".into()),
"the valid prefix remains applied when a sibling hits the gate"
);
}
#[test]
fn reference_properties_reject_indexed_read_property() {
use bacnet_objects::accumulator::PulseConverterObject;
use bacnet_objects::averaging::AveragingObject;
use bacnet_objects::loop_obj::LoopObject;
use bacnet_types::enums::EngineeringUnits;
let mut db = ObjectDatabase::new();
db.add(Box::new(
LoopObject::new(1, "LOOP-1", EngineeringUnits::DEGREES_CELSIUS).unwrap(),
))
.unwrap();
db.add(Box::new(
PulseConverterObject::new(1, "PC-1", EngineeringUnits::DEGREES_CELSIUS).unwrap(),
))
.unwrap();
db.add(Box::new(AveragingObject::new(1, "AVG-1").unwrap()))
.unwrap();
let targets: &[(ObjectIdentifier, PropertyIdentifier)] = &[
(
oid(ObjectType::LOOP, 1),
PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE,
),
(
oid(ObjectType::LOOP, 1),
PropertyIdentifier::MANIPULATED_VARIABLE_REFERENCE,
),
(
oid(ObjectType::LOOP, 1),
PropertyIdentifier::SETPOINT_REFERENCE,
),
(
oid(ObjectType::PULSE_CONVERTER, 1),
PropertyIdentifier::INPUT_REFERENCE,
),
(
oid(ObjectType::AVERAGING, 1),
PropertyIdentifier::OBJECT_PROPERTY_REFERENCE,
),
];
for &(target, property) in targets {
assert_not_an_array(
read_indexed(&db, target, property, 2),
&format!("ReadProperty {target:?}.{property:?}[2]"),
);
}
}