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