use super::*;
use bacnet_types::calendar::SpecificDate;
use ScheduleTargetOutcome::{Accepted, DatatypeRefused, Failed, ReferenceRefused};
fn monday() -> SpecificDate {
SpecificDate::new(2026, 9, 14).unwrap()
}
fn at(hour: u8, minute: u8) -> Time {
Time {
hour,
minute,
second: 0,
hundredths: 0,
}
}
fn reference(object_type: ObjectType, instance: u32) -> BACnetObjectPropertyReference {
BACnetObjectPropertyReference::new(
ObjectIdentifier::new(object_type, instance).unwrap(),
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)
}
fn av2() -> BACnetObjectPropertyReference {
reference(ObjectType::ANALOG_VALUE, 2)
}
fn av9() -> BACnetObjectPropertyReference {
reference(ObjectType::ANALOG_VALUE, 9)
}
fn ao4() -> BACnetObjectPropertyReference {
reference(ObjectType::ANALOG_OUTPUT, 4)
}
fn tick(sched: &mut ScheduleObject, time: Time) -> Option<ScheduleWrite> {
sched.tick_schedule(monday(), time, &|_| false)
}
fn faulted(sched: &ScheduleObject) -> bool {
sched
.read_property(PropertyIdentifier::RELIABILITY, None)
.unwrap()
== PropertyValue::Enumerated(Reliability::CONFIGURATION_ERROR.to_raw())
}
fn refused_by_av9(refusal: ScheduleTargetOutcome) -> ScheduleObject {
let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(10.0)).unwrap();
sched.set_priority_for_writing(9).unwrap();
sched
.set_object_property_references(vec![av2(), av9(), ao4()])
.unwrap();
let write = tick(&mut sched, at(9, 0)).expect("the first pass in the period writes");
assert!(!write.retry);
assert_eq!(write.references, [av2(), av9(), ao4()]);
assert!(sched.complete_schedule_write(&write, &[Accepted, refusal, Accepted]));
assert!(faulted(&sched));
sched
}
fn retry_to_av9() -> ScheduleWrite {
ScheduleWrite {
value: PropertyValue::Real(10.0),
priority: 9,
references: vec![av9()],
retry: true,
}
}
#[test]
fn a_pass_with_nothing_else_to_send_retries_only_the_refused_reference() {
let mut sched = refused_by_av9(ReferenceRefused);
for minute in [1, 2] {
let retry = tick(&mut sched, at(9, minute)).expect("a retry");
assert_eq!(retry, retry_to_av9());
assert!(!sched.complete_schedule_write(&retry, &[ReferenceRefused]));
assert!(faulted(&sched), "AV-9 still refuses");
}
let retry = tick(&mut sched, at(9, 3)).expect("a retry");
assert!(sched.complete_schedule_write(&retry, &[Accepted]));
assert!(!faulted(&sched));
assert_eq!(tick(&mut sched, at(9, 4)), None);
}
#[test]
fn a_datatype_refusal_is_retried_too() {
let mut sched = refused_by_av9(DatatypeRefused);
let retry = tick(&mut sched, at(9, 1)).expect("a retry");
assert_eq!(retry, retry_to_av9());
assert!(sched.complete_schedule_write(&retry, &[Accepted]));
assert!(!faulted(&sched));
}
#[test]
fn a_retry_that_fails_otherwise_ends_the_refusal() {
for refusal in [ReferenceRefused, DatatypeRefused] {
let mut sched = refused_by_av9(refusal);
let write = ScheduleWrite {
retry: false,
..retry_to_av9()
};
assert!(!sched.complete_schedule_write(&write, &[Failed]));
assert!(faulted(&sched), "{refusal:?}: not a retry");
let retry = tick(&mut sched, at(9, 1)).expect("a retry");
assert!(sched.complete_schedule_write(&retry, &[Failed]));
assert!(!faulted(&sched), "{refusal:?}: the retry failed otherwise");
assert_eq!(tick(&mut sched, at(9, 2)), None);
}
}
#[test]
fn a_retry_leaves_the_held_slots_alone() {
let mut sched = refused_by_av9(ReferenceRefused);
let retry = tick(&mut sched, at(9, 1)).expect("a retry");
sched.complete_schedule_write(&retry, &[Accepted]);
sched.set_object_property_references(vec![av9()]).unwrap();
assert_eq!(
sched.take_owed_schedule_writes(),
[ScheduleWrite {
value: PropertyValue::Null,
priority: 9,
references: vec![av2(), ao4()],
retry: false,
}]
);
}
#[test]
fn a_new_value_goes_to_the_whole_list_not_as_a_retry() {
let mut sched = refused_by_av9(ReferenceRefused);
sched
.set_weekly_schedule(
0,
vec![BACnetTimeValue {
time: at(10, 0),
value: PropertyValue::Real(20.0),
}],
)
.unwrap();
let write = tick(&mut sched, at(10, 0)).expect("a change writes");
assert!(!write.retry);
assert_eq!(write.value, PropertyValue::Real(20.0));
assert_eq!(write.references, [av2(), av9(), ao4()]);
}
#[test]
fn nothing_is_retried_without_a_value_the_schedule_sends() {
let mut sched = refused_by_av9(ReferenceRefused);
sched
.write_property(
PropertyIdentifier::SCHEDULE_DEFAULT,
None,
PropertyValue::Null,
None,
)
.unwrap();
let relinquish = tick(&mut sched, at(9, 1)).expect("the change to NULL writes");
assert!(!relinquish.retry);
assert_eq!(relinquish.value, PropertyValue::Null);
sched.complete_schedule_write(&relinquish, &[Accepted, ReferenceRefused, Accepted]);
assert!(faulted(&sched), "a NULL counts for nothing");
assert_eq!(tick(&mut sched, at(9, 2)), None);
let mut sched = refused_by_av9(ReferenceRefused);
sched
.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.unwrap();
assert_eq!(tick(&mut sched, at(9, 1)), None);
let mut sched = refused_by_av9(ReferenceRefused);
sched
.set_effective_period(BACnetDateRange {
start_date: SpecificDate::new(2026, 10, 1).unwrap().to_date(),
end_date: unspecified_date(),
})
.unwrap();
assert_eq!(tick(&mut sched, at(9, 1)), None);
assert!(faulted(&sched));
}
#[test]
fn a_created_object_gets_a_retry_of_the_references_naming_it_alone() {
let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(10.0)).unwrap();
sched.set_priority_for_writing(9).unwrap();
sched
.set_object_property_references(vec![av2(), av9(), ao4()])
.unwrap();
let write = tick(&mut sched, at(9, 0)).expect("the first pass in the period writes");
sched.complete_schedule_write(&write, &[Accepted, ReferenceRefused, DatatypeRefused]);
let av9_id = av9().object_identifier;
assert_eq!(sched.retry_refusals_naming(av9_id), Some(retry_to_av9()));
let retry = ScheduleWrite {
references: vec![ao4()],
..retry_to_av9()
};
assert_eq!(
sched.retry_refusals_naming(ao4().object_identifier),
Some(retry)
);
assert_eq!(sched.retry_refusals_naming(av2().object_identifier), None);
let elsewhere = ObjectIdentifier::new(ObjectType::ANALOG_VALUE, 10).unwrap();
assert_eq!(sched.retry_refusals_naming(elsewhere), None);
sched.complete_schedule_write(&retry_to_av9(), &[Accepted]);
assert_eq!(sched.retry_refusals_naming(av9_id), None);
}
#[test]
fn a_created_object_gets_no_retry_where_a_pass_would_send_none() {
let av9_id = av9().object_identifier;
let mut sched = refused_by_av9(ReferenceRefused);
sched
.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.unwrap();
assert_eq!(sched.retry_refusals_naming(av9_id), None);
let mut sched = refused_by_av9(ReferenceRefused);
sched
.set_effective_period(BACnetDateRange {
start_date: SpecificDate::new(2026, 10, 1).unwrap().to_date(),
end_date: unspecified_date(),
})
.unwrap();
assert_eq!(tick(&mut sched, at(9, 1)), None);
assert_eq!(sched.retry_refusals_naming(av9_id), None);
let mut sched = refused_by_av9(ReferenceRefused);
sched
.write_property(
PropertyIdentifier::SCHEDULE_DEFAULT,
None,
PropertyValue::Null,
None,
)
.unwrap();
let relinquish = tick(&mut sched, at(9, 1)).expect("the change to NULL writes");
sched.complete_schedule_write(&relinquish, &[Accepted, ReferenceRefused, Accepted]);
assert_eq!(sched.retry_refusals_naming(av9_id), None);
}