use super::*;
use crate::color::{ColorObject, ColorTemperatureObject};
use crate::lighting::{BinaryLightingOutputObject, ColorLink, LightingOutputObject};
use crate::property_metadata::PropertyConformance;
use crate::traits::BACnetObject;
use bacnet_types::enums::ObjectType;
use bacnet_types::primitives::ObjectIdentifier;
fn assert_code<T: std::fmt::Debug>(result: Result<T, Error>, expected: ErrorCode) {
match result {
Err(Error::Protocol { code, .. }) => {
assert_eq!(code, expected.to_raw() as u32, "expected {expected:?}")
}
other => panic!("expected {expected:?}, got {other:?}"),
}
}
fn two_tags() -> Vec<BACnetNameValue> {
vec![
BACnetNameValue::semantic("exhaust"),
BACnetNameValue::valued("floor", PropertyValue::Unsigned(3)),
]
}
const EXHAUST: [u8; 10] = [0x0D, 0x08, 0x00, 0x65, 0x78, 0x68, 0x61, 0x75, 0x73, 0x74];
const FLOOR: [u8; 9] = [0x0D, 0x06, 0x00, 0x66, 0x6C, 0x6F, 0x6F, 0x72, 0x21];
const ZONE: [u8; 13] = [
0x0D, 0x05, 0x00, 0x7A, 0x6F, 0x6E, 0x65, 0x75, 0x05, 0x00, 0x65, 0x61, 0x73,
];
fn floor() -> Vec<u8> {
[&FLOOR[..], &[0x03]].concat()
}
fn zone() -> Vec<u8> {
[&ZONE[..], &[0x74]].concat()
}
fn data(octets: &[u8]) -> PropertyValue {
PropertyValue::ApplicationData(octets.to_vec())
}
fn tagged() -> ObjectProfile {
ObjectProfile {
tags: Some(two_tags()),
..ObjectProfile::default()
}
}
#[test]
fn an_unprovisioned_profile_serves_and_lists_nothing() {
let mut profile = ObjectProfile::default();
profile.check().unwrap();
assert_eq!(profile.metadata().count(), 0);
for property in [P::TAGS, P::PROFILE_LOCATION, P::PROFILE_NAME] {
assert_code(
profile.read(property, None).unwrap(),
ErrorCode::UNKNOWN_PROPERTY,
);
assert_code(
profile.write(property, None, &data(&EXHAUST)).unwrap(),
ErrorCode::UNKNOWN_PROPERTY,
);
}
assert!(profile.read(P::DESCRIPTION, None).is_none());
assert!(profile
.write(P::DESCRIPTION, None, &PropertyValue::Null)
.is_none());
}
#[test]
fn tags_read_whole_by_size_and_by_element() {
let profile = tagged();
assert_eq!(
profile.read(P::TAGS, None).unwrap().unwrap(),
PropertyValue::List(vec![data(&EXHAUST), data(&floor())])
);
assert_eq!(
profile.read(P::TAGS, Some(0)).unwrap().unwrap(),
PropertyValue::Unsigned(2)
);
assert_eq!(
profile.read(P::TAGS, Some(2)).unwrap().unwrap(),
data(&floor())
);
assert_code(
profile.read(P::TAGS, Some(3)).unwrap(),
ErrorCode::INVALID_ARRAY_INDEX,
);
}
#[test]
fn tags_are_written_whole_resized_at_index_0_and_by_element() {
let mut profile = tagged();
let mut write = |index, value: PropertyValue| profile.write(P::TAGS, index, &value).unwrap();
write(None, data(&[zone(), EXHAUST.to_vec()].concat())).unwrap();
write(
None,
PropertyValue::List(vec![data(&zone()), data(&EXHAUST)]),
)
.unwrap();
write(Some(0), PropertyValue::Unsigned(3)).unwrap();
write(Some(2), data(&floor())).unwrap();
let expected = vec![
BACnetNameValue::valued("zone", PropertyValue::CharacterString("east".into())),
BACnetNameValue::valued("floor", PropertyValue::Unsigned(3)),
BACnetNameValue::semantic(""),
];
assert_eq!(profile.tags.as_deref(), Some(expected.as_slice()));
let mut profile = tagged();
let mut write = |index, value: PropertyValue| profile.write(P::TAGS, index, &value).unwrap();
write(Some(0), PropertyValue::Unsigned(1)).unwrap();
assert_code(
write(Some(2), data(&floor())),
ErrorCode::INVALID_ARRAY_INDEX,
);
assert_code(
write(Some(0), PropertyValue::Unsigned(MAX_TAGS as u64 + 1)),
ErrorCode::NO_SPACE_TO_WRITE_PROPERTY,
);
assert_code(
write(Some(0), PropertyValue::Real(2.0)),
ErrorCode::INVALID_DATA_TYPE,
);
assert_code(
write(None, PropertyValue::Null),
ErrorCode::INVALID_DATA_TYPE,
);
assert_code(
write(Some(1), data(&[0x44, 0, 0, 0, 0])),
ErrorCode::INVALID_DATA_TYPE,
);
assert_code(
write(None, data(&EXHAUST[..4])),
ErrorCode::INVALID_DATA_ENCODING,
);
assert_code(
write(Some(1), data(&[EXHAUST, EXHAUST].concat())),
ErrorCode::INVALID_DATA_ENCODING,
);
let semicolon = [0x0B, 0x00, 0x61, 0x3B];
assert_code(
write(Some(1), data(&semicolon)),
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_code(write(None, data(&semicolon)), ErrorCode::VALUE_OUT_OF_RANGE);
assert_eq!(profile.tags.as_deref(), Some(&two_tags()[..1]));
profile.write(P::TAGS, None, &data(&[])).unwrap().unwrap();
assert_eq!(profile.tags.as_deref(), Some(&[][..]));
assert_eq!(profile.metadata().count(), 1);
}
#[test]
fn profile_rows_are_read_only_and_checked_when_provisioned() {
let profile = ObjectProfile {
profile_location: Some("https://example.com/profiles.xdd".into()),
profile_name: Some("555-lighting".into()),
..ObjectProfile::default()
};
profile.check().unwrap();
let mut profile = profile;
assert_eq!(
profile.read(P::PROFILE_NAME, None).unwrap().unwrap(),
PropertyValue::CharacterString("555-lighting".into())
);
assert_code(
profile.read(P::PROFILE_LOCATION, Some(1)).unwrap(),
ErrorCode::PROPERTY_IS_NOT_AN_ARRAY,
);
for property in [P::PROFILE_LOCATION, P::PROFILE_NAME] {
let value = PropertyValue::CharacterString("555-other".into());
assert_code(
profile.write(property, None, &value).unwrap(),
ErrorCode::WRITE_ACCESS_DENIED,
);
}
let rows: Vec<_> = profile.metadata().collect();
assert_eq!(
rows,
[
PropertyMetadata::new(P::PROFILE_LOCATION, Optional, None, ReadOnly),
PropertyMetadata::new(P::PROFILE_NAME, Optional, None, ReadOnly),
]
);
for location in ["", "HTTP://x", "bacnet://5/device,5", "http:x"] {
let ok = ObjectProfile {
profile_location: Some(location.into()),
..ObjectProfile::default()
};
ok.check().unwrap();
}
let refused = [
ObjectProfile {
profile_location: Some("ftp://example.com/p.xdd".into()),
..ObjectProfile::default()
},
ObjectProfile {
profile_location: Some("example.com".into()),
..ObjectProfile::default()
},
ObjectProfile {
profile_name: Some("acme-lighting".into()),
..ObjectProfile::default()
},
ObjectProfile {
profile_name: Some("70000-lighting".into()),
..ObjectProfile::default()
},
ObjectProfile {
profile_name: Some("555lighting".into()),
..ObjectProfile::default()
},
ObjectProfile {
tags: Some(vec![BACnetNameValue::semantic("a;b")]),
..ObjectProfile::default()
},
];
for profile in refused {
assert_code(profile.check(), ErrorCode::VALUE_OUT_OF_RANGE);
}
for value in [
PropertyValue::List(vec![]),
PropertyValue::ApplicationData(vec![0]),
] {
let constructed = ObjectProfile {
tags: Some(vec![BACnetNameValue::valued("a", value)]),
..ObjectProfile::default()
};
assert_code(constructed.check(), ErrorCode::INVALID_DATA_TYPE);
}
let too_many = ObjectProfile {
tags: Some(vec![BACnetNameValue::semantic("a"); MAX_TAGS + 1]),
..ObjectProfile::default()
};
assert_code(too_many.check(), ErrorCode::NO_SPACE_TO_WRITE_PROPERTY);
}
fn full_profile() -> ObjectProfile {
ObjectProfile {
tags: Some(two_tags()),
profile_location: Some("https://example.com/p.xdd".into()),
profile_name: Some("555-lighting".into()),
}
}
type Case = (Box<dyn BACnetObject>, Box<dyn BACnetObject>, P);
fn objects() -> Vec<Case> {
let mut color = ColorObject::new(1, "CLR-1").unwrap();
color.set_profile(full_profile()).unwrap();
let mut temperature = ColorTemperatureObject::new(1, "CT-1").unwrap();
temperature.set_profile(full_profile()).unwrap();
let reference = ObjectIdentifier::new(ObjectType::COLOR, 1).unwrap();
let mut lighting = LightingOutputObject::new(1, "LO-1").unwrap();
lighting
.set_color_link(Some(ColorLink::new(reference)))
.unwrap();
lighting.set_high_end_trim(Some(90.0)).unwrap();
let mut lit = lighting.clone();
lit.set_profile(full_profile()).unwrap();
let mut binary = BinaryLightingOutputObject::new(1, "BLO-1").unwrap();
binary.set_profile(full_profile()).unwrap();
vec![
(
Box::new(ColorObject::new(1, "CLR-1").unwrap()),
Box::new(color),
P::TRANSITION,
),
(
Box::new(ColorTemperatureObject::new(1, "CT-1").unwrap()),
Box::new(temperature),
P::TRANSITION,
),
(Box::new(lighting), Box::new(lit), P::COLOR_REFERENCE),
(
Box::new(BinaryLightingOutputObject::new(1, "BLO-1").unwrap()),
Box::new(binary),
P::CURRENT_COMMAND_PRIORITY,
),
]
}
#[test]
fn objects_list_provisioned_rows_in_table_order_and_serve_them() {
for (fresh, mut provisioned, after) in objects() {
let kind = fresh.object_identifier().object_type();
let before = fresh.property_list().into_owned();
for property in [P::TAGS, P::PROFILE_LOCATION, P::PROFILE_NAME] {
assert!(!before.contains(&property), "{kind:?}");
assert_code(
fresh.read_property(property, None),
ErrorCode::UNKNOWN_PROPERTY,
);
}
let mut expected = before.clone();
let at = expected.iter().position(|p| *p == after).unwrap() + 1;
expected.splice(at..at, [P::TAGS, P::PROFILE_LOCATION, P::PROFILE_NAME]);
assert_eq!(provisioned.property_list().as_ref(), expected, "{kind:?}");
let metadata = provisioned.property_metadata();
for row in metadata.iter().filter(|row| {
matches!(
row.property_identifier,
P::TAGS | P::PROFILE_LOCATION | P::PROFILE_NAME
)
}) {
assert_eq!(row.conformance, PropertyConformance::Optional);
assert_eq!(row.presence_condition, None);
assert_eq!(
row.write_capability.is_writable(),
row.property_identifier == P::TAGS
);
}
assert!(!provisioned.required_properties().contains(&P::TAGS));
assert_eq!(
provisioned.read_property(P::TAGS, Some(1)).unwrap(),
data(&EXHAUST)
);
provisioned
.write_property(P::TAGS, Some(0), PropertyValue::Unsigned(1), None)
.unwrap();
assert_eq!(
provisioned.read_property(P::TAGS, Some(0)).unwrap(),
PropertyValue::Unsigned(1)
);
assert_code(
provisioned.write_property(
P::PROFILE_NAME,
None,
PropertyValue::CharacterString("1-x".into()),
None,
),
ErrorCode::WRITE_ACCESS_DENIED,
);
let mut color = ColorObject::new(1, "CLR-1").unwrap();
let bad = ObjectProfile {
profile_name: Some("lighting".into()),
..full_profile()
};
assert_code(color.set_profile(bad), ErrorCode::VALUE_OUT_OF_RANGE);
assert!(!color.property_list().contains(&P::TAGS));
}
}