use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use std::fmt;
use futures::future::{ready, BoxFuture};
use futures::stream::BoxStream;
use crate::dev::{AttrConfig, AttrInfo, AttrRead, AttrValueEvent, CommandInfo, DevInfo, DevSource, DevState, Named, NamedResult};
use crate::error::{Error, ErrorKind, Result, reason};
use crate::value::{Scalar, Spectrum, Value};
use crate::backend::BackendDev;
pub const NAME: &str = "sys/database/2";
const ADMIN_CLASS: &str = "DServer";
#[derive(Default)]
pub struct TestDatabase {
state: Mutex<State>,
log: Mutex<Vec<(String, Value)>>,
failing: Mutex<HashMap<String, FailWith>>,
}
type FailWith = Box<dyn Fn() -> Error + Send + Sync>;
impl fmt::Debug for TestDatabase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TestDatabase")
.field("state", &self.state)
.field("log", &self.log)
.field("failing", &self.failing.lock().unwrap().keys())
.finish()
}
}
impl TestDatabase {
pub fn log(&self) -> Vec<(String, Value)> {
self.log.lock().unwrap().clone()
}
pub fn clear_log(&self) {
self.log.lock().unwrap().clear();
}
pub fn add_fail(&self, name: &str, err: impl Fn() -> Error + Send + Sync + 'static) {
self.failing.lock().unwrap().insert(name.to_string(), Box::new(err));
}
pub fn remove_fail(&self, name: &str) {
self.failing.lock().unwrap().remove(name);
}
pub fn command(&self, name: &str, input: Value) -> Result<Value> {
self.log.lock().unwrap().push((name.to_string(), input.clone()));
if let Some(err) = self.failing.lock().unwrap().get(name) {
return Err(err());
}
let mut state = self.state.lock().unwrap();
let res = match name {
"DbAddServer" => state.add_server_cmd(input),
"DbDeleteServer" => state.delete_server_cmd(input),
"DbAddDevice" => state.add_dev_cmd(input),
"DbDeleteDevice" => state.delete_dev_cmd(input),
"DbGetDeviceClassList" => state.list_server_dev(input),
"DbExportDevice" | "DbExportServer" => state.export(input),
"DbUnExportDevice" => state.unexport_dev(input),
"DbUnExportServer" => state.unexport_server(input),
"DbImportDevice" => state.import_dev(input),
"DbGetDeviceProperty" | "DbGetClassProperty" => state.get_prop(Kind::of(name), input),
"DbGetDevicePropertyList" | "DbGetClassPropertyList" => state.list_prop(Kind::of(name), input),
"DbPutDeviceProperty" | "DbPutClassProperty" => state.put_prop(Kind::of(name), input),
"DbDeleteDeviceProperty" | "DbDeleteClassProperty" => state.delete_prop(Kind::of(name), input),
"DbGetDeviceAttributeProperty2" | "DbGetClassAttributeProperty2" => state.get_attr_prop(Kind::of(name), input),
"DbGetDeviceAttributeList" | "DbGetClassAttributeList" => state.list_attr(Kind::of(name), input),
"DbPutDeviceAttributeProperty2" | "DbPutClassAttributeProperty2" => state.put_attr_prop(Kind::of(name), input),
"DbDeleteDeviceAttributeProperty" | "DbDeleteClassAttributeProperty" => state.delete_attr_prop(Kind::of(name), input),
_ => Err(Error::not_implemented()),
};
res.map_err(|err| err.with_append_description(format_args!("The command {name} of the database of a test")))
}
}
#[derive(Debug, Default)]
struct State {
servers: HashMap<String, Vec<String>>,
devs: HashMap<String, Dev>,
props: HashMap<Object, Props>,
attr_props: HashMap<Object, Entries<Props>>,
}
#[derive(Debug)]
struct Dev {
name: String,
class: String,
server: String,
export: Option<Export>,
exported: bool,
}
#[derive(Debug)]
struct Export {
itr: String,
host: String,
version: String,
pid: i32,
}
type Object = (Kind, String);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Kind {
Device,
Class,
}
impl Kind {
fn of(command: &str) -> Self {
if command.contains("Class") { Self::Class } else { Self::Device }
}
fn object(self, name: &str) -> Object {
(self, name.to_lowercase())
}
}
type Props = Entries<Vec<String>>;
#[derive(Debug)]
struct Entries<T>(Vec<(String, T)>);
impl<T> Default for Entries<T> {
fn default() -> Self {
Self(Vec::new())
}
}
impl<T> Entries<T> {
fn get(&self, name: &str) -> Option<&T> {
self.0.iter().find(|(other, _)| other.eq_ignore_ascii_case(name)).map(|(_, value)| value)
}
fn entry(&mut self, name: &str) -> &mut T where T: Default {
let index = match self.0.iter().position(|(other, _)| other.eq_ignore_ascii_case(name)) {
Some(index) => index,
None => {
self.0.push((name.to_string(), T::default()));
self.0.len() - 1
}
};
&mut self.0[index].1
}
fn remove(&mut self, names: &[String]) {
self.0.retain(|(name, _)| !names.iter().any(|removed| removed.eq_ignore_ascii_case(name)));
}
fn names(&self, filter: &str) -> Vec<&str> {
self.0.iter().map(|(name, _)| name.as_str()).filter(|name| wildcard(filter, name)).collect()
}
}
impl State {
fn add_server_cmd(&mut self, input: Value) -> Result<Value> {
let args = strings(input)?;
let [server, pairs @ ..] = &args[..] else { return Err(malformed_err()) };
if pairs.len() % 2 != 0 {
return Err(malformed_err());
}
self.add_server(server);
for pair in pairs.chunks_exact(2) {
self.add_dev(server, &pair[0], &pair[1]);
}
Ok(Value::Void)
}
fn delete_server_cmd(&mut self, input: Value) -> Result<Value> {
let server = String::try_from(input)?;
for dev in self.servers.remove(&server.to_lowercase()).unwrap_or_default() {
self.devs.remove(&dev);
}
Ok(Value::Void)
}
fn add_dev_cmd(&mut self, input: Value) -> Result<Value> {
let args = strings(input)?;
let [server, dev, class] = &args[..] else { return Err(malformed_err()) };
self.add_server(server);
self.add_dev(server, dev, class);
Ok(Value::Void)
}
fn delete_dev_cmd(&mut self, input: Value) -> Result<Value> {
let dev = String::try_from(input)?.to_lowercase();
if let Some(removed) = self.devs.remove(&dev) {
self.server_devs(&removed.server).retain(|other| *other != dev);
}
Ok(Value::Void)
}
fn list_server_dev(&mut self, input: Value) -> Result<Value> {
let server = String::try_from(input)?;
let listing = self.servers.get(&server.to_lowercase())
.into_iter()
.flatten()
.filter_map(|dev| self.devs.get(dev))
.flat_map(|dev| [dev.name.as_str(), dev.class.as_str()])
.collect::<Vec<_>>();
Ok(Value::from(Spectrum::<str>::from(&listing[..])))
}
fn add_server(&mut self, server: &str) {
if !self.servers.contains_key(&server.to_lowercase()) {
self.add_dev(server, &format!("dserver/{server}"), ADMIN_CLASS);
}
}
fn add_dev(&mut self, server: &str, name: &str, class: &str) {
let key = name.to_lowercase();
let previous = self.devs.remove(&key);
if let Some(previous) = &previous {
self.server_devs(&previous.server).retain(|other| *other != key);
}
self.server_devs(server).push(key.clone());
let (export, exported) = previous.map_or((None, false), |dev| (dev.export, dev.exported));
self.devs.insert(key, Dev {
name: name.to_string(),
class: class.to_string(),
server: server.to_string(),
export,
exported,
});
}
fn server_devs(&mut self, server: &str) -> &mut Vec<String> {
self.servers.entry(server.to_lowercase()).or_default()
}
}
impl State {
fn export(&mut self, input: Value) -> Result<Value> {
let args = strings(input)?;
if args.len() % 5 != 0 {
return Err(malformed_err());
}
for info in args.chunks_exact(5) {
let [name, itr, host, pid, version] = info else { unreachable!() };
let dev = self.devs.get_mut(&name.to_lowercase())
.ok_or_else(|| not_defined_err(name))?;
dev.export = Some(Export {
itr: itr.clone(),
host: host.clone(),
pid: pid.parse().map_err(|_| malformed_err())?,
version: version.clone(),
});
dev.exported = true;
}
Ok(Value::Void)
}
fn unexport_dev(&mut self, input: Value) -> Result<Value> {
let dev = String::try_from(input)?;
if let Some(dev) = self.devs.get_mut(&dev.to_lowercase()) {
dev.exported = false;
}
Ok(Value::Void)
}
fn unexport_server(&mut self, input: Value) -> Result<Value> {
let server = String::try_from(input)?;
for dev in self.servers.get(&server.to_lowercase()).into_iter().flatten() {
if let Some(dev) = self.devs.get_mut(dev) {
dev.exported = false;
}
}
Ok(Value::Void)
}
fn import_dev(&mut self, input: Value) -> Result<Value> {
let name = String::try_from(input)?;
let dev = self.devs.get(&name.to_lowercase())
.ok_or_else(|| not_defined_err(&name))?;
let (itr, version, host, pid) = match &dev.export {
Some(export) => (&*export.itr, &*export.version, &*export.host, export.pid),
None => ("nada", "0", "nada", 0),
};
let strings = [&*dev.name, itr, version, &*dev.server, host, &*dev.class];
Ok(Value::LongString(
Spectrum::from(vec![i32::from(dev.exported), pid]),
Spectrum::<str>::from(&strings[..]),
))
}
}
impl State {
fn get_prop(&mut self, kind: Kind, input: Value) -> Result<Value> {
let args = strings(input)?;
let [obj_name, prop_names @ ..] = &args[..] else { return Err(malformed_err()) };
let props = self.props.get(&kind.object(obj_name));
let mut listing = vec![obj_name.clone(), prop_names.len().to_string()];
for prop_name in prop_names {
let values = props.and_then(|props| props.get(prop_name)).map_or(&[][..], Vec::as_slice);
listing.push(prop_name.clone());
listing.push(values.len().to_string());
listing.extend(values.iter().cloned());
}
Ok(Value::from(Spectrum::<str>::from(&listing[..])))
}
fn list_prop(&mut self, kind: Kind, input: Value) -> Result<Value> {
let args = strings(input)?;
let [obj_name, filter] = &args[..] else { return Err(malformed_err()) };
let names = self.props.get(&kind.object(obj_name)).map_or_else(Vec::new, |props| props.names(filter));
Ok(Value::from(Spectrum::<str>::from(&names[..])))
}
fn put_prop(&mut self, kind: Kind, input: Value) -> Result<Value> {
let args = strings(input)?;
let mut cursor = Cursor::new(&args);
let props = self.props.entry(kind.object(cursor.next()?)).or_default();
for _ in 0..cursor.count()? {
let prop_name = cursor.next()?;
*props.entry(prop_name) = cursor.values()?.to_vec();
}
cursor.end()?;
Ok(Value::Void)
}
fn delete_prop(&mut self, kind: Kind, input: Value) -> Result<Value> {
let args = strings(input)?;
let [obj_name, prop_names @ ..] = &args[..] else { return Err(malformed_err()) };
if let Some(props) = self.props.get_mut(&kind.object(obj_name)) {
props.remove(prop_names);
}
Ok(Value::Void)
}
}
impl State {
fn get_attr_prop(&mut self, kind: Kind, input: Value) -> Result<Value> {
let args = strings(input)?;
let [obj_name, attr_names @ ..] = &args[..] else { return Err(malformed_err()) };
let attrs = self.attr_props.get(&kind.object(obj_name));
let mut listing = vec![obj_name.clone(), attr_names.len().to_string()];
for attr_name in attr_names {
let props = attrs.and_then(|attrs| attrs.get(attr_name)).map_or(&[][..], |props| &props.0[..]);
listing.push(attr_name.clone());
listing.push(props.len().to_string());
for (prop_name, values) in props {
listing.push(prop_name.clone());
listing.push(values.len().to_string());
if values.is_empty() {
listing.push(" ".to_string());
}
listing.extend(values.iter().cloned());
}
}
Ok(Value::from(Spectrum::<str>::from(&listing[..])))
}
fn list_attr(&mut self, kind: Kind, input: Value) -> Result<Value> {
let args = strings(input)?;
let [obj_name, filter] = &args[..] else { return Err(malformed_err()) };
let names = self.attr_props.get(&kind.object(obj_name)).map_or_else(Vec::new, |attrs| attrs.names(filter));
Ok(Value::from(Spectrum::<str>::from(&names[..])))
}
fn put_attr_prop(&mut self, kind: Kind, input: Value) -> Result<Value> {
let args = strings(input)?;
let mut cursor = Cursor::new(&args);
let attrs = self.attr_props.entry(kind.object(cursor.next()?)).or_default();
for _ in 0..cursor.count()? {
let props = attrs.entry(cursor.next()?);
for _ in 0..cursor.count()? {
let prop_name = cursor.next()?;
*props.entry(prop_name) = cursor.values()?.to_vec();
}
}
cursor.end()?;
Ok(Value::Void)
}
fn delete_attr_prop(&mut self, kind: Kind, input: Value) -> Result<Value> {
let args = strings(input)?;
let [obj_name, attr_name, prop_names @ ..] = &args[..] else { return Err(malformed_err()) };
if let Some(attrs) = self.attr_props.get_mut(&kind.object(obj_name)) {
attrs.entry(attr_name).remove(prop_names);
attrs.0.retain(|(_, props)| !props.0.is_empty());
}
Ok(Value::Void)
}
}
struct Cursor<'a> {
args: &'a [String],
pos: usize,
}
impl<'a> Cursor<'a> {
fn new(args: &'a [String]) -> Self {
Self { args, pos: 0 }
}
fn next(&mut self) -> Result<&'a str> {
let arg = self.args.get(self.pos).ok_or_else(malformed_err)?;
self.pos += 1;
Ok(arg)
}
fn count(&mut self) -> Result<usize> {
self.next()?.parse().map_err(|_| malformed_err())
}
fn values(&mut self) -> Result<&'a [String]> {
let len = self.count()?;
let values = self.args.get(self.pos..self.pos + len).ok_or_else(malformed_err)?;
self.pos += len;
Ok(values)
}
fn end(&self) -> Result<()> {
match self.pos == self.args.len() {
true => Ok(()),
false => Err(malformed_err()),
}
}
}
fn strings(input: Value) -> Result<Vec<String>> {
Vec::<String>::try_from(input)
}
fn wildcard(filter: &str, name: &str) -> bool {
let (filter, name) = (filter.to_lowercase(), name.to_lowercase());
let mut parts = filter.split('*');
let first = parts.next().unwrap_or_default();
let Some(mut rest) = name.strip_prefix(first) else { return false };
let parts = parts.collect::<Vec<_>>();
let Some((last, middle)) = parts.split_last() else { return rest.is_empty() };
for part in middle {
match rest.find(part) {
Some(index) => rest = &rest[index + part.len()..],
None => return false,
}
}
rest.ends_with(last)
}
fn malformed_err() -> Error {
Error::new(ErrorKind::InvalidData)
.with_description("The argument is malformed")
}
fn not_defined_err(dev: &str) -> Error {
Error::new(ErrorKind::Reason(reason::DB_DeviceNotDefined))
.with_description(format!("The device {dev} is not defined in the database"))
}
#[derive(Debug)]
pub struct TestDatabaseDev(pub Arc<TestDatabase>);
fn not_implemented<'a, T: Send + 'a>(what: &'static str) -> BoxFuture<'a, Result<T>> {
Box::pin(ready(Err(Error::not_implemented()
.with_append_description(format_args!("{what} of the database of a test")))))
}
impl BackendDev for TestDatabaseDev {
fn name(&self) -> BoxFuture<'_, Result<Arc<str>>> {
Box::pin(ready(Ok(Arc::from(NAME))))
}
fn admin_dev_name(&self) -> BoxFuture<'_, Result<Arc<str>>> {
not_implemented("The admin device")
}
fn description(&self) -> BoxFuture<'_, Result<Arc<str>>> {
Box::pin(ready(Ok(Arc::from("The Tango database of a test"))))
}
fn info(&self) -> BoxFuture<'_, Result<DevInfo>> {
not_implemented("The info")
}
fn ping(&self) -> BoxFuture<'_, Result<()>> {
Box::pin(ready(Ok(())))
}
fn state(&self) -> BoxFuture<'_, Result<DevState>> {
Box::pin(ready(Ok(DevState::On)))
}
fn status(&self) -> BoxFuture<'_, Result<String>> {
Box::pin(ready(Ok("The database of a test".to_string())))
}
fn black_box(&self, _max: Option<usize>) -> BoxFuture<'_, Result<Vec<String>>> {
not_implemented("The black box")
}
fn command(&self, name: Scalar<str>, input: Value, _source: DevSource) -> BoxFuture<'_, Result<Value>> {
Box::pin(ready(self.0.command(name.value(), input)))
}
fn read_attr(&self, _names: Option<Spectrum<str>>, _source: DevSource) -> BoxFuture<'_, Result<Vec<NamedResult<AttrRead<Value>>>>> {
not_implemented("Reading the attributes")
}
fn write_attr(&self, _values: Vec<Named<Value>>) -> BoxFuture<'_, Result<Vec<Result<()>>>> {
not_implemented("Writing the attributes")
}
fn write_read_attr(&self, _values: Vec<Named<Value>>) -> BoxFuture<'_, Result<Vec<NamedResult<AttrRead<Value>>>>> {
not_implemented("Writing the attributes")
}
fn command_info(&self, _names: Option<Spectrum<str>>) -> BoxFuture<'_, Result<Vec<NamedResult<CommandInfo>>>> {
not_implemented("The commands")
}
fn attr_info(&self, _names: Option<Spectrum<str>>) -> BoxFuture<'_, Result<Vec<NamedResult<AttrInfo>>>> {
not_implemented("The attributes")
}
fn set_attr_config(&self, _configs: Vec<Named<AttrConfig>>) -> BoxFuture<'_, Result<Vec<Result<()>>>> {
Box::pin(ready(Err(Error::not_implemented()
.with_append_description("Writing the configuration of an attribute"))))
}
fn command_history<'a>(&'a self, _name: &'a str, _max: usize) -> BoxFuture<'a, Result<Vec<(SystemTime, Result<Value>)>>> {
Box::pin(ready(Err(Error::not_implemented()
.with_append_description("The recorded history of a command"))))
}
fn attr_history<'a>(&'a self, _name: &'a str, _max: usize) -> BoxFuture<'a, Result<Vec<Result<AttrRead<Value>>>>> {
Box::pin(ready(Err(Error::not_implemented()
.with_append_description("The recorded history of an attribute"))))
}
fn subscribe_attr_value<'a>(&'a self, _admin: Arc<dyn BackendDev>, _name: &'a str, _event: AttrValueEvent) -> BoxFuture<'a, Result<BoxStream<'static, Result<AttrRead<Value>>>>> {
Box::pin(ready(Err(Error::not_implemented()
.with_append_description("Subscribing to the readings of an attribute"))))
}
fn subscribe_attr_ready<'a>(&'a self, _admin: Arc<dyn BackendDev>, _name: &'a str) -> BoxFuture<'a, Result<BoxStream<'static, Result<()>>>> {
Box::pin(ready(Err(Error::not_implemented()
.with_append_description("Subscribing to an attribute being ready"))))
}
fn subscribe_attr_conf<'a>(&'a self, _admin: Arc<dyn BackendDev>, _name: &'a str) -> BoxFuture<'a, Result<BoxStream<'static, Result<AttrInfo>>>> {
Box::pin(ready(Err(Error::not_implemented()
.with_append_description("Subscribing to the configuration of an attribute"))))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::database::ExportDevInfo;
use crate::testing::TestContext;
fn context() -> TestContext {
TestContext::new().unwrap()
}
#[test]
fn wildcards() {
assert!(wildcard("*", "anything"));
assert!(wildcard("Pos*", "position"));
assert!(wildcard("*tion", "Position"));
assert!(wildcard("p*s*n", "position"));
assert!(wildcard("position", "POSITION"));
assert!(!wildcard("pos", "position"));
assert!(!wildcard("p*x*n", "position"));
}
#[test]
fn servers_and_devices() {
let ctx = context();
let db = ctx.database();
db.add_server("Motor/test", &[("test/motor/1", "Motor"), ("test/motor/2", "Motor")]).unwrap();
db.add_dev("test/axis/1", "Axis", "Motor/test").unwrap();
let listed = db.list_server_dev("motor/TEST").unwrap();
let listed = listed.iter().map(|dev| (dev.name.value(), dev.value.value())).collect::<Vec<_>>();
assert_eq!(listed, [
("dserver/Motor/test", "DServer"),
("test/motor/1", "Motor"),
("test/motor/2", "Motor"),
("test/axis/1", "Axis"),
]);
db.delete_dev("TEST/MOTOR/2").unwrap();
assert_eq!(db.list_server_dev("Motor/test").unwrap().len(), 3);
db.delete_server("Motor/test").unwrap();
assert!(db.list_server_dev("Motor/test").unwrap().is_empty());
assert_eq!(db.import_dev("test/motor/1").unwrap_err().kind(), ErrorKind::DevNotFound);
}
#[test]
fn export_and_import() {
let ctx = context();
let db = ctx.database();
db.add_dev("test/motor/1", "Motor", "Motor/test").unwrap();
let info = db.import_dev("test/motor/1").unwrap();
assert!(!info.exported);
assert_eq!(info.class.as_ref().map(|class| class.value()), Some("Motor"));
db.export_server(&[ExportDevInfo {
name: "test/motor/1".into(),
itr: "IOR:0123".into(),
host: "localhost".into(),
version: "6".into(),
pid: 42,
}]).unwrap();
let info = db.import_dev("test/motor/1").unwrap();
assert!(info.exported);
assert_eq!((info.itr.value(), info.host.value(), info.version.value(), info.server.value(), info.pid),
("IOR:0123", "localhost", "6", "Motor/test", 42));
db.unexport_server("motor/test").unwrap();
let info = db.import_dev("test/motor/1").unwrap();
assert!(!info.exported);
assert_eq!(info.itr.value(), "IOR:0123");
}
#[test]
fn properties() {
let ctx = context();
let db = ctx.database();
db.set_dev_prop("test/motor/1", [("Limit", "90"), ("Unit", "deg")]).unwrap();
db.set_dev_prop("TEST/motor/1", ("limit", "45")).unwrap();
assert_eq!(db.get_dev_prop::<f64, _>("test/motor/1", "Limit").unwrap(), 45.0);
assert_eq!(db.list_dev_prop("test/motor/1", "*").unwrap().len(), 2);
db.del_dev_prop("test/motor/1", "unit").unwrap();
assert_eq!(db.list_dev_prop("test/motor/1", "*").unwrap().len(), 1);
db.set_class_prop("Motor", ("Limit", "10")).unwrap();
assert_eq!(db.get_class_prop::<f64, _>("Motor", "Limit").unwrap(), 10.0);
assert_eq!(db.get_dev_prop::<f64, _>("test/motor/1", "Limit").unwrap(), 45.0);
db.set_dev_attr_prop("test/motor/1", ("Position", [("unit", "mm"), ("min_value", "-5")])).unwrap();
assert_eq!(db.get_dev_attr_prop::<String, _, _>("test/motor/1", "position", "Unit").unwrap(), "mm");
db.del_dev_attr_prop("test/motor/1", "Position", ["unit", "min_value"]).unwrap();
assert_eq!(db.get_dev_attr_prop::<String, _, _>("test/motor/1", "Position", "unit").unwrap(), "");
}
#[test]
fn clones_share() {
let ctx = context();
let db = ctx.database();
let clone = db.clone();
clone.add_dev("test/motor/1", "Motor", "Motor/test").unwrap();
assert_eq!(db.list_server_dev("Motor/test").unwrap().len(), 2);
let other = context();
assert!(other.database().list_server_dev("Motor/test").unwrap().is_empty());
assert_ne!(db, other.database());
}
}