Skip to main content

rusteron_media_driver/
testing.rs

1//! Test/example support: an embedded media driver with RAII teardown.
2//!
3//! Replaces the five-line launch incantation and two-line teardown that tests and examples
4//! previously copied everywhere. The driver stops and joins on `Drop`, so teardown ordering
5//! lives in exactly one place.
6
7use crate::{Aeron, AeronCError, AeronDriver, AeronDriverContext, IntoCString};
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::Arc;
10use std::thread::JoinHandle;
11
12/// An embedded media driver running on a background thread, with a unique directory,
13/// stopped and joined automatically on `Drop`.
14///
15/// ```no_run
16/// # use rusteron_media_driver::testing::EmbeddedDriver;
17/// let driver = EmbeddedDriver::launch().unwrap();
18/// // connect clients against driver.dir() ...
19/// // driver stops + joins when it goes out of scope
20/// ```
21pub struct EmbeddedDriver {
22    dir: String,
23    context: AeronDriverContext,
24    stop: Arc<AtomicBool>,
25    handle: Option<JoinHandle<Result<(), AeronCError>>>,
26}
27
28impl EmbeddedDriver {
29    /// Launch an embedded driver in a unique directory (deleted on start and shutdown).
30    pub fn launch() -> Result<Self, AeronCError> {
31        Self::launch_with(|_| Ok(()))
32    }
33
34    /// Launch with extra [`AeronDriverContext`] configuration (timeouts, idle strategies…)
35    /// applied before the driver starts.
36    pub fn launch_with(
37        configure: impl FnOnce(&AeronDriverContext) -> Result<(), AeronCError>,
38    ) -> Result<Self, AeronCError> {
39        let context = AeronDriverContext::new()?;
40        context.set_dir_delete_on_shutdown(true)?;
41        context.set_dir_delete_on_start(true)?;
42        context.set_dir(&format!("{}{}", context.get_dir(), Aeron::nano_clock()).into_c_string())?;
43        configure(&context)?;
44        let dir = context.get_dir().to_string();
45        let (stop, handle) = AeronDriver::launch_embedded(context.clone(), false);
46        Ok(Self {
47            dir,
48            context,
49            stop,
50            handle: Some(handle),
51        })
52    }
53
54    /// The aeron directory clients should connect to.
55    #[inline]
56    pub fn dir(&self) -> &str {
57        &self.dir
58    }
59
60    /// The driver's context, e.g. for reading configured values.
61    #[inline]
62    pub fn context(&self) -> &AeronDriverContext {
63        &self.context
64    }
65
66    /// Stop the driver now instead of waiting for `Drop`.
67    pub fn stop(mut self) {
68        self.stop_and_join();
69    }
70
71    fn stop_and_join(&mut self) {
72        self.stop.store(true, Ordering::SeqCst);
73        if let Some(handle) = self.handle.take() {
74            let _ = handle.join();
75        }
76    }
77}
78
79impl Drop for EmbeddedDriver {
80    fn drop(&mut self) {
81        self.stop_and_join();
82    }
83}
84
85/// First free UDP port at or above `start` (binds a probe socket to check).
86pub fn find_unused_udp_port(start: u16) -> Option<u16> {
87    (start..65535).find(|p| std::net::UdpSocket::bind(("127.0.0.1", *p)).is_ok())
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn embedded_driver_launches_and_stops_on_drop() {
96        let driver = EmbeddedDriver::launch().unwrap();
97        assert!(!driver.dir().is_empty());
98        let dir = driver.dir().to_string();
99        drop(driver); // stops + joins; dir deleted on shutdown
100        assert!(!std::path::Path::new(&dir).join("cnc.dat").exists());
101    }
102
103    #[test]
104    fn find_unused_udp_port_returns_bindable_port() {
105        let port = find_unused_udp_port(23000).unwrap();
106        assert!(std::net::UdpSocket::bind(("127.0.0.1", port)).is_ok());
107    }
108}