mod common;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, SystemTime};
use std::sync::{Arc, Mutex};
use std::thread;
use futures::executor::block_on;
use tango::dev::{All, AttrConfig, AttrMemorized, AttrMode, AttrNumber, AttrQuality, AttrRead, AttrReady, AttrValueEvent, DevProxy,
DevSource, DevState, Named, ToNames};
use tango::value::{AttrDataKind, FromValue, Scalar, Spectrum, Value};
use tango::server::{AttrDef, ClassDef, CommandDef, Dev, ServerBuilder};
use tango::error::{Error, ErrorKind, Result};
use common::{local_and_wire, test_context, test_server, is_local, commands, exported, unexported};
#[test]
fn boot() {
let ctx = test_context(&[
("dserver/Test/test", "DServer"),
("b/dev/1", "B"),
("a/dev/1", "A"),
("b/dev/2", "B"),
("c/dev/1", "C"),
]);
let server = ctx.serve(test_server(true)
.class(ClassDef::<()>::new_default("A"))
.class(ClassDef::<()>::new_default("B")))
.unwrap();
assert_eq!(exported(ctx.test_database()), ["dserver/Test/test", "a/dev/1", "b/dev/1", "b/dev/2"]);
assert_eq!(commands(ctx.test_database()), [
"DbGetDeviceAttributeProperty2", "DbGetDeviceClassList", "DbGetDeviceProperty",
"DbGetDeviceAttributeProperty2", "DbGetDeviceProperty",
"DbGetDeviceAttributeProperty2", "DbGetDeviceProperty",
"DbGetDeviceAttributeProperty2", "DbGetDeviceProperty",
"DbExportDevice", "DbExportDevice", "DbExportDevice", "DbExportDevice",
]);
assert!(!server.has_dev("c/dev/1"));
}
#[test]
fn boot_failed() {
let ctx = test_context(&[]);
ctx.test_database().add_fail("DbGetDeviceClassList", Error::new(ErrorKind::Internal));
assert!(ctx.serve(test_server(true)).is_err());
assert_eq!(unexported(ctx.test_database()), ["Test/test"]);
}
#[test]
fn dynamic_devs() {
let ctx = test_context(&[("a/dev/1", "A")]);
let server = ctx.serve(test_server(true)
.class(ClassDef::<()>::new_default("A")))
.unwrap();
ctx.database().add_dev("a/dev/2", "A", "Test/test").unwrap();
server.add_dev("A", "a/dev/2").unwrap();
assert!(server.remove_dev("a/dev/1"));
server.sync().unwrap();
assert_eq!(exported(ctx.test_database()), ["dserver/Test/test", "a/dev/1", "a/dev/2"]);
assert_eq!(unexported(ctx.test_database()), ["a/dev/1"]);
server.add_dev("A", "a/dev/3").unwrap();
assert!(server.remove_dev("a/dev/3"));
server.sync().unwrap();
assert_eq!(exported(ctx.test_database()).len(), 3);
assert_eq!(unexported(ctx.test_database()), ["a/dev/1"]);
assert!(server.remove_dev("a/dev/2"));
server.add_dev("A", "a/dev/2").unwrap();
server.sync().unwrap();
assert_eq!(unexported(ctx.test_database()), ["a/dev/1", "a/dev/2"]);
assert_eq!(exported(ctx.test_database()).last().unwrap(), "a/dev/2");
}
#[test]
fn shutdown() {
let deleted = Arc::new(Mutex::new(Vec::new()));
let deleting = |name| {
let deleted = Arc::clone(&deleted);
ClassDef::<()>::new_default(name)
.delete(move |(), dev| deleted.lock().unwrap().push(dev.name().to_string()))
};
let ctx = test_context(&[("a/dev/1", "A"), ("b/dev/1", "B")]);
let server = ctx.serve(test_server(true)
.class(deleting("A"))
.class(deleting("B")))
.unwrap();
ctx.stop("Test/test").unwrap();
assert_eq!(unexported(ctx.test_database()), ["Test/test"]);
assert!(!is_local(&ctx, "dserver/Test/test"));
assert_eq!(*deleted.lock().unwrap(), ["b/dev/1", "a/dev/1"]);
server.add_dev("A", "a/dev/2").unwrap();
server.sync().unwrap();
let database = Arc::clone(ctx.test_database());
drop(server);
drop(ctx);
assert_eq!(unexported(&database), ["Test/test"]);
assert_eq!(exported(&database).len(), 3);
}
#[test]
fn wait_timeout() {
let deleted = Arc::new(Mutex::new(Vec::new()));
let deleting = {
let deleted = Arc::clone(&deleted);
ClassDef::<()>::new_default("A")
.delete(move |(), dev| deleted.lock().unwrap().push(dev.name().to_string()))
};
let ctx = test_context(&[("a/dev/1", "A")]);
let handle = test_server(true)
.class(deleting)
.database(ctx.database().clone())
.host("127.0.0.1")
.serve().unwrap();
let err = handle.wait_timeout(Duration::from_millis(50)).unwrap_err();
assert_eq!(err.kind(), ErrorKind::TimedOut);
assert!(deleted.lock().unwrap().is_empty());
handle.server().stop().unwrap();
handle.wait_timeout(Duration::from_secs(5)).unwrap();
assert_eq!(*deleted.lock().unwrap(), ["a/dev/1"]);
}
local_and_wire! {
fn kill(local: bool) {
let ctx = test_context(&[]);
ctx.serve(test_server(local)).unwrap();
let admin = ctx.backend_dev("dserver/Test/test").unwrap();
block_on(admin.command(Scalar::from("Kill"), Value::Void, DevSource::Dev)).unwrap();
ctx.stop("Test/test").unwrap();
assert_eq!(unexported(ctx.test_database()), ["Test/test"]);
}
}
local_and_wire! {
fn delete_async(local: bool) {
struct Recording(Arc<Mutex<Vec<String>>>);
async fn record(model: Recording, dev: &Dev<Recording>) {
model.0.lock().unwrap().push(dev.name().to_string());
}
let deleted = Arc::new(Mutex::new(Vec::new()));
let def = {
let deleted = Arc::clone(&deleted);
ClassDef::new("A", move |_| Ok(Recording(Arc::clone(&deleted))))
.delete_async(record)
};
let ctx = test_context(&[("a/dev/1", "A")]);
let server = ctx.serve(test_server(local)
.class(def))
.unwrap();
assert!(deleted.lock().unwrap().is_empty());
let served = ctx.backend_dev("a/dev/1").unwrap();
block_on(served.command(Scalar::from("Init"), Value::Void, DevSource::Dev)).unwrap();
assert_eq!(*deleted.lock().unwrap(), ["a/dev/1"]);
assert!(server.remove_dev("a/dev/1"));
server.sync().unwrap();
assert_eq!(*deleted.lock().unwrap(), ["a/dev/1", "a/dev/1"]);
assert!(block_on(served.ping()).is_err());
}
}
#[test]
fn drop_no_side_effect() {
let deleted = Arc::new(Mutex::new(Vec::new()));
let deleting = {
let deleted = Arc::clone(&deleted);
ClassDef::<()>::new_default("A")
.delete(move |(), dev| deleted.lock().unwrap().push(dev.name().to_string()))
};
let ctx = test_context(&[("a/dev/1", "A")]);
let server = ctx.serve(test_server(true)
.class(deleting))
.unwrap();
let backend = server.backend().unwrap();
let at = format!("tango://{}:{}/a/dev/1#dbase=no", backend.host(), backend.port());
drop((server, backend));
let database = Arc::clone(ctx.test_database());
drop(ctx);
assert!(unexported(&database).is_empty());
assert!(deleted.lock().unwrap().is_empty());
assert!(DevProxy::new(&*at).ping().is_err());
}
#[test]
fn props() {
let seen = Arc::new(Mutex::new(None));
let def = {
let seen = Arc::clone(&seen);
ClassDef::<()>::new_async_with("Motor", move |dev| {
let seen = Arc::clone(&seen);
Box::pin(async move {
let speed = dev.get_prop_async::<f64, _>("speed").await?;
let unit = dev.class().get_prop_async::<String, _>("unit").await?;
*seen.lock().unwrap() = Some((speed, unit));
dev.set_prop_async(("started", true)).await?;
dev.delete_prop_async("started").await?;
Ok(())
})
})
};
let ctx = test_context(&[]);
let server = ctx.serve(test_server(true)).unwrap();
server.database().set_dev_prop("motor/dev/1", ("speed", 2.5)).unwrap();
server.database().set_class_prop("Motor", ("unit", "mm")).unwrap();
let class = server.add_class(def).unwrap();
ctx.database().add_dev("motor/dev/1", "Motor", "Test/test").unwrap();
let dev = class.add_dev("motor/dev/1").unwrap();
server.sync().unwrap();
assert_eq!(*seen.lock().unwrap(), Some((2.5, "mm".to_string())));
dev.set_prop(("accel", 1)).unwrap();
assert_eq!(dev.list_prop("*").unwrap().iter().collect::<Vec<_>>(), ["speed", "accel"]);
dev.delete_prop("speed").unwrap();
assert_eq!(dev.list_prop("*").unwrap().iter().collect::<Vec<_>>(), ["accel"]);
}
#[test]
fn attr_props() {
let ctx = test_context(&[]);
let server = ctx.serve(test_server(true)).unwrap();
let database = server.database();
database.set_class_attr_prop("Motor", ("Position", ("unit", "mm"))).unwrap();
database.set_dev_attr_prop("motor/dev/1", [
("Position", vec![("label", "Pos"), ("__value", "2.5")]),
("Speed", vec![("rel_change", "1")]),
]).unwrap();
let unit = database.get_class_attr_prop::<String, _, _>("Motor", "Position", "unit").unwrap();
assert_eq!(unit, "mm");
let units = database.get_class_attr_prop::<String, _, _>("Motor", ["Position", "Speed"], "unit").unwrap();
assert_eq!(units.map(Result::unwrap), ["mm", ""]);
let attrs = database.get_dev_attr_prop::<Spectrum<str>, _, _>("motor/dev/1", All, All).unwrap();
let names = attrs.iter().map(|attr| attr.name.value()).collect::<Vec<_>>();
assert_eq!(names, ["Position", "Speed"]);
let position = attrs[0].value.as_ref().unwrap();
let props = position.iter().map(|prop| prop.name.value()).collect::<Vec<_>>();
assert_eq!(props, ["label", "__value"]);
let (label, value) = database.get_dev_attr_prop::<(String, f64), _, _>("motor/dev/1", "Position", ("label", "__value")).unwrap();
assert_eq!((label.unwrap(), value.unwrap()), ("Pos".to_string(), 2.5));
database.delete_dev_attr_prop("motor/dev/1", "Position", "__value").unwrap();
let value = database.get_dev_attr_prop::<Spectrum<str>, _, _>("motor/dev/1", "Position", "__value").unwrap();
assert!(value.is_empty());
let err = database.get_dev_attr_prop::<f64, _, _>("motor/dev/1", "Position", "__value").unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidProp);
}
local_and_wire! {
fn attr_config(local: bool) {
fn position(_: &(), _: &Dev<()>) -> Result<i32> { Ok(0) }
fn speed(_: &(), _: &Dev<()>) -> Result<f64> { Ok(1.0) }
let def = ClassDef::<()>::new_async_with("Motor", |dev| Box::pin(async move {
dev.add_attr(AttrDef::new("Speed").read(speed))
}))
.attr(AttrDef::new("Position").read(position).label("Position").unit("um").range(-10..=10))
.attr(AttrDef::new("Broken").read(speed));
let ctx = test_context(&[]);
let server = ctx.serve(test_server(local)).unwrap();
server.database().set_class_attr_prop("Motor", [
("Position", vec![("unit", "mm"), ("min_value", "-5")]),
("Speed", vec![("rel_change", "-1,2"), ("event_period", "500")]),
]).unwrap();
server.database().set_dev_attr_prop("motor/dev/1", [
("Position", vec![("label", "Pos"), ("max_value", "Not specified")]),
("Speed", vec![("delta_t", "250"), ("delta_val", "0.5")]),
("Broken", vec![("max_alarm", "high")]),
]).unwrap();
let class = server.add_class(def).unwrap();
ctx.database().add_dev("motor/dev/1", "Motor", "Test/test").unwrap();
let dev = class.add_dev("motor/dev/1").unwrap();
server.sync().unwrap();
let position = dev.get_attr("Position").unwrap().config();
assert_eq!(position.label.as_deref(), Some("Pos"));
assert_eq!(position.unit.as_deref(), Some("mm"));
assert_eq!((position.range.min, position.range.max), (Some(AttrNumber::Long(-5)), None));
let speed = dev.get_attr("Speed").unwrap().config();
assert_eq!(speed.change_event.rel_change, Some(1.0));
assert_eq!(speed.periodic_event.period, Some(Duration::from_millis(500)));
let delta = speed.delta.unwrap();
assert_eq!((delta.value, delta.time), (AttrNumber::Double(0.5), Some(Duration::from_millis(250))));
let asked = |command| commands(ctx.test_database()).iter().filter(|asked| *asked == command).count();
assert_eq!(asked("DbGetClassAttributeProperty2"), 2);
assert_eq!(asked("DbGetDeviceAttributeProperty2"), 3);
let served = ctx.backend_dev("motor/dev/1").unwrap();
let reads = block_on(served.read_attr(["Position", "Broken"].to_names(), DevSource::Dev)).unwrap();
assert!(reads[0].value.is_ok());
let err = reads[1].value.as_ref().unwrap_err();
assert!(err.parent().unwrap().description().contains("max_alarm"), "{err}");
let infos = block_on(served.attr_info("Broken".to_names())).unwrap();
assert!(infos[0].value.is_ok());
ctx.database().add_dev("motor/dev/2", "Motor", "Test/test").unwrap();
class.add_dev("motor/dev/2").unwrap();
server.sync().unwrap();
assert_eq!(asked("DbGetClassAttributeProperty2"), 2);
assert_eq!(asked("DbGetDeviceAttributeProperty2"), 5);
}
}
local_and_wire! {
fn attr_config_change(local: bool) {
fn position(_: &(), _: &Dev<()>) -> Result<i32> { Ok(0) }
let def = ClassDef::<()>::new_default("Motor")
.attr(AttrDef::new("Position").read(position).label("Position"));
let ctx = test_context(&[]);
let server = ctx.serve(test_server(local)).unwrap();
server.database().set_class_attr_prop("Motor", ("Position", ("unit", "mm"))).unwrap();
let class = server.add_class(def).unwrap();
ctx.database().add_dev("motor/dev/1", "Motor", "Test/test").unwrap();
let dev = class.add_dev("motor/dev/1").unwrap();
server.sync().unwrap();
let served = ctx.backend_dev("motor/dev/1").unwrap();
let change = |name: &str, change: &dyn Fn(&mut AttrConfig)| {
let mut info = block_on(served.attr_info(name.to_names())).unwrap().pop().unwrap().value.unwrap();
change(&mut info.config);
block_on(served.set_attr_config(vec![Named::new(Scalar::from(name), info.config)])).unwrap().pop().unwrap()
};
let stored_of = |attr: &str, name: &str| server.database().get_dev_attr_prop::<String, _, _>("motor/dev/1", attr, name).unwrap();
let stored = |name: &str| stored_of("Position", name);
change("Position", &|config| {
config.unit = Some("cm".into());
config.range.min = Some(AttrNumber::Long(-5));
}).unwrap();
let config = dev.get_attr("Position").unwrap().config();
assert_eq!((config.unit.as_deref(), config.range.min), (Some("cm"), Some(AttrNumber::Long(-5))));
assert_eq!((stored("unit"), stored("label"), stored("min_value")), ("cm".to_string(), String::new(), "-5".to_string()));
change("Position", &|config| config.unit = None).unwrap();
assert_eq!(dev.get_attr("Position").unwrap().config().unit.as_deref(), Some("mm"));
assert_eq!(stored("unit"), "");
change("State", &|config| config.label = Some("S".into())).unwrap();
assert_eq!(stored_of("State", "label"), "S");
let info = block_on(served.attr_info("State".to_names())).unwrap().pop().unwrap().value.unwrap();
assert_eq!(info.config.label.as_deref(), Some("S"));
assert!(change("State", &|config| config.alarm.max = Some(AttrNumber::Long(1))).is_err());
}
}
local_and_wire! {
fn attr_without_handlers(local: bool) {
let def = ClassDef::<()>::new_default("Motor")
.attr(AttrDef::new("Position").kind(AttrDataKind::Long).scalar());
let ctx = test_context(&[]);
let server = ctx.serve(test_server(local)).unwrap();
ctx.database().add_dev("motor/dev/1", "Motor", "Test/test").unwrap();
server.add_class(def).unwrap().add_dev("motor/dev/1").unwrap();
server.sync().unwrap();
let served = ctx.backend_dev("motor/dev/1").unwrap();
let info = block_on(served.attr_info("Position".to_names())).unwrap().pop().unwrap().value.unwrap();
assert_eq!(info.mode, AttrMode::ReadWrite);
let err = block_on(served.read_attr("Position".to_names(), DevSource::Dev)).unwrap().pop().unwrap().value.unwrap_err();
assert!(err.description().contains("no handler defined"), "{err}");
let err = block_on(served.write_attr(vec![Named::new(Scalar::from("Position"), Value::from(1))])).unwrap().pop().unwrap().unwrap_err();
assert!(err.description().contains("no handler defined"), "{err}");
}
}
local_and_wire! {
fn command_invalid_data(local: bool) {
fn move_to(_model: &(), _dev: &Dev<()>, target: f64) -> Result<f64> {
Err(Error::new(ErrorKind::InvalidData)
.with_description(format!("Cannot go to {target}")))
}
let def = ClassDef::<()>::new_default("Motor")
.command(CommandDef::new("MoveTo").exec(move_to));
let ctx = test_context(&[]);
let server = ctx.serve(test_server(local)).unwrap();
ctx.database().add_dev("motor/dev/1", "Motor", "Test/test").unwrap();
server.add_class(def).unwrap().add_dev("motor/dev/1").unwrap();
server.sync().unwrap();
let served = ctx.backend_dev("motor/dev/1").unwrap();
let command = |arg: Value| block_on(served.command(Scalar::from("MoveTo"), arg, DevSource::Dev)).unwrap_err();
let err = command(Value::from(1000.0));
assert_eq!(err.reason(), "API_InternalError", "{err}");
assert!(err.description().contains("Cannot go to 1000"), "{err}");
let err = command(Value::from("north"));
assert_eq!(err.reason(), "API_InternalError", "{err}");
block_on(served.ping()).unwrap();
}
}
local_and_wire! {
fn state_status_config(local: bool) {
let def = ClassDef::<()>::new_default("Motor")
.status_config(AttrConfig { label: Some("Motor status".into()), ..AttrConfig::default() });
let ctx = test_context(&[]);
let server = ctx.serve(test_server(local)).unwrap();
server.database().set_class_attr_prop("Motor", ("State", ("label", "Motor state"))).unwrap();
server.database().set_dev_attr_prop("motor/dev/1", ("State", ("unit", "none"))).unwrap();
let class = server.add_class(def).unwrap();
ctx.database().add_dev("motor/dev/1", "Motor", "Test/test").unwrap();
class.add_dev("motor/dev/1").unwrap();
server.sync().unwrap();
let served = ctx.backend_dev("motor/dev/1").unwrap();
let configs = || block_on(served.attr_info(["State", "Status"].to_names())).unwrap()
.into_iter()
.map(|info| info.value.unwrap().config)
.collect::<Vec<_>>();
let check = || {
let configs = configs();
assert_eq!((configs[0].label.as_deref(), configs[0].unit.as_deref()), (Some("State"), Some("none")));
assert_eq!((configs[1].label.as_deref(), configs[1].doc.as_deref()), (Some("Motor status"), None));
};
check();
server.database().set_dev_attr_prop("motor/dev/1", ("State", ("unit", "-"))).unwrap();
block_on(served.command(Scalar::from("Init"), Value::Void, DevSource::Dev)).unwrap();
assert_eq!(configs()[0].unit.as_deref(), Some("-"));
assert!(!commands(ctx.test_database()).iter().any(|command| command == "DbGetClassAttributeProperty2"));
}
}
local_and_wire! {
fn attr_limits(local: bool) {
#[derive(Default)]
struct Motor {
actual: i32,
}
impl Motor {
fn actual(&self, _dev: &Dev<Self>) -> Result<i32> { Ok(self.actual) }
fn set_actual(&mut self, _dev: &Dev<Self>, actual: i32) -> Result<()> { self.actual = actual; Ok(()) }
fn set_target(&mut self, _dev: &Dev<Self>, _target: i32) -> Result<()> { Ok(()) }
}
let def = ClassDef::<Motor>::new_default("Motor")
.attr(AttrDef::new("Actual").read(Motor::actual).write_exclusive(Motor::set_actual))
.attr(AttrDef::new("Position").read(Motor::actual).write_exclusive(Motor::set_target)
.range(-10..=10).alarm(-8..=8).warning(-5..=5))
.attr(AttrDef::new("Target").read(Motor::actual).write_exclusive(Motor::set_target)
.delta(1, None))
.attr(AttrDef::new("Slow").read(Motor::actual).write_exclusive(Motor::set_target)
.delta(1, Duration::from_secs(3600)));
let ctx = test_context(&[]);
let server = ctx.serve(test_server(local)).unwrap();
let class = server.add_class(def).unwrap();
ctx.database().add_dev("motor/dev/1", "Motor", "Test/test").unwrap();
class.add_dev("motor/dev/1").unwrap();
server.sync().unwrap();
let served = ctx.backend_dev("motor/dev/1").unwrap();
let write = |name: &str, value: i32| block_on(served.write_attr(vec![Named::new(Scalar::from(name), Value::from(value))]))
.unwrap().pop().unwrap();
let quality = |name: &str| block_on(served.read_attr(name.to_names(), DevSource::Dev))
.unwrap().pop().unwrap().value.unwrap().quality;
let err = write("Position", 11).unwrap_err();
assert_eq!(err.reason(), "API_WAttrOutsideLimit");
write("Position", 10).unwrap();
for (actual, expected) in [(5, AttrQuality::Valid), (6, AttrQuality::Warning), (-8, AttrQuality::Warning), (9, AttrQuality::Alarm)] {
write("Actual", actual).unwrap();
assert_eq!(quality("Position"), expected, "{actual}");
}
write("Actual", 0).unwrap();
assert_eq!(quality("Target"), AttrQuality::Valid);
write("Target", 5).unwrap();
assert_eq!(quality("Target"), AttrQuality::Alarm);
write("Target", 1).unwrap();
assert_eq!(quality("Target"), AttrQuality::Valid);
write("Slow", 5).unwrap();
assert_eq!(quality("Slow"), AttrQuality::Valid);
}
}
local_and_wire! {
fn auto_alarm_state(local: bool) {
#[derive(Default)]
struct Motor {
actual: i32,
moving: bool,
}
impl Motor {
fn actual(&self, _dev: &Dev<Self>) -> Result<i32> { Ok(self.actual) }
fn set_actual(&mut self, _dev: &Dev<Self>, actual: i32) -> Result<()> { self.actual = actual; Ok(()) }
fn moving(&self, _dev: &Dev<Self>) -> Result<bool> { Ok(self.moving) }
fn set_moving(&mut self, _dev: &Dev<Self>, moving: bool) -> Result<()> { self.moving = moving; Ok(()) }
fn state(&self, _dev: &Dev<Self>) -> Result<DevState> { Ok(if self.moving { DevState::Moving } else { DevState::On }) }
}
let def = ClassDef::<Motor>::new_default("Motor")
.state(Motor::state)
.attr(AttrDef::new("Moving").read(Motor::moving).write_exclusive(Motor::set_moving))
.attr(AttrDef::new("Position").read(Motor::actual).write_exclusive(Motor::set_actual)
.alarm(-8..=8).warning(-5..=5));
let ctx = test_context(&[]);
let server = ctx.serve(test_server(local)).unwrap();
let class = server.add_class(def).unwrap();
ctx.database().add_dev("motor/dev/1", "Motor", "Test/test").unwrap();
let dev = class.add_dev("motor/dev/1").unwrap();
server.sync().unwrap();
let served = ctx.backend_dev("motor/dev/1").unwrap();
let write = |name: &str, value: Value| block_on(served.write_attr(vec![Named::new(Scalar::from(name), value)]))
.unwrap().pop().unwrap().unwrap();
let state = || block_on(served.state()).unwrap();
let status = || block_on(served.status()).unwrap().value().to_string();
assert_eq!(state(), DevState::On);
assert_eq!(status(), "Device is on");
write("Position", Value::from(6));
assert_eq!(state(), DevState::Alarm);
assert_eq!(status(), "Device is on\nWarning: the attribute Position is outside of its warning range");
write("Moving", Value::from(true));
assert_eq!(state(), DevState::Moving);
dev.set_auto_alarm_states(&[DevState::On, DevState::Moving]);
assert_eq!(state(), DevState::Alarm);
dev.set_auto_alarm_states(&[]);
assert_eq!(state(), DevState::Moving);
assert_eq!(status(), "Device is on");
}
}
local_and_wire! {
fn alarm_read_once(local: bool) {
#[derive(Default)]
struct Motor {
reads: AtomicUsize,
states: AtomicUsize,
}
impl Motor {
fn position(&self, _dev: &Dev<Self>) -> Result<i32> {
self.reads.fetch_add(1, Ordering::Relaxed);
Ok(9)
}
fn calls(&self, _dev: &Dev<Self>) -> Result<Vec<i32>> {
Ok(vec![self.reads.load(Ordering::Relaxed) as i32, self.states.load(Ordering::Relaxed) as i32])
}
fn state(&self, _dev: &Dev<Self>) -> Result<DevState> {
self.states.fetch_add(1, Ordering::Relaxed);
Ok(DevState::On)
}
}
let def = ClassDef::<Motor>::new_default("Motor")
.state(Motor::state)
.attr(AttrDef::new("Position").read(Motor::position).alarm(-8..=8))
.attr(AttrDef::new("Calls").read(Motor::calls));
let ctx = test_context(&[]);
let server = ctx.serve(test_server(local)).unwrap();
ctx.database().add_dev("motor/dev/1", "Motor", "Test/test").unwrap();
server.add_class(def).unwrap().add_dev("motor/dev/1").unwrap();
server.sync().unwrap();
let served = ctx.backend_dev("motor/dev/1").unwrap();
let read = |names: &[&str]| block_on(served.read_attr(names.to_names(), DevSource::Dev)).unwrap();
let calls = || Vec::<i32>::from_value(read(&["Calls"]).pop().unwrap().value.unwrap().value.unwrap()).unwrap();
let answered = read(&["State", "Position", "Status"]);
let names = answered.iter().map(|read| read.name.value()).collect::<Vec<_>>();
assert_eq!(names, ["State", "Position", "Status"]);
assert_eq!(answered[0].value.as_ref().unwrap().value, Some(Value::from(DevState::Alarm)));
assert_eq!(answered[1].value.as_ref().unwrap().quality, AttrQuality::Alarm);
assert_eq!(calls(), [1, 1]);
read(&["State", "Status"]);
assert_eq!(calls(), [2, 2]);
}
}
local_and_wire! {
fn device_quality_alarm(local: bool) {
fn reading(quality: AttrQuality) -> Result<AttrRead<i32>> {
Ok(AttrRead { value: Some(0), w_value: None, quality, time: SystemTime::now() })
}
#[derive(Default)]
struct Motor {
quality: Option<AttrQuality>,
}
impl Motor {
fn alarmed(&self, _dev: &Dev<Self>) -> Result<AttrRead<i32>> { reading(self.quality.unwrap_or(AttrQuality::Valid)) }
fn plain(&self, _dev: &Dev<Self>) -> Result<AttrRead<i32>> { reading(AttrQuality::Alarm) }
fn set_quality(&mut self, _dev: &Dev<Self>, warning: bool) -> Result<()> {
self.quality = Some(if warning { AttrQuality::Warning } else { AttrQuality::Valid });
Ok(())
}
}
let def = ClassDef::<Motor>::new_default("Motor")
.attr(AttrDef::new("Alarmed").read(Motor::alarmed).alarm(-100..=100))
.attr(AttrDef::new("Plain").read(Motor::plain))
.attr(AttrDef::new("Warning").read(|_: &Motor, _: &Dev<Motor>| Ok(false)).write_exclusive(Motor::set_quality));
let ctx = test_context(&[]);
let server = ctx.serve(test_server(local)).unwrap();
ctx.database().add_dev("motor/dev/1", "Motor", "Test/test").unwrap();
server.add_class(def).unwrap().add_dev("motor/dev/1").unwrap();
server.sync().unwrap();
let served = ctx.backend_dev("motor/dev/1").unwrap();
assert_eq!(block_on(served.state()).unwrap(), DevState::On);
block_on(served.write_attr(vec![Named::new(Scalar::from("Warning"), Value::from(true))])).unwrap();
assert_eq!(block_on(served.state()).unwrap(), DevState::Alarm);
assert_eq!(block_on(served.status()).unwrap().value(), "Device is on\nWarning: the attribute Alarmed is in WARNING");
}
}
local_and_wire! {
fn attr_config_coherence(local: bool) {
#[derive(Default)]
struct Motor {
target: i32,
}
impl Motor {
fn target(&self, _dev: &Dev<Self>) -> Result<i32> { Ok(self.target) }
fn set_target(&mut self, _dev: &Dev<Self>, target: i32) -> Result<()> { self.target = target; Ok(()) }
}
let def = ClassDef::<Motor>::new_default("Motor")
.attr(AttrDef::new("Position").read(Motor::target).write_exclusive(Motor::set_target).range(-10..=10))
.attr(AttrDef::new("Target").read(Motor::target).write_exclusive(Motor::set_target)
.memorized(AttrMemorized::RestoreSetPoint))
.attr(AttrDef::new("Broken").read(Motor::target));
let ctx = test_context(&[]);
let server = ctx.serve(test_server(local)).unwrap();
server.database().set_dev_attr_prop("motor/dev/1", ("Broken", [("min_value", "5"), ("max_value", "3")])).unwrap();
ctx.database().add_dev("motor/dev/1", "Motor", "Test/test").unwrap();
server.add_class(def).unwrap().add_dev("motor/dev/1").unwrap();
server.sync().unwrap();
let served = ctx.backend_dev("motor/dev/1").unwrap();
let change = |name: &str, change: &dyn Fn(&mut AttrConfig)| {
let mut info = block_on(served.attr_info(name.to_names())).unwrap().pop().unwrap().value.unwrap();
change(&mut info.config);
block_on(served.set_attr_config(vec![Named::new(Scalar::from(name), info.config)])).unwrap().pop().unwrap()
};
let stored = |attr: &str, name: &str| server.database().get_dev_attr_prop::<String, _, _>("motor/dev/1", attr, name).unwrap();
let read = block_on(served.read_attr("Broken".to_names(), DevSource::Dev)).unwrap().pop().unwrap();
let err = read.value.unwrap_err();
assert!(err.parent().unwrap().description().contains("is not below its maximum"), "{err}");
assert!(change("Broken", &|config| config.label = Some("Broken".into())).is_err());
change("Broken", &|config| {
config.label = Some("Broken".into());
config.range.max = Some(AttrNumber::Long(10));
}).unwrap();
assert_eq!((stored("Broken", "label"), stored("Broken", "max_value")), ("Broken".to_string(), "10".to_string()));
let read = block_on(served.read_attr("Broken".to_names(), DevSource::Dev)).unwrap().pop().unwrap();
assert!(read.value.is_ok());
let err = change("Position", &|config| config.range.min = Some(AttrNumber::Long(20))).unwrap_err();
assert_eq!(err.reason(), "API_IncoherentValues");
assert_eq!(stored("Position", "min_value"), "");
block_on(served.write_attr(vec![Named::new(Scalar::from("Target"), Value::from(12))])).unwrap().pop().unwrap().unwrap();
assert!(change("Target", &|config| config.range.max = Some(AttrNumber::Long(10))).is_err());
change("Target", &|config| config.range.max = Some(AttrNumber::Long(20))).unwrap();
assert_eq!(stored("Target", "max_value"), "20");
}
}
local_and_wire! {
fn memorized(local: bool) {
#[derive(Default)]
struct Motor {
target: i32,
speed_writes: usize,
}
impl Motor {
fn target(&self, _dev: &Dev<Self>) -> Result<i32> { Ok(self.target) }
fn set_target(&mut self, _dev: &Dev<Self>, target: i32) -> Result<()> { self.target = target; Ok(()) }
fn speed(&self, _dev: &Dev<Self>) -> Result<f64> { Ok(self.speed_writes as f64) }
fn set_speed(&mut self, _dev: &Dev<Self>, _speed: f64) -> Result<()> { self.speed_writes += 1; Ok(()) }
}
let def = ClassDef::<Motor>::new_default("Motor")
.attr(AttrDef::new("Target").read(Motor::target).write_exclusive(Motor::set_target)
.memorized(AttrMemorized::RestoreAndWrite))
.attr(AttrDef::new("Speed").read(Motor::speed).write_exclusive(Motor::set_speed)
.memorized(AttrMemorized::RestoreSetPoint))
.attr(AttrDef::new("Broken").read(Motor::target).write_exclusive(Motor::set_target)
.memorized(AttrMemorized::RestoreAndWrite));
let ctx = test_context(&[]);
let server = ctx.serve(test_server(local)).unwrap();
server.database().set_dev_attr_prop("motor/dev/1", [
("Target", ("__value", "12")),
("Speed", ("__value", "2.5")),
("Broken", ("__value", "fast")),
]).unwrap();
let class = server.add_class(def).unwrap();
ctx.database().add_dev("motor/dev/1", "Motor", "Test/test").unwrap();
let dev = class.add_dev("motor/dev/1").unwrap();
server.sync().unwrap();
let served = ctx.backend_dev("motor/dev/1").unwrap();
let read = |name: &str| block_on(served.read_attr(name.to_names(), DevSource::Dev)).unwrap().pop().unwrap().value;
let mem_value = |name: &str| server.database().get_dev_attr_prop::<String, _, _>("motor/dev/1", name, "__value").unwrap();
let target = read("Target").unwrap();
assert_eq!((target.value, target.w_value), (Some(Value::from(12)), Some(Value::from(12))));
let speed = read("Speed").unwrap();
assert_eq!((speed.value, speed.w_value), (Some(Value::from(0.0)), Some(Value::from(2.5))));
let err = read("Broken").unwrap_err();
assert_eq!(err.reason(), "API_MemAttFailedDuringInit");
block_on(served.write_attr(vec![Named::new(Scalar::from("Broken"), Value::from(3))])).unwrap().pop().unwrap().unwrap();
assert!(read("Broken").is_ok());
assert_eq!(mem_value("Broken"), "3");
block_on(served.write_attr(vec![Named::new(Scalar::from("Target"), Value::from(-4))])).unwrap().pop().unwrap().unwrap();
assert_eq!(mem_value("Target"), "-4");
dev.set_attr_w_value("Speed", 0.5).unwrap();
assert_eq!(mem_value("Speed"), "0.5");
assert_eq!(read("Speed").unwrap().w_value, Some(Value::from(0.5)));
}
}
#[test]
fn dev_by_class_name() {
let ctx = test_context(&[]);
let server = ctx.serve(test_server(true)
.class(ClassDef::<()>::new_default("Motor")))
.unwrap();
server.add_dev("motor", "test/dev/2").unwrap();
assert_eq!(server.get_dev::<()>("test/dev/2").unwrap().name(), "test/dev/2");
assert!(matches!(server.add_dev("Motor", "TEST/dev/2").unwrap_err().kind(), ErrorKind::DevAlreadyDefined));
assert!(matches!(server.add_dev("Pump", "test/dev/3").unwrap_err().kind(), ErrorKind::ClassNotFound));
assert!(server.remove_dev("test/dev/2"));
assert!(!server.remove_dev("test/dev/2"));
assert!(!server.has_dev("test/dev/2"));
}
#[test]
#[should_panic(expected = "the class name \"Mo/tor\" is invalid")]
fn invalid_class_name() {
let _ = test_server(true).class(ClassDef::<()>::new_default("Mo/tor"));
}
#[test]
fn invalid_names() {
let ctx = test_context(&[]);
let err = ctx.serve(ServerBuilder::new("Test", "te st")).unwrap_err();
assert_eq!(err.kind(), ErrorKind::ServerNameInvalid);
let server = ctx.serve(test_server(true)
.class(ClassDef::<()>::new_default("Motor")))
.unwrap();
assert_eq!(server.add_class(ClassDef::<()>::new_default("")).unwrap_err().kind(), ErrorKind::ClassNameInvalid);
for name in ["test/dev", "test/dev/1/2", "test//1", "test/dev/_1", "test/dev/1.2"] {
assert_eq!(server.add_dev("Motor", name).unwrap_err().kind(), ErrorKind::DevNameInvalid, "{name}");
}
server.add_dev("Motor", "test/dev/1").unwrap();
let dev = server.get_dev::<()>("test/dev/1").unwrap();
assert_eq!(dev.add_command(CommandDef::new("Do it")).unwrap_err().kind(), ErrorKind::CommandNameInvalid);
assert_eq!(dev.add_attr(AttrDef::new("").kind(AttrDataKind::Double).scalar()).unwrap_err().kind(), ErrorKind::AttrNameInvalid);
}
local_and_wire! {
#[cfg_attr(miri, ignore = "Miri is too slow: the wire variant times out subscribing, before even reaching ZMQ")]
fn subscribe(local: bool) {
let ctx = test_context(&[("a/dev/1", "A")]);
let _server = ctx.serve(test_server(local)
.class(ClassDef::<()>::new_default("A").push_state_events(Some(false), None)))
.unwrap();
let dev = ctx.dev("a/dev/1");
let subscribe = |name: &str, event: AttrValueEvent| {
dev.subscribe_attr_value::<DevState>(name, event).ensure_subscribed()
};
subscribe("State", AttrValueEvent::Change).unwrap();
subscribe("State", AttrValueEvent::User).unwrap();
let not_sent = "API_AttributePollingNotStarted";
assert_eq!(subscribe("State", AttrValueEvent::Archive).unwrap_err().reason(), not_sent);
assert_eq!(subscribe("State", AttrValueEvent::Periodic).unwrap_err().reason(), not_sent);
assert_eq!(subscribe("Status", AttrValueEvent::Change).unwrap_err().reason(), not_sent);
assert!(subscribe("Missing", AttrValueEvent::Change).is_err());
}
}
local_and_wire! {
#[cfg_attr(miri, ignore = "Miri is too slow: the wire variant times out subscribing, before even reaching ZMQ")]
fn push_event(local: bool) {
fn position(_: &(), _: &Dev<()>) -> Result<f64> { Ok(0.0) }
let def = ClassDef::<()>::new_default("Motor")
.attr(AttrDef::new("Position").read(position).push_change_event(false));
let ctx = test_context(&[]);
let server = ctx.serve(test_server(local)).unwrap();
let class = server.add_class(def).unwrap();
ctx.database().add_dev("motor/dev/1", "Motor", "Test/test").unwrap();
let dev = class.add_dev("motor/dev/1").unwrap();
server.sync().unwrap();
let client = ctx.dev("motor/dev/1");
let mut change = client.subscribe_attr_value::<f64>("Position", AttrValueEvent::Change);
change.ensure_subscribed().unwrap();
let weak = Arc::downgrade(&dev);
thread::spawn(move || weak.upgrade().unwrap().push_change_event("Position", 1.5).unwrap())
.join()
.unwrap();
let read = change.wait(Duration::from_secs(5)).unwrap();
assert_eq!(read.value, Some(1.5));
assert_eq!(read.quality, AttrQuality::Valid);
let err = Error::new(ErrorKind::Reason("Motor_Unplugged")).with_description("No motor");
dev.push_attr_event("Position", AttrValueEvent::Change, Err(err)).unwrap();
let err = change.wait(Duration::from_secs(5)).unwrap_err();
assert_eq!(err.kind(), ErrorKind::Reason("Motor_Unplugged"));
}
}
local_and_wire! {
#[cfg_attr(miri, ignore = "Miri is too slow: the wire variant times out subscribing, before even reaching ZMQ")]
fn push_data_ready_event(local: bool) {
fn position(_: &(), _: &Dev<()>) -> Result<f64> { Ok(0.0) }
let def = ClassDef::<()>::new_default("Motor")
.attr(AttrDef::new("Position").read(position).push_data_ready_event())
.attr(AttrDef::new("Speed").read(position));
let ctx = test_context(&[]);
let server = ctx.serve(test_server(local)).unwrap();
let class = server.add_class(def).unwrap();
ctx.database().add_dev("motor/dev/1", "Motor", "Test/test").unwrap();
let dev = class.add_dev("motor/dev/1").unwrap();
server.sync().unwrap();
let client = ctx.dev("motor/dev/1");
let mut ready = client.subscribe_attr_ready("Position");
ready.ensure_subscribed().unwrap();
for counter in 1..=3 {
dev.push_data_ready_event("Position", counter).unwrap();
let event = ready.wait(Duration::from_secs(5)).unwrap();
assert_eq!(event, AttrReady { data_kind: AttrDataKind::Double, counter });
}
let not_announced = "API_AttributeNotDataReadyEnabled";
assert_eq!(dev.push_data_ready_event("Speed", 1).unwrap_err().reason(), not_announced);
assert_eq!(client.subscribe_attr_ready("Speed").ensure_subscribed().unwrap_err().reason(), not_announced);
}
}
local_and_wire! {
fn history(local: bool) {
fn position(_: &(), _: &Dev<()>) -> Result<f64> { Ok(0.0) }
fn speed(_: &(), _: &Dev<()>, _: ()) -> Result<i32> { Ok(0) }
let def = ClassDef::<()>::new_default("Motor")
.attr(AttrDef::new("Position").read(position))
.command(CommandDef::new("Speed").exec(speed));
let ctx = test_context(&[]);
let server = ctx.serve(test_server(local)).unwrap();
let class = server.add_class(def).unwrap();
ctx.database().add_dev("motor/dev/1", "Motor", "Test/test").unwrap();
let dev = class.add_dev("motor/dev/1").unwrap();
server.sync().unwrap();
let client = ctx.dev("motor/dev/1");
assert_eq!(client.attr_history::<f64>("Position", 10).unwrap_err().reason(), "API_AttrNotPolled");
assert_eq!(client.command_history::<i32>("Speed", 10).unwrap_err().reason(), "API_CmdNotPolled");
let failed = || Error::new(ErrorKind::Reason("Motor_Unplugged")).with_description("No motor");
let position = dev.get_attr("Position").unwrap();
let first = AttrRead::new(Value::from(1.5));
position.push_history(Ok(first.clone()), None).unwrap();
position.push_history(Err(failed()), None).unwrap();
position.push_history(Ok(AttrRead::new(Value::from(2.5)).with_quality(AttrQuality::Alarm)), None).unwrap();
let history = client.attr_history::<f64>("Position", 10).unwrap();
assert_eq!(history.len(), 3);
assert_eq!(history[0].time, first.time);
assert_eq!(history[0].value.as_ref().unwrap().value, Some(1.5));
assert_eq!(history[1].value.as_ref().unwrap_err().reason(), "Motor_Unplugged");
let last = history[2].value.as_ref().unwrap();
assert_eq!((last.value, last.quality), (Some(2.5), AttrQuality::Alarm));
assert_eq!(client.attr_history::<f64>("Position", 1).unwrap().len(), 1);
let speed = dev.get_command("Speed").unwrap();
speed.push_history(Ok(Value::from(3)), None).unwrap();
speed.push_history(Err(failed()), None).unwrap();
let history = client.command_history::<i32>("Speed", 10).unwrap();
assert_eq!(history[0].value.as_ref().unwrap(), &3);
assert_eq!(history[1].value.as_ref().unwrap_err().reason(), "Motor_Unplugged");
dev.get_command("State").unwrap().push_history(Ok(Value::from(DevState::Moving)), None).unwrap();
let history = client.command_history::<DevState>("State", 10).unwrap();
assert_eq!(history[0].value.as_ref().unwrap(), &DevState::Moving);
let history = client.attr_history::<DevState>("State", 10).unwrap();
assert_eq!(history[0].value.as_ref().unwrap().value, Some(DevState::Moving));
}
}