use super::*;
use bacnet_encoding::constructed::encode_bacnet_property_value;
use bacnet_objects::lighting::BinaryLightingOutputObject;
use bacnet_services::common::BACnetPropertyValue;
use bacnet_types::error::RemoteError;
use std::time::Duration;
fn encode_value(value: PropertyValue) -> Vec<u8> {
let mut bytes = BytesMut::new();
encode_property_value(&mut bytes, &value).unwrap();
bytes.to_vec()
}
fn database() -> (ObjectDatabase, ObjectIdentifier) {
let mut db = ObjectDatabase::new();
let object = BinaryLightingOutputObject::new(1, "BLO-1").unwrap();
let oid = object.object_identifier();
db.add(Box::new(object)).unwrap();
(db, oid)
}
fn wp(
db: &mut ObjectDatabase,
oid: ObjectIdentifier,
property: PropertyIdentifier,
array_index: Option<u32>,
value: PropertyValue,
priority: Option<u8>,
) -> Result<(), Error> {
let request = WritePropertyRequest {
object_identifier: oid,
property_identifier: property,
property_array_index: array_index,
property_value: encode_value(value),
priority,
};
let mut bytes = BytesMut::new();
request.encode(&mut bytes).unwrap();
sourced_wp(db, &bytes).map(|_| ())
}
fn wpm(
db: &mut ObjectDatabase,
oid: ObjectIdentifier,
writes: Vec<(PropertyIdentifier, Option<u32>, PropertyValue, Option<u8>)>,
) -> Result<(), Error> {
let mut bytes = BytesMut::new();
bacnet_encoding::primitives::encode_ctx_object_id(&mut bytes, 0, &oid);
bacnet_encoding::tags::encode_opening_tag(&mut bytes, 1);
for (property_identifier, property_array_index, value, priority) in writes {
encode_bacnet_property_value(
&BACnetPropertyValue {
property_identifier,
property_array_index,
value: encode_value(value),
priority,
},
&mut bytes,
);
}
bacnet_encoding::tags::encode_closing_tag(&mut bytes, 1);
sourced_wpm(db, &bytes).map(|_| ())
}
fn read(
db: &ObjectDatabase,
oid: ObjectIdentifier,
property: PropertyIdentifier,
index: Option<u32>,
) -> PropertyValue {
db.get(&oid)
.unwrap()
.read_property(property, index)
.unwrap()
}
fn blink_count(db: &ObjectDatabase, oid: ObjectIdentifier) -> u64 {
db.get(&oid).unwrap().lighting_blink_count_internal()
}
fn advance(db: &mut ObjectDatabase, oid: ObjectIdentifier, elapsed: Duration) -> bool {
db.get_mut(&oid).unwrap().advance_time_internal(elapsed)
}
fn assert_property_error(error: Error, expected: ErrorCode) {
match error {
Error::Remote(RemoteError::Error { class, code }) => {
assert_eq!(class, ErrorClass::PROPERTY);
assert_eq!(code, expected);
}
other => panic!("expected PROPERTY/{expected:?}, got {other:?}"),
}
}
fn assert_priority_range_error(error: Error) {
assert!(
matches!(error, Error::Remote(RemoteError::Error { class, code })
if class == ErrorClass::SERVICES
&& code == ErrorCode::PARAMETER_OUT_OF_RANGE),
"{error:?}"
);
}
fn configure_eligible_warn_off(db: &mut ObjectDatabase, oid: ObjectIdentifier, seconds: u64) {
for (property, value, priority) in [
(
PropertyIdentifier::BLINK_WARN_ENABLE,
PropertyValue::Boolean(true),
None,
),
(
PropertyIdentifier::EGRESS_TIME,
PropertyValue::Unsigned(seconds),
None,
),
(
PropertyIdentifier::PRESENT_VALUE,
PropertyValue::Enumerated(1),
Some(8),
),
] {
wp(db, oid, property, None, value, priority).unwrap();
}
}
#[test]
fn present_value_named_commands_and_null_decode_at_valid_and_default_priorities() {
for (value, expected_slot, expected_pv) in [
(
PropertyValue::Enumerated(0),
PropertyValue::Enumerated(0),
0,
),
(
PropertyValue::Enumerated(1),
PropertyValue::Enumerated(1),
1,
),
(PropertyValue::Enumerated(2), PropertyValue::Null, 0),
(
PropertyValue::Enumerated(3),
PropertyValue::Enumerated(0),
0,
),
(PropertyValue::Enumerated(4), PropertyValue::Null, 0),
(PropertyValue::Enumerated(5), PropertyValue::Null, 0),
(PropertyValue::Null, PropertyValue::Null, 0),
] {
for priority in [None, Some(1), Some(16)] {
let (mut db, oid) = database();
wp(
&mut db,
oid,
PropertyIdentifier::PRESENT_VALUE,
None,
value.clone(),
priority,
)
.unwrap();
let slot = priority.unwrap_or(16) as u32;
assert_eq!(
read(&db, oid, PropertyIdentifier::PRIORITY_ARRAY, Some(slot)),
expected_slot
);
assert_eq!(
read(&db, oid, PropertyIdentifier::PRESENT_VALUE, None),
PropertyValue::Enumerated(expected_pv)
);
let (mut db, oid) = database();
wpm(
&mut db,
oid,
vec![(
PropertyIdentifier::PRESENT_VALUE,
None,
value.clone(),
priority,
)],
)
.unwrap();
assert_eq!(
read(&db, oid, PropertyIdentifier::PRIORITY_ARRAY, Some(slot)),
expected_slot
);
assert_eq!(
read(&db, oid, PropertyIdentifier::PRESENT_VALUE, None),
PropertyValue::Enumerated(expected_pv)
);
}
}
}
#[test]
fn write_property_priority_errors_are_atomic_and_wpm_keeps_prior_prefix() {
let (mut db, oid) = database();
configure_eligible_warn_off(&mut db, oid, 5);
wp(
&mut db,
oid,
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(3),
Some(8),
)
.unwrap();
for priority in [0, 17, u8::MAX] {
let mut bytes = BytesMut::new();
WritePropertyRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
property_value: encode_value(PropertyValue::Enumerated(0)),
priority: None,
}
.encode(&mut bytes)
.unwrap();
bacnet_encoding::primitives::encode_ctx_unsigned(&mut bytes, 4, u64::from(priority));
assert_priority_range_error(sourced_wp(&mut db, &bytes).unwrap_err());
assert_eq!(
read(&db, oid, PropertyIdentifier::EGRESS_ACTIVE, None),
PropertyValue::Boolean(true)
);
assert_eq!(blink_count(&db, oid), 1);
}
assert_priority_range_error(
wpm(
&mut db,
oid,
vec![
(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(1),
Some(4),
),
(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(0),
Some(0),
),
],
)
.unwrap_err(),
);
assert_eq!(
read(&db, oid, PropertyIdentifier::PRIORITY_ARRAY, Some(4)),
PropertyValue::Enumerated(1),
"the valid WPM prefix commits before an out-of-range priority"
);
assert_eq!(
read(&db, oid, PropertyIdentifier::EGRESS_ACTIVE, None),
PropertyValue::Boolean(false)
);
assert!(!advance(&mut db, oid, Duration::from_secs(5)));
}
#[test]
fn direct_priority_array_operation_values_are_rejected_with_exact_errors() {
let (mut db, oid) = database();
for denied in [
PropertyValue::Enumerated(0),
PropertyValue::Enumerated(1),
PropertyValue::Null,
] {
assert_property_error(
wp(
&mut db,
oid,
PropertyIdentifier::PRIORITY_ARRAY,
Some(8),
denied,
None,
)
.unwrap_err(),
ErrorCode::WRITE_ACCESS_DENIED,
);
}
for value in [2, 3, 4, 5, 64, 255] {
assert_property_error(
wp(
&mut db,
oid,
PropertyIdentifier::PRIORITY_ARRAY,
Some(8),
PropertyValue::Enumerated(value),
None,
)
.unwrap_err(),
ErrorCode::WRITE_ACCESS_DENIED,
);
assert_eq!(
read(&db, oid, PropertyIdentifier::PRIORITY_ARRAY, Some(8)),
PropertyValue::Null
);
}
assert_property_error(
wp(
&mut db,
oid,
PropertyIdentifier::PRIORITY_ARRAY,
Some(8),
PropertyValue::Unsigned(1),
None,
)
.unwrap_err(),
ErrorCode::WRITE_ACCESS_DENIED,
);
assert_property_error(
wp(
&mut db,
oid,
PropertyIdentifier::PRIORITY_ARRAY,
Some(17),
PropertyValue::Null,
None,
)
.unwrap_err(),
ErrorCode::WRITE_ACCESS_DENIED,
);
assert_property_error(
wpm(
&mut db,
oid,
vec![
(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(1),
Some(4),
),
(
PropertyIdentifier::PRIORITY_ARRAY,
Some(8),
PropertyValue::Enumerated(2),
None,
),
],
)
.unwrap_err(),
ErrorCode::WRITE_ACCESS_DENIED,
);
assert_eq!(
read(&db, oid, PropertyIdentifier::PRIORITY_ARRAY, Some(4)),
PropertyValue::Enumerated(1),
"a later priority-array rejection keeps the earlier write"
);
}
#[test]
fn eligible_warn_warn_off_warn_relinquish_and_stop_execute_over_wp() {
let (mut warn_db, warn_oid) = database();
configure_eligible_warn_off(&mut warn_db, warn_oid, 2);
wp(
&mut warn_db,
warn_oid,
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(2),
Some(8),
)
.unwrap();
assert_eq!(blink_count(&warn_db, warn_oid), 1);
assert_eq!(
read(
&warn_db,
warn_oid,
PropertyIdentifier::PRIORITY_ARRAY,
Some(8)
),
PropertyValue::Enumerated(1)
);
let (mut off_db, off_oid) = database();
configure_eligible_warn_off(&mut off_db, off_oid, 2);
wp(
&mut off_db,
off_oid,
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(3),
Some(8),
)
.unwrap();
assert!(advance(&mut off_db, off_oid, Duration::from_secs(2)));
assert_eq!(
read(
&off_db,
off_oid,
PropertyIdentifier::PRIORITY_ARRAY,
Some(8)
),
PropertyValue::Enumerated(0)
);
let (mut relinquish_db, relinquish_oid) = database();
configure_eligible_warn_off(&mut relinquish_db, relinquish_oid, 2);
wp(
&mut relinquish_db,
relinquish_oid,
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(4),
Some(8),
)
.unwrap();
assert!(advance(
&mut relinquish_db,
relinquish_oid,
Duration::from_secs(2)
));
assert_eq!(
read(
&relinquish_db,
relinquish_oid,
PropertyIdentifier::PRIORITY_ARRAY,
Some(8)
),
PropertyValue::Null
);
let (mut stop_db, stop_oid) = database();
configure_eligible_warn_off(&mut stop_db, stop_oid, 2);
wp(
&mut stop_db,
stop_oid,
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(3),
Some(8),
)
.unwrap();
wp(
&mut stop_db,
stop_oid,
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(5),
Some(8),
)
.unwrap();
assert!(!advance(&mut stop_db, stop_oid, Duration::from_secs(10)));
assert_eq!(
read(
&stop_db,
stop_oid,
PropertyIdentifier::PRIORITY_ARRAY,
Some(8)
),
PropertyValue::Enumerated(1)
);
}
#[test]
fn successful_wpm_can_arm_one_operation() {
let (mut db, oid) = database();
wpm(
&mut db,
oid,
vec![
(
PropertyIdentifier::BLINK_WARN_ENABLE,
None,
PropertyValue::Boolean(true),
None,
),
(
PropertyIdentifier::EGRESS_TIME,
None,
PropertyValue::Unsigned(3),
None,
),
(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(1),
Some(8),
),
(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(3),
Some(8),
),
],
)
.unwrap();
assert_eq!(blink_count(&db, oid), 1);
assert_eq!(
read(&db, oid, PropertyIdentifier::EGRESS_ACTIVE, None),
PropertyValue::Boolean(true)
);
assert!(advance(&mut db, oid, Duration::from_secs(3)));
}
#[test]
fn failed_wpm_keeps_new_timer_from_successful_prefix() {
let (mut db, oid) = database();
configure_eligible_warn_off(&mut db, oid, 5);
assert_property_error(
wpm(
&mut db,
oid,
vec![
(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(3),
Some(8),
),
(
PropertyIdentifier::RELINQUISH_DEFAULT,
None,
PropertyValue::Enumerated(2),
None,
),
],
)
.unwrap_err(),
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_eq!(blink_count(&db, oid), 1);
assert_eq!(
read(&db, oid, PropertyIdentifier::EGRESS_ACTIVE, None),
PropertyValue::Boolean(true)
);
assert!(advance(&mut db, oid, Duration::from_secs(5)));
assert_eq!(
read(&db, oid, PropertyIdentifier::PRIORITY_ARRAY, Some(8)),
PropertyValue::Enumerated(0)
);
}
#[test]
fn present_value_priority_wpm_halts_and_failed_wpm_keeps_prefix() {
let (mut successful_db, successful_oid) = database();
configure_eligible_warn_off(&mut successful_db, successful_oid, 5);
wp(
&mut successful_db,
successful_oid,
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(3),
Some(8),
)
.unwrap();
wpm(
&mut successful_db,
successful_oid,
vec![(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(1),
Some(4),
)],
)
.unwrap();
assert_eq!(
read(
&successful_db,
successful_oid,
PropertyIdentifier::EGRESS_ACTIVE,
None
),
PropertyValue::Boolean(false)
);
assert_eq!(
read(
&successful_db,
successful_oid,
PropertyIdentifier::PRIORITY_ARRAY,
Some(8)
),
PropertyValue::Enumerated(0)
);
assert_eq!(
read(
&successful_db,
successful_oid,
PropertyIdentifier::PRIORITY_ARRAY,
Some(4)
),
PropertyValue::Enumerated(1)
);
let (mut db, oid) = database();
configure_eligible_warn_off(&mut db, oid, 5);
wp(
&mut db,
oid,
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(3),
Some(8),
)
.unwrap();
assert!(!advance(&mut db, oid, Duration::from_millis(1_500)));
assert_property_error(
wpm(
&mut db,
oid,
vec![
(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(1),
Some(4),
),
(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(6),
Some(4),
),
],
)
.unwrap_err(),
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_eq!(blink_count(&db, oid), 1);
assert_eq!(
read(&db, oid, PropertyIdentifier::PRIORITY_ARRAY, Some(4)),
PropertyValue::Enumerated(1),
"the successful prioritized Present_Value write stays committed"
);
assert_eq!(
read(&db, oid, PropertyIdentifier::EGRESS_ACTIVE, None),
PropertyValue::Boolean(false)
);
assert!(!advance(&mut db, oid, Duration::from_millis(3_500)));
}
#[test]
fn invalid_command_does_not_halt_existing_operation() {
let (mut db, oid) = database();
configure_eligible_warn_off(&mut db, oid, 2);
wp(
&mut db,
oid,
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(3),
Some(8),
)
.unwrap();
assert_property_error(
wp(
&mut db,
oid,
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(64),
Some(4),
)
.unwrap_err(),
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_eq!(blink_count(&db, oid), 1);
assert_eq!(
read(&db, oid, PropertyIdentifier::EGRESS_ACTIVE, None),
PropertyValue::Boolean(true)
);
assert!(advance(&mut db, oid, Duration::from_secs(2)));
}