#![allow(clippy::unwrap_used, clippy::expect_used, dead_code)] //! A BEAM node that lives as long as the test using it. use beamdev::connection::ConnectionManager; use std::process::{Child, Command, Stdio}; use std::sync::Arc; use std::sync::atomic::{AtomicU32, Ordering}; use std::time::{Duration, Instant}; /// How long to wait for a freshly spawned node to accept a connection. const BOOT_TIMEOUT: Duration = Duration::from_secs(30); pub const COOKIE: &str = "beamdev_test"; /// Which runtime starts the node. Both are BEAM nodes; only Elixir has the /// Elixir standard library loaded. #[derive(Debug, Clone, Copy)] pub enum Runtime { Erlang, Elixir, } impl Runtime { fn command(&self, node: &str) -> Command { let mut command = match self { Runtime::Erlang => { let mut c = Command::new("erl"); c.args(["-sname", node, "-setcookie", COOKIE, "-noshell", "-noinput"]); c } Runtime::Elixir => { let mut c = Command::new("elixir"); c.args(["--sname", node, "--cookie", COOKIE, "--no-halt", "-e", ""]); c } }; command .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::piped()); command } fn binary(&self) -> &'static str { match self { Runtime::Erlang => "erl", Runtime::Elixir => "elixir", } } } /// A node started for one test, and the manager bound to it. /// /// Both die when the test drops this: holding the manager means holding the /// node, so a test cannot outlive the BEAM it is talking to. pub struct TestNode { child: Child, pub name: String, manager: Arc, } impl Drop for TestNode { fn drop(&mut self) { let _ = self.child.kill(); let _ = self.child.wait(); } } /// The short hostname `-sname` will graft onto the node name. fn short_hostname() -> String { let out = Command::new("hostname") .output() .expect("`hostname` should be available to work out the node name"); String::from_utf8_lossy(&out.stdout) .trim() .split('.') .next() .expect("hostname should not be empty") .to_string() } /// A name no other test, and no other run on this machine, is using. fn unique(prefix: &str) -> String { static COUNTER: AtomicU32 = AtomicU32::new(0); format!( "{}_{}_{}", prefix, std::process::id(), COUNTER.fetch_add(1, Ordering::Relaxed) ) } impl TestNode { /// Starts a node, waits until it answers, and connects to it. /// /// Each test connects under its own local node name, so tests can run in /// parallel: a node rejects concurrent handshakes from the same peer name. pub async fn start(runtime: Runtime) -> Self { let short = unique("beamdev_test"); let name = format!("{}@{}", short, short_hostname()); let child = runtime.command(&short).spawn().unwrap_or_else(|e| { panic!( "could not start `{}`: {}. These tests need a BEAM runtime on PATH; \ `nix develop` provides one.", runtime.binary(), e ) }); let manager = Arc::new(ConnectionManager::new( format!("{}@{}", unique("mcp"), short_hostname()), name.clone(), )); let node = Self { child, name, manager, }; node.await_connection().await; node } /// The manager connected to this node. pub fn manager(&self) -> &ConnectionManager { &self.manager } /// The manager, for a test that needs to move it into a task. pub fn shared(&self) -> Arc { Arc::clone(&self.manager) } /// Retries until the node has finished booting and registered with epmd. async fn await_connection(&self) { let deadline = Instant::now() + BOOT_TIMEOUT; let mut last = String::new(); while Instant::now() < deadline { match self.manager.connect(COOKIE).await { Ok(()) => return, Err(e) => last = e.to_string(), } tokio::time::sleep(Duration::from_millis(200)).await; } panic!("{} never accepted a connection: {}", self.name, last); } }