use super::channel_wire_tests::ch;
use super::command_action_wire_tests::{ao, cmd, idle, read_db, slot8};
use super::command_run_stop_tests::{channel_status, db_flags, db_state, objects, with_channel};
use super::cov_wire_test_support::*;
use super::*;
use bacnet_types::enums::WriteStatus;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
type Write<'a> = Pin<Box<dyn Future<Output = Result<(), Error>> + 'a>>;
fn write_local<'a>(
h: &'a Harness,
object: &'a ObjectIdentifier,
value: PropertyValue,
priority: Option<u8>,
) -> Write<'a> {
Box::pin(h.server.write_local(
object,
PV,
None,
value,
priority,
crate::LocalCommandSource::ServerDevice,
))
}
fn commit_and_drop(h: &Harness, mut write: Write<'_>) {
assert!(write
.as_mut()
.poll(&mut Context::from_waker(Waker::noop()))
.is_pending());
assert!(h.server.database().try_read().is_err());
drop(write);
}
async fn subscribe_in_process(h: &mut Harness) -> Vec<u8> {
let mut body = BytesMut::new();
bacnet_services::cov::SubscribeCOVPropertyRequest {
subscriber_process_identifier: 7,
monitored_object_identifier: cmd(1),
issue_confirmed_notifications: Some(false),
lifetime: Some(300),
monitored_property_identifier: PropertyIdentifier::IN_PROCESS,
monitored_property_array_index: None,
cov_increment: None,
}
.encode(&mut body)
.unwrap();
h.request(ConfirmedServiceChoice::SUBSCRIBE_COV_PROPERTY, body)
.await;
response(h).await.unwrap();
in_process(h).await
}
async fn in_process(h: &Harness) -> Vec<u8> {
let notification = h.cov_notification().await;
assert_eq!(notification.monitored_object_identifier, cmd(1));
notification.list_of_values[0].value.clone()
}
#[tokio::test(start_paused = true)]
async fn write_local_dropped_after_its_commit_still_notifies_cov_subscribers() {
let mut h = Harness::start(ServerConfig::default()).await;
h.subscribe_cov().await;
h.cov_notification().await;
let table = Arc::clone(&h.server.cov_table).write_owned().await;
let target = av1();
commit_and_drop(
&h,
write_local(&h, &target, PropertyValue::Real(70.0), None),
);
drop(table);
let notification = h.cov_notification().await;
assert_eq!(notification.monitored_object_identifier, av1());
assert_eq!(notification.list_of_values[0].property_identifier, PV);
assert_eq!(notification.list_of_values[0].value, real(70.0));
h.server.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn write_local_dropped_after_its_commit_still_runs_the_command_and_reports_its_end() {
let mut h = Harness::start_with(ServerConfig::default(), objects).await;
assert_eq!(subscribe_in_process(&mut h).await, [0x10]);
let table = Arc::clone(&h.server.cov_table).write_owned().await;
let target = cmd(1);
commit_and_drop(
&h,
write_local(&h, &target, PropertyValue::Unsigned(2), None),
);
drop(table);
idle(&h, 1).await;
assert_eq!(in_process(&h).await, [0x11]);
assert_eq!(in_process(&h).await, [0x10]);
assert_eq!(db_state(&h, 1).await, (false, true));
assert_eq!(db_flags(&h, 2).await, [true, true]);
assert_eq!(slot8(&h, ao(2)).await, PropertyValue::Real(30.0));
assert_eq!(slot8(&h, ao(1)).await, PropertyValue::Real(70.0));
h.server.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn write_local_dropped_after_its_commit_still_runs_the_distribution() {
let h = Harness::start_with(ServerConfig::default(), with_channel).await;
let table = Arc::clone(&h.server.cov_table).write_owned().await;
let target = ch(5);
commit_and_drop(
&h,
write_local(&h, &target, PropertyValue::Real(12.0), Some(8)),
);
drop(table);
assert_eq!(channel_status(&h).await, WriteStatus::IN_PROGRESS);
tokio::time::sleep(Duration::from_millis(250)).await;
assert_eq!(channel_status(&h).await, WriteStatus::SUCCESSFUL);
assert_eq!(slot8(&h, ao(2)).await, PropertyValue::Real(12.0));
}
#[tokio::test(start_paused = true)]
async fn stop_aborts_the_work_a_committed_write_owes_and_ends_its_run() {
let mut h = Harness::start_with(ServerConfig::default(), objects).await;
let table = Arc::clone(&h.server.cov_table).write_owned().await;
let target = cmd(1);
commit_and_drop(
&h,
write_local(&h, &target, PropertyValue::Unsigned(2), None),
);
drop(table);
assert!(
!h.server.request_tasks.is_empty(),
"the work the write owes is a request task"
);
h.server.stop().await.unwrap();
assert_eq!(db_state(&h, 1).await, (false, false));
assert_eq!(db_flags(&h, 2).await, [false, false]);
assert_eq!(slot8(&h, ao(2)).await, PropertyValue::Null);
assert_eq!(
read_db(&h, cmd(1), PropertyIdentifier::PRESENT_VALUE, None).await,
PropertyValue::Unsigned(2)
);
}
struct PanicsAfterCommit;
impl bacnet_objects::traits::BACnetObject for PanicsAfterCommit {
fn object_identifier(&self) -> ObjectIdentifier {
ObjectIdentifier::new(ObjectType::ANALOG_VALUE, 99).unwrap()
}
fn object_name(&self) -> &str {
"AFTER-99"
}
fn read_property(
&self,
property: PropertyIdentifier,
_: Option<u32>,
) -> Result<PropertyValue, Error> {
Ok(match property {
PropertyIdentifier::OBJECT_IDENTIFIER => {
PropertyValue::ObjectIdentifier(self.object_identifier())
}
PropertyIdentifier::OBJECT_NAME => PropertyValue::CharacterString("AFTER-99".into()),
_ => PropertyValue::Real(0.0),
})
}
fn write_property(
&mut self,
_: PropertyIdentifier,
_: Option<u32>,
_: PropertyValue,
_: Option<u8>,
) -> Result<(), Error> {
Ok(())
}
fn property_list(&self) -> std::borrow::Cow<'static, [PropertyIdentifier]> {
std::borrow::Cow::Borrowed(&[])
}
fn evaluate_intrinsic_reporting(&mut self) -> Option<bacnet_objects::event::TransitionOutcome> {
panic!("AFTER-99 panics in the work after its write");
}
}
#[tokio::test(start_paused = true)]
async fn a_panic_in_the_work_after_the_commit_reaches_the_caller() {
use futures_util::FutureExt;
let h = Harness::start_with(ServerConfig::default(), |db| {
db.add(Box::new(PanicsAfterCommit)).unwrap();
})
.await;
let target = ObjectIdentifier::new(ObjectType::ANALOG_VALUE, 99).unwrap();
let write = write_local(&h, &target, PropertyValue::Real(1.0), None);
let panicked = std::panic::AssertUnwindSafe(write).catch_unwind().await;
assert!(
panicked.is_err(),
"the caller sees the panic, as it would were the work done in place"
);
}
#[tokio::test(start_paused = true)]
async fn a_write_whose_work_is_aborted_returns_ok_and_stays_made() {
let h = Harness::start(ServerConfig::default()).await;
let table = Arc::clone(&h.server.cov_table).write_owned().await;
let target = av1();
let mut write = write_local(&h, &target, PropertyValue::Real(70.0), None);
assert!(write
.as_mut()
.poll(&mut Context::from_waker(Waker::noop()))
.is_pending());
h.server.request_tasks.close();
drop(table);
assert!(matches!(write.await, Ok(())), "the write was made");
assert_eq!(
read_db(&h, av1(), PV, None).await,
PropertyValue::Real(70.0)
);
}
#[tokio::test(start_paused = true)]
async fn a_panic_after_the_caller_went_away_leaves_the_server_working() {
let h = Harness::start_with(ServerConfig::default(), |db| {
db.add(Box::new(PanicsAfterCommit)).unwrap();
})
.await;
let target = ObjectIdentifier::new(ObjectType::ANALOG_VALUE, 99).unwrap();
commit_and_drop(&h, write_local(&h, &target, PropertyValue::Real(1.0), None));
h.settle().await;
let next = av1();
write_local(&h, &next, PropertyValue::Real(42.0), None)
.await
.unwrap();
assert_eq!(
read_db(&h, av1(), PV, None).await,
PropertyValue::Real(42.0)
);
}
#[test]
fn a_local_write_outside_a_tokio_runtime_fails_and_writes_nothing() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let mut h = runtime.block_on(Harness::start(ServerConfig::default()));
let target = av1();
let before = runtime.block_on(read_db(&h, target, PV, None));
let mut write = write_local(&h, &target, PropertyValue::Real(70.0), None);
let polled = write.as_mut().poll(&mut Context::from_waker(Waker::noop()));
assert!(
matches!(polled, Poll::Ready(Err(Error::Encoding(_)))),
"{polled:?}"
);
drop(write);
assert_eq!(runtime.block_on(read_db(&h, target, PV, None)), before);
runtime.block_on(h.server.stop()).unwrap();
}