use super::super::channel_wire_tests::{ch, channel, settled};
use super::super::command_action_wire_tests::{cmd, idle, read_db};
use super::super::cov_wire_test_support::*;
use super::super::*;
use bacnet_encoding::constructed::decode_action_list;
use bacnet_objects::analog::AnalogInputObject;
use bacnet_objects::binary::BinaryValueObject;
use bacnet_objects::command::CommandObject;
use bacnet_objects::multistate::MultiStateValueObject;
use bacnet_types::constructed::{
BACnetActionCommand, BACnetActionList, BACnetDeviceObjectPropertyReference,
};
use bacnet_types::enums::{EngineeringUnits, ObjectType, Reliability, WriteStatus};
use bacnet_types::error::RemoteError;
use PropertyIdentifier as P;
fn ai() -> ObjectIdentifier {
ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap()
}
fn bv() -> ObjectIdentifier {
ObjectIdentifier::new(ObjectType::BINARY_VALUE, 1).unwrap()
}
fn msv() -> ObjectIdentifier {
ObjectIdentifier::new(ObjectType::MULTI_STATE_VALUE, 1).unwrap()
}
fn text(value: &str) -> PropertyValue {
PropertyValue::CharacterString(value.into())
}
fn indexed(
object: ObjectIdentifier,
property: PropertyIdentifier,
index: u32,
value: &str,
) -> BACnetActionCommand {
BACnetActionCommand {
device_identifier: None,
object_identifier: object,
property_identifier: property,
property_array_index: Some(index),
property_value: text(value),
priority: None,
post_delay: None,
quit_on_failure: false,
write_successful: true,
}
}
fn member(
object: ObjectIdentifier,
property: PropertyIdentifier,
index: u32,
) -> BACnetDeviceObjectPropertyReference {
BACnetDeviceObjectPropertyReference {
property_array_index: Some(index),
..BACnetDeviceObjectPropertyReference::new_local(object, property.to_raw())
}
}
fn objects(db: &mut ObjectDatabase) {
db.add(Box::new(
AnalogInputObject::new(1, "AI-1", EngineeringUnits::DEGREES_CELSIUS).unwrap(),
))
.unwrap();
db.add(Box::new(BinaryValueObject::new(1, "BV-1").unwrap()))
.unwrap();
db.add(Box::new(MultiStateValueObject::new(1, "MSV-1", 3).unwrap()))
.unwrap();
let mut command = CommandObject::new(1, "CMD-1").unwrap();
command
.set_action(vec![BACnetActionList {
commands: vec![
indexed(ai(), P::DESCRIPTION, 1, "command"),
indexed(msv(), P::STATE_TEXT, 2, "command"),
indexed(msv(), P::STATE_TEXT, 9, "command"),
],
}])
.unwrap();
db.add(Box::new(command)).unwrap();
db.add(Box::new(channel(
1,
1,
vec![
(member(ai(), P::DESCRIPTION, 1), 0),
(member(msv(), P::STATE_TEXT, 3), 0),
],
)))
.unwrap();
}
async fn start() -> Harness {
Harness::start_with(ServerConfig::default(), objects).await
}
async fn snapshot(h: &Harness) -> Vec<PropertyValue> {
let mut values = Vec::new();
for (object, property) in [
(ai(), P::DESCRIPTION),
(ai(), P::OBJECT_NAME),
(bv(), P::ACTIVE_TEXT),
(bv(), P::PRESENT_VALUE),
(msv(), P::STATE_TEXT),
] {
values.push(read_db(h, object, property, None).await);
}
values
}
#[tokio::test(start_paused = true)]
async fn write_local_checks_an_array_index_before_the_object_sees_the_value() {
let h = start().await;
let before = snapshot(&h).await;
let missing = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 9).unwrap();
for (object, property, index, value, class, code) in [
(
ai(),
P::DESCRIPTION,
Some(1),
text("indexed"),
ErrorClass::PROPERTY,
ErrorCode::PROPERTY_IS_NOT_AN_ARRAY,
),
(
bv(),
P::ACTIVE_TEXT,
Some(1),
text("indexed"),
ErrorClass::PROPERTY,
ErrorCode::PROPERTY_IS_NOT_AN_ARRAY,
),
(
bv(),
P::PRESENT_VALUE,
Some(1),
PropertyValue::Enumerated(1),
ErrorClass::PROPERTY,
ErrorCode::PROPERTY_IS_NOT_AN_ARRAY,
),
(
ai(),
P::STATE_TEXT,
Some(1),
text("indexed"),
ErrorClass::PROPERTY,
ErrorCode::UNKNOWN_PROPERTY,
),
(
msv(),
P::STATE_TEXT,
Some(9),
text("indexed"),
ErrorClass::PROPERTY,
ErrorCode::INVALID_ARRAY_INDEX,
),
(
ai(),
P::OBJECT_NAME,
None,
text("BV-1"),
ErrorClass::PROPERTY,
ErrorCode::DUPLICATE_NAME,
),
(
missing,
P::DESCRIPTION,
None,
text("indexed"),
ErrorClass::OBJECT,
ErrorCode::UNKNOWN_OBJECT,
),
] {
let error = h
.server
.write_local(
&object,
property,
index,
value,
None,
crate::LocalCommandSource::ServerDevice,
)
.await
.unwrap_err();
assert!(
matches!(error, Error::Remote(RemoteError::Error { class: c, code: e })
if c == class && e == code),
"{object} {property:?} {index:?}: {error:?}"
);
}
assert_eq!(snapshot(&h).await, before);
h.server
.write_local(
&msv(),
P::STATE_TEXT,
Some(2),
text("Two"),
None,
crate::LocalCommandSource::ServerDevice,
)
.await
.unwrap();
let states = [
read_db(&h, msv(), P::STATE_TEXT, Some(1)).await,
read_db(&h, msv(), P::STATE_TEXT, Some(2)).await,
read_db(&h, msv(), P::STATE_TEXT, Some(3)).await,
];
let PropertyValue::List(old) = &before[4] else {
panic!("State_Text read {:?}", before[4]);
};
assert_eq!(states, [old[0].clone(), text("Two"), old[2].clone()]);
}
async fn flags(h: &Harness) -> Vec<bool> {
let PropertyValue::ApplicationData(element) = read_db(h, cmd(1), P::ACTION, Some(1)).await
else {
panic!("Action[1] is one encoded list");
};
let (list, _) = decode_action_list(&element, 0).unwrap();
list.commands
.iter()
.map(|command| command.write_successful)
.collect()
}
#[tokio::test(start_paused = true)]
async fn a_command_write_whose_index_the_property_refuses_fails() {
let h = start().await;
let description = read_db(&h, ai(), P::DESCRIPTION, None).await;
h.server
.write_local(
&cmd(1),
P::PRESENT_VALUE,
None,
PropertyValue::Unsigned(1),
None,
crate::LocalCommandSource::ServerDevice,
)
.await
.unwrap();
idle(&h, 1).await;
assert_eq!(flags(&h).await, [false, true, false]);
assert_eq!(
read_db(&h, cmd(1), P::ALL_WRITES_SUCCESSFUL, None).await,
PropertyValue::Boolean(false)
);
assert_eq!(read_db(&h, ai(), P::DESCRIPTION, None).await, description);
assert_eq!(
read_db(&h, msv(), P::STATE_TEXT, Some(2)).await,
text("command")
);
}
#[tokio::test(start_paused = true)]
async fn a_channel_member_whose_index_the_property_refuses_is_a_configuration_failure() {
let mut h = start().await;
let description = read_db(&h, ai(), P::DESCRIPTION, None).await;
h.server
.write_local(
&ch(1),
P::PRESENT_VALUE,
None,
text("channel"),
Some(8),
crate::LocalCommandSource::ServerDevice,
)
.await
.unwrap();
assert_eq!(settled(&mut h, 1).await, WriteStatus::FAILED);
assert_eq!(
read_db(&h, ch(1), P::RELIABILITY, None).await,
PropertyValue::Enumerated(Reliability::CONFIGURATION_ERROR.to_raw())
);
assert_eq!(read_db(&h, ai(), P::DESCRIPTION, None).await, description);
assert_eq!(
read_db(&h, msv(), P::STATE_TEXT, Some(3)).await,
text("channel")
);
}