use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use super::*;
use crate::traits::BACnetObject;
use bacnet_types::enums::ObjectType;
use bacnet_types::enums::PropertyIdentifier;
use bacnet_types::primitives::ObjectIdentifier;
struct FileCase {
directory: PathBuf,
path: PathBuf,
}
impl FileCase {
fn new() -> Self {
static NEXT: AtomicU64 = AtomicU64::new(0);
let directory = std::env::temp_dir().join(format!(
"rusty-color-{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
std::fs::create_dir(&directory).unwrap();
let path = directory.join("output.bin");
Self { directory, path }
}
fn store(&self) -> Arc<FileColorOutputPersistence> {
Arc::new(FileColorOutputPersistence::new(&self.path).unwrap())
}
}
impl Drop for FileCase {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.directory).unwrap();
}
}
fn color_oid() -> ObjectIdentifier {
ObjectIdentifier::new(ObjectType::COLOR, 1).unwrap()
}
fn ct_oid() -> ObjectIdentifier {
ObjectIdentifier::new(ObjectType::COLOR_TEMPERATURE, 1).unwrap()
}
#[test]
fn color_round_trip_through_a_file() {
let case = FileCase::new();
let blue = BACnetXyColor::new(0.15, 0.06);
{
let mut color = ColorObject::new(1, "c")
.unwrap()
.with_output_persistence(case.store())
.unwrap();
assert!(!color.output_restored());
color.set_present_value(blue).unwrap();
}
let color = ColorObject::new(1, "c")
.unwrap()
.with_output_persistence(case.store())
.unwrap();
assert!(color.output_restored());
assert_eq!(
color
.read_property(PropertyIdentifier::PRESENT_VALUE, None)
.unwrap(),
xy_value(blue)
);
}
#[test]
fn temperature_round_trip_through_a_file() {
let case = FileCase::new();
{
let mut ct = ColorTemperatureObject::new(1, "ct")
.unwrap()
.with_output_persistence(case.store())
.unwrap();
ct.write_property(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Unsigned(2700),
None,
)
.unwrap();
ct.wait_for_output_saves();
}
assert_eq!(
case.store().load(ct_oid()).unwrap(),
Some(ColorOutputSnapshot {
present_value: ColorOutputValue::Kelvin(2700)
})
);
let ct = ColorTemperatureObject::new(1, "ct")
.unwrap()
.with_output_persistence(case.store())
.unwrap();
assert_eq!(
ct.read_property(PropertyIdentifier::PRESENT_VALUE, None)
.unwrap(),
PropertyValue::Unsigned(2700)
);
}
fn write_body(case: &FileCase, oid: ObjectIdentifier, body: &[u8]) {
let mut bytes = b"RBNCLR01".to_vec();
bytes.extend_from_slice(&oid.encode());
bytes.extend_from_slice(body);
std::fs::write(&case.path, bytes).unwrap();
}
#[test]
fn corrupt_files_do_not_load() {
let half = 0.5f32.to_be_bytes();
let two = 2.0f32.to_be_bytes();
let nan = f32::NAN.to_be_bytes();
let cases: Vec<(&str, Vec<u8>)> = vec![
("empty body", vec![]),
("unknown kind", vec![9, 0, 0, 0, 0]),
("short xy", [&[1u8][..], &half].concat()),
("long xy", [&[1u8][..], &half, &half, &[0]].concat()),
("x above 1.0", [&[1u8][..], &two, &half].concat()),
("NaN y", [&[1u8][..], &half, &nan].concat()),
("short kelvin", vec![2, 0, 0, 0x0F]),
(
"kelvin below 1000",
[&[2u8][..], &999u32.to_be_bytes()].concat(),
),
(
"kelvin above 30000",
[&[2u8][..], &30_001u32.to_be_bytes()].concat(),
),
];
for (what, body) in cases {
let case = FileCase::new();
write_body(&case, color_oid(), &body);
assert!(case.store().load(color_oid()).is_err(), "{what} loaded");
assert!(
ColorObject::new(1, "c")
.unwrap()
.with_output_persistence(case.store())
.is_err(),
"{what} built"
);
}
let case = FileCase::new();
std::fs::write(&case.path, b"RBNNCL01\x01\x00\x00\x00\x01").unwrap();
assert!(case.store().load(color_oid()).is_err());
write_body(&case, ct_oid(), &[2, 0, 0, 0x0F, 0xA0]);
assert!(case.store().load(color_oid()).is_err());
assert!(case.store().load(ct_oid()).unwrap().is_some());
std::fs::write(&case.path, b"junk").unwrap();
assert!(case.store().load(color_oid()).is_err());
std::fs::write(&case.path, vec![0u8; 65]).unwrap();
assert!(case.store().load(color_oid()).is_err());
}
#[test]
fn the_wrong_kind_of_value_does_not_build() {
let kelvin = Memory::holding(ColorOutputValue::Kelvin(3000));
assert!(
ColorObject::new(1, "c")
.unwrap()
.with_output_persistence(kelvin)
.is_err()
);
let xy = Memory::holding(ColorOutputValue::Xy(BACnetXyColor::new(0.3, 0.3)));
assert!(
ColorTemperatureObject::new(1, "ct")
.unwrap()
.with_output_persistence(xy)
.is_err()
);
assert!(FileColorOutputPersistence::new("").is_err());
}
#[derive(Default)]
struct Memory {
held: Mutex<Option<ColorOutputSnapshot>>,
refusals: Mutex<usize>,
saves: AtomicUsize,
}
impl Memory {
fn holding(value: ColorOutputValue) -> Arc<Self> {
let memory = Self::default();
*memory.held.lock().unwrap() = Some(ColorOutputSnapshot {
present_value: value,
});
Arc::new(memory)
}
fn held(&self) -> Option<ColorOutputValue> {
self.held.lock().unwrap().map(|saved| saved.present_value)
}
}
impl ColorOutputPersistence for Memory {
fn load(&self, _object: ObjectIdentifier) -> Result<Option<ColorOutputSnapshot>, Error> {
Ok(*self.held.lock().unwrap())
}
fn save(&self, _object: ObjectIdentifier, snapshot: &ColorOutputSnapshot) -> Result<(), Error> {
self.saves.fetch_add(1, Ordering::SeqCst);
{
let mut refusals = self.refusals.lock().unwrap();
if *refusals > 0 {
*refusals -= 1;
return Err(Error::Encoding("refused".into()));
}
}
*self.held.lock().unwrap() = Some(*snapshot);
Ok(())
}
}
#[test]
fn a_failed_save_is_tried_again_at_settle() {
let storage = Arc::new(Memory::default());
*storage.refusals.lock().unwrap() = 1;
let mut ct = ColorTemperatureObject::new(1, "ct")
.unwrap()
.with_output_persistence(storage.clone())
.unwrap();
let counters = ct.output_save_counters().expect("storage counts its saves");
ct.set_present_value(5000).unwrap();
ct.wait_for_output_saves();
assert_eq!(storage.held(), None);
assert_eq!(counters.failed_saves(), 1);
ct.durable_writes_internal()
.expect("the object keeps its output")
.settle_forgotten_writes()
.expect("a wait for the saves")
.block();
assert_eq!(storage.held(), Some(ColorOutputValue::Kelvin(5000)));
assert_eq!(storage.saves.load(Ordering::SeqCst), 2);
assert_eq!(counters.failed_saves(), 1);
ct.durable_writes_internal()
.unwrap()
.settle_forgotten_writes()
.unwrap()
.block();
ct.set_present_value(5000).unwrap();
ct.wait_for_output_saves();
assert_eq!(storage.saves.load(Ordering::SeqCst), 2);
assert_eq!(counters.failed_saves(), 1);
assert!(
ColorObject::new(1, "c")
.unwrap()
.output_save_counters()
.is_none()
);
}
#[test]
fn a_failed_save_is_tried_again_as_the_object_drops() {
let storage = Arc::new(Memory::default());
*storage.refusals.lock().unwrap() = 1;
let mut color = ColorObject::new(1, "c")
.unwrap()
.with_output_persistence(storage.clone())
.unwrap();
let blue = BACnetXyColor::new(0.15, 0.06);
color.set_present_value(blue).unwrap();
color.wait_for_output_saves();
assert_eq!(storage.held(), None);
drop(color);
assert_eq!(storage.held(), Some(ColorOutputValue::Xy(blue)));
}
#[test]
fn clones_keep_no_storage() {
let storage = Arc::new(Memory::default());
let color = ColorObject::new(1, "c")
.unwrap()
.with_output_persistence(storage.clone())
.unwrap();
let mut copy = color.clone();
assert!(copy.durable_writes_internal().is_none());
copy.set_present_value(BACnetXyColor::new(0.2, 0.2))
.unwrap();
drop(copy);
color.wait_for_output_saves();
assert_eq!(storage.saves.load(Ordering::SeqCst), 0);
}
#[test]
fn settle_joins_the_tags_wait() {
let case = FileCase::new();
let tags = Arc::new(crate::object_profile::FileTagsPersistence::new(&case.path).unwrap());
let storage = Arc::new(Memory::default());
let mut color = ColorObject::with_tags_persistence(1, "c", tags)
.unwrap()
.with_output_persistence(storage.clone())
.unwrap();
color
.set_present_value(BACnetXyColor::new(0.2, 0.2))
.unwrap();
let wait = color
.durable_writes_internal()
.unwrap()
.settle_forgotten_writes()
.unwrap();
wait.block();
assert!(wait.is_ready());
assert_eq!(
storage.held(),
Some(ColorOutputValue::Xy(BACnetXyColor::new(0.2, 0.2)))
);
}