use super::*;
use PropertyIdentifier as P;
use bacnet_objects::clock::{ClockFrame, ClockReader};
use bacnet_objects::device::{DeviceConfig, DeviceObject};
use bacnet_objects::traits::BACnetObject;
use bacnet_types::bitstring::ServicesSupported;
use bacnet_types::enums::{Segmentation, ServiceSupported};
use bacnet_types::error::RemoteError;
use bacnet_types::primitives::{Date, PropertyValue, Time};
use std::sync::{Arc, Mutex};
struct TestClock(Mutex<Option<ClockFrame>>);
impl ClockReader for TestClock {
fn read_clock(&self) -> Option<ClockFrame> {
*self.0.lock().unwrap()
}
}
#[test]
fn rpm_device_property_metadata_pics_and_database_are_exact() {
let required = [
P::APDU_TIMEOUT,
P::APPLICATION_SOFTWARE_VERSION,
P::DEVICE_ADDRESS_BINDING,
P::FIRMWARE_REVISION,
P::MAX_APDU_LENGTH_ACCEPTED,
P::MODEL_NAME,
P::NUMBER_OF_APDU_RETRIES,
P::OBJECT_IDENTIFIER,
P::OBJECT_LIST,
P::OBJECT_NAME,
P::OBJECT_TYPE,
P::PROTOCOL_OBJECT_TYPES_SUPPORTED,
P::PROTOCOL_SERVICES_SUPPORTED,
P::PROTOCOL_VERSION,
P::SEGMENTATION_SUPPORTED,
P::SYSTEM_STATUS,
P::VENDOR_IDENTIFIER,
P::VENDOR_NAME,
P::PROTOCOL_REVISION,
P::DATABASE_REVISION,
P::PROPERTY_LIST,
];
let frame = ClockFrame {
local_date: Date {
year: 126,
month: 9,
day: 14,
day_of_week: 1,
},
local_time: Time {
hour: 12,
minute: 30,
second: 15,
hundredths: 25,
},
utc_offset: 300,
daylight_savings_status: true,
};
for segmentation_supported in [
Segmentation::NONE,
Segmentation::TRANSMIT,
Segmentation::RECEIVE,
Segmentation::BOTH,
] {
let mut db = ObjectDatabase::new();
let mut object = DeviceObject::new(DeviceConfig {
identity: bacnet_objects::DeviceIdentity::new(77, 0)
.expect("valid Device identity")
.with_name("Device metadata")
.with_segmentation(segmentation_supported),
..DeviceConfig::default()
})
.unwrap();
object.set_description("configured description".repeat(40));
let oid = object.object_identifier();
let other = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 3).unwrap();
object.set_object_list(vec![oid, other]);
db.add(Box::new(object)).unwrap();
let clock = Arc::new(TestClock(Mutex::new(None)));
for state in [0, 1, 2, 1, 0] {
*clock.0.lock().unwrap() = (state == 1).then_some(frame);
db.set_clock_reader((state != 0).then(|| clock.clone() as Arc<dyn ClockReader>));
let mut optional = vec![
P::DESCRIPTION,
P::ACTIVE_COV_SUBSCRIPTIONS,
P::LAST_RESTART_REASON,
P::ACTIVE_COV_MULTIPLE_SUBSCRIPTIONS,
P::DEVICE_UUID,
];
if segmentation_supported != Segmentation::NONE {
optional.extend([P::APDU_SEGMENT_TIMEOUT, P::MAX_SEGMENTS_ACCEPTED]);
}
if state == 1 {
optional.extend([
P::LOCAL_DATE,
P::LOCAL_TIME,
P::UTC_OFFSET,
P::DAYLIGHT_SAVINGS_STATUS,
]);
}
optional.sort_by_key(|p| p.to_raw());
let mut all = required.to_vec();
all.extend(&optional);
all.sort_by_key(|p| p.to_raw());
assert_eq!(db.find_by_type(ObjectType::DEVICE), vec![oid]);
assert_eq!(db.list_objects(), vec![oid]);
let object = db.get(&oid).unwrap();
assert_eq!(object.property_list().as_ref(), all);
assert_eq!(object.required_properties().as_ref(), required);
for property in [
P::LOCAL_DATE,
P::LOCAL_TIME,
P::UTC_OFFSET,
P::DAYLIGHT_SAVINGS_STATUS,
] {
let read = object.read_property(property, None);
if state == 1 {
read.unwrap();
} else {
assert!(
matches!(read, Err(Error::Remote(RemoteError::Error { class, code }))
if class == bacnet_types::enums::ErrorClass::PROPERTY
&& code == bacnet_types::enums::ErrorCode::UNKNOWN_PROPERTY)
);
}
}
let PropertyValue::BitString { unused_bits, data } = object
.read_property(P::PROTOCOL_SERVICES_SUPPORTED, None)
.unwrap()
else {
panic!("expected supported-service bits");
};
assert_eq!((unused_bits, data.len()), (7, 7));
let services = ServicesSupported::from_bacnet(&data);
let expected_services: Vec<_> = bacnet_objects::device::EXECUTED_SERVICES
.iter()
.copied()
.filter(|s| {
state == 1
|| !matches!(
*s,
ServiceSupported::TIME_SYNCHRONIZATION
| ServiceSupported::UTC_TIME_SYNCHRONIZATION
)
})
.collect();
assert_eq!(services.iter().count(), expected_services.len());
assert!(expected_services.iter().all(|s| services.contains(*s)));
let metadata = object.property_metadata();
assert!(matches!(metadata, std::borrow::Cow::Borrowed(_)));
assert_eq!(
metadata
.iter()
.map(|r| r.property_identifier)
.collect::<Vec<_>>(),
all
);
for row in metadata.iter() {
assert_eq!(
row.conformance.is_required(),
required.contains(&row.property_identifier)
);
assert_eq!(
row.write_capability.is_writable(),
row.property_identifier == P::DESCRIPTION
);
}
let pics = crate::pics::generate_pics(
&db,
&crate::server::ServerConfig::default(),
&crate::pics::PicsConfig::default(),
);
assert_eq!(pics.supported_object_types.len(), 1);
let support = &pics.supported_object_types[0];
assert_eq!(support.object_type, ObjectType::DEVICE);
assert!(!support.createable);
assert!(!support.deleteable);
assert_eq!(
support
.supported_properties
.iter()
.map(|r| {
assert!(r.access.readable);
(r.property_id, r.access.optional, r.access.writable)
})
.collect::<Vec<_>>(),
all.iter()
.map(|&p| (p, optional.contains(&p), p == P::DESCRIPTION))
.collect::<Vec<_>>()
);
let mut all_rpm = all.clone();
all_rpm.retain(|&p| p != P::PROPERTY_LIST);
let required_rpm: Vec<_> = required
.iter()
.copied()
.filter(|&p| p != P::PROPERTY_LIST)
.collect();
for (selector, expected) in [
(P::ALL, all_rpm.as_slice()),
(P::REQUIRED, required_rpm.as_slice()),
(P::OPTIONAL, optional.as_slice()),
(P::PROPERTY_LIST, &[P::PROPERTY_LIST]),
] {
assert_rpm_selector_bytes(&db, oid, selector, expected);
}
assert_eq!(
object.read_property(P::OBJECT_LIST, None).unwrap(),
PropertyValue::List(vec![
PropertyValue::ObjectIdentifier(oid),
PropertyValue::ObjectIdentifier(other)
])
);
assert_eq!(
object.read_property(P::OBJECT_LIST, Some(0)).unwrap(),
PropertyValue::Unsigned(2)
);
for list in [
P::ACTIVE_COV_SUBSCRIPTIONS,
P::ACTIVE_COV_MULTIPLE_SUBSCRIPTIONS,
] {
assert_eq!(
object.read_property(list, None).unwrap(),
PropertyValue::ApplicationData(vec![])
);
}
}
}
}