use std::sync::{Arc, Mutex};
use futures::executor::block_on;
use crate::backend::{default_backend, Backend, BackendDev, BackendServer, BackendServerConfig};
use crate::server::{Server, ServerBuilder, ServerHandle};
use crate::database::{DatabaseProxy, ExportDevInfo};
use crate::dev::{DevProxy, DATABASE_DEV_NAME};
use crate::error::{Error, ErrorKind, Result};
use crate::local::{self, LocalDevHandle};
use super::database::{TestDatabase, TestDatabaseDev};
const HOST: &str = "127.0.0.1";
#[derive(Debug)]
pub struct TestContext {
servers: Mutex<Vec<ServerHandle>>,
devs: Mutex<Vec<(Arc<str>, LocalDevHandle)>>,
_database_handle: LocalDevHandle,
own: Arc<dyn BackendServer>,
test_database: Arc<TestDatabase>,
database: DatabaseProxy,
host: Arc<str>,
backend: Arc<dyn Backend>,
}
impl TestContext {
pub fn new() -> Result<Self> {
Self::with_port(0)
}
pub fn with_port(port: u16) -> Result<Self> {
let backend = default_backend();
let own = block_on(backend.serve(&BackendServerConfig::new()
.with_host(HOST)
.with_port(port)))?;
let test_database = Arc::new(TestDatabase::default());
let dev = Arc::new(TestDatabaseDev(Arc::clone(&test_database))) as Arc<dyn BackendDev>;
let itr = own.add(DATABASE_DEV_NAME, Arc::clone(&dev))?;
let database_handle = local::register(&dev, itr, own.host(), own.port(), Arc::from(DATABASE_DEV_NAME));
let database = DatabaseProxy::new_with_backend(Arc::clone(&backend), &own.host(), own.port());
Ok(Self {
servers: Mutex::default(),
devs: Mutex::default(),
_database_handle: database_handle,
host: own.host(),
own,
test_database,
database,
backend,
})
}
pub fn host(&self) -> &str {
&self.host
}
pub fn port(&self) -> u16 {
self.own.port()
}
pub fn database(&self) -> &DatabaseProxy {
&self.database
}
pub fn test_database(&self) -> &Arc<TestDatabase> {
&self.test_database
}
pub fn serve(&self, builder: ServerBuilder) -> Result<Arc<Server>> {
let handle = builder
.backend(Arc::clone(&self.backend))
.database(self.database.clone())
.host(HOST)
.port(0)
.stop_on_signal(false)
.serve()?;
let server = handle.server_arc();
self.servers.lock().unwrap().push(handle);
Ok(server)
}
pub fn server(&self, name: &str) -> Option<Arc<Server>> {
self.servers.lock().unwrap().iter()
.find(|handle| handle.server().name().eq_ignore_ascii_case(name))
.map(ServerHandle::server_arc)
}
pub fn stop(&self, name: &str) -> Result<()> {
let server = self.server(name).ok_or_else(|| Error::new(ErrorKind::DevNotFound)
.with_description(format!("No server {name} is served by this context")))?;
block_on(server.stop())?;
let servers = self.servers.lock().unwrap();
let handle = servers.iter().find(|handle| handle.server().name().eq_ignore_ascii_case(name)).unwrap();
handle.wait()
}
pub fn serve_dev(&self, dev: Arc<dyn BackendDev>) -> Result<()> {
let name = block_on(dev.name())?;
let mut devs = self.devs.lock().unwrap();
if let Some(index) = devs.iter().position(|(other, _)| other.eq_ignore_ascii_case(&name)) {
devs.remove(index);
self.own.remove(&name);
}
let itr = self.own.add(&name, Arc::clone(&dev))?;
let handle = local::register(&dev, Arc::clone(&itr), self.own.host(), self.own.port(), Arc::clone(&name));
let exported = self.database.export_dev(ExportDevInfo {
name: (&*name).into(),
itr: (&*itr).into(),
host: (&*self.own.host()).into(),
version: self.own.version().to_string().into(),
pid: std::process::id() as i32,
});
if let Err(err) = exported {
drop(handle);
self.own.remove(&name);
return Err(err);
}
devs.push((name, handle));
Ok(())
}
pub fn dev(&self, name: &str) -> DevProxy {
DevProxy::new_from_database_with_backend(Arc::clone(&self.backend), &self.database, name)
}
pub fn is_local(&self, name: &str) -> Result<bool> {
let info = self.database.import_dev(name)?;
Ok(local::find(info.itr.value()).is_some())
}
pub fn backend_dev(&self, name: &str) -> Result<Arc<dyn BackendDev>> {
let info = self.database.import_dev(name)?;
match local::find(info.itr.value()) {
Some(dev) => Ok(dev),
None => block_on(self.backend.connect(info.itr.value())),
}
}
}
impl Drop for TestContext {
fn drop(&mut self) {
self.servers.get_mut().unwrap().clear();
self.devs.get_mut().unwrap().clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::ClassDef;
#[test]
fn two_servers() {
let ctx = TestContext::new().unwrap();
ctx.database().add_server("Motor/test", &[("test/motor/1", "Motor"), ("test/motor/2", "Motor")]).unwrap();
ctx.database().add_server("Slit/test", &[("test/slit/1", "Slit")]).unwrap();
let motor = ctx.serve(ServerBuilder::new("Motor", "test").class(ClassDef::<()>::new_default("Motor")).unwrap()).unwrap();
assert_ne!(motor.backend().unwrap().port(), 0);
ctx.serve(ServerBuilder::new("Slit", "test").class(ClassDef::<()>::new_default("Slit")).unwrap()).unwrap();
for name in ["test/motor/1", "test/motor/2", "test/slit/1"] {
let dev = ctx.dev(name);
dev.ping().unwrap();
assert_eq!(&*dev.name().unwrap(), name);
}
let admin = ctx.dev("test/motor/1").admin_dev().unwrap();
assert_eq!(admin.name().unwrap().to_lowercase(), "dserver/motor/test");
ctx.stop("Motor/test").unwrap();
ctx.stop("Slit/test").unwrap();
let unexported = ctx.test_database().log().into_iter().filter(|(name, _)| name == "DbUnExportServer").count();
assert_eq!(unexported, 2);
}
#[test]
fn not_local() {
let ctx = TestContext::new().unwrap();
ctx.database().add_server("Motor/test", &[("test/motor/1", "Motor")]).unwrap();
ctx.serve(ServerBuilder::new("Motor", "test").local(false).class(ClassDef::<()>::new_default("Motor")).unwrap()).unwrap();
let database = block_on(default_backend().connect_at(ctx.host(), ctx.port(), DATABASE_DEV_NAME)).unwrap();
block_on(database.ping()).unwrap();
let itr = ctx.database().import_dev("test/motor/1").unwrap().itr;
assert!(local::find(itr.value()).is_none());
let dev = ctx.dev("test/motor/1");
assert_eq!(dev.admin_dev_name().unwrap().to_lowercase(), "dserver/motor/test");
let remote = ctx.backend_dev("test/motor/1").unwrap();
assert_eq!(block_on(remote.admin_dev_name()).unwrap().to_lowercase(), "dserver/motor/test");
}
#[test]
fn stop() {
let ctx = TestContext::new().unwrap();
ctx.database().add_server("Motor/test", &[("test/motor/1", "Motor")]).unwrap();
ctx.serve(ServerBuilder::new("Motor", "test").class(ClassDef::<()>::new_default("Motor")).unwrap()).unwrap();
ctx.stop("motor/TEST").unwrap();
assert!(ctx.dev("test/motor/1").ping().is_err());
assert!(ctx.server("Motor/test").is_some());
assert!(ctx.stop("Other/test").is_err());
}
#[test]
fn serve_dev() {
let other = TestContext::new().unwrap();
other.database().add_server("Motor/other", &[("test/motor/1", "Motor")]).unwrap();
other.serve(ServerBuilder::new("Motor", "other").class(ClassDef::<()>::new_default("Motor")).unwrap()).unwrap();
let dev = other.backend_dev("test/motor/1").unwrap();
let ctx = TestContext::new().unwrap();
assert!(ctx.serve_dev(Arc::clone(&dev)).is_err());
ctx.database().add_dev("test/motor/1", "Motor", "Motor/test").unwrap();
ctx.serve_dev(dev).unwrap();
let proxy = ctx.dev("test/motor/1");
proxy.ping().unwrap();
assert_eq!(proxy.admin_dev_name().unwrap().to_lowercase(), "dserver/motor/other");
let itr = ctx.database().import_dev("test/motor/1").unwrap().itr;
let remote = block_on(default_backend().connect(itr.value())).unwrap();
assert_eq!(block_on(remote.admin_dev_name()).unwrap().to_lowercase(), "dserver/motor/other");
let database = ctx.database().clone();
drop(ctx);
assert!(local::find(itr.value()).is_none());
assert!(database.ping().is_err());
other.dev("test/motor/1").ping().unwrap();
}
}