use super::acquisition_tests::{at, set_clock};
use super::*;
use bacnet_encoding::constructed::decode_log_record;
use bacnet_types::bitstring::LogStatus;
use bacnet_types::primitives::StatusFlags;
fn records(db: &ObjectDatabase, oid: ObjectIdentifier) -> Vec<BACnetLogRecord> {
let records = db.get(&oid).unwrap().log_buffer_internal().unwrap();
(0..records.record_count())
.map(|index| {
let mut bytes = BytesMut::new();
records.encode_record(index, &mut bytes);
decode_log_record(&bytes, 0).unwrap().0
})
.collect()
}
fn logged(db: &ObjectDatabase, oid: ObjectIdentifier) -> Vec<(LogDatum, Option<StatusFlags>)> {
records(db, oid)
.into_iter()
.map(|record| (record.log_datum, record.status_flags))
.collect()
}
fn set_value(db: &mut ObjectDatabase, value: f32) {
db.get_mut(&target())
.unwrap()
.set_present_value_internal(PropertyValue::Real(value))
.unwrap();
}
const NORMAL: Option<StatusFlags> = Some(StatusFlags::empty());
fn real(value: f32) -> (LogDatum, Option<StatusFlags>) {
(LogDatum::RealValue(value), NORMAL)
}
fn status(status: LogStatus) -> (LogDatum, Option<StatusFlags>) {
(LogDatum::LogStatus(status), None)
}
fn cov_fixture(increment: PropertyValue) -> (ObjectDatabase, ObjectIdentifier, Arc<WallClock>) {
let (mut db, oid, _, clock) = fixture(0);
db.remove(&target()).unwrap();
db.add(Box::new(
AnalogValueObject::with_access(
1,
"AV",
EngineeringUnits::NO_UNITS,
crate::present_value_access::PresentValueAccess::Writable,
)
.unwrap(),
))
.unwrap();
write(
&mut db,
oid,
P::LOGGING_TYPE,
PropertyValue::Enumerated(LoggingType::COV.to_raw()),
);
write(&mut db, oid, P::CLIENT_COV_INCREMENT, increment);
(db, oid, clock)
}
#[test]
fn a_change_meeting_the_increment_logs_one_record_with_status_flags() {
let (mut db, oid, _) = cov_fixture(PropertyValue::Real(1.0));
assert_eq!(db.poll_trend_logs(), RECONCILE);
assert_eq!(logged(&db, oid), [real(0.0)]);
assert_eq!(records(&db, oid)[0].time, frame().local_time);
db.poll_trend_logs();
assert_eq!(logged(&db, oid), [real(0.0)]);
set_value(&mut db, 0.5);
db.poll_trend_logs();
assert_eq!(logged(&db, oid), [real(0.0)]);
set_value(&mut db, 1.5);
db.poll_trend_logs();
db.poll_trend_logs();
assert_eq!(logged(&db, oid), [real(0.0), real(1.5)]);
set_value(&mut db, 0.6);
db.poll_trend_logs();
assert_eq!(logged(&db, oid).len(), 2);
set_value(&mut db, 0.5);
db.poll_trend_logs();
assert_eq!(logged(&db, oid), [real(0.0), real(1.5), real(0.5)]);
write(
&mut db,
target(),
P::OUT_OF_SERVICE,
PropertyValue::Boolean(true),
);
db.poll_trend_logs();
assert_eq!(
logged(&db, oid)[3],
(LogDatum::RealValue(0.5), Some(StatusFlags::OUT_OF_SERVICE))
);
db.poll_trend_logs();
assert_eq!(logged(&db, oid).len(), 4);
}
#[test]
fn a_null_increment_uses_the_targets_cov_increment() {
let (mut db, oid, _) = cov_fixture(PropertyValue::Null);
write(
&mut db,
target(),
P::COV_INCREMENT,
PropertyValue::Real(2.0),
);
db.poll_trend_logs();
set_value(&mut db, 1.0);
db.poll_trend_logs();
assert_eq!(logged(&db, oid), [real(0.0)]);
set_value(&mut db, 2.0);
db.poll_trend_logs();
assert_eq!(logged(&db, oid), [real(0.0), real(2.0)]);
write(
&mut db,
oid,
P::CLIENT_COV_INCREMENT,
PropertyValue::Real(0.25),
);
db.poll_trend_logs();
assert_eq!(logged(&db, oid).len(), 2);
set_value(&mut db, 2.25);
db.poll_trend_logs();
assert_eq!(logged(&db, oid), [real(0.0), real(2.0), real(2.25)]);
}
#[test]
fn an_increment_change_alone_keeps_the_baseline() {
let (mut db, oid, _) = cov_fixture(PropertyValue::Real(1.0));
db.poll_trend_logs();
for increment in [PropertyValue::Real(5.0), PropertyValue::Null] {
write(&mut db, oid, P::CLIENT_COV_INCREMENT, increment);
db.poll_trend_logs();
assert_eq!(logged(&db, oid), [real(0.0)]);
}
set_value(&mut db, 0.5);
db.poll_trend_logs();
assert_eq!(logged(&db, oid), [real(0.0), real(0.5)]);
}
#[test]
fn enable_gates_cov_records_and_restarts_from_the_current_value() {
let (mut db, oid, _) = cov_fixture(PropertyValue::Real(1.0));
db.poll_trend_logs();
write(&mut db, oid, P::LOG_ENABLE, PropertyValue::Boolean(false));
set_value(&mut db, 5.0);
db.poll_trend_logs();
set_value(&mut db, 0.5);
db.poll_trend_logs();
assert_eq!(
logged(&db, oid),
[real(0.0), status(LogStatus::LOG_DISABLED)]
);
write(&mut db, oid, P::LOG_ENABLE, PropertyValue::Boolean(true));
db.poll_trend_logs();
assert_eq!(
logged(&db, oid),
[
real(0.0),
status(LogStatus::LOG_DISABLED),
status(LogStatus::empty()),
real(0.5),
]
);
}
#[test]
fn the_window_gates_cov_records_and_restarts_from_the_current_value() {
let (mut db, oid, clock) = cov_fixture(PropertyValue::Real(1.0));
db.poll_trend_logs();
assert_eq!(logged(&db, oid), [real(0.0)]);
let start = at(12, 30, 0, 0);
write(
&mut db,
oid,
P::START_TIME,
PropertyValue::List(vec![
PropertyValue::Date(start.local_date),
PropertyValue::Time(start.local_time),
]),
);
db.poll_trend_logs();
set_value(&mut db, 3.0);
db.poll_trend_logs();
set_value(&mut db, 0.5);
db.poll_trend_logs();
assert_eq!(
logged(&db, oid),
[real(0.0), status(LogStatus::LOG_DISABLED)]
);
set_clock(&clock, Some(at(12, 30, 0, 0)));
db.poll_trend_logs();
assert_eq!(
logged(&db, oid),
[
real(0.0),
status(LogStatus::LOG_DISABLED),
status(LogStatus::empty()),
real(0.5),
]
);
assert_eq!(records(&db, oid)[3].time, start.local_time);
}
#[test]
fn another_devices_reference_logs_its_failure_once() {
let (mut db, _, _) = cov_fixture(PropertyValue::Null);
let mut log = TrendLogObject::new(2, "Remote", 8).unwrap();
log.set_log_device_object_property(Some(BACnetDeviceObjectPropertyReference {
object_identifier: target(),
property_identifier: P::PRESENT_VALUE.to_raw(),
property_array_index: None,
device_identifier: Some(ObjectIdentifier::new(ObjectType::DEVICE, 77).unwrap()),
}))
.unwrap();
log.set_logging_type(LoggingType::COV).unwrap();
let remote = log.object_identifier();
db.add(Box::new(log)).unwrap();
for value in [1.0, 2.0, 3.0] {
db.poll_trend_logs();
set_value(&mut db, value);
}
assert_eq!(
logged(&db, remote),
[(
LogDatum::Failure {
error_class: ErrorClass::PROPERTY,
error_code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED,
},
None,
)]
);
}
#[test]
fn a_cov_log_without_a_clock_looks_again_after_the_retry_delay() {
let (mut db, oid, time, clock) = fixture(0);
write(
&mut db,
oid,
P::LOGGING_TYPE,
PropertyValue::Enumerated(LoggingType::COV.to_raw()),
);
set_clock(&clock, None);
db.poll_trend_logs();
assert!(logged(&db, oid).is_empty());
set_clock(&clock, Some(frame()));
db.poll_trend_logs();
assert!(logged(&db, oid).is_empty());
*time.lock().unwrap() = RECONCILE;
db.poll_trend_logs();
assert_eq!(logged(&db, oid), [real(0.0)]);
}
fn cov_log_of(db: &mut ObjectDatabase, object: ObjectIdentifier) -> ObjectIdentifier {
let mut log = TrendLogObject::new(2, "Second", 16).unwrap();
log.set_log_device_object_property(Some(BACnetDeviceObjectPropertyReference::new_local(
object,
P::PRESENT_VALUE.to_raw(),
)))
.unwrap();
log.set_logging_type(LoggingType::COV).unwrap();
let oid = log.object_identifier();
db.add(Box::new(log)).unwrap();
oid
}
fn set_present_value(db: &mut ObjectDatabase, object: ObjectIdentifier, value: PropertyValue) {
db.get_mut(&object)
.unwrap()
.set_present_value_internal(value)
.unwrap();
}
#[test]
fn a_binary_value_logs_every_change() {
let (mut db, _, _) = cov_fixture(PropertyValue::Real(1.0));
let bv = crate::binary::BinaryValueObject::with_access(
1,
"BV",
crate::present_value_access::PresentValueAccess::Writable,
)
.unwrap();
let bv_oid = bv.object_identifier();
db.add(Box::new(bv)).unwrap();
let oid = cov_log_of(&mut db, bv_oid);
db.poll_trend_logs();
db.poll_trend_logs();
for value in [1, 0, 1] {
set_present_value(&mut db, bv_oid, PropertyValue::Enumerated(value));
db.poll_trend_logs();
db.poll_trend_logs();
}
let values: Vec<_> = logged(&db, oid)
.into_iter()
.map(|(datum, _)| datum)
.collect();
assert_eq!(values, [0, 1, 0, 1].map(LogDatum::EnumValue));
}
#[test]
fn a_target_without_a_cov_increment_logs_every_change_under_a_null_increment() {
let (mut db, _, _) = cov_fixture(PropertyValue::Real(1.0));
let msv = crate::multistate::MultiStateValueObject::with_access(
1,
"MSV",
4,
crate::present_value_access::PresentValueAccess::Writable,
)
.unwrap();
let msv_oid = msv.object_identifier();
assert_eq!(msv.cov_increment(), None);
db.add(Box::new(msv)).unwrap();
let oid = cov_log_of(&mut db, msv_oid);
db.poll_trend_logs();
for value in [2, 3] {
set_present_value(&mut db, msv_oid, PropertyValue::Unsigned(value));
db.poll_trend_logs();
}
let values: Vec<_> = logged(&db, oid)
.into_iter()
.map(|(datum, _)| datum)
.collect();
assert_eq!(values, [1, 2, 3].map(LogDatum::UnsignedValue));
}
#[test]
fn a_target_removed_logs_its_failure_once_then_its_value_again() {
let (mut db, oid, _) = cov_fixture(PropertyValue::Real(1.0));
db.poll_trend_logs();
let removed = db.remove(&target()).unwrap().unwrap();
db.poll_trend_logs();
db.poll_trend_logs();
db.add(removed).unwrap();
db.poll_trend_logs();
db.poll_trend_logs();
assert_eq!(
logged(&db, oid),
[
real(0.0),
(
LogDatum::Failure {
error_class: ErrorClass::OBJECT,
error_code: ErrorCode::UNKNOWN_OBJECT,
},
None,
),
real(0.0),
]
);
}