use super::*;
use crate::mutation::{MutationPolicy, MutationService, MutationTarget, WriteGroupTarget};
use crate::server::channel_wire_tests::{ch, channel, member, slot};
use crate::server::command_action_wire_tests::ao;
use crate::server::cov_wire_test_support::{Harness, PEER, PV};
use bacnet_encoding::apdu::encode_apdu;
use bacnet_encoding::npdu::{Npdu, encode_npdu};
use bacnet_objects::analog::AnalogOutputObject;
use bacnet_services::write_group::GroupChannelValue;
use bacnet_transport::port::{ReceivedNpdu, TransportProvenance};
use bacnet_types::enums::{EngineeringUnits, WriteStatus};
use std::num::NonZeroU32;
fn objects(db: &mut ObjectDatabase) {
for instance in 1..=5 {
db.add(Box::new(
AnalogOutputObject::new(
instance,
format!("AO-{instance}"),
EngineeringUnits::DEGREES_CELSIUS,
)
.unwrap(),
))
.unwrap();
}
let channels: [(u32, u16, &[u32], u32, bool); 5] = [
(1, 11, &[27], 0, false),
(2, 11, &[14], 0, false),
(3, 12, &[5, 27], 0, false),
(4, 13, &[27], 500, true),
(5, 13, &[0, 27], 500, false),
];
for (instance, number, groups, delay, may_skip) in channels {
let mut object = channel(instance, number, vec![(member(ao(instance), PV), delay)]);
object.set_control_groups(groups.to_vec()).unwrap();
object.set_allow_group_delay_inhibit(may_skip);
db.add(Box::new(object)).unwrap();
}
}
async fn start(config: ServerConfig) -> Harness {
Harness::start_with(config, objects).await
}
fn encoded(value: PropertyValue) -> Vec<u8> {
let mut bytes = BytesMut::new();
encode_property_value(&mut bytes, &value).unwrap();
bytes.to_vec()
}
fn real(value: f32) -> Vec<u8> {
encoded(PropertyValue::Real(value))
}
fn entry(channel: u16, override_priority: Option<u8>, value: Vec<u8>) -> GroupChannelValue {
GroupChannelValue {
channel,
override_priority,
value,
}
}
fn request(group: u32, priority: u8, change_list: Vec<GroupChannelValue>) -> WriteGroupRequest {
WriteGroupRequest {
group_number: NonZeroU32::new(group).unwrap(),
write_priority: priority,
change_list,
inhibit_delay: None,
}
}
fn service(request: &WriteGroupRequest) -> Vec<u8> {
let mut service = BytesMut::new();
request.encode(&mut service).unwrap();
service.to_vec()
}
async fn send_raw(h: &Harness, service: &[u8]) {
let mut apdu = BytesMut::new();
encode_apdu(
&mut apdu,
&Apdu::UnconfirmedRequest(UnconfirmedRequestPdu {
service_choice: UnconfirmedServiceChoice::WRITE_GROUP,
service_request: Bytes::copy_from_slice(service),
}),
)
.unwrap();
let mut npdu = BytesMut::new();
encode_npdu(
&mut npdu,
&Npdu {
payload: apdu.freeze(),
..Npdu::default()
},
)
.unwrap();
h.tx.send(ReceivedNpdu {
direct_response: None,
npdu: npdu.freeze(),
source_mac: MacAddr::from_slice(&PEER),
link_layer_group: true,
data_attributes: Vec::new(),
provenance: TransportProvenance::unverified(),
reply_tx: None,
})
.await
.unwrap();
h.settle().await;
}
async fn send(h: &Harness, request: &WriteGroupRequest) {
send_raw(h, &service(request)).await;
}
async fn read(h: &Harness, instance: u32, property: PropertyIdentifier) -> PropertyValue {
h.server
.database()
.read()
.await
.get(&ch(instance))
.unwrap()
.read_property(property, None)
.unwrap()
}
async fn present_value(h: &Harness, instance: u32) -> PropertyValue {
read(h, instance, PV).await
}
async fn status(h: &Harness, instance: u32) -> WriteStatus {
match read(h, instance, PropertyIdentifier::WRITE_STATUS).await {
PropertyValue::Enumerated(raw) => WriteStatus::from_raw(raw),
other => panic!("Write_Status read {other:?}"),
}
}
fn assert_silent(h: &Harness) {
let frames = h.frames.lock().unwrap();
assert!(frames.is_empty(), "WriteGroup was answered: {frames:?}");
}
async fn assert_untouched(h: &Harness) {
for instance in 1..=5 {
assert_eq!(present_value(h, instance).await, PropertyValue::Null);
assert_eq!(status(h, instance).await, WriteStatus::IDLE);
for priority in [8, 16] {
assert_eq!(slot(h, ao(instance), priority).await, PropertyValue::Null);
}
}
}
#[test]
fn write_group_plan_matches_group_and_number_per_channel_in_instance_order() {
let mut db = ObjectDatabase::new();
objects(&mut db);
let planned = |request: &WriteGroupRequest| -> Vec<(u32, u16, u8, Vec<u8>)> {
plan(&db, request)
.into_iter()
.map(|write| {
let instance = write.channel.instance_number();
(instance, write.number, write.priority, write.value.to_vec())
})
.collect()
};
assert_eq!(
planned(&request(
27,
8,
vec![
entry(13, Some(3), real(1.0)),
entry(11, None, real(2.0)),
entry(11, None, real(3.0)),
entry(12, None, real(4.0)),
],
)),
[
(4, 13, 3, real(1.0)),
(5, 13, 3, real(1.0)),
(1, 11, 8, real(2.0)),
(1, 11, 8, real(3.0)),
(3, 12, 8, real(4.0)),
]
);
assert_eq!(
planned(&request(14, 16, vec![entry(11, None, real(5.0))])),
[(2, 11, 16, real(5.0))]
);
assert!(planned(&request(6, 8, vec![entry(11, None, real(5.0))])).is_empty());
assert!(planned(&request(5, 8, vec![entry(11, None, real(5.0))])).is_empty());
}
#[tokio::test(start_paused = true)]
async fn write_group_writes_the_channels_in_the_group_with_each_number_at_the_entry_priority() {
let h = start(ServerConfig::default()).await;
send(
&h,
&request(
27,
8,
vec![
entry(11, None, real(5.0)),
entry(12, Some(10), encoded(PropertyValue::Unsigned(7))),
entry(99, None, real(1.0)),
],
),
)
.await;
assert_eq!(present_value(&h, 1).await, PropertyValue::Real(5.0));
assert_eq!(
read(&h, 1, PropertyIdentifier::LAST_PRIORITY).await,
PropertyValue::Unsigned(8)
);
assert_eq!(status(&h, 1).await, WriteStatus::SUCCESSFUL);
assert_eq!(slot(&h, ao(1), 8).await, PropertyValue::Real(5.0));
assert_eq!(present_value(&h, 3).await, PropertyValue::Unsigned(7));
assert_eq!(
read(&h, 3, PropertyIdentifier::LAST_PRIORITY).await,
PropertyValue::Unsigned(10)
);
assert_eq!(status(&h, 3).await, WriteStatus::SUCCESSFUL);
assert_eq!(slot(&h, ao(3), 10).await, PropertyValue::Real(7.0));
assert_eq!(slot(&h, ao(3), 8).await, PropertyValue::Null);
assert_eq!(present_value(&h, 2).await, PropertyValue::Null);
assert_eq!(slot(&h, ao(2), 8).await, PropertyValue::Null);
for instance in [2, 4, 5] {
assert_eq!(status(&h, instance).await, WriteStatus::IDLE);
}
send(&h, &request(27, 8, vec![entry(11, None, vec![0x00])])).await;
assert_eq!(present_value(&h, 1).await, PropertyValue::Null);
assert_eq!(slot(&h, ao(1), 8).await, PropertyValue::Null);
send(&h, &request(14, 9, vec![entry(11, None, real(2.0))])).await;
assert_eq!(slot(&h, ao(2), 9).await, PropertyValue::Real(2.0));
assert_eq!(slot(&h, ao(1), 9).await, PropertyValue::Null);
assert_silent(&h);
}
#[tokio::test(start_paused = true)]
async fn write_group_inhibit_delay_skips_delays_only_where_the_channel_allows_it() {
let h = start(ServerConfig::default()).await;
let mut inhibited = request(27, 8, vec![entry(13, None, real(3.0))]);
inhibited.inhibit_delay = Some(true);
send(&h, &inhibited).await;
assert_eq!(slot(&h, ao(4), 8).await, PropertyValue::Real(3.0));
assert_eq!(status(&h, 4).await, WriteStatus::SUCCESSFUL);
assert_eq!(slot(&h, ao(5), 8).await, PropertyValue::Null);
assert_eq!(status(&h, 5).await, WriteStatus::IN_PROGRESS);
send(
&h,
&request(
27,
8,
vec![entry(13, None, real(9.0)), entry(11, None, real(6.0))],
),
)
.await;
assert_eq!(present_value(&h, 4).await, PropertyValue::Real(9.0));
assert_eq!(slot(&h, ao(4), 8).await, PropertyValue::Real(3.0));
assert_eq!(present_value(&h, 5).await, PropertyValue::Real(3.0));
assert_eq!(slot(&h, ao(1), 8).await, PropertyValue::Real(6.0));
tokio::time::sleep(Duration::from_millis(500)).await;
h.settle().await;
assert_eq!(status(&h, 4).await, WriteStatus::SUCCESSFUL);
assert_eq!(status(&h, 5).await, WriteStatus::SUCCESSFUL);
assert_eq!(slot(&h, ao(4), 8).await, PropertyValue::Real(9.0));
assert_eq!(slot(&h, ao(5), 8).await, PropertyValue::Real(3.0));
assert_silent(&h);
}
#[tokio::test(start_paused = true)]
async fn write_group_runs_under_disable_initiation() {
let h = start(ServerConfig::default()).await;
let write = request(27, 8, vec![entry(11, None, real(5.0))]);
h.server
.comm_state
.set_for_test(DccState::DisableInitiation);
send(&h, &write).await;
assert_eq!(present_value(&h, 1).await, PropertyValue::Real(5.0));
assert_eq!(slot(&h, ao(1), 8).await, PropertyValue::Real(5.0));
assert_silent(&h);
}
#[tokio::test(start_paused = true)]
async fn deny_all_denies_each_channel_write_of_a_write_group() {
let h = start(ServerConfig {
mutation_policy: MutationPolicy::DenyAll,
policy: Some(Arc::new(crate::FnPolicy::new().mutation(|_| true))),
..ServerConfig::default()
})
.await;
let writes = vec![entry(11, None, real(5.0)), entry(13, None, real(6.0))];
send(&h, &request(27, 8, writes)).await;
assert_untouched(&h).await;
assert_silent(&h);
let counted = h.server.mutation_decision_counters().write_group;
assert_eq!(
(
counted.allow_total,
counted.deny_total,
counted.policy_deny_total
),
(0, 3, 3)
);
}
#[tokio::test(start_paused = true)]
async fn the_policy_decides_each_channel_write_of_a_write_group() {
let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
let shown = Arc::clone(&seen);
let h = start(ServerConfig {
policy: Some(Arc::new(crate::FnPolicy::new().mutation(move |context| {
shown.lock().unwrap().push(context.clone());
matches!(&context.target, MutationTarget::WriteGroup(target)
if [3, 5].contains(&target.channel.instance_number()))
}))),
..ServerConfig::default()
})
.await;
let writes = vec![
entry(12, None, real(5.0)),
entry(11, Some(4), real(6.0)),
entry(13, None, real(7.0)),
];
send(&h, &request(27, 8, writes)).await;
assert_eq!(present_value(&h, 3).await, PropertyValue::Real(5.0));
assert_eq!(slot(&h, ao(3), 8).await, PropertyValue::Real(5.0));
assert_eq!(present_value(&h, 5).await, PropertyValue::Real(7.0));
for denied in [1, 4] {
assert_eq!(present_value(&h, denied).await, PropertyValue::Null);
assert_eq!(status(&h, denied).await, WriteStatus::IDLE);
}
assert_silent(&h);
let contexts = seen.lock().unwrap();
let targets: Vec<WriteGroupTarget> = contexts
.iter()
.map(|context| {
assert_eq!(context.invoke_id, None);
let write_group = UnconfirmedServiceChoice::WRITE_GROUP;
assert_eq!(
context.service_choice,
MutationService::Unconfirmed(write_group)
);
assert_eq!(context.source_mac.as_slice(), PEER);
match &context.target {
MutationTarget::WriteGroup(target) => target.clone(),
other => panic!("a WriteGroup target, not {other:?}"),
}
})
.collect();
let target = |channel, number, priority, value| WriteGroupTarget {
channel: ch(channel),
group_number: 27,
channel_number: number,
priority,
value,
inhibit_delay: false,
};
assert_eq!(
targets,
[
target(3, 12, 8, real(5.0)),
target(1, 11, 4, real(6.0)),
target(4, 13, 8, real(7.0)),
target(5, 13, 8, real(7.0)),
]
);
let counted = h.server.mutation_decision_counters().write_group;
assert_eq!((counted.allow_total, counted.deny_total), (2, 2));
}
#[tokio::test(start_paused = true)]
async fn a_panicking_policy_denies_the_channel_write() {
let h = start(ServerConfig {
policy: Some(Arc::new(
crate::FnPolicy::new().mutation(|_| panic!("authorizer bug")),
)),
..ServerConfig::default()
})
.await;
send(&h, &request(27, 8, vec![entry(11, None, real(5.0))])).await;
assert_untouched(&h).await;
assert_eq!(
h.server.mutation_decision_counters().write_group.deny_total,
1
);
}
#[tokio::test(start_paused = true)]
async fn write_group_malformed_requests_are_dropped_without_an_answer() {
let h = start(ServerConfig::default()).await;
let good = service(&request(27, 8, vec![entry(11, None, real(5.0))]));
let mut trailing = good.clone();
trailing.push(0x00);
let mut group_zero = good.clone();
assert_eq!(group_zero[..2], [0x09, 27]);
group_zero[1] = 0;
let truncated = good[..good.len() - 1].to_vec();
let mut lighting_cut_short = good[..7].to_vec();
assert_eq!(lighting_cut_short[4..], [0x2E, 0x09, 11]);
lighting_cut_short.extend_from_slice(&[0x0E, 0x09]);
for bad in [
trailing,
group_zero,
truncated,
lighting_cut_short,
Vec::new(),
] {
send_raw(&h, &bad).await;
}
assert_untouched(&h).await;
send_raw(&h, &good).await;
assert_eq!(slot(&h, ao(1), 8).await, PropertyValue::Real(5.0));
assert_silent(&h);
}
async fn plan_change_apply(
h: &Harness,
request: &WriteGroupRequest,
change: impl FnOnce(&mut ObjectDatabase),
) {
let writes = plan(&*h.server.database().read().await, request);
assert!(!writes.is_empty(), "nothing planned");
change(&mut *h.server.database().write().await);
let runner = CommandRunner::for_server(&h.server);
apply(&runner, &requester(h), request, &writes).await;
h.settle().await;
}
fn requester(h: &Harness) -> Requester<'_> {
Requester {
config: &h.server.config,
decisions: &h.server.mutation_decisions,
source_mac: &PEER,
source_network: None,
provenance: TransportProvenance::unverified(),
audit_source: BACnetRecipient::Address(bacnet_types::constructed::BACnetAddress {
network_number: 0,
mac_address: MacAddr::from_slice(&PEER),
}),
}
}
fn write_channel_property(
db: &mut ObjectDatabase,
instance: u32,
property: PropertyIdentifier,
value: PropertyValue,
) {
db.get_mut(&ch(instance))
.unwrap()
.write_property(property, None, value, None)
.unwrap();
}
#[tokio::test(start_paused = true)]
async fn write_group_skips_a_channel_that_left_the_group_after_the_plan() {
let h = start(ServerConfig::default()).await;
let write = request(
27,
8,
vec![entry(11, None, real(5.0)), entry(12, None, real(6.0))],
);
plan_change_apply(&h, &write, |db| {
let groups = PropertyValue::List(vec![PropertyValue::Unsigned(14)]);
write_channel_property(db, 1, PropertyIdentifier::CONTROL_GROUPS, groups);
let number = PropertyValue::Unsigned(13);
write_channel_property(db, 3, PropertyIdentifier::CHANNEL_NUMBER, number);
})
.await;
assert_untouched(&h).await;
assert_silent(&h);
}
#[tokio::test(start_paused = true)]
async fn write_group_reads_allow_group_delay_inhibit_at_the_write() {
let h = start(ServerConfig::default()).await;
let mut inhibited = request(27, 8, vec![entry(13, None, real(3.0))]);
inhibited.inhibit_delay = Some(true);
plan_change_apply(&h, &inhibited, |db| {
let allow = PropertyValue::Boolean(false);
write_channel_property(db, 4, PropertyIdentifier::ALLOW_GROUP_DELAY_INHIBIT, allow);
})
.await;
assert_eq!(present_value(&h, 4).await, PropertyValue::Real(3.0));
assert_eq!(slot(&h, ao(4), 8).await, PropertyValue::Null);
assert_eq!(status(&h, 4).await, WriteStatus::IN_PROGRESS);
tokio::time::sleep(Duration::from_millis(500)).await;
h.settle().await;
assert_eq!(slot(&h, ao(4), 8).await, PropertyValue::Real(3.0));
assert_eq!(status(&h, 4).await, WriteStatus::SUCCESSFUL);
assert_silent(&h);
}
#[tokio::test(start_paused = true)]
async fn write_group_dropped_after_a_channel_took_its_value_ends_that_distribution_failed() {
let h = start(ServerConfig::default()).await;
let write = request(27, 9, vec![entry(13, None, real(4.0))]);
let writes = plan(&*h.server.database().read().await, &write);
let runner = CommandRunner::for_server(&h.server);
let table = Arc::clone(&h.server.cov_table).read_owned().await;
let requester = requester(&h);
let mut applying = Box::pin(apply(&runner, &requester, &write, &writes));
let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
assert!(std::future::Future::poll(applying.as_mut(), &mut cx).is_pending());
assert_eq!(status(&h, 4).await, WriteStatus::IN_PROGRESS);
drop(applying);
assert_eq!(status(&h, 4).await, WriteStatus::FAILED);
assert_eq!(status(&h, 5).await, WriteStatus::IDLE);
assert!(h.server.request_tasks.is_empty());
drop(table);
h.settle().await;
assert_eq!(slot(&h, ao(4), 9).await, PropertyValue::Null);
assert_silent(&h);
}
#[tokio::test(start_paused = true)]
async fn write_group_lighting_command_reaches_a_lighting_output() {
use crate::server::lighting_command_member_tests::{FRAMED, lighting_command, objects};
let h = Harness::start_with(ServerConfig::default(), objects).await;
send(&h, &request(27, 10, vec![entry(11, None, FRAMED.to_vec())])).await;
assert_eq!(
present_value(&h, 1).await,
PropertyValue::ApplicationData(FRAMED.to_vec())
);
assert_eq!(status(&h, 1).await, WriteStatus::SUCCESSFUL);
assert_eq!(lighting_command(&h).await, FRAMED[1..FRAMED.len() - 1]);
assert_silent(&h);
}