use super::*;
use crate::server::event_notifications_tests::recording_transport;
use crate::server::test_transport::{SendLog, TestTransport};
use bacnet_objects::analog::AnalogInputObject;
use bacnet_objects::device::{DeviceConfig, DeviceObject};
use bacnet_objects::event_enrollment::EventEnrollmentObject;
use bacnet_objects::traits::BACnetObject;
use bacnet_types::constructed::{BACnetDeviceObjectPropertyReference, BACnetEventParameter};
use bacnet_types::enums::{EngineeringUnits, EventState, EventType};
#[tokio::test(start_paused = true)]
async fn spawned_task_advances_and_fires_the_time_delay_countdown() {
let (transport, sent) = recording_transport();
let mut ai = AnalogInputObject::new(1, "AI-1", EngineeringUnits::DEGREES_CELSIUS).unwrap();
ai.set_present_value(85.0); let ai_oid = ai.object_identifier();
let mut ee = EventEnrollmentObject::new(1, "EE-OOR", EventType::OUT_OF_RANGE).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)))
.unwrap();
ee.set_event_parameters(BACnetEventParameter::OutOfRange {
time_delay: 2,
low_limit: 20.0,
high_limit: 80.0,
deadband: 2.0,
});
ee.set_event_enable(bacnet_types::bitstring::EventTransitionBits::all());
let ee_oid = ee.object_identifier();
let mut db = ObjectDatabase::new();
db.add(Box::new(ai)).unwrap();
db.add(Box::new(ee)).unwrap();
db.add(Box::new(
DeviceObject::new(DeviceConfig {
identity: bacnet_objects::DeviceIdentity::default().with_name("Dev"),
..DeviceConfig::default()
})
.unwrap(),
))
.unwrap();
db.add(Box::new(
super::event_notifications_tests::notification_class_0_broadcasting(),
))
.unwrap();
let config = ServerConfig {
event_enrollment_interval_secs: 1,
..ServerConfig::default()
};
let server = BACnetServer::start(config, db, transport)
.await
.expect("server should start");
let read_state = |db: &ObjectDatabase| match db
.get(&ee_oid)
.unwrap()
.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap()
{
PropertyValue::Enumerated(v) => EventState::from_raw(v),
other => panic!("EVENT_STATE must read Enumerated, got {other:?}"),
};
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(
read_state(&*server.database().read().await),
EventState::NORMAL,
"first pass seeds the countdown; the confirmed state must not move"
);
tokio::time::sleep(Duration::from_millis(1300)).await;
assert_eq!(
read_state(&*server.database().read().await),
EventState::NORMAL,
"delay not yet elapsed: NORMAL holds (13.3)"
);
tokio::time::sleep(Duration::from_millis(1200)).await;
assert_eq!(
read_state(&*server.database().read().await),
EventState::HIGH_LIMIT,
"Time_Delay elapsed under the real spawned task: the transition fired"
);
assert_eq!(
sent.len(),
1,
"the committed delayed Event Enrollment transition must distribute once"
);
}
async fn default_interval_server(
time_delay: u32,
) -> (BACnetServer<TestTransport>, ObjectIdentifier, SendLog) {
let (transport, sent) = recording_transport();
let mut ai = AnalogInputObject::new(1, "AI-1", EngineeringUnits::DEGREES_CELSIUS).unwrap();
ai.set_present_value(85.0);
let ai_oid = ai.object_identifier();
let mut ee = EventEnrollmentObject::new(1, "EE-OOR", EventType::OUT_OF_RANGE).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)))
.unwrap();
ee.set_event_parameters(BACnetEventParameter::OutOfRange {
time_delay,
low_limit: 20.0,
high_limit: 80.0,
deadband: 2.0,
});
ee.set_event_enable(bacnet_types::bitstring::EventTransitionBits::all());
let ee_oid = ee.object_identifier();
let mut db = ObjectDatabase::new();
db.add(Box::new(ai)).unwrap();
db.add(Box::new(ee)).unwrap();
db.add(Box::new(
DeviceObject::new(DeviceConfig {
identity: bacnet_objects::DeviceIdentity::default().with_name("Dev"),
..DeviceConfig::default()
})
.unwrap(),
))
.unwrap();
let server = BACnetServer::start(ServerConfig::default(), db, transport)
.await
.expect("server should start");
(server, ee_oid, sent)
}
fn ee_event_state(db: &ObjectDatabase, ee_oid: &ObjectIdentifier) -> EventState {
match db
.get(ee_oid)
.unwrap()
.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap()
{
PropertyValue::Enumerated(v) => EventState::from_raw(v),
other => panic!("EVENT_STATE must read Enumerated, got {other:?}"),
}
}
#[tokio::test(start_paused = true)]
async fn delay_5s_fires_on_second_default_interval_tick() {
let (server, ee_oid, _sent) = default_interval_server(5).await;
tokio::time::sleep(Duration::from_millis(100)).await; tokio::time::sleep(Duration::from_millis(8900)).await; assert_eq!(
ee_event_state(&*server.database().read().await, &ee_oid),
EventState::NORMAL,
"one seeded pass still counting at t≈9s"
);
tokio::time::sleep(Duration::from_millis(2000)).await; assert_eq!(
ee_event_state(&*server.database().read().await, &ee_oid),
EventState::HIGH_LIMIT,
"ceil(5s/10s)=1 pass: fired by the t=10s tick — under the old \
per-pass misreading this would have taken ~50s"
);
}
#[tokio::test(start_paused = true)]
async fn delay_15s_fires_on_third_default_interval_tick() {
let (server, ee_oid, _sent) = default_interval_server(15).await;
tokio::time::sleep(Duration::from_secs(19)).await; assert_eq!(
ee_event_state(&*server.database().read().await, &ee_oid),
EventState::NORMAL,
"t≈19s: still counting (second seeded pass)"
);
tokio::time::sleep(Duration::from_secs(2)).await; assert_eq!(
ee_event_state(&*server.database().read().await, &ee_oid),
EventState::HIGH_LIMIT,
"ceil(15/10)=2 passes: fired by the t=20s tick"
);
}
#[tokio::test(start_paused = true)]
async fn delay_25s_fires_on_fourth_default_interval_tick() {
let (server, ee_oid, _sent) = default_interval_server(25).await;
tokio::time::sleep(Duration::from_secs(29)).await;
assert_eq!(
ee_event_state(&*server.database().read().await, &ee_oid),
EventState::NORMAL,
"t≈29s: three seeded passes not yet elapsed"
);
tokio::time::sleep(Duration::from_secs(2)).await; assert_eq!(
ee_event_state(&*server.database().read().await, &ee_oid),
EventState::HIGH_LIMIT,
"ceil(25/10)=3 passes: fired by the t=30s tick"
);
}
#[tokio::test(start_paused = true)]
async fn zero_delay_fires_on_the_first_tick() {
let (server, ee_oid, _sent) = default_interval_server(0).await;
tokio::time::sleep(Duration::from_millis(500)).await;
assert_eq!(
ee_event_state(&*server.database().read().await, &ee_oid),
EventState::HIGH_LIMIT,
"TD=0: the first (immediate) tick transitions without any countdown"
);
}