use super::*;
use PropertyIdentifier as P;
use bacnet_objects::{command::CommandObject, traits::BACnetObject};
use bacnet_types::constructed::{
BACnetActionCommand, BACnetActionList, PropertyReference, ReadAccessSpecification,
};
use bacnet_types::error::RemoteError;
use bacnet_types::primitives::PropertyValue;
const ACTION_1: &[u8] = &[
0x0E, 0x1C, 0x00, 0x40, 0x00, 0x01, 0x29, 0x55, 0x4E, 0x44, 0x42, 0x48, 0x00, 0x00, 0x4F, 0x59, 0x08, 0x79, 0x00, 0x89, 0x01, 0x0F, ];
const ACTION_2: &[u8] = &[0x0E, 0x0F];
const ACTION_3: &[u8] = &[
0x0E, 0x0C, 0x02, 0x00, 0x00, 0x09, 0x1C, 0x01, 0x40, 0x00, 0x03, 0x29, 0x55, 0x4E, 0x91, 0x01, 0x4F, 0x6A, 0x01, 0x2C, 0x79, 0x01, 0x89, 0x00, 0x0F, ];
fn action_lists() -> Vec<BACnetActionList> {
let ao1 = BACnetActionCommand {
device_identifier: None,
object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
property_identifier: P::PRESENT_VALUE,
property_array_index: None,
property_value: PropertyValue::Real(50.0),
priority: Some(8),
post_delay: None,
quit_on_failure: false,
write_successful: true,
};
let bv3 = BACnetActionCommand {
device_identifier: Some(ObjectIdentifier::new(ObjectType::DEVICE, 9).unwrap()),
object_identifier: ObjectIdentifier::new(ObjectType::BINARY_VALUE, 3).unwrap(),
property_value: PropertyValue::Enumerated(1),
priority: None,
post_delay: Some(300),
quit_on_failure: true,
write_successful: false,
..ao1.clone()
};
vec![
BACnetActionList {
commands: vec![ao1],
},
BACnetActionList::default(),
BACnetActionList {
commands: vec![bv3],
},
]
}
#[test]
fn rpm_command_action_serves_one_action_list_per_index() {
let whole_action = [ACTION_1, ACTION_2, ACTION_3].concat();
for configured in [false, true] {
let mut object = CommandObject::new(7, "CMD-7").unwrap();
if configured {
object
.write_property(
P::DESCRIPTION,
None,
bacnet_types::primitives::PropertyValue::CharacterString(
"long command label".repeat(100),
),
None,
)
.unwrap();
object.set_action(action_lists()).unwrap();
object
.write_property(
P::PRESENT_VALUE,
None,
bacnet_types::primitives::PropertyValue::Unsigned(2),
None,
)
.unwrap();
}
let oid = object.object_identifier();
let mut db = ObjectDatabase::new();
db.add(Box::new(object)).unwrap();
type ExpectedRead<'a> = Result<&'a [u8], ErrorCode>;
let action: &[(Option<u32>, ExpectedRead)] = if configured {
&[
(None, Ok(&whole_action)),
(Some(0), Ok(&[0x21, 3])),
(Some(1), Ok(ACTION_1)),
(Some(2), Ok(ACTION_2)),
(Some(3), Ok(ACTION_3)),
(Some(4), Err(ErrorCode::INVALID_ARRAY_INDEX)),
(Some(u32::MAX), Err(ErrorCode::INVALID_ARRAY_INDEX)),
]
} else {
&[
(None, Ok(&[])),
(Some(0), Ok(&[0x21, 0])),
(Some(1), Err(ErrorCode::INVALID_ARRAY_INDEX)),
(Some(u32::MAX), Err(ErrorCode::INVALID_ARRAY_INDEX)),
]
};
let mut cases: Vec<(P, Option<u32>, ExpectedRead)> = action
.iter()
.map(|&(index, expected)| (P::ACTION, index, expected))
.collect();
cases.extend_from_slice(&[
(
P::PRESENT_VALUE,
None,
Ok(if configured { &[0x21, 2] } else { &[0x21, 0] }),
),
(
P::PRESENT_VALUE,
Some(0),
Err(ErrorCode::PROPERTY_IS_NOT_AN_ARRAY),
),
(
P::PRESENT_VALUE,
Some(1),
Err(ErrorCode::PROPERTY_IS_NOT_AN_ARRAY),
),
(P::IN_PROCESS, None, Ok(&[0x10])),
(
P::IN_PROCESS,
Some(0),
Err(ErrorCode::PROPERTY_IS_NOT_AN_ARRAY),
),
(
P::IN_PROCESS,
Some(1),
Err(ErrorCode::PROPERTY_IS_NOT_AN_ARRAY),
),
(P::ALL_WRITES_SUCCESSFUL, None, Ok(&[0x11])),
(
P::ALL_WRITES_SUCCESSFUL,
Some(0),
Err(ErrorCode::PROPERTY_IS_NOT_AN_ARRAY),
),
(P::STATUS_FLAGS, None, Ok(&[0x82, 4, 0])),
(
P::STATUS_FLAGS,
Some(0),
Err(ErrorCode::PROPERTY_IS_NOT_AN_ARRAY),
),
(P::OUT_OF_SERVICE, None, Err(ErrorCode::UNKNOWN_PROPERTY)),
(
P::OUT_OF_SERVICE,
Some(0),
Err(ErrorCode::PROPERTY_IS_NOT_AN_ARRAY),
),
(P::RELIABILITY, None, Ok(&[0x91, 0])),
(
P::RELIABILITY,
Some(0),
Err(ErrorCode::PROPERTY_IS_NOT_AN_ARRAY),
),
(
P::PROPERTY_LIST,
None,
Ok(&[
0x91, 28, 0x91, 85, 0x91, 47, 0x91, 9, 0x91, 2, 0x91, 111, 0x91, 103,
]),
),
(P::PROPERTY_LIST, Some(0), Ok(&[0x21, 7])),
(P::PROPERTY_LIST, Some(1), Ok(&[0x91, 28])),
(P::PROPERTY_LIST, Some(2), Ok(&[0x91, 85])),
(P::PROPERTY_LIST, Some(3), Ok(&[0x91, 47])),
(P::PROPERTY_LIST, Some(4), Ok(&[0x91, 9])),
(P::PROPERTY_LIST, Some(5), Ok(&[0x91, 2])),
(P::PROPERTY_LIST, Some(6), Ok(&[0x91, 111])),
(P::PROPERTY_LIST, Some(7), Ok(&[0x91, 103])),
(
P::PROPERTY_LIST,
Some(8),
Err(ErrorCode::INVALID_ARRAY_INDEX),
),
(
P::PROPERTY_LIST,
Some(u32::MAX),
Err(ErrorCode::INVALID_ARRAY_INDEX),
),
(P::ACTION_TEXT, None, Err(ErrorCode::UNKNOWN_PROPERTY)),
(P::ACTION_TEXT, Some(1), Err(ErrorCode::UNKNOWN_PROPERTY)),
(P::EVENT_STATE, None, Err(ErrorCode::UNKNOWN_PROPERTY)),
(
P::EVENT_STATE,
Some(1),
Err(ErrorCode::PROPERTY_IS_NOT_AN_ARRAY),
),
]);
let mut request = BytesMut::new();
ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: cases
.iter()
.map(|&(p, i, _)| PropertyReference {
property_identifier: p,
property_array_index: i,
})
.collect(),
}],
}
.encode(&mut request)
.unwrap();
let mut legacy = BytesMut::new();
handle_read_property_multiple(&db, &request, &mut legacy).unwrap();
let ack = ReadPropertyMultipleACK::decode(&legacy).unwrap();
assert_eq!(ack.list_of_read_access_results.len(), 1);
let access = &ack.list_of_read_access_results[0];
assert_eq!(access.object_identifier, oid);
assert_eq!(access.list_of_results.len(), cases.len());
for (result, &(p, i, expected)) in access.list_of_results.iter().zip(&cases) {
assert_eq!(result.property_identifier, p);
let response_index = if matches!(
expected,
Err(ErrorCode::PROPERTY_IS_NOT_AN_ARRAY | ErrorCode::UNKNOWN_PROPERTY)
) {
None
} else {
i
};
assert_eq!(result.property_array_index, response_index);
let mut rp_request = BytesMut::new();
ReadPropertyRequest {
object_identifier: oid,
property_identifier: p,
property_array_index: i,
}
.encode(&mut rp_request);
let mut response = BytesMut::new();
let rp = handle_read_property(&db, &rp_request, &mut response);
match expected {
Ok(bytes) => {
assert!(result.error.is_none(), "{p:?} {i:?}");
assert_eq!(result.property_value.as_deref(), Some(bytes), "{p:?} {i:?}");
rp.unwrap();
let rp_ack = ReadPropertyACK::decode(&response).unwrap();
assert_eq!(rp_ack.object_identifier, oid);
assert_eq!(rp_ack.property_identifier, p);
assert_eq!(rp_ack.property_array_index, i);
assert_eq!(rp_ack.property_value, bytes);
}
Err(expected) => {
assert!(result.property_value.is_none());
assert_eq!(result.error, Some((ErrorClass::PROPERTY, expected)));
assert!(
matches!(rp, Err(Error::Remote(RemoteError::Error { class, code }))
if class == ErrorClass::PROPERTY && code == expected)
);
assert!(response.is_empty());
}
}
}
use crate::handlers::{ReadFailure, rpm_budget::handle_rpm_budgeted};
let budget = crate::server::ReadPropertyMultipleBudget {
max_result_elements: cases.len(),
max_service_ack_bytes: legacy.len(),
};
let mut bounded = BytesMut::new();
handle_rpm_budgeted(&db, &request, &mut bounded, budget).unwrap();
assert_eq!(bounded, legacy);
let mut prefix = BytesMut::from(&b"prefix"[..]);
assert!(matches!(
handle_rpm_budgeted(
&db,
&request,
&mut prefix,
crate::server::ReadPropertyMultipleBudget {
max_result_elements: cases.len() - 1,
..budget
}
),
Err(ReadFailure::Work)
));
assert_eq!(&prefix[..], b"prefix");
assert!(matches!(
handle_rpm_budgeted(
&db,
&request,
&mut prefix,
crate::server::ReadPropertyMultipleBudget {
max_service_ack_bytes: legacy.len() - 1,
..budget
}
),
Err(ReadFailure::Bytes)
));
assert_eq!(&prefix[..], b"prefix");
}
}