use super::*;
use crate::common::assert_list_element_refused;
use bacnet_types::constructed::BACnetAddress;
use bacnet_types::enums::ObjectType;
use bacnet_types::primitives::ObjectIdentifier;
use std::sync::atomic::{AtomicU64, Ordering};
const MINUTE: Duration = Duration::from_secs(60);
const NANO: Duration = Duration::from_nanos(1);
fn device(instance: u32, minutes: u32) -> BACnetEventNotificationSubscription {
BACnetEventNotificationSubscription {
recipient: BACnetRecipient::Device(
ObjectIdentifier::new(ObjectType::DEVICE, instance).unwrap(),
),
process_identifier: instance,
issue_confirmed_notifications: false,
time_remaining: minutes,
}
}
fn address(minutes: u32) -> BACnetEventNotificationSubscription {
BACnetEventNotificationSubscription {
recipient: BACnetRecipient::Address(BACnetAddress {
network_number: 5,
mac_address: bacnet_types::MacAddr::from_slice(&[10, 0, 0, 9, 0xBA, 0xC0]),
}),
process_identifier: 77,
issue_confirmed_notifications: true,
time_remaining: minutes,
}
}
fn framed(subscriptions: &[BACnetEventNotificationSubscription]) -> PropertyValue {
let mut buf = BytesMut::new();
encode_event_notification_subscription_list(&mut buf, subscriptions).unwrap();
PropertyValue::ApplicationData(buf.to_vec())
}
fn stored(subscriptions: &[BACnetEventNotificationSubscription]) -> SubscribedRecipients {
let mut store = SubscribedRecipients::new();
store.write(framed(subscriptions)).unwrap();
store
}
fn minutes(store: &SubscribedRecipients) -> Vec<u32> {
store
.subscriptions()
.iter()
.map(|subscription| subscription.time_remaining)
.collect()
}
#[test]
fn written_entries_read_back_in_order() {
let entries = [device(1, 60), address(1440)];
let store = stored(&entries);
assert_eq!(store.subscriptions(), entries);
assert_eq!(store.read(), framed(&entries));
assert_eq!(SubscribedRecipients::new().read(), framed(&[]));
}
#[test]
fn time_remaining_counts_down_in_whole_minutes_rounded_up() {
let mut store = stored(&[device(1, 2)]);
assert_eq!(minutes(&store), [2]);
store.advance_by(NANO);
assert_eq!(minutes(&store), [2], "part of a minute rounds up");
store.advance_by(MINUTE - NANO);
assert_eq!(minutes(&store), [1], "one whole minute left");
store.advance_by(MINUTE - NANO);
assert_eq!(minutes(&store), [1], "a live entry never reads as zero");
}
#[test]
fn an_entry_lapses_at_its_deadline() {
let mut store = stored(&[device(1, 1), device(2, 2)]);
assert_eq!(store.next_deadline(), Some(MINUTE));
assert!(!store.advance_by(MINUTE - NANO));
assert_eq!(minutes(&store), [1, 2]);
assert!(store.advance_by(NANO), "the first entry lapses");
assert_eq!(store.subscriptions(), [device(2, 1)]);
assert_eq!(store.next_deadline(), Some(2 * MINUTE));
assert!(store.advance_by(MINUTE));
assert_eq!(store.read(), framed(&[]));
assert_eq!(store.next_deadline(), None);
}
#[test]
fn a_bound_clock_drives_reads_and_expiry() {
let nanos = Arc::new(AtomicU64::new(0));
let clock = Arc::clone(&nanos);
let mut store = SubscribedRecipients::new();
store.bind_monotonic_clock(Some(Arc::new(move || {
Duration::from_nanos(clock.load(Ordering::SeqCst))
})));
let set = |at: Duration| nanos.store(u64::try_from(at.as_nanos()).unwrap(), Ordering::SeqCst);
set(10 * MINUTE);
store.write(framed(&[device(1, 3)])).unwrap();
assert_eq!(store.next_deadline(), Some(13 * MINUTE));
set(11 * MINUTE + NANO);
assert_eq!(minutes(&store), [2]);
set(13 * MINUTE);
assert_eq!(store.read(), framed(&[]));
assert!(store.advance_to(13 * MINUTE));
assert_eq!(store.next_deadline(), None);
}
#[test]
fn unbinding_the_clock_keeps_the_time_each_entry_has_left() {
let nanos = Arc::new(AtomicU64::new(0));
let clock = Arc::clone(&nanos);
let mut store = SubscribedRecipients::new();
store.bind_monotonic_clock(Some(Arc::new(move || {
Duration::from_nanos(clock.load(Ordering::SeqCst))
})));
nanos.store(
u64::try_from((10 * MINUTE).as_nanos()).unwrap(),
Ordering::SeqCst,
);
store.write(framed(&[device(1, 5)])).unwrap();
nanos.store(
u64::try_from((12 * MINUTE).as_nanos()).unwrap(),
Ordering::SeqCst,
);
assert_eq!(minutes(&store), [3]);
store.bind_monotonic_clock(None);
assert_eq!(minutes(&store), [3]);
assert!(!store.advance_by(3 * MINUTE - NANO));
assert_eq!(minutes(&store), [1]);
assert!(store.advance_by(NANO));
assert_eq!(store.subscriptions(), []);
}
#[test]
fn time_remaining_must_be_one_minute_to_a_day() {
let mut store = stored(&[device(1, 5)]);
for refused in [0, MAX_SUBSCRIPTION_MINUTES + 1, u32::MAX] {
assert_list_element_refused(
store.write(framed(&[device(2, 5), device(3, refused)])),
ErrorClass::PROPERTY,
ErrorCode::VALUE_OUT_OF_RANGE,
2,
&format!("{refused} minutes"),
);
assert_eq!(store.subscriptions(), [device(1, 5)], "nothing changes");
}
store
.write(framed(&[device(2, 1), device(3, MAX_SUBSCRIPTION_MINUTES)]))
.unwrap();
assert_eq!(minutes(&store), [1, MAX_SUBSCRIPTION_MINUTES]);
}
#[test]
fn a_full_list_refuses_another_entry_but_not_a_renewal() {
let full: Vec<_> = (1..=MAX_SUBSCRIBED_RECIPIENTS as u32)
.map(|instance| device(instance, 5))
.collect();
let mut store = stored(&full);
let mut over = full.clone();
over.push(device(1, 9));
over.push(device(999, 5));
assert_list_element_refused(
store.write(framed(&over)),
ErrorClass::RESOURCES,
ErrorCode::NO_SPACE_TO_WRITE_PROPERTY,
MAX_SUBSCRIBED_RECIPIENTS as u32 + 2,
"the entry past the cap",
);
assert_eq!(store.subscriptions(), full, "nothing changes");
over.pop();
store.write(framed(&over)).unwrap();
assert_eq!(store.subscriptions().len(), MAX_SUBSCRIBED_RECIPIENTS);
assert_eq!(store.subscriptions()[0], device(1, 9), "renewed in place");
}
#[test]
fn entries_naming_the_same_recipient_and_process_are_one_entry() {
let mut renewed = device(1, 10);
renewed.issue_confirmed_notifications = true;
let store = stored(&[device(1, 5), address(5), renewed.clone()]);
assert_eq!(store.subscriptions(), [renewed, address(5)]);
let mut other_process = device(1, 5);
other_process.process_identifier = 2;
let store = stored(&[device(1, 5), other_process.clone()]);
assert_eq!(store.subscriptions(), [device(1, 5), other_process]);
}
#[test]
fn a_rewrite_keeps_the_deadline_of_each_entry_it_leaves_alone() {
let mut store = stored(&[device(1, 2)]);
store.advance_by(MINUTE / 2);
let mut list = store.subscriptions();
assert_eq!(list, [device(1, 2)]);
list.push(device(2, 3));
store.write(framed(&list)).unwrap();
assert_eq!(store.next_deadline(), Some(2 * MINUTE), "not stretched");
assert!(store.advance_by(MINUTE + MINUTE / 2));
assert_eq!(store.subscriptions(), [device(2, 2)]);
}
#[test]
fn a_rewrite_keeps_a_deadline_when_a_minute_boundary_falls_between_read_and_write() {
let mut store = stored(&[device(1, 2)]);
store.advance_by(MINUTE / 2);
let mut list = store.subscriptions();
assert_eq!(list, [device(1, 2)]);
list.push(device(2, 3));
store.advance_by(MINUTE / 2 + NANO);
assert_eq!(minutes(&store), [1], "the boundary has passed");
store.write(framed(&list)).unwrap();
assert_eq!(store.next_deadline(), Some(2 * MINUTE), "not stretched");
assert_eq!(minutes(&store), [1, 3]);
}
#[test]
fn a_rewrite_restarts_an_entry_written_two_minutes_or_more_off() {
let mut store = stored(&[device(1, 5)]);
store.advance_by(MINUTE / 2);
assert_eq!(minutes(&store), [5]);
for written in [3, 7] {
let mut store = store.clone();
store.write(framed(&[device(1, written)])).unwrap();
assert_eq!(
store.next_deadline(),
Some(MINUTE / 2 + MINUTE * written),
"{written}"
);
}
}
#[test]
fn a_rewrite_with_other_members_restarts_the_lifetime() {
let mut store = stored(&[device(1, 2), device(2, 2)]);
store.advance_by(MINUTE / 2);
let mut confirmed = device(2, 2);
confirmed.issue_confirmed_notifications = true;
store
.write(framed(&[device(1, 5), confirmed.clone()]))
.unwrap();
assert_eq!(store.subscriptions(), [device(1, 5), confirmed]);
assert_eq!(store.next_deadline(), Some(MINUTE / 2 + 2 * MINUTE));
}
#[test]
fn only_a_framed_list_of_whole_entries_is_written() {
let mut store = stored(&[device(1, 5)]);
let PropertyValue::ApplicationData(bytes) = framed(&[device(2, 5)]) else {
unreachable!()
};
for refused in [
PropertyValue::List(vec![PropertyValue::Unsigned(5)]),
PropertyValue::Unsigned(5),
PropertyValue::ApplicationData(bytes[..bytes.len() - 1].to_vec()),
PropertyValue::ApplicationData([&bytes[..], &[0x21, 0x05]].concat()),
] {
let result = store.write(refused.clone());
assert!(
matches!(
result,
Err(Error::Protocol { class, code })
if class == ErrorClass::PROPERTY.to_raw() as u32
&& code == ErrorCode::INVALID_DATA_TYPE.to_raw() as u32
),
"{refused:?}: {result:?}"
);
assert_eq!(store.subscriptions(), [device(1, 5)], "nothing changes");
}
}
#[test]
fn a_full_list_of_the_longest_entries_reads_in_1184_octets() {
let longest: Vec<_> = (0..MAX_SUBSCRIBED_RECIPIENTS)
.map(|index| BACnetEventNotificationSubscription {
recipient: BACnetRecipient::Address(BACnetAddress {
network_number: u16::MAX,
mac_address: bacnet_types::MacAddr::from_slice(
&[index as u8; BACnetAddress::MAX_MAC_LEN],
),
}),
process_identifier: u32::MAX,
issue_confirmed_notifications: true,
time_remaining: MAX_SUBSCRIPTION_MINUTES,
})
.collect();
let PropertyValue::ApplicationData(bytes) = stored(&longest).read() else {
unreachable!()
};
assert_eq!(bytes.len(), 1184);
}