//! # Keyforkd Test Utilities //! //! This module adds a helper to set up a Tokio runtime, start a Tokio runtime with a given seed, //! start a Keyfork server on that runtime, and run a given test closure. use crate::{middleware, Keyforkd, ServiceBuilder, UnixServer}; use tokio::runtime::Builder; use keyfork_bug::bug; #[derive(Debug, thiserror::Error)] #[error("This error can never be instantiated")] #[doc(hidden)] pub enum UninstantiableError {} /// A panicable result. This type can be used when a closure chooses to panic instead of /// returning an error. This doesn't necessarily mean a closure _has_ to panic, and its absence /// doesn't imply a closure _can't_ panic, but this is a useful utility function for writing tests, /// to avoid the necessity of making custom error types. /// /// ```rust /// use keyforkd::test_util::Panicable; /// let closure = || { /// Panicable::Ok(()) /// }; /// assert!(closure().is_ok()); /// ``` pub type Panicable = std::result::Result<(), UninstantiableError>; /// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a /// test suite, or (as shown in the example below) a simple seed may be used solely to ensure /// the server is capable of being interacted with. The test is in the form of a closure, expected /// to return a [`Result`] where success is a unit type (test passed) and the error is any error /// that happened during the test (alternatively, a panic may be used, and will be returned as an /// error). /// /// # Panics /// The function may panic if any errors arise while configuring and using the Tokio multithreaded /// runtime. /// /// # Examples /// The test utility provides a socket that can be connected to for deriving keys. /// /// ```rust /// use std::os::unix::net::UnixStream; /// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// keyforkd::test_util::run_test(seed.as_slice(), |path| { /// UnixStream::connect(&path).map(|_| ()) /// }).unwrap(); /// ``` /// /// The `keyforkd-client` crate uses the `KEYFORKD_SOCKET_PATH` variable to determine the default /// socket path. The test will export the environment variable so it may be used by default. /// /// ```rust /// use std::os::unix::net::UnixStream; /// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// keyforkd::test_util::run_test(seed.as_slice(), |path| { /// assert_eq!(std::env::var_os("KEYFORKD_SOCKET_PATH").unwrap(), path.as_os_str()); /// UnixStream::connect(&path).map(|_| ()) /// }).unwrap(); /// ``` #[allow(clippy::missing_errors_doc)] pub fn run_test(seed: &[u8], closure: F) -> std::result::Result<(), E> where F: FnOnce(&std::path::Path) -> std::result::Result<(), E> + Send + 'static, E: Send + 'static, { let rt = Builder::new_multi_thread() .worker_threads(2) .enable_io() .build() .expect(bug!( "can't make tokio threaded IO runtime, should be enabled via feature flags" )); let socket_dir = tempfile::tempdir().expect(bug!("can't create tempdir")); let socket_path = socket_dir.path().join("keyforkd.sock"); let result = rt.block_on(async move { let (tx, mut rx) = tokio::sync::mpsc::channel(1); let server_handle = tokio::spawn({ let socket_path = socket_path.clone(); let seed = seed.to_vec(); async move { let mut server = UnixServer::bind(&socket_path).expect(bug!("can't bind unix socket")); tx.send(()) .await .expect(bug!("couldn't send server start signal")); let service = ServiceBuilder::new() .layer(middleware::BincodeLayer::new()) .service(Keyforkd::new(seed.clone())); server .run(service) .await .expect(bug!("Unable to start service")); } }); rx.recv() .await .expect(bug!("can't receive server start signal from channel")); std::env::set_var("KEYFORKD_SOCKET_PATH", &socket_path); let test_handle = tokio::task::spawn_blocking(move || closure(&socket_path)); let result = test_handle.await; server_handle.abort(); result }); if let Err(e) = result { if let Ok(reason) = e.try_into_panic() { std::panic::resume_unwind(reason); } } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_run_test() { let seed = b"beefbeef"; run_test(seed, |_path| Panicable::Ok(())).expect("infallible"); } }