use bacnet_types::error::RemoteError;
use std::borrow::Cow;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use bacnet_objects::life_safety::{
LifeSafetyPointObject, LifeSafetyPointResetCommit, LifeSafetyPointResetContext,
LifeSafetyPointResetExecutor, LifeSafetyResetError, LifeSafetyZoneObject,
LifeSafetyZoneResetCommit, LifeSafetyZoneResetContext, LifeSafetyZoneResetExecutor,
};
use bacnet_objects::traits::{LifeSafetyOperationEffect, LifeSafetyOperationOutcome};
use bacnet_services::life_safety::LifeSafetyOperationRequest;
use bacnet_types::enums::{
ErrorClass, ErrorCode, LifeSafetyOperation, LifeSafetyState, ObjectType, PropertyIdentifier,
SilencedState,
};
use bacnet_types::error::Error;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
use super::*;
use crate::life_safety_cov::LifeSafetyCovChange;
fn request(
operation: LifeSafetyOperation,
object_identifier: Option<ObjectIdentifier>,
) -> LifeSafetyOperationRequest {
LifeSafetyOperationRequest {
requesting_process_identifier: 7,
requesting_source: "operator".into(),
request: operation,
object_identifier,
}
}
fn read(db: &ObjectDatabase, oid: ObjectIdentifier, property: PropertyIdentifier) -> u32 {
match db.get(&oid).unwrap().read_property(property, None).unwrap() {
PropertyValue::Enumerated(value) => value,
other => panic!("expected enumerated value, got {other:?}"),
}
}
fn assert_protocol_error(error: Error, class: ErrorClass, code: ErrorCode) {
assert!(matches!(
error,
Error::Remote(RemoteError::Error {
class: actual_class,
code: actual_code,
}) if actual_class == class && actual_code == code
));
}
#[test]
fn targeted_reset_variants_apply_to_point_and_zone() {
for operation in [
LifeSafetyOperation::RESET,
LifeSafetyOperation::RESET_ALARM,
LifeSafetyOperation::RESET_FAULT,
] {
let point_oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_POINT, 1).unwrap();
let mut point = LifeSafetyPointObject::new(1, "point").unwrap();
point.set_present_value(LifeSafetyState::ALARM);
point.set_tracking_value(LifeSafetyState::FAULT);
point.set_silenced(SilencedState::ALL_SILENCED);
point.set_operation_expected(operation);
point.set_reset_executor(Arc::new(move |context| {
assert_eq!(
*context,
LifeSafetyPointResetContext {
object_identifier: point_oid,
operation,
present_value: LifeSafetyState::ALARM,
tracking_value: LifeSafetyState::FAULT,
silenced: SilencedState::ALL_SILENCED,
operation_expected: operation,
}
);
Ok(LifeSafetyPointResetCommit {
present_value: Some(LifeSafetyState::QUIET),
tracking_value: None,
silenced: Some(SilencedState::UNSILENCED),
})
}));
let zone_oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_ZONE, 1).unwrap();
let mut zone = LifeSafetyZoneObject::new(1, "zone").unwrap();
zone.set_present_value(LifeSafetyState::FAULT_ALARM);
zone.set_tracking_value(LifeSafetyState::FAULT);
zone.set_silenced(SilencedState::VISIBLE_SILENCED);
zone.set_operation_expected(operation);
zone.set_reset_executor(Arc::new(move |context| {
assert_eq!(
*context,
LifeSafetyZoneResetContext {
object_identifier: zone_oid,
operation,
present_value: LifeSafetyState::FAULT_ALARM,
tracking_value: LifeSafetyState::FAULT,
silenced: SilencedState::VISIBLE_SILENCED,
operation_expected: operation,
}
);
Ok(LifeSafetyZoneResetCommit {
present_value: Some(LifeSafetyState::SUPERVISORY),
tracking_value: Some(LifeSafetyState::QUIET),
silenced: None,
})
}));
let mut db = ObjectDatabase::new();
db.add(Box::new(point)).unwrap();
db.add(Box::new(zone)).unwrap();
assert_eq!(
handle_life_safety_operation(&mut db, &request(operation, Some(point_oid))).unwrap(),
vec![LifeSafetyCovChange {
object_identifier: point_oid,
changed_properties: vec![
PropertyIdentifier::PRESENT_VALUE,
PropertyIdentifier::SILENCED,
PropertyIdentifier::OPERATION_EXPECTED
]
}]
);
assert_eq!(
handle_life_safety_operation(&mut db, &request(operation, Some(zone_oid))).unwrap(),
vec![LifeSafetyCovChange {
object_identifier: zone_oid,
changed_properties: vec![
PropertyIdentifier::PRESENT_VALUE,
PropertyIdentifier::TRACKING_VALUE,
PropertyIdentifier::OPERATION_EXPECTED
]
}]
);
assert_eq!(
read(&db, point_oid, PropertyIdentifier::PRESENT_VALUE),
LifeSafetyState::QUIET.to_raw()
);
assert_eq!(
read(&db, point_oid, PropertyIdentifier::TRACKING_VALUE),
LifeSafetyState::FAULT.to_raw(),
"an omitted update remains unchanged"
);
assert_eq!(
read(&db, zone_oid, PropertyIdentifier::TRACKING_VALUE),
LifeSafetyState::QUIET.to_raw()
);
assert_eq!(
read(&db, zone_oid, PropertyIdentifier::SILENCED),
SilencedState::VISIBLE_SILENCED.to_raw(),
"reset never implicitly unsilences"
);
for oid in [point_oid, zone_oid] {
assert_eq!(
read(&db, oid, PropertyIdentifier::OPERATION_EXPECTED),
LifeSafetyOperation::NONE.to_raw()
);
}
}
}
struct ResetHookSpy {
oid: ObjectIdentifier,
calls: Arc<AtomicUsize>,
}
impl BACnetObject for ResetHookSpy {
fn object_identifier(&self) -> ObjectIdentifier {
self.oid
}
fn object_name(&self) -> &str {
"reset-spy"
}
fn read_property(
&self,
_property: PropertyIdentifier,
_array_index: Option<u32>,
) -> Result<PropertyValue, Error> {
Err(Error::Encoding("unused reset-spy read".into()))
}
fn write_property(
&mut self,
_property: PropertyIdentifier,
_array_index: Option<u32>,
_value: PropertyValue,
_priority: Option<u8>,
) -> Result<(), Error> {
Err(Error::Encoding("unused reset-spy write".into()))
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
Cow::Borrowed(&[])
}
fn apply_life_safety_operation(
&mut self,
_operation: LifeSafetyOperation,
) -> Result<LifeSafetyOperationOutcome, Error> {
self.calls.fetch_add(1, Ordering::AcqRel);
Ok(LifeSafetyOperationOutcome {
effect: LifeSafetyOperationEffect::Applied,
changed_properties: vec![],
})
}
}
#[test]
fn targeted_reset_never_calls_non_life_safety_hook() {
let calls = Arc::new(AtomicUsize::new(0));
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 9).unwrap();
let mut db = ObjectDatabase::new();
db.add(Box::new(ResetHookSpy {
oid,
calls: Arc::clone(&calls),
}))
.unwrap();
let error =
handle_life_safety_operation(&mut db, &request(LifeSafetyOperation::RESET, Some(oid)))
.unwrap_err();
assert_protocol_error(
error,
ErrorClass::OBJECT,
ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED,
);
assert_eq!(calls.load(Ordering::Acquire), 0);
}
#[test]
fn targetless_reset_attempts_only_ordered_point_and_zone_candidates() {
let order = Arc::new(Mutex::new(Vec::new()));
let make_point_executor = |oid: ObjectIdentifier,
result: Result<LifeSafetyPointResetCommit, LifeSafetyResetError>|
-> LifeSafetyPointResetExecutor {
let order = Arc::clone(&order);
Arc::new(move |_: &LifeSafetyPointResetContext| {
order.lock().unwrap().push(oid);
result
})
};
let make_zone_executor = |oid: ObjectIdentifier,
result: Result<LifeSafetyZoneResetCommit, LifeSafetyResetError>|
-> LifeSafetyZoneResetExecutor {
let order = Arc::clone(&order);
Arc::new(move |_: &LifeSafetyZoneResetContext| {
order.lock().unwrap().push(oid);
result
})
};
let point_success_oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_POINT, 1).unwrap();
let mut point_success = LifeSafetyPointObject::new(1, "point-success").unwrap();
point_success.set_present_value(LifeSafetyState::ALARM);
point_success.set_operation_expected(LifeSafetyOperation::RESET);
point_success.set_reset_executor(make_point_executor(
point_success_oid,
Ok(LifeSafetyPointResetCommit {
present_value: Some(LifeSafetyState::QUIET),
..Default::default()
}),
));
let point_no_executor_oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_POINT, 2).unwrap();
let mut point_no_executor = LifeSafetyPointObject::new(2, "point-no-executor").unwrap();
point_no_executor.set_operation_expected(LifeSafetyOperation::RESET);
let point_rejected_oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_POINT, 3).unwrap();
let mut point_rejected = LifeSafetyPointObject::new(3, "point-rejected").unwrap();
point_rejected.set_operation_expected(LifeSafetyOperation::RESET);
point_rejected.set_reset_executor(make_point_executor(
point_rejected_oid,
Err(LifeSafetyResetError::InvalidOperationInThisState),
));
let point_panics_oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_POINT, 4).unwrap();
let panic_order = Arc::clone(&order);
let mut point_panics = LifeSafetyPointObject::new(4, "point-panics").unwrap();
point_panics.set_operation_expected(LifeSafetyOperation::RESET);
point_panics.set_reset_executor(Arc::new(move |_| {
panic_order.lock().unwrap().push(point_panics_oid);
panic!("executor bug")
}));
let zone_success_oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_ZONE, 1).unwrap();
let mut zone_success = LifeSafetyZoneObject::new(1, "zone-success").unwrap();
zone_success.set_present_value(LifeSafetyState::FAULT);
zone_success.set_operation_expected(LifeSafetyOperation::RESET);
zone_success.set_reset_executor(make_zone_executor(
zone_success_oid,
Ok(LifeSafetyZoneResetCommit {
present_value: Some(LifeSafetyState::ACTIVE),
..Default::default()
}),
));
let zone_wrong_expected_oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_ZONE, 2).unwrap();
let mut zone_wrong_expected = LifeSafetyZoneObject::new(2, "zone-wrong-expected").unwrap();
zone_wrong_expected.set_operation_expected(LifeSafetyOperation::RESET_ALARM);
zone_wrong_expected.set_reset_executor(make_zone_executor(
zone_wrong_expected_oid,
Ok(LifeSafetyZoneResetCommit::default()),
));
let zone_denied_oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_ZONE, 3).unwrap();
let mut zone_denied = LifeSafetyZoneObject::new(3, "zone-denied").unwrap();
zone_denied.set_operation_expected(LifeSafetyOperation::RESET);
zone_denied.set_reset_executor(make_zone_executor(
zone_denied_oid,
Err(LifeSafetyResetError::ServiceRequestDenied),
));
let non_life_calls = Arc::new(AtomicUsize::new(0));
let non_life_oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let mut db = ObjectDatabase::new();
for object in [
Box::new(point_success) as Box<dyn BACnetObject>,
Box::new(point_no_executor),
Box::new(point_rejected),
Box::new(point_panics),
Box::new(zone_success),
Box::new(zone_wrong_expected),
Box::new(zone_denied),
Box::new(ResetHookSpy {
oid: non_life_oid,
calls: Arc::clone(&non_life_calls),
}),
] {
db.add(object).unwrap();
}
let changed =
handle_life_safety_operation(&mut db, &request(LifeSafetyOperation::RESET, None)).unwrap();
assert_eq!(
changed,
[point_success_oid, zone_success_oid].map(|oid| LifeSafetyCovChange {
object_identifier: oid,
changed_properties: vec![
PropertyIdentifier::PRESENT_VALUE,
PropertyIdentifier::OPERATION_EXPECTED
],
})
);
assert_eq!(
*order.lock().unwrap(),
vec![
point_success_oid,
point_rejected_oid,
point_panics_oid,
zone_success_oid,
zone_denied_oid,
]
);
assert_eq!(non_life_calls.load(Ordering::Acquire), 0);
assert_eq!(
read(&db, point_success_oid, PropertyIdentifier::PRESENT_VALUE),
LifeSafetyState::QUIET.to_raw()
);
assert_eq!(
read(&db, zone_success_oid, PropertyIdentifier::PRESENT_VALUE),
LifeSafetyState::ACTIVE.to_raw()
);
for oid in [
point_no_executor_oid,
point_rejected_oid,
point_panics_oid,
zone_denied_oid,
] {
assert_eq!(
read(&db, oid, PropertyIdentifier::OPERATION_EXPECTED),
LifeSafetyOperation::RESET.to_raw()
);
}
assert_eq!(
read(
&db,
zone_wrong_expected_oid,
PropertyIdentifier::OPERATION_EXPECTED,
),
LifeSafetyOperation::RESET_ALARM.to_raw()
);
}