Skip to main content

tango/testing/
context.rs

1//! A context in which servers are run and their devices reached, see [`TestContext`].
2
3use std::ops::RangeBounds;
4use std::sync::{Arc, Mutex};
5
6use futures::executor::block_on;
7
8use crate::backend::{default_backend, Backend, BackendDev, BackendServer, BackendServerConfig};
9use crate::server::{Server, ServerBuilder, ServerHandle};
10use crate::database::{DatabaseProxy, ExportDevInfo};
11use crate::dev::{DevProxy, DATABASE_DEV_NAME};
12use crate::error::{Error, ErrorKind, Result};
13use crate::local::{self, LocalDevHandle};
14
15use super::database::{TestDatabase, TestDatabaseDev};
16
17
18/// The host everything a [`TestContext`] serves listens at, which keeps it to this machine.
19const HOST: &str = "127.0.0.1";
20
21/// Servers run for a test, and what they are reached through, which is what pytango's
22/// `MultiDeviceTestContext` is.
23///
24/// A context serves a database of its own, see [`TestDatabase`], which is filled
25/// through [`Self::database`] as a real one is, then the servers registered in it, each on
26/// a port the system picks, see [`Self::serve`], whose devices are then found through that
27/// database as a client of a real one finds them, see [`Self::dev`].
28///
29/// ```no_run
30/// # use tango::testing::TestContext;
31/// # use tango::server::{ClassDef, ServerBuilder};
32/// let ctx = TestContext::new()?;
33/// ctx.database().add_server("Motor/test", &[("test/motor/1", "Motor")])?;
34/// ctx.serve(ServerBuilder::new("Motor", "test").class(ClassDef::<()>::new_default("Motor")))?;
35/// ctx.dev("test/motor/1").ping()?;
36/// # Ok::<_, tango::error::Error>(())
37/// ```
38///
39/// Everything is served for real, through the default backend: the database and the
40/// devices are reached by any process, at [`Self::host`] and [`Self::port`] for the
41/// database, and by this one directly, as every device this process serves, see
42/// [`Self::is_local`]. Any implementation of a device can also be served on its own, see
43/// [`Self::serve_dev`].
44///
45/// Everything it serves is dropped with it, with no side effect: a server that is to be
46/// stopped properly, its devices deleted, is stopped through the context, see
47/// [`Self::stop`].
48#[derive(Debug)]
49pub struct TestContext {
50    /// The servers served, each dropped with its handle.
51    servers: Mutex<Vec<ServerHandle>>,
52    /// The devices served on their own, see [`Self::serve_dev`], by name.
53    devs: Mutex<Vec<(Arc<str>, LocalDevHandle)>>,
54    /// What this process reaches the database by directly.
55    _database_handle: LocalDevHandle,
56    /// What serves the database and the devices served on their own.
57    own: Arc<dyn BackendServer>,
58    test_database: Arc<TestDatabase>,
59    database: DatabaseProxy,
60    /// The host the database answers at, see [`Self::host`].
61    host: Arc<str>,
62    backend: Arc<dyn Backend>,
63}
64
65impl TestContext {
66
67    /// A context serving an empty database, on a port the system picks.
68    pub fn new() -> Result<Self> {
69        Self::with_port(0..=0)
70    }
71
72    /// A context serving an empty database on that port, one the system picks if `0`,
73    /// which is what a database that clients are told of in advance needs.
74    pub fn with_port<R: RangeBounds<u16>>(port: R) -> Result<Self> {
75
76        let backend = default_backend();
77        let own = block_on(backend.serve(&BackendServerConfig::new()
78            .host(HOST)
79            .port(port)))?;
80
81        // Served under the name a client connects to a database by, which is the object key
82        // a client of cppTango asks for too.
83        let test_database = Arc::new(TestDatabase::default());
84        let dev = Arc::new(TestDatabaseDev(Arc::clone(&test_database))) as Arc<dyn BackendDev>;
85        let itr = own.add(DATABASE_DEV_NAME, Arc::clone(&dev))?;
86        let database_handle = local::register(&dev, itr, own.host(), own.port(), Arc::from(DATABASE_DEV_NAME));
87
88        let database = DatabaseProxy::new_with_backend(Arc::clone(&backend), &own.host(), own.port());
89
90        Ok(Self {
91            servers: Mutex::default(),
92            devs: Mutex::default(),
93            _database_handle: database_handle,
94            host: own.host(),
95            own,
96            test_database,
97            database,
98            backend,
99        })
100
101    }
102
103    /// The host the database answers at.
104    pub fn host(&self) -> &str {
105        &self.host
106    }
107
108    /// The port the database answers at.
109    pub fn port(&self) -> u16 {
110        self.own.port()
111    }
112
113    /// The database of this context, which is where the servers are registered before
114    /// being served, with [`DatabaseProxy::add_server`], and their properties set.
115    pub fn database(&self) -> &DatabaseProxy {
116        &self.database
117    }
118
119    /// The database itself, which tells what it was asked and is made to fail, see
120    /// [`TestDatabase`].
121    pub fn test_database(&self) -> &Arc<TestDatabase> {
122        &self.test_database
123    }
124
125    /// Serve that server until it is stopped, see [`Self::stop`], or this context is
126    /// dropped: it is served through the backend of this context, registered in its
127    /// database, and listens on this machine only, on a port the system picks, and is never
128    /// stopped on termination, whatever the builder was told of it, see
129    /// [`ServerBuilder::stop_on_terminate`]. Its devices are then reached with [`Self::dev`].
130    ///
131    /// The server given back is shared with the context, which keeps owning it: it is
132    /// dropped all the same when the context is.
133    pub fn serve(&self, builder: ServerBuilder) -> Result<Arc<Server>> {
134
135        let builder = builder.stop_on_terminate(false);
136        #[cfg(feature = "cli")]
137        let builder = builder.terminate_on_signals(false);
138
139        let handle = builder
140            .backend(Arc::clone(&self.backend))
141            .database(self.database.clone())
142            .host(HOST)
143            .serve()?;
144        
145        let server = handle.server_arc();
146        self.servers.lock().unwrap().push(handle);
147        Ok(server)
148
149    }
150
151    /// The server of that `exe/inst` name this context serves, if it does.
152    pub fn server(&self, name: &str) -> Option<Arc<Server>> {
153        self.servers.lock().unwrap().iter()
154            .find(|handle| handle.server().name().eq_ignore_ascii_case(name))
155            .map(ServerHandle::server_arc)
156    }
157
158    /// Stop the server of that `exe/inst` name, which deletes its devices, giving back the
159    /// first error met, see [`Server::stop_async`], and wait for it to be through, see
160    /// [`Server::wait`]. A server that stopped on its own is only waited for.
161    ///
162    /// The server stays with the context, stopped, until it is dropped.
163    pub fn stop(&self, name: &str) -> Result<()> {
164        let server = self.server(name).ok_or_else(|| Error::new(ErrorKind::DevNotFound)
165            .with_description(format!("Server {name:?} is not served by this context")))?;
166        let stopped = server.stop();
167        server.wait();
168        stopped
169    }
170
171    /// Serve that device, under the name it gives, until this context is dropped, through
172    /// the backend of this context, and export it in its database, which is where
173    /// [`Self::dev`] then finds it: any implementation of a device is tested that way, a
174    /// client of it above all, without any server around it.
175    ///
176    /// The device must be defined in the database first, with [`DatabaseProxy::add_dev`],
177    /// as a real database refuses to export a device it does not know. A device served
178    /// again under the same name replaces the one that was.
179    pub fn serve_dev(&self, dev: Arc<dyn BackendDev>) -> Result<()> {
180
181        let name = block_on(dev.name())?;
182        let mut devs = self.devs.lock().unwrap();
183
184        if let Some(index) = devs.iter().position(|(other, _)| other.eq_ignore_ascii_case(&name)) {
185            devs.remove(index);
186            self.own.remove(&name);
187        }
188
189        let itr = self.own.add(&name, Arc::clone(&dev))?;
190        let handle = local::register(&dev, Arc::clone(&itr), self.own.host(), self.own.port(), Arc::clone(&name));
191
192        let exported = self.database.export_dev(ExportDevInfo {
193            name: (&*name).into(),
194            itr: (&*itr).into(),
195            host: (&*self.own.host()).into(),
196            version: self.own.version().to_string().into(),
197            pid: std::process::id() as i32,
198        });
199
200        if let Err(err) = exported {
201            drop(handle);
202            self.own.remove(&name);
203            return Err(err);
204        }
205
206        devs.push((name, handle));
207        Ok(())
208
209    }
210
211    /// A proxy to the device of that name, found through the database of this context.
212    pub fn dev(&self, name: &str) -> DevProxy {
213        DevProxy::new_from_database_with_backend(Arc::clone(&self.backend), &self.database, name)
214    }
215
216    /// Whether the device of that name, as the database exports it, is reached directly by
217    /// this process, which it is when this process serves it, unless its server was built
218    /// with `local(false)`, see [`ServerBuilder::local`]: it is then reached through the
219    /// backend, as another process reaches it.
220    pub fn is_local(&self, name: &str) -> Result<bool> {
221        let info = self.database.import_dev(name)?;
222        Ok(local::find(info.itr.value()).is_some())
223    }
224
225    /// The device of that name, as the database exports it, reached as a client of this
226    /// process reaches it: directly if this process serves it, see [`Self::is_local`], unless
227    /// its server was built with `local(false)`, see [`ServerBuilder::local`], and through
228    /// the backend otherwise.
229    pub fn backend_dev(&self, name: &str) -> Result<Arc<dyn BackendDev>> {
230        let info = self.database.import_dev(name)?;
231        match local::find(info.itr.value()) {
232            Some(dev) => Ok(dev),
233            None => block_on(self.backend.connect(info.itr.value())),
234        }
235    }
236
237}
238
239impl Drop for TestContext {
240
241    /// Drop every server, then every device, then the database, with no side effect:
242    /// whatever was in flight is never answered, see [`BackendServer`].
243    fn drop(&mut self) {
244        self.servers.get_mut().unwrap().clear();
245        self.devs.get_mut().unwrap().clear();
246    }
247
248}
249
250#[cfg(test)]
251mod tests {
252
253    use super::*;
254
255    use crate::server::ClassDef;
256
257    /// Two servers in the same context, each reached through its database, and stopped
258    /// through the context.
259    #[test]
260    fn two_servers() {
261
262        let ctx = TestContext::new().unwrap();
263        ctx.database().add_server("Motor/test", &[("test/motor/1", "Motor"), ("test/motor/2", "Motor")]).unwrap();
264        ctx.database().add_server("Slit/test", &[("test/slit/1", "Slit")]).unwrap();
265
266        let motor = ctx.serve(ServerBuilder::new("Motor", "test").class(ClassDef::<()>::new_default("Motor"))).unwrap();
267        assert_ne!(motor.backend().unwrap().port(), 0);
268        ctx.serve(ServerBuilder::new("Slit", "test").class(ClassDef::<()>::new_default("Slit"))).unwrap();
269
270        for name in ["test/motor/1", "test/motor/2", "test/slit/1"] {
271            let dev = ctx.dev(name);
272            dev.ping().unwrap();
273            assert_eq!(&*dev.name().unwrap(), name);
274        }
275
276        let admin = ctx.dev("test/motor/1").admin_dev().unwrap();
277        assert_eq!(admin.name().unwrap().to_lowercase(), "dserver/motor/test");
278
279        ctx.stop("Motor/test").unwrap();
280        ctx.stop("Slit/test").unwrap();
281        let unexported = ctx.test_database().log().into_iter().filter(|(name, _)| name == "DbUnExportServer").count();
282        assert_eq!(unexported, 2);
283
284    }
285
286    /// The database and the devices of a server told not to be reached directly, reached
287    /// through the protocol, as another process does.
288    #[test]
289    fn not_local() {
290
291        let ctx = TestContext::new().unwrap();
292        ctx.database().add_server("Motor/test", &[("test/motor/1", "Motor")]).unwrap();
293        ctx.serve(ServerBuilder::new("Motor", "test").local(false).class(ClassDef::<()>::new_default("Motor"))).unwrap();
294
295        let database = block_on(default_backend().connect_at(ctx.host(), ctx.port(), DATABASE_DEV_NAME)).unwrap();
296        block_on(database.ping()).unwrap();
297
298        let itr = ctx.database().import_dev("test/motor/1").unwrap().itr;
299        assert!(local::find(itr.value()).is_none());
300
301        let dev = ctx.dev("test/motor/1");
302        assert_eq!(dev.admin_dev_name().unwrap().to_lowercase(), "dserver/motor/test");
303        let remote = ctx.backend_dev("test/motor/1").unwrap();
304        assert_eq!(block_on(remote.admin_dev_name()).unwrap().to_lowercase(), "dserver/motor/test");
305
306    }
307
308    /// A server stopped before the context, which keeps it, stopped.
309    #[test]
310    fn stop() {
311
312        let ctx = TestContext::new().unwrap();
313        ctx.database().add_server("Motor/test", &[("test/motor/1", "Motor")]).unwrap();
314        ctx.serve(ServerBuilder::new("Motor", "test").class(ClassDef::<()>::new_default("Motor"))).unwrap();
315
316        ctx.stop("motor/TEST").unwrap();
317        assert!(ctx.dev("test/motor/1").ping().is_err());
318        assert!(ctx.server("Motor/test").is_some());
319        assert!(ctx.stop("Other/test").is_err());
320
321    }
322
323    /// A device served on its own, which is a client of a device of the same name served by
324    /// another context, in a database of its own: reached directly by this process and
325    /// through the backend by any other, and no longer served once the context is dropped.
326    #[test]
327    fn serve_dev() {
328
329        let other = TestContext::new().unwrap();
330        other.database().add_server("Motor/other", &[("test/motor/1", "Motor")]).unwrap();
331        other.serve(ServerBuilder::new("Motor", "other").class(ClassDef::<()>::new_default("Motor"))).unwrap();
332        let dev = other.backend_dev("test/motor/1").unwrap();
333
334        let ctx = TestContext::new().unwrap();
335
336        // Not defined, so not exported.
337        assert!(ctx.serve_dev(Arc::clone(&dev)).is_err());
338
339        ctx.database().add_dev("test/motor/1", "Motor", "Motor/test").unwrap();
340        ctx.serve_dev(dev).unwrap();
341
342        // Directly, as this process does, the device being a client of the other one.
343        let proxy = ctx.dev("test/motor/1");
344        proxy.ping().unwrap();
345        assert_eq!(proxy.admin_dev_name().unwrap().to_lowercase(), "dserver/motor/other");
346
347        // Through the backend, as another process does.
348        let itr = ctx.database().import_dev("test/motor/1").unwrap().itr;
349        let remote = block_on(default_backend().connect(itr.value())).unwrap();
350        assert_eq!(block_on(remote.admin_dev_name()).unwrap().to_lowercase(), "dserver/motor/other");
351
352        // Nothing of the context is found anymore, while a client that found its database
353        // already holds it, see `local::register`.
354        let port = ctx.port();
355        drop(ctx);
356        assert!(local::find(itr.value()).is_none());
357        assert!(local::find_at("localhost", port, DATABASE_DEV_NAME).is_none());
358        other.dev("test/motor/1").ping().unwrap();
359
360    }
361
362}
363