From 8dcc07d78a57c6964e90d024a2c59c2bcbf5d02e Mon Sep 17 00:00:00 2001 From: Will Date: Sat, 14 Sep 2024 16:07:52 -0700 Subject: [PATCH] Refactor testing (#210) * Overhaul testing. --- .config/nextest.toml | 4 +- Cargo.toml | 4 +- README.md | 3 +- docs/contrib/closure_callbacks.md | 4 +- docs/features.md | 7 - docs/logging.md | 7 +- docs/quickstart.md | 2 +- examples/internal_client.rs | 2 +- examples/playback_capture.rs | 2 +- examples/set_transport.rs | 2 +- examples/show_midi.rs | 6 +- examples/show_transport.rs | 2 +- examples/sine.rs | 2 +- src/client/async_client.rs | 30 +-- src/client/callbacks.rs | 82 +++++-- src/client/client_impl.rs | 45 ++-- src/client/client_options.rs | 6 + src/client/mod.rs | 7 - src/client/test.rs | 262 -------------------- src/client/test_callback.rs | 256 -------------------- src/contrib/closure.rs | 2 +- src/jack_enums.rs | 70 +++++- src/lib.rs | 5 +- src/port/audio.rs | 57 +---- src/port/midi.rs | 317 +----------------------- src/port/mod.rs | 6 - src/port/test_client.rs | 388 ------------------------------ src/port/test_port.rs | 201 ---------------- src/properties.rs | 15 +- src/ringbuffer.rs | 144 +---------- src/tests/client.rs | 87 +++++++ src/tests/log.rs | 32 +++ src/tests/mod.rs | 11 + src/tests/processing.rs | 195 +++++++++++++++ src/tests/ringbuffer.rs | 93 +++++++ src/tests/time.rs | 30 +++ src/tests/transport.rs | 30 +++ src/transport.rs | 379 ++--------------------------- 38 files changed, 722 insertions(+), 2075 deletions(-) delete mode 100644 src/client/test.rs delete mode 100644 src/client/test_callback.rs delete mode 100644 src/port/test_client.rs delete mode 100644 src/port/test_port.rs create mode 100644 src/tests/client.rs create mode 100644 src/tests/log.rs create mode 100644 src/tests/mod.rs create mode 100644 src/tests/processing.rs create mode 100644 src/tests/ringbuffer.rs create mode 100644 src/tests/time.rs create mode 100644 src/tests/transport.rs diff --git a/.config/nextest.toml b/.config/nextest.toml index 5138ff906..f641fab8d 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -1,5 +1,5 @@ [profile.default] test-threads = 1 fail-fast = false -slow-timeout = { period = "30s", terminate-after = 4 } -retries = { backoff = "fixed", count = 2, delay = "1s" } \ No newline at end of file +slow-timeout = { period = "2s", terminate-after = 2 } +retries = { backoff = "fixed", count = 3, delay = "1s" } diff --git a/Cargo.toml b/Cargo.toml index 228310530..8f720a4c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,10 @@ libc = "0.2" log = { version = "0.4", optional = true} [dev-dependencies] +approx = "0.5" crossbeam-channel = "0.5" +ctor = "0.2" [features] default = ["dynamic_loading", "log"] -dynamic_loading = ["jack-sys/dynamic_loading"] \ No newline at end of file +dynamic_loading = ["jack-sys/dynamic_loading"] diff --git a/README.md b/README.md index 8d5d369e0..776acd754 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ Rust bindings for [JACK Audio Connection Kit](). The JACK server is usually started by the user or system. Clients can request that the JACK server is started on demand when they connect, but this can be -disabled by creating a client with the `NO_START_SERVER` option. +disabled by creating a client with the `NO_START_SERVER` option or +`ClientOptions::default()`. - Linux and BSD users may install JACK1, JACK2 (preferred for low latency), or Pipewire JACK (preferred for ease of use) from their system package manager. diff --git a/docs/contrib/closure_callbacks.md b/docs/contrib/closure_callbacks.md index dd4268273..c154c1807 100644 --- a/docs/contrib/closure_callbacks.md +++ b/docs/contrib/closure_callbacks.md @@ -18,7 +18,7 @@ contains captures the required state and then activating it. ```rust // 1. Create the client. let (client, _status) = - jack::Client::new("silence", jack::ClientOptions::NO_START_SERVER).unwrap(); + jack::Client::new("silence", jack::ClientOptions::default()).unwrap(); // 2. Define the state. let mut output = client.register_port("out", jack::AudioOut::default()); @@ -45,7 +45,7 @@ buffer size. ```rust // 1. Create the client. let (client, _status) = - jack::Client::new("silence", jack::ClientOptions::NO_START_SERVER).unwrap(); + jack::Client::new("silence", jack::ClientOptions::default()).unwrap(); // 2. Define the state. struct State { diff --git a/docs/features.md b/docs/features.md index bbfb2d9b3..0d8b98792 100644 --- a/docs/features.md +++ b/docs/features.md @@ -37,10 +37,3 @@ Default: Yes Load `libjack` at runtime as opposed to the standard dynamic linking. This is preferred as it allows `pw-jack` to intercept the loading at runtime to provide the Pipewire JACK server implementation. - -## `metadata` - -Default: No - -Provides access to the metadata API. This is experimental. Details on the JACK -metadata API can be found at . diff --git a/docs/logging.md b/docs/logging.md index 9184d3fb7..0a8fcab01 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -27,15 +27,16 @@ enabled by default. The `log` crate provides a *facade* for logging; it provides macros to perform logging, but another mechanism or crate is required to actually perform the logging. -In the example below, we use the [`env_logger` crate]() to display logging for -info and error severity level messages. +In the example below, we use the [`env_logger` +crate](https://crates.io/crates/env_logger) to display logging for info and +error severity level messages. ```rust env_logger::builder().filter(None, log::LevelFilter::Info).init(); // JACK may log things to `info!` or `error!`. let (client, _status) = - jack::Client::new("rust_jack_simple", jack::ClientOptions::NO_START_SERVER).unwrap(); + jack::Client::new("rust_jack_simple", jack::ClientOptions::default()).unwrap(); ``` diff --git a/docs/quickstart.md b/docs/quickstart.md index a9c795794..a5bdb7d89 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -55,7 +55,7 @@ program that can take inputs and forward them to outputs. fn main() { // 1. Create client let (client, _status) = - jack::Client::new("rust_jack_simple", jack::ClientOptions::NO_START_SERVER).unwrap(); + jack::Client::new("rust_jack_simple", jack::ClientOptions::default()).unwrap(); // 2. Register ports. They will be used in a callback that will be // called when new data is available. diff --git a/examples/internal_client.rs b/examples/internal_client.rs index 7125ca867..8de2a06c6 100644 --- a/examples/internal_client.rs +++ b/examples/internal_client.rs @@ -6,7 +6,7 @@ fn main() { // Create client let (client, _status) = jack::Client::new( "rust_jack_internal_client_tester", - jack::ClientOptions::NO_START_SERVER, + jack::ClientOptions::default(), ) .unwrap(); diff --git a/examples/playback_capture.rs b/examples/playback_capture.rs index 38ccc28f5..a15f7aaf0 100644 --- a/examples/playback_capture.rs +++ b/examples/playback_capture.rs @@ -6,7 +6,7 @@ fn main() { // Create client jack::set_logger(jack::LoggerType::Stdio); let (client, _status) = - jack::Client::new("rust_jack_simple", jack::ClientOptions::NO_START_SERVER).unwrap(); + jack::Client::new("rust_jack_simple", jack::ClientOptions::default()).unwrap(); // Register ports. They will be used in a callback that will be // called when new data is available. diff --git a/examples/set_transport.rs b/examples/set_transport.rs index 30b18a415..cda4dd3fd 100644 --- a/examples/set_transport.rs +++ b/examples/set_transport.rs @@ -2,7 +2,7 @@ use std::env; fn main() { let (client, _status) = - jack::Client::new("rust_jack_trans", jack::ClientOptions::NO_START_SERVER).unwrap(); + jack::Client::new("rust_jack_trans", jack::ClientOptions::default()).unwrap(); let transport = client.transport(); diff --git a/examples/show_midi.rs b/examples/show_midi.rs index 01a6170c1..f1acd87e2 100644 --- a/examples/show_midi.rs +++ b/examples/show_midi.rs @@ -43,17 +43,17 @@ impl std::fmt::Debug for MidiCopy { fn main() { // Open the client. let (client, _status) = - jack::Client::new("rust_jack_show_midi", jack::ClientOptions::NO_START_SERVER).unwrap(); + jack::Client::new("rust_jack_show_midi", jack::ClientOptions::default()).unwrap(); // Create a sync channel to send back copies of midi messages we get. let (sender, receiver) = sync_channel(64); // Define process logic. let mut maker = client - .register_port("rust_midi_maker", jack::MidiOut) + .register_port("rust_midi_maker", jack::MidiOut::default()) .unwrap(); let shower = client - .register_port("rust_midi_shower", jack::MidiIn) + .register_port("rust_midi_shower", jack::MidiIn::default()) .unwrap(); let cback = move |_: &jack::Client, ps: &jack::ProcessScope| -> jack::Control { let show_p = shower.iter(ps); diff --git a/examples/show_transport.rs b/examples/show_transport.rs index e2ba2948e..87368940e 100644 --- a/examples/show_transport.rs +++ b/examples/show_transport.rs @@ -7,7 +7,7 @@ use std::sync::{ fn main() { // Create client let (client, _status) = - jack::Client::new("rust_jack_trans", jack::ClientOptions::NO_START_SERVER).unwrap(); + jack::Client::new("rust_jack_trans", jack::ClientOptions::default()).unwrap(); let transport = client.transport(); let stop = Arc::new(AtomicBool::new(false)); diff --git a/examples/sine.rs b/examples/sine.rs index 10d78cc61..cee000308 100644 --- a/examples/sine.rs +++ b/examples/sine.rs @@ -8,7 +8,7 @@ use std::str::FromStr; fn main() { // 1. open a client let (client, _status) = - jack::Client::new("rust_jack_sine", jack::ClientOptions::NO_START_SERVER).unwrap(); + jack::Client::new("rust_jack_sine", jack::ClientOptions::default()).unwrap(); // 2. register port let out_port = client diff --git a/src/client/async_client.rs b/src/client/async_client.rs index e09ba22bd..fc5566510 100644 --- a/src/client/async_client.rs +++ b/src/client/async_client.rs @@ -7,7 +7,7 @@ use std::sync::atomic::AtomicBool; use super::callbacks::clear_callbacks; use super::callbacks::{CallbackContext, NotificationHandler, ProcessHandler}; use crate::client::client_impl::Client; -use crate::client::common::{sleep_on_test, CREATE_OR_DESTROY_CLIENT_MUTEX}; +use crate::client::common::CREATE_OR_DESTROY_CLIENT_MUTEX; use crate::Error; /// A JACK client that is processing data asynchronously, in real-time. @@ -20,7 +20,7 @@ use crate::Error; /// ``` /// // Create a client and a handler /// let (client, _status) = -/// jack::Client::new("my_client", jack::ClientOptions::NO_START_SERVER).unwrap(); +/// jack::Client::new("my_client", jack::ClientOptions::default()).unwrap(); /// let process_handler = jack::contrib::ClosureProcessHandler::new( /// move |_: &jack::Client, _: &jack::ProcessScope| jack::Control::Continue, /// ); @@ -56,19 +56,15 @@ where pub fn new(client: Client, notification_handler: N, process_handler: P) -> Result { let _m = CREATE_OR_DESTROY_CLIENT_MUTEX.lock().ok(); unsafe { - sleep_on_test(); let mut callback_context = Box::new(CallbackContext { client, notification: notification_handler, process: process_handler, - is_valid: AtomicBool::new(true), + is_valid_for_callback: AtomicBool::new(true), + has_panic: AtomicBool::new(false), }); CallbackContext::register_callbacks(&mut callback_context)?; - sleep_on_test(); let res = j::jack_activate(callback_context.client.raw()); - for _ in 0..4 { - sleep_on_test(); - } match res { 0 => Ok(AsyncClient { callback: Some(callback_context), @@ -114,25 +110,21 @@ impl AsyncClient { return Err(Error::ClientIsNoLongerAlive); } let cb = self.callback.take().ok_or(Error::ClientIsNoLongerAlive)?; - let client = cb.client.raw(); + let client_ptr = cb.client.raw(); // deactivate - sleep_on_test(); - if j::jack_deactivate(client) != 0 { + if j::jack_deactivate(client_ptr) != 0 { return Err(Error::ClientDeactivationError); } // clear the callbacks - sleep_on_test(); - clear_callbacks(client)?; + clear_callbacks(client_ptr)?; // done, take ownership of callback - if cb.is_valid.load(std::sync::atomic::Ordering::Relaxed) { - Ok(cb) - } else { - std::mem::forget(cb.notification); - std::mem::forget(cb.process); - Err(Error::ClientIsNoLongerAlive) + if cb.has_panic.load(std::sync::atomic::Ordering::Relaxed) { + std::mem::forget(cb); + return Err(Error::ClientPanicked); } + Ok(cb) } } diff --git a/src/client/callbacks.rs b/src/client/callbacks.rs index eb34d2534..307d5be73 100644 --- a/src/client/callbacks.rs +++ b/src/client/callbacks.rs @@ -134,7 +134,9 @@ where ctx.notification.thread_init(&ctx.client); }); if let Err(err) = res { - CallbackContext::::from_raw(data).map(CallbackContext::mark_invalid); + if let Some(ctx) = CallbackContext::::from_raw(data) { + ctx.mark_invalid(true) + } eprintln!("{err:?}"); std::mem::forget(err); } @@ -160,7 +162,9 @@ unsafe extern "C" fn shutdown( ); }); if let Err(err) = res { - CallbackContext::::from_raw(data).map(CallbackContext::mark_invalid); + if let Some(ctx) = CallbackContext::::from_raw(data) { + ctx.mark_invalid(true) + } eprintln!("{err:?}"); std::mem::forget(err); } @@ -178,14 +182,17 @@ where let scope = ProcessScope::from_raw(n_frames, ctx.client.raw()); let c = ctx.process.process(&ctx.client, &scope); if c == Control::Quit { - ctx.mark_invalid(); + ctx.mark_invalid(false); } c }); match res { Ok(res) => res.to_ffi(), Err(err) => { - CallbackContext::::from_raw(data).map(CallbackContext::mark_invalid); + eprintln!("harhar"); + if let Some(ctx) = CallbackContext::::from_raw(data) { + ctx.mark_invalid(true) + } eprintln!("{err:?}"); std::mem::forget(err); Control::Quit.to_ffi() @@ -212,7 +219,7 @@ where &*(pos as *mut crate::TransportPosition), ); if !is_ready { - ctx.mark_invalid(); + ctx.mark_invalid(false); } is_ready }); @@ -220,7 +227,9 @@ where Ok(true) => 1, Ok(false) => 0, Err(err) => { - CallbackContext::::from_raw(data).map(CallbackContext::mark_invalid); + if let Some(ctx) = CallbackContext::::from_raw(data) { + ctx.mark_invalid(true) + } eprintln!("{err:?}"); std::mem::forget(err); 0 @@ -241,7 +250,9 @@ where ctx.notification.freewheel(&ctx.client, is_starting); }); if let Err(err) = res { - CallbackContext::::from_raw(data).map(CallbackContext::mark_invalid); + if let Some(ctx) = CallbackContext::::from_raw(data) { + ctx.mark_invalid(true) + } eprintln!("{err:?}"); std::mem::forget(err); } @@ -258,14 +269,16 @@ where }; let c = ctx.process.buffer_size(&ctx.client, n_frames); if c == Control::Quit { - ctx.mark_invalid(); + ctx.mark_invalid(false); } c }); match res { Ok(c) => c.to_ffi(), Err(err) => { - CallbackContext::::from_raw(data).map(CallbackContext::mark_invalid); + if let Some(ctx) = CallbackContext::::from_raw(data) { + ctx.mark_invalid(true) + } eprintln!("{err:?}"); std::mem::forget(err); Control::Quit.to_ffi() @@ -284,14 +297,16 @@ where }; let c = ctx.notification.sample_rate(&ctx.client, n_frames); if c == Control::Quit { - ctx.mark_invalid(); + ctx.mark_invalid(false); } c }); match res { Ok(c) => c.to_ffi(), Err(err) => { - CallbackContext::::from_raw(data).map(CallbackContext::mark_invalid); + if let Some(ctx) = CallbackContext::::from_raw(data) { + ctx.mark_invalid(true) + } eprintln!("{err:?}"); std::mem::forget(err); Control::Quit.to_ffi() @@ -317,7 +332,9 @@ unsafe extern "C" fn client_registration( .client_registration(&ctx.client, name, register); }); if let Err(err) = res { - CallbackContext::::from_raw(data).map(CallbackContext::mark_invalid); + if let Some(ctx) = CallbackContext::::from_raw(data) { + ctx.mark_invalid(true) + } eprintln!("{err:?}"); std::mem::forget(err); } @@ -340,7 +357,9 @@ unsafe extern "C" fn port_registration( .port_registration(&ctx.client, port_id, register); }); if let Err(err) = res { - CallbackContext::::from_raw(data).map(CallbackContext::mark_invalid); + if let Some(ctx) = CallbackContext::::from_raw(data) { + ctx.mark_invalid(true) + } eprintln!("{err:?}"); std::mem::forget(err); } @@ -367,14 +386,16 @@ where .notification .port_rename(&ctx.client, port_id, old_name, new_name); if c == Control::Quit { - ctx.mark_invalid(); + ctx.mark_invalid(false); } c }); match res { Ok(c) => c.to_ffi(), Err(err) => { - CallbackContext::::from_raw(data).map(CallbackContext::mark_invalid); + if let Some(ctx) = CallbackContext::::from_raw(data) { + ctx.mark_invalid(true) + } eprintln!("{err:?}"); std::mem::forget(err); Control::Quit.to_ffi() @@ -400,7 +421,9 @@ unsafe extern "C" fn port_connect( .ports_connected(&ctx.client, port_id_a, port_id_b, are_connected); }); if let Err(err) = res { - CallbackContext::::from_raw(data).map(CallbackContext::mark_invalid); + if let Some(ctx) = CallbackContext::::from_raw(data) { + ctx.mark_invalid(true) + } eprintln!("{err:?}"); std::mem::forget(err); } @@ -417,14 +440,16 @@ where }; let c = ctx.notification.graph_reorder(&ctx.client); if c == Control::Quit { - ctx.mark_invalid(); + ctx.mark_invalid(false); } c }); match res { Ok(c) => c.to_ffi(), Err(err) => { - CallbackContext::::from_raw(data).map(CallbackContext::mark_invalid); + if let Some(ctx) = CallbackContext::::from_raw(data) { + ctx.mark_invalid(true) + } eprintln!("{err:?}"); std::mem::forget(err); Control::Quit.to_ffi() @@ -443,14 +468,16 @@ where }; let c = ctx.notification.xrun(&ctx.client); if c == Control::Quit { - ctx.mark_invalid(); + ctx.mark_invalid(false); } c }); match res { Ok(c) => c.to_ffi(), Err(err) => { - CallbackContext::::from_raw(data).map(CallbackContext::mark_invalid); + if let Some(ctx) = CallbackContext::::from_raw(data) { + ctx.mark_invalid(true) + } eprintln!("{err:?}"); std::mem::forget(err); Control::Quit.to_ffi() @@ -487,10 +514,12 @@ pub struct CallbackContext { pub notification: N, /// The handler for processing. pub process: P, - /// True if the callback is valid. + /// True if the callback is valid for callbacks. /// - /// This becomes false after a panic. - pub is_valid: AtomicBool, + /// This becomes false after quit an event that causes processing to quit. + pub is_valid_for_callback: AtomicBool, + /// True if the callback has panicked. + pub has_panic: AtomicBool, } impl CallbackContext @@ -502,7 +531,7 @@ where debug_assert!(!ptr.is_null()); let obj_ptr = ptr as *mut CallbackContext; let obj_ref = &mut *obj_ptr; - if obj_ref.is_valid.load(Ordering::Relaxed) { + if obj_ref.is_valid_for_callback.load(Ordering::Relaxed) { Some(obj_ref) } else { None @@ -513,8 +542,9 @@ where /// /// This usually happens after a panic. #[cold] - pub fn mark_invalid(&mut self) { - self.is_valid.store(true, Ordering::Relaxed); + pub fn mark_invalid(&mut self, did_panic: bool) { + self.is_valid_for_callback.store(true, Ordering::Relaxed); + self.has_panic.store(did_panic, Ordering::Relaxed); } fn raw(b: &mut Box) -> *mut libc::c_void { diff --git a/src/client/client_impl.rs b/src/client/client_impl.rs index 611a35a2e..4490ea3aa 100644 --- a/src/client/client_impl.rs +++ b/src/client/client_impl.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use std::{ffi, fmt, ptr}; use crate::client::common::{sleep_on_test, CREATE_OR_DESTROY_CLIENT_MUTEX}; +use crate::jack_enums::CodeOrMessage; use crate::jack_utils::collect_strs; use crate::properties::PropertyChangeHandler; use crate::transport::Transport; @@ -16,7 +17,7 @@ use crate::{ /// /// # Example /// ``` -/// let c_res = jack::Client::new("rusty_client", jack::ClientOptions::NO_START_SERVER); +/// let c_res = jack::Client::new("rusty_client", jack::ClientOptions::default()); /// match c_res { /// Ok((client, status)) => println!( /// "Managed to open client {}, with @@ -407,17 +408,11 @@ impl Client { /// (not the process callback). The return value can be compared with the value of /// `last_frame_time` to relate time in other threads to JACK time. To obtain better time /// information from within the process callback, see `ProcessScope`. - /// - /// # TODO - /// - test pub fn frame_time(&self) -> Frames { unsafe { j::jack_frame_time(self.raw()) } } /// The estimated time in microseconds of the specified frame time - /// - /// # TODO - /// - Improve test pub fn frames_to_time(&self, n_frames: Frames) -> Time { unsafe { j::jack_frames_to_time(self.raw(), n_frames) } } @@ -498,8 +493,7 @@ impl Client { /// 4. Both ports must be owned by active clients. /// /// # Panics - /// Panics if it is not possible to convert `source_port` or - /// `destination_port` to a `CString`. + /// Panics if it is not possible to convert `source_port` or `destination_port` to a `CString`. pub fn connect_ports_by_name( &self, source_port: &str, @@ -507,7 +501,6 @@ impl Client { ) -> Result<(), Error> { let source_cstr = ffi::CString::new(source_port).unwrap(); let destination_cstr = ffi::CString::new(destination_port).unwrap(); - let res = unsafe { j::jack_connect(self.raw(), source_cstr.as_ptr(), destination_cstr.as_ptr()) }; match res { @@ -516,10 +509,32 @@ impl Client { source_port.to_string(), destination_port.to_string(), )), - _ => Err(Error::PortConnectionError( - source_port.to_string(), - destination_port.to_string(), - )), + code => { + let code_or_message = if self + .port_by_name(source_port) + .map(|p| p.flags().contains(PortFlags::IS_INPUT)) + .unwrap_or(false) + { + CodeOrMessage::Message( + "source port does not produce a signal, it is not an input port", + ) + } else if self + .port_by_name(destination_port) + .map(|p| p.flags().contains(PortFlags::IS_OUTPUT)) + .unwrap_or(false) + { + CodeOrMessage::Message( + "destination port cannot be written to, it is not an output port", + ) + } else { + CodeOrMessage::Code(code) + }; + Err(Error::PortConnectionError { + source: source_port.to_string(), + destination: destination_port.to_string(), + code_or_message, + }) + } } } @@ -633,7 +648,7 @@ impl Client { /// /// # Panics /// Calling this method more than once on any given client with cause a panic. - pub fn register_property_change_handler( + pub fn register_property_change_handler( &mut self, handler: H, ) -> Result<(), Error> { diff --git a/src/client/client_options.rs b/src/client/client_options.rs index d7d66b510..84659e176 100644 --- a/src/client/client_options.rs +++ b/src/client/client_options.rs @@ -32,3 +32,9 @@ bitflags! { const SESSION_ID = j::JackSessionID; } } + +impl Default for ClientOptions { + fn default() -> Self { + ClientOptions::NO_START_SERVER + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs index e78d2705d..b337131ce 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -19,10 +19,3 @@ pub use self::common::CLIENT_NAME_SIZE; #[allow(deprecated)] pub use self::handler_impls::ClosureProcessHandler; - -// client.rs excluding functionality that involves ports or callbacks -#[cfg(test)] -mod test; - -#[cfg(test)] -mod test_callback; diff --git a/src/client/test.rs b/src/client/test.rs deleted file mode 100644 index bc7f6c4b1..000000000 --- a/src/client/test.rs +++ /dev/null @@ -1,262 +0,0 @@ -use crate::client::*; -use crate::contrib::ClosureProcessHandler; -use crate::jack_enums::Error; -use crate::{Control, RingBuffer}; - -fn open_test_client(name: &str) -> (Client, ClientStatus) { - Client::new(name, ClientOptions::NO_START_SERVER).unwrap() -} - -#[test] -fn time_can_get_time() { - open_test_client("tcgt").0.time(); -} - -#[test] -fn time_is_monotonically_increasing() { - let c = open_test_client("tcgt").0; - let initial_t = c.time(); - std::thread::sleep(std::time::Duration::from_millis(100)); - let later_t = c.time(); - assert!(initial_t < later_t); -} - -#[test] -fn client_valid_client_name_size() { - assert!(*CLIENT_NAME_SIZE > 0); -} - -#[test] -fn client_can_open() { - open_test_client("client_can_open"); -} - -#[test] -#[should_panic] -fn client_fails_to_open_with_large_name() { - let name = (0..=*CLIENT_NAME_SIZE) - .map(|_| "a") - .collect::>() - .join("_"); - Client::new(&name, ClientOptions::NO_START_SERVER).unwrap(); - // fails on travis, switched to should_panic for a catch all - // assert_eq!(Client::new(&name, ClientOptions::NO_START_SERVER).err(), - // Some(Error::ClientError(client_status::FAILURE | - // client_status::SERVER_ERROR))); -} - -#[test] -fn client_can_be_named() { - let name = "client_can_be_named"; - let (c, _) = open_test_client(name); - assert_eq!(c.name(), name); -} - -#[test] -fn client_can_activate() { - let (c, _) = open_test_client("client_can_activate"); - let _ac = c.activate_async((), ()).unwrap(); -} - -#[test] -fn client_can_set_buffer_size() { - let (c, _) = open_test_client("client_can_set_buffer_size"); - let initial_size = c.buffer_size(); - let new_size = 2 * initial_size; - c.set_buffer_size(new_size).unwrap(); - assert_eq!(c.buffer_size(), new_size); - c.set_buffer_size(initial_size).unwrap(); - assert_eq!(c.buffer_size(), initial_size); -} - -#[test] -fn client_detects_bad_buffer_size() { - let (c, _) = open_test_client("client_detects_bad_buffer_size"); - let initial_size = c.buffer_size(); - assert_eq!(c.set_buffer_size(0), Err(Error::SetBufferSizeError)); - c.set_buffer_size(initial_size).unwrap(); - assert_eq!(c.buffer_size(), initial_size); -} - -#[test] -fn client_can_deactivate() { - let (c, _) = open_test_client("client_can_deactivate"); - let a = c.activate_async((), ()).unwrap(); - a.deactivate().unwrap(); -} - -#[test] -fn client_knows_buffer_size() { - let (c, _) = open_test_client("client_knows_buffer_size"); - assert!(c.buffer_size() > 0); -} - -#[test] -fn client_knows_sample_rate() { - let (c, _) = open_test_client("client_knows_sample_rate"); - // 44100 - As started by dummy_jack_server.sh - assert_eq!(c.sample_rate(), 44100); -} - -#[test] -fn client_knows_cpu_load() { - let (c, _) = open_test_client("client_knows_cpu_load"); - let _load = c.cpu_load(); -} - -#[test] -fn client_can_estimate_frame_times() { - let (c, _) = open_test_client("client_knows_frame_times"); - let current_frame_time = c.frame_time(); - let time = c.frames_to_time(44_100); - let frames = c.time_to_frames(1_000_000); - assert!(current_frame_time > 0); - assert!(time > 0); - assert!(frames > 0); -} - -#[test] -fn client_debug_printing() { - let (c, _) = open_test_client("client_has_debug_string"); - let got = format!("{c:?}"); - assert_ne!("", got); -} - -#[test] -fn client_can_use_ringbuffer() { - let (c, _) = open_test_client("client_can_use_ringbuffer"); - - let ringbuf = RingBuffer::new(1024).unwrap(); - let (mut reader, mut writer) = ringbuf.into_reader_writer(); - - let buf = [0_u8, 1, 2, 3]; - let mut sent = false; - let _a = c - .activate_async( - (), - ClosureProcessHandler::new(move |_, _| { - if !sent { - for (item, bufitem) in writer.peek_iter().zip(buf.iter()) { - *item = *bufitem; - } - - writer.advance(buf.len()); - sent = true; - } - Control::Continue - }), - ) - .unwrap(); - - // spin until realtime closure has been run - while reader.space() == 0 {} - - let mut outbuf = [0_u8; 8]; - let num = reader.read_buffer(&mut outbuf); - assert_eq!(num, buf.len()); - - assert_eq!(outbuf[..num], buf[..]); -} - -#[test] -fn client_uuid() { - let (c1, _) = open_test_client("uuidtest-client1"); - let (c2, _) = open_test_client("uuidtest-client2"); - - let uuid1s = c1.uuid_string(); - let uuid2s = c2.uuid_string(); - assert_ne!(uuid1s, uuid2s); - - assert_eq!( - c1.name_by_uuid_str(&uuid1s), - Some("uuidtest-client1".to_string()) - ); - assert_eq!( - c2.name_by_uuid_str(&uuid1s), - Some("uuidtest-client1".to_string()) - ); - - assert_eq!( - c1.name_by_uuid_str(&uuid2s), - Some("uuidtest-client2".to_string()) - ); - assert_eq!( - c2.name_by_uuid_str(&uuid2s), - Some("uuidtest-client2".to_string()) - ); - - //create and then dealloc a client, get the uuid. - let uuid3s = { - let (c3, _) = open_test_client("uuidtest-client3"); - c3.uuid_string() - }; - assert_eq!(c1.name_by_uuid_str(&uuid3s), None); - assert_eq!(c2.name_by_uuid_str(&uuid3s), None); -} - -#[test] -fn client_numeric_uuid() { - let (c1, _) = open_test_client("numeric-uuid-client1"); - let (c2, _) = open_test_client("numeric-uuid-client2"); - - let ac1 = c1.activate_async((), ()).unwrap(); - let ac2 = c2.activate_async((), ()).unwrap(); - - let c1 = ac1.as_client(); - let c2 = ac2.as_client(); - - let uuid1 = c1.uuid(); - let uuid2 = c2.uuid(); - assert_ne!(uuid1, uuid2); - assert_ne!(0, uuid1); - assert_ne!(0, uuid2); - - let uuid1s = c1.uuid_string(); - let uuid2s = c2.uuid_string(); - assert_ne!(uuid1s, uuid2s); - - assert_eq!(c1.name_by_uuid(0), None); - assert_eq!(c2.name_by_uuid(0), None); - - assert_eq!( - c1.name_by_uuid(uuid1), - Some("numeric-uuid-client1".to_string()) - ); - assert_eq!( - c2.name_by_uuid(uuid1), - Some("numeric-uuid-client1".to_string()) - ); - assert_eq!( - c1.name_by_uuid_str(&uuid1s), - Some("numeric-uuid-client1".to_string()) - ); - assert_eq!( - c2.name_by_uuid_str(&uuid1s), - Some("numeric-uuid-client1".to_string()) - ); - - assert_eq!( - c1.name_by_uuid(uuid2), - Some("numeric-uuid-client2".to_string()) - ); - assert_eq!( - c2.name_by_uuid(uuid2), - Some("numeric-uuid-client2".to_string()) - ); - assert_eq!( - c1.name_by_uuid_str(&uuid2s), - Some("numeric-uuid-client2".to_string()) - ); - assert_eq!( - c2.name_by_uuid_str(&uuid2s), - Some("numeric-uuid-client2".to_string()) - ); - - //create and then dealloc a client, get the uuid. - let uuid3 = { - let (c3, _) = open_test_client("numeric-uuid-client3"); - c3.uuid() - }; - assert_eq!(c1.name_by_uuid(uuid3), None); - assert_eq!(c2.name_by_uuid(uuid3), None); -} diff --git a/src/client/test_callback.rs b/src/client/test_callback.rs deleted file mode 100644 index fe0f9ae81..000000000 --- a/src/client/test_callback.rs +++ /dev/null @@ -1,256 +0,0 @@ -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::{ptr, thread, time}; - -use super::*; -use crate::{AudioIn, Client, Control, Frames, NotificationHandler, PortId, ProcessHandler}; - -#[derive(Debug, Default)] -pub struct Counter { - pub induce_xruns: bool, - pub thread_init_count: AtomicUsize, - pub frames_processed: usize, - pub process_thread: Option, - pub buffer_size_thread_history: Vec, - pub buffer_size_change_history: Vec, - pub registered_client_history: Vec, - pub unregistered_client_history: Vec, - pub port_register_history: Vec, - pub port_unregister_history: Vec, - pub xruns_count: usize, - pub last_frame_time: Frames, - pub frames_since_cycle_start: Frames, -} - -impl NotificationHandler for Counter { - fn thread_init(&self, _: &Client) { - self.thread_init_count.fetch_add(1, Ordering::Relaxed); - } - - fn client_registration(&mut self, _: &Client, name: &str, is_registered: bool) { - if is_registered { - self.registered_client_history.push(name.to_string()) - } else { - self.unregistered_client_history.push(name.to_string()) - } - } - - fn port_registration(&mut self, _: &Client, pid: PortId, is_registered: bool) { - if is_registered { - self.port_register_history.push(pid) - } else { - self.port_unregister_history.push(pid) - } - } - - fn xrun(&mut self, _: &Client) -> Control { - self.xruns_count += 1; - Control::Continue - } -} - -impl ProcessHandler for Counter { - fn process(&mut self, _: &Client, ps: &ProcessScope) -> Control { - self.frames_processed += ps.n_frames() as usize; - self.last_frame_time = ps.last_frame_time(); - self.frames_since_cycle_start = ps.frames_since_cycle_start(); - let _cycle_times = ps.cycle_times(); - if self.induce_xruns { - thread::sleep(time::Duration::from_millis(100)); - self.induce_xruns = false; - } - self.process_thread = Some(thread::current().id()); - Control::Continue - } - - fn buffer_size(&mut self, _: &Client, size: Frames) -> Control { - self.buffer_size_change_history.push(size); - self.buffer_size_thread_history.push(thread::current().id()); - Control::Continue - } -} - -fn open_test_client(name: &str) -> Client { - Client::new(name, ClientOptions::NO_START_SERVER).unwrap().0 -} - -fn active_test_client(name: &str) -> AsyncClient { - let c = open_test_client(name); - c.activate_async(Counter::default(), Counter::default()) - .unwrap() -} - -#[test] -fn client_cback_has_proper_default_callbacks() { - // defaults shouldn't care about these params - let wc = unsafe { Client::from_raw(ptr::null_mut()) }; - let ps = unsafe { ProcessScope::from_raw(0, ptr::null_mut()) }; - // check each callbacks - ().thread_init(&wc); - unsafe { ().shutdown(client_status::ClientStatus::empty(), "mock") }; - assert_eq!(().process(&wc, &ps), Control::Continue); - ().freewheel(&wc, true); - ().freewheel(&wc, false); - assert_eq!(().buffer_size(&wc, 0), Control::Continue); - assert_eq!(().sample_rate(&wc, 0), Control::Continue); - ().client_registration(&wc, "mock", true); - ().client_registration(&wc, "mock", false); - ().port_registration(&wc, 0, true); - ().port_registration(&wc, 0, false); - assert_eq!( - ().port_rename(&wc, 0, "old_mock", "new_mock"), - Control::Continue - ); - ().ports_connected(&wc, 0, 1, true); - ().ports_connected(&wc, 2, 3, false); - assert_eq!(().graph_reorder(&wc), Control::Continue); - assert_eq!(().xrun(&wc), Control::Continue); - - std::mem::forget(wc); -} - -#[test] -fn client_cback_calls_thread_init() { - let ac = active_test_client("client_cback_calls_thread_init"); - let counter = ac.deactivate().unwrap().1; - // IDK why this isn't 1, even with a single thread. - assert!(counter.thread_init_count.load(Ordering::Relaxed) > 0); -} - -#[test] -fn client_cback_calls_process() { - let ac = active_test_client("client_cback_calls_process"); - std::thread::sleep(std::time::Duration::from_secs(1)); - let counter = ac.deactivate().unwrap().2; - assert!(counter.frames_processed > 0); - assert!(counter.last_frame_time > 0); - assert!(counter.frames_since_cycle_start > 0); -} - -#[test] -fn client_cback_calls_buffer_size() { - let ac = active_test_client("client_cback_calls_buffer_size"); - let initial = ac.as_client().buffer_size(); - let second = initial / 2; - let third = second / 2; - if let Err(crate::Error::SetBufferSizeError) = ac.as_client().set_buffer_size(second) { - eprintln!("Client does not support setting buffer size"); - return; - } - ac.as_client().set_buffer_size(third).unwrap(); - ac.as_client().set_buffer_size(initial).unwrap(); - let counter = ac.deactivate().unwrap().2; - let mut history_iter = counter.buffer_size_change_history.iter().cloned(); - assert_eq!(history_iter.find(|&s| s == initial), Some(initial)); - assert_eq!(history_iter.find(|&s| s == second), Some(second)); - assert_eq!(history_iter.find(|&s| s == third), Some(third)); - assert_eq!(history_iter.find(|&s| s == initial), Some(initial)); -} - -/// Tests the assumption that the buffer_size callback is called on the process -/// thread. See issue #137 -#[test] -fn client_cback_calls_buffer_size_on_process_thread() { - let ac = active_test_client("cback_buffer_size_process_thr"); - let initial = ac.as_client().buffer_size(); - let second = initial / 2; - if let Err(crate::Error::SetBufferSizeError) = ac.as_client().set_buffer_size(second) { - eprintln!("Client does not support setting buffer size"); - return; - } - let counter = ac.deactivate().unwrap().2; - let process_thread = counter.process_thread.unwrap(); - assert_eq!(counter.buffer_size_thread_history.len(), 2); - assert_eq!( - // TODO: The process thread should be used on the first and second callback. However, this - // is not the case. Figure out if this is due to a thread safety issue or not. - &counter.buffer_size_thread_history[0..1], - [process_thread], - "Note: This does not hold for JACK2", - ); -} - -#[test] -fn client_cback_calls_after_client_registered() { - let ac = active_test_client("client_cback_cacr"); - let _other_client = open_test_client("client_cback_cacr_other"); - let counter = ac.deactivate().unwrap().1; - assert!(counter - .registered_client_history - .contains(&"client_cback_cacr_other".to_string(),)); - assert!(!counter - .unregistered_client_history - .contains(&"client_cback_cacr_other".to_string(),)); -} - -#[test] -fn client_cback_calls_after_client_unregistered() { - let ac = active_test_client("client_cback_cacu"); - let other_client = open_test_client("client_cback_cacu_other"); - drop(other_client); - let counter = ac.deactivate().unwrap().1; - assert!(counter - .registered_client_history - .contains(&"client_cback_cacu_other".to_string(),)); - assert!(counter - .unregistered_client_history - .contains(&"client_cback_cacu_other".to_string(),)); -} - -#[test] -fn client_cback_reports_xruns() { - let c = open_test_client("client_cback_reports_xruns"); - let counter = Counter { - induce_xruns: true, - ..Counter::default() - }; - let ac = c.activate_async(Counter::default(), counter).unwrap(); - let counter = ac.deactivate().unwrap().1; - assert!(counter.xruns_count > 0, "No xruns encountered."); -} - -#[test] -fn client_cback_calls_port_registered() { - let ac = active_test_client("client_cback_cpr"); - let _pa = ac - .as_client() - .register_port("pa", AudioIn::default()) - .unwrap(); - let _pb = ac - .as_client() - .register_port("pb", AudioIn::default()) - .unwrap(); - let counter = ac.deactivate().unwrap().1; - assert_eq!( - counter.port_register_history.len(), - 2, - "Did not detect port registrations." - ); - assert!( - counter.port_unregister_history.is_empty(), - "Detected false port deregistrations." - ); -} - -#[test] -fn client_cback_calls_port_unregistered() { - let ac = active_test_client("client_cback_cpr"); - let pa = ac - .as_client() - .register_port("pa", AudioIn::default()) - .unwrap(); - let pb = ac - .as_client() - .register_port("pb", AudioIn::default()) - .unwrap(); - ac.as_client().unregister_port(pa).unwrap(); - ac.as_client().unregister_port(pb).unwrap(); - let counter = ac.deactivate().unwrap().1; - assert!( - counter.port_register_history.len() >= 2, - "Did not detect port registrations." - ); - assert!( - counter.port_unregister_history.len() >= 2, - "Did not detect port deregistrations." - ); -} diff --git a/src/contrib/closure.rs b/src/contrib/closure.rs index ee407ad70..74e9382ca 100644 --- a/src/contrib/closure.rs +++ b/src/contrib/closure.rs @@ -58,7 +58,7 @@ where /// /// ```rust /// // 1. Create the client. - /// let (client, _status) = jack::Client::new("silence", jack::ClientOptions::NO_START_SERVER).unwrap(); + /// let (client, _status) = jack::Client::new("silence", jack::ClientOptions::default()).unwrap(); /// /// // 2. Define the state. /// struct State{ diff --git a/src/jack_enums.rs b/src/jack_enums.rs index 225c4a9d1..572186d89 100644 --- a/src/jack_enums.rs +++ b/src/jack_enums.rs @@ -14,7 +14,11 @@ pub enum Error { NotEnoughSpace, PortAliasError, PortAlreadyConnected(String, String), - PortConnectionError(String, String), + PortConnectionError { + source: String, + destination: String, + code_or_message: CodeOrMessage, + }, PortDisconnectionError, PortMonitorError, PortNamingError, @@ -23,18 +27,78 @@ pub enum Error { TimeError, WeakFunctionNotFound(&'static str), ClientIsNoLongerAlive, + ClientPanicked, RingbufferCreateFailed, - UnknownError { error_code: libc::c_int }, + UnknownError { + error_code: libc::c_int, + }, } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{self:?}") + match self { + Error::LibraryError(err) => write!(f, "library error {err}"), + Error::CallbackDeregistrationError => write!(f, "callback deregistration error"), + Error::CallbackRegistrationError => write!(f, "callback registration error"), + Error::ClientActivationError => write!(f, "client activation error"), + Error::ClientDeactivationError => write!(f, "client deactivation error"), + Error::ClientError(status) => write!(f, "client error, status is {status:?}"), + Error::FreewheelError => write!(f, "freewheel error"), + Error::InvalidDeactivation => write!(f, "invalid deactivation"), + Error::NotEnoughSpace => write!(f, "not enough space"), + Error::PortAliasError => write!(f, "port alias error"), + Error::PortAlreadyConnected(a, b) => write!(f, "port {a} is already connected to {b}"), + Error::PortConnectionError { + source, + destination, + code_or_message: CodeOrMessage::Message(message), + } => write!( + f, + "error connecting port {source} to port {destination}: {message}" + ), + Error::PortConnectionError { + source, + destination, + code_or_message: CodeOrMessage::Code(code), + } => write!( + f, + "error (code={code}) connecting port {source} to port {destination}, perhaps the source or destination port is not part of an active client" + ), + Error::PortDisconnectionError => write!(f, "port disconnection error"), + Error::PortMonitorError => write!(f, "port monitoring error"), + Error::PortNamingError => write!(f, "port naming error"), + Error::PortRegistrationError(p) => write!(f, "failed to register port {p}"), + Error::SetBufferSizeError => write!( + f, + "set buffer size error, setting buffer size is likely not supported" + ), + Error::TimeError => write!(f, "time error"), + Error::WeakFunctionNotFound(func) => write!(f, "weak function {func} not found"), + Error::ClientIsNoLongerAlive => write!(f, "client is no longer alive"), + Error::ClientPanicked => write!(f, "client notifcation or processor panicked"), + Error::RingbufferCreateFailed => write!(f, "ringbuffer creation failed"), + Error::UnknownError { error_code } => write!(f, "unkown error with code {error_code}"), + } } } impl std::error::Error for Error {} +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum CodeOrMessage { + Code(libc::c_int), + Message(&'static str), +} + +impl std::fmt::Display for CodeOrMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CodeOrMessage::Code(c) => write!(f, "code(code{c})"), + CodeOrMessage::Message(msg) => write!(f, "{msg}"), + } + } +} + /// Specify an option, either to continue processing, or to stop. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum Control { diff --git a/src/lib.rs b/src/lib.rs index ce7c3e28d..4c376bd14 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,8 +74,11 @@ pub mod contrib { pub use closure::ClosureProcessHandler; } +#[cfg(test)] +mod tests; + static TIME_CLIENT: std::sync::LazyLock = std::sync::LazyLock::new(|| { - Client::new("deprecated_get_time", ClientOptions::NO_START_SERVER) + Client::new("deprecated_get_time", ClientOptions::default()) .unwrap() .0 }); diff --git a/src/port/audio.rs b/src/port/audio.rs index 3a328670c..fb128eb9d 100644 --- a/src/port/audio.rs +++ b/src/port/audio.rs @@ -11,7 +11,7 @@ use crate::{Port, PortFlags, PortSpec, ProcessScope}; /// /// # Example /// ``` -/// let client = jack::Client::new("rusty_client", jack::ClientOptions::NO_START_SERVER) +/// let client = jack::Client::new("rusty_client", jack::ClientOptions::default()) /// .unwrap() /// .0; /// let spec = jack::AudioIn::default(); @@ -30,7 +30,7 @@ pub struct AudioIn { /// /// # Example /// ``` -/// let client = jack::Client::new("rusty_client", jack::ClientOptions::NO_START_SERVER) +/// let client = jack::Client::new("rusty_client", jack::ClientOptions::default()) /// .unwrap() /// .0; /// let spec = jack::AudioIn::default(); @@ -96,56 +96,3 @@ impl Port { } } } - -#[cfg(test)] -mod test { - use super::*; - use crate::{contrib::ClosureProcessHandler, Client, ClientOptions, Control}; - - fn open_test_client(name: &str) -> Client { - Client::new(name, ClientOptions::NO_START_SERVER).unwrap().0 - } - - #[test] - fn port_audio_can_read_write() { - let c = open_test_client("port_audio_crw"); - let in_a = c.register_port("ia", AudioIn::default()).unwrap(); - let in_b = c.register_port("ib", AudioIn::default()).unwrap(); - let mut out_a = c.register_port("oa", AudioOut::default()).unwrap(); - let mut out_b = c.register_port("ob", AudioOut::default()).unwrap(); - let (success_sender, success_receiver) = std::sync::mpsc::sync_channel(1); - let process_callback = move |_: &Client, ps: &ProcessScope| -> Control { - let exp_a = 0.312_443; - let exp_b = -0.612_120; - let out_a = out_a.as_mut_slice(ps); - let out_b = out_b.as_mut_slice(ps); - for v in out_a.iter_mut() { - *v = exp_a; - } - for v in out_b.iter_mut() { - *v = exp_b; - } - - let in_a = in_a.as_slice(ps); - let in_b = in_b.as_slice(ps); - if in_a.iter().all(|v| (*v - exp_a).abs() < 1E-5) - && in_b.iter().all(|v| (*v - exp_b).abs() < 1E-5) - { - _ = success_sender.try_send(true); - } - Control::Continue - }; - let ac = c - .activate_async((), ClosureProcessHandler::new(process_callback)) - .unwrap(); - ac.as_client() - .connect_ports_by_name("port_audio_crw:oa", "port_audio_crw:ia") - .unwrap(); - ac.as_client() - .connect_ports_by_name("port_audio_crw:ob", "port_audio_crw:ib") - .unwrap(); - assert!(success_receiver - .recv_timeout(std::time::Duration::from_secs(2)) - .unwrap(),); - } -} diff --git a/src/port/midi.rs b/src/port/midi.rs index 841e43181..902f567a8 100644 --- a/src/port/midi.rs +++ b/src/port/midi.rs @@ -19,12 +19,16 @@ pub struct RawMidi<'a> { /// `MidiIn` implements the `PortSpec` trait, which defines an endpoint for JACK. In this case, it /// defines midi input. #[derive(Copy, Clone, Debug, Default)] -pub struct MidiIn; +pub struct MidiIn { + _internal: (), +} /// `MidiOut` implements the `PortSpec` trait, which defines an endpoint for JACK. In this case, it /// defines a midi output. #[derive(Copy, Clone, Debug, Default)] -pub struct MidiOut; +pub struct MidiOut { + _internal: (), +} unsafe impl PortSpec for MidiIn { fn jack_port_type(&self) -> &'static str { @@ -208,312 +212,3 @@ impl<'a> MidiWriter<'a> { unsafe { j::jack_midi_max_event_size(self.buffer) } } } - -#[cfg(test)] -mod test { - use super::*; - use crate::client::Client; - use crate::client::ProcessHandler; - use crate::contrib::ClosureProcessHandler; - use crate::jack_enums::Control; - use crate::primitive_types::Frames; - use crate::ClientOptions; - use lazy_static::lazy_static; - use std::iter::Iterator; - use std::sync::atomic::{AtomicUsize, Ordering}; - - use std::sync::Mutex; - use std::{thread, time}; - - fn open_test_client(name: &str) -> Client { - Client::new(name, ClientOptions::NO_START_SERVER).unwrap().0 - } - - struct Connector { - src: String, - dst: String, - } - - impl Connector { - fn connect(&self, c: &Client) { - c.connect_ports_by_name(&self.src, &self.dst).unwrap(); - } - } - - #[derive(Clone, Debug, PartialEq, Eq)] - struct OwnedRawMidi { - time: Frames, - bytes: Vec, - } - - impl OwnedRawMidi { - fn new(m: &RawMidi) -> OwnedRawMidi { - OwnedRawMidi { - time: m.time, - bytes: m.bytes.to_vec(), - } - } - - fn unowned(&self) -> RawMidi<'_> { - RawMidi { - time: self.time, - bytes: &self.bytes, - } - } - } - - struct IterTest Vec> { - stream: Vec, - collected: Vec, - collector: F, - midi_in: Port, - midi_out: Port, - } - - impl Vec> IterTest { - fn new(client: &Client, stream: Vec, collector: F) -> IterTest { - IterTest { - stream, - collected: Vec::new(), - collector, - midi_in: client.register_port("in", MidiIn).unwrap(), - midi_out: client.register_port("out", MidiOut).unwrap(), - } - } - - fn connector(&self) -> Connector { - Connector { - src: self.midi_out.name().unwrap(), - dst: self.midi_in.name().unwrap(), - } - } - } - - impl Vec> ProcessHandler for IterTest { - fn process(&mut self, _: &Client, ps: &ProcessScope) -> Control { - let (midi_in, mut midi_out) = (self.midi_in.iter(ps), self.midi_out.writer(ps)); - // Write to output. - for m in self.stream.iter() { - _ = midi_out.write(&m.unowned()); - } - // Collect in input. - if self.collected.is_empty() { - self.collected = (self.collector)(midi_in); - } - Control::Continue - } - } - - #[test] - fn port_midi_can_read_write() { - // open clients and ports - let c = open_test_client("port_midi_crw"); - let in_a = c.register_port("ia", MidiIn).unwrap(); - let in_b = c.register_port("ib", MidiIn).unwrap(); - let mut out_a = c.register_port("oa", MidiOut).unwrap(); - let mut out_b = c.register_port("ob", MidiOut).unwrap(); - - // set callback routine - let (signal_succeed, did_succeed) = std::sync::mpsc::sync_channel(1); - let process_callback = move |_: &Client, ps: &ProcessScope| -> Control { - let exp_a = RawMidi { - time: 0, - bytes: &[0b1001_0000, 0b0100_0000], - }; - let exp_b = RawMidi { - time: ps.n_frames() - 1, - bytes: &[0b1000_0000, 0b0100_0000], - }; - let (in_a, in_b) = (in_a.iter(ps), in_b.iter(ps)); - let (mut out_a, mut out_b) = (out_a.writer(ps), out_b.writer(ps)); - _ = out_a.write(&exp_a); - _ = out_b.write(&exp_b); - if in_a.clone().next().is_some() - && in_a.clone().all(|m| m == exp_a) - && in_b.clone().all(|m| m == exp_b) - { - _ = signal_succeed.try_send(true); - } - Control::Continue - }; - - // activate - let ac = c - .activate_async((), ClosureProcessHandler::new(process_callback)) - .unwrap(); - - // connect ports to each other - ac.as_client() - .connect_ports_by_name("port_midi_crw:oa", "port_midi_crw:ia") - .unwrap(); - ac.as_client() - .connect_ports_by_name("port_midi_crw:ob", "port_midi_crw:ib") - .unwrap(); - - // check correctness - assert!(did_succeed - .recv_timeout(std::time::Duration::from_secs(1)) - .unwrap()); - ac.deactivate().unwrap(); - } - - #[test] - fn port_midi_can_get_max_event_size() { - // open clients and ports - let c = open_test_client("port_midi_cglc"); - let mut out_p = c.register_port("op", MidiOut).unwrap(); - - // set callback routine - let (size_sender, size_receiver) = std::sync::mpsc::sync_channel(1); - let process_callback = move |_: &Client, ps: &ProcessScope| -> Control { - let out_p = out_p.writer(ps); - _ = size_sender.try_send(out_p.max_event_size()); - Control::Continue - }; - - // check correctness - let ac = c - .activate_async((), ClosureProcessHandler::new(process_callback)) - .unwrap(); - assert!( - size_receiver - .recv_timeout(std::time::Duration::from_secs(1)) - .unwrap() - > 0 - ); - ac.deactivate().unwrap(); - } - - #[test] - fn port_midi_cant_exceed_max_event_size() { - // Open clients and ports. - let c = open_test_client("port_midi_cemes"); - let mut out_p = c.register_port("midi_out", MidiOut).unwrap(); - - // Set callback routine. - let (result_sender, result_receiver) = std::sync::mpsc::sync_channel(1); - let process_callback = move |_: &Client, ps: &ProcessScope| -> Control { - let mut out_p = out_p.writer(ps); - let msg = RawMidi { - time: 0, - bytes: &[0xF6], - }; - for _ in 0..out_p.max_event_size() { - _ = out_p.write(&msg); - } - _ = result_sender.try_send(out_p.write(&msg)); - - Control::Continue - }; - - // Check correctness. - let ac = c - .activate_async((), ClosureProcessHandler::new(process_callback)) - .unwrap(); - assert_eq!( - result_receiver - .recv_timeout(std::time::Duration::from_secs(1)) - .unwrap(), - Err(Error::NotEnoughSpace) - ); - ac.deactivate().unwrap(); - } - - static PMI_COUNT: AtomicUsize = AtomicUsize::new(0); - lazy_static! { - static ref PMI_NEXT: Mutex)>> = Mutex::default(); - static ref PMI_SIZE_HINT: Mutex<(usize, Option)> = Mutex::new((0, None)); - static ref PMI_LAST: Mutex)>> = Mutex::default(); - static ref PMI_THIRD: Mutex)>> = Mutex::default(); - } - - #[test] - fn port_midi_iter() { - // open clients and ports - let c = open_test_client("port_midi_iter"); - let in_p = c.register_port("ip", MidiIn).unwrap(); - let mut out_p = c.register_port("op", MidiOut).unwrap(); - - // set callback routine - let process_callback = move |_: &Client, ps: &ProcessScope| -> Control { - let in_p = in_p.iter(ps); - let mut out_p = out_p.writer(ps); - - for i in 10..14 { - let msg = RawMidi { - time: i, - bytes: &[i as u8], - }; - out_p.write(&msg).ok(); - } - - let rm_to_owned = |m: &RawMidi| (m.time, m.bytes.to_vec()); - *PMI_NEXT.lock().unwrap() = in_p.clone().next().map(|m| rm_to_owned(&m)); - *PMI_SIZE_HINT.lock().unwrap() = in_p.size_hint(); - PMI_COUNT.store(in_p.clone().count(), Ordering::Relaxed); - *PMI_LAST.lock().unwrap() = in_p.clone().last().map(|m| rm_to_owned(&m)); - *PMI_THIRD.lock().unwrap() = in_p.clone().nth(2).map(|m| rm_to_owned(&m)); - - Control::Continue - }; - - // run - let ac = c - .activate_async((), ClosureProcessHandler::new(process_callback)) - .unwrap(); - ac.as_client() - .connect_ports_by_name("port_midi_iter:op", "port_midi_iter:ip") - .unwrap(); - thread::sleep(time::Duration::from_millis(200)); - ac.deactivate().unwrap(); - - // check correctness - assert_eq!(*PMI_NEXT.lock().unwrap(), Some((10, [10].to_vec()))); - assert_eq!(*PMI_SIZE_HINT.lock().unwrap(), (4, Some(4))); - assert_eq!(PMI_COUNT.load(Ordering::Relaxed), 4); - assert_eq!(*PMI_LAST.lock().unwrap(), Some((13, [13].to_vec()))); - assert_eq!(*PMI_THIRD.lock().unwrap(), Some((12, [12].to_vec()))); - } - - #[test] - fn port_midi_iter_next_if() { - let c = open_test_client("pmi_nib"); - let stream = vec![ - OwnedRawMidi { - time: 0, - bytes: vec![1], - }, - OwnedRawMidi { - time: 10, - bytes: vec![3, 4, 5], - }, - OwnedRawMidi { - time: 11, - bytes: vec![6], - }, - OwnedRawMidi { - time: 12, - bytes: vec![7, 8], - }, - ]; - let collect = |midi_in: MidiIter| { - let mut collected = Vec::with_capacity(midi_in.clone().count()); - let mut iter = midi_in.clone(); - while let Some(m) = iter.next_if(|m| m.time < 11) { - collected.push(OwnedRawMidi::new(&m)); - } - collected - }; - let processor = IterTest::new(&c, stream.clone(), collect); - let connector = processor.connector(); - - let ac = c.activate_async((), processor).unwrap(); - connector.connect(ac.as_client()); - thread::sleep(time::Duration::from_millis(200)); - - let (_, _, processor) = ac.deactivate().unwrap(); - let expected: &[OwnedRawMidi] = &stream[0..2]; - let got: &[OwnedRawMidi] = &processor.collected; - assert_eq!(expected, got); - } -} diff --git a/src/port/mod.rs b/src/port/mod.rs index 76085f8f0..9e66951a1 100644 --- a/src/port/mod.rs +++ b/src/port/mod.rs @@ -9,9 +9,3 @@ pub use self::audio::{AudioIn, AudioOut}; pub use self::midi::{MidiIn, MidiIter, MidiOut, MidiWriter, RawMidi}; pub use self::port_flags::PortFlags; pub use self::port_impl::{Port, PortSpec, Unowned, PORT_NAME_SIZE, PORT_TYPE_SIZE}; - -#[cfg(test)] -mod test_client; - -#[cfg(test)] -mod test_port; diff --git a/src/port/test_client.rs b/src/port/test_client.rs deleted file mode 100644 index 9d63bd8c4..000000000 --- a/src/port/test_client.rs +++ /dev/null @@ -1,388 +0,0 @@ -use super::*; -use crate::Client; -use crate::ClientOptions; -use crate::Error; -use crate::NotificationHandler; -use crate::PortId; -use crate::PORT_NAME_SIZE; -use std::collections::HashSet; -use std::sync::mpsc; -use std::sync::Mutex; -fn open_test_client(name: &str) -> Client { - Client::new(name, ClientOptions::NO_START_SERVER).unwrap().0 -} - -#[test] -fn client_port_can_register_port() { - let c = open_test_client("cp_can_register_port"); - c.register_port("cpcrp_a", AudioIn::default()).unwrap(); -} - -#[test] -fn client_port_register_port_enforces_unique_names() { - let pname = "cprpeun_a"; - let c = open_test_client("cp_can_register_port"); - c.register_port(pname, AudioIn::default()).unwrap(); - assert_eq!( - c.register_port(pname, AudioIn::default()).err(), - Some(Error::PortRegistrationError(pname.to_string())) - ); -} - -#[test] -fn client_port_register_port_enforces_name_length() { - let c = open_test_client("cp_can_register_port"); - let pname = (0..=*PORT_NAME_SIZE) - .map(|_| "a") - .collect::>() - .join("_"); - assert_eq!( - c.register_port(&pname, AudioIn::default()).err(), - Some(Error::PortRegistrationError(pname.to_string())) - ); -} - -#[test] -fn client_port_can_request_monitor_by_name() { - let c = open_test_client("cp_can_request_monitor_by_name"); - let p = c.register_port("cpcrmbn_a", AudioIn::default()).unwrap(); - c.request_monitor_by_name(&p.name().unwrap(), true).unwrap(); - c.request_monitor_by_name(&p.name().unwrap(), false) - .unwrap(); -} - -#[test] -fn client_port_can_get_port_by_name() { - let c = open_test_client("cp_can_get_port_by_name"); - let p = c.register_port("named_port", AudioIn::default()).unwrap(); - let _p = c.port_by_name(&p.name().unwrap()).unwrap(); -} - -pub struct PortIdHandler { - pub reg_tx: Mutex>, -} - -impl NotificationHandler for PortIdHandler { - fn port_registration(&mut self, _: &Client, pid: PortId, is_registered: bool) { - if is_registered { - self.reg_tx.lock().unwrap().send(pid).unwrap() - } - } -} - -#[test] -fn client_port_can_get_port_by_id() { - let (client_name, port_name) = ("cp_can_get_port_by_id", "cp_registered_port_name"); - - // Create handler - let (reg_tx, reg_rx) = mpsc::sync_channel(200); - let h = PortIdHandler { - reg_tx: Mutex::new(reg_tx), - }; - - // Open and activate client - let c = open_test_client(client_name); - let ac = c.activate_async(h, ()).unwrap(); - - // Register port - let _pa = ac - .as_client() - .register_port(port_name, AudioIn::default()) - .unwrap(); - - // Get by id - let c = ac.deactivate().unwrap().0; - let mut registered_ports = reg_rx - .iter() - .flat_map(|i| c.port_by_id(i)) - .map(|p| p.name().unwrap()); - let port_name = format!("{client_name}:{port_name}"); - assert!(registered_ports.any(|n| n == port_name)); - - // Port that doesn't exist - // TODO: Restore when JACK doesn't exit when this happens. - // let nonexistant_port = c.port_by_id(10000); - // assert!( - // nonexistant_port.is_none(), - // format!("Expected None but got: {:?}", nonexistant_port) - // ); -} - -#[test] -fn client_port_fails_to_nonexistant_port() { - let c = open_test_client("cp_can_request_monitor_by_name"); - let p = c.register_port("cpcrmbn_a", AudioIn::default()).unwrap(); - let _p = c.port_by_name(&p.name().unwrap()).unwrap(); -} - -#[test] -fn client_port_recognizes_my_ports() { - let ca = open_test_client("cp_cprmp_ca"); - let cb = open_test_client("cp_cprmp_cb"); - let first = ca.register_port("cpcprmp_pa", AudioIn::default()).unwrap(); - let second = cb.register_port("cpcprmp_pb", AudioIn::default()).unwrap(); - let first_alt = ca.port_by_name(&first.name().unwrap()).unwrap(); - let second_alt = ca.port_by_name(&second.name().unwrap()).unwrap(); - assert!(ca.is_mine(&first)); - assert!(ca.is_mine(&first_alt)); - assert!(!ca.is_mine(&second)); - assert!(!ca.is_mine(&second_alt)); -} - -#[test] -fn client_port_can_connect_ports() { - let client = open_test_client("client_port_ccp"); - - // initialize ports - let in_p = client.register_port("inp", AudioIn::default()).unwrap(); - let out_p = client.register_port("outp", AudioOut::default()).unwrap(); - - // start client - let client = client.activate_async((), ()).unwrap(); - - // connect them - client.as_client().connect_ports(&out_p, &in_p).unwrap(); -} - -#[test] -fn client_port_can_connect_ports_by_name() { - let client = open_test_client("client_port_ccpbn"); - - // initialize ports - let _in_p = client.register_port("inp", AudioIn::default()).unwrap(); - let _out_p = client.register_port("outp", AudioOut::default()).unwrap(); - - // start client - let client = client.activate_async((), ()).unwrap(); - - // connect them - client - .as_client() - .connect_ports_by_name("client_port_ccpbn:outp", "client_port_ccpbn:inp") - .unwrap(); -} - -#[test] -fn client_port_can_connect_unowned_ports() { - let client = open_test_client("client_port_ccup"); - let connector = open_test_client("client_port_ccup_conn"); - - // initialize ports - let _in_p = client.register_port("inp", AudioIn::default()).unwrap(); - let _out_p = client.register_port("outp", AudioOut::default()).unwrap(); - - // start client - let _client = client.activate_async((), ()).unwrap(); - - // connect them - connector - .connect_ports_by_name("client_port_ccup:outp", "client_port_ccup:inp") - .unwrap(); -} - -#[test] -fn client_port_cant_connect_inactive_client() { - let client = open_test_client("client_port_ccic"); - let other = open_test_client("client_port_ccic_other"); - - // initialize ports - let in_p = other - .register_port("inp", AudioIn::default()) - .unwrap() - .name() - .unwrap(); - let out_p = other - .register_port("outp", AudioOut::default()) - .unwrap() - .name() - .unwrap(); - - // Normally we start a client before we begin connecting, but in this case - // we're checking for errors that happen when we connect before activating. - // - // let client = client.activate_async((), ()).unwrap(); - - // connect them - assert_eq!( - client.connect_ports_by_name(&in_p, &out_p).err(), - Some(Error::PortConnectionError(in_p, out_p)) - ); -} - -#[test] -fn client_port_recognizes_already_connected_ports() { - let client = open_test_client("client_port_racp"); - - // initialize ports - let in_p = client.register_port("conna", AudioIn::default()).unwrap(); - let out_p = client.register_port("connb", AudioOut::default()).unwrap(); - - // start client - let client = client.activate_async((), ()).unwrap(); - - // attempt to connect the ports twice - client.as_client().connect_ports(&out_p, &in_p).unwrap(); - assert_eq!( - client.as_client().connect_ports(&out_p, &in_p), - Err(Error::PortAlreadyConnected( - out_p.name().unwrap(), - in_p.name().unwrap(), - )) - ); -} - -#[test] -fn client_port_fails_to_connect_nonexistant_ports() { - let client = open_test_client("client_port_ftcnp") - .activate_async((), ()) - .unwrap(); - assert_eq!( - client - .as_client() - .connect_ports_by_name("doesnt_exist", "also_no_exist"), - Err(Error::PortConnectionError( - "doesnt_exist".to_string(), - "also_no_exist".to_string(), - )) - ); -} - -#[test] -fn client_port_can_disconnect_port_from_all() { - let client = open_test_client("client_port_cdpfa"); - - // initialize ports - let in_p = client.register_port("conna", AudioIn::default()).unwrap(); - let out_p = client.register_port("connb", AudioOut::default()).unwrap(); - - // start client - let client = client.activate_async((), ()).unwrap(); - - // connect and disconnect - client.as_client().connect_ports(&out_p, &in_p).unwrap(); - client.as_client().disconnect(&in_p).unwrap(); -} - -#[test] -fn client_port_can_disconnect_ports() { - let client = open_test_client("client_port_cdp"); - - // initialize ports - let in_p = client.register_port("conna", AudioIn::default()).unwrap(); - let out_p = client.register_port("connb", AudioOut::default()).unwrap(); - - // start client - let client = client.activate_async((), ()).unwrap(); - - // connect and disconnect - client.as_client().connect_ports(&out_p, &in_p).unwrap(); - client.as_client().disconnect_ports(&out_p, &in_p).unwrap(); -} - -#[test] -fn client_port_can_disconnect_ports_by_name() { - let client = open_test_client("client_port_cdpbn"); - - // initialize ports - let in_p = client.register_port("conna", AudioIn::default()).unwrap(); - let out_p = client.register_port("connb", AudioOut::default()).unwrap(); - - // start client - let client = client.activate_async((), ()).unwrap(); - - // connect and disconnect - client - .as_client() - .connect_ports_by_name(&out_p.name().unwrap(), &in_p.name().unwrap()) - .unwrap(); - client - .as_client() - .disconnect_ports_by_name(&out_p.name().unwrap(), &in_p.name().unwrap()) - .unwrap(); -} - -#[test] -fn client_port_can_disconnect_unowned_ports() { - let client = open_test_client("client_port_cdup"); - let disconnector = open_test_client("client_port_cdup_disc"); - - // initialize ports - let in_p = client.register_port("conna", AudioIn::default()).unwrap(); - let out_p = client.register_port("connb", AudioOut::default()).unwrap(); - - // start client - let client = client.activate_async((), ()).unwrap(); - - // connect and disconnect - client - .as_client() - .connect_ports_by_name(&out_p.name().unwrap(), &in_p.name().unwrap()) - .unwrap(); - disconnector - .disconnect_ports_by_name(&out_p.name().unwrap(), &in_p.name().unwrap()) - .unwrap(); -} - -#[test] -fn client_port_can_get_existing_ports() { - let client = open_test_client("client_port_cgep"); - let port_getter = open_test_client("client_port_cgep_getter"); - - // initialize ports - let in_p = client.register_port("conna", AudioIn::default()).unwrap(); - let out_p = client.register_port("connb", AudioOut::default()).unwrap(); - - // retrieve - let known_ports = [ - in_p.name().unwrap(), - out_p.name().unwrap(), - "system:playback_2".to_string(), - "system:playback_1".to_string(), - "system:capture_1".to_string(), - "system:capture_2".to_string(), - ]; - let exp: HashSet = known_ports.iter().cloned().collect(); - let got: HashSet = port_getter - .ports(None, None, PortFlags::empty()) - .into_iter() - .collect(); - let intersection: HashSet = exp.intersection(&got).cloned().collect(); - assert_eq!(exp, intersection); -} - -#[test] -fn client_port_can_get_port_by_name_pattern() { - let client = open_test_client("client_port_cgpbnp"); - - // retrieve - let known_ports = [ - "system:playback_2".to_string(), - "system:capture_2".to_string(), - ]; - let exp: HashSet = known_ports.iter().cloned().collect(); - let got: HashSet = client - .ports(Some("2"), None, PortFlags::empty()) - .into_iter() - .collect(); - assert_eq!(got, exp); -} - -#[test] -fn client_port_can_get_port_by_type_pattern() { - let c_name = "client_port_cgpbtp"; - let p_name = "midip"; - let full_name = format!("{c_name}:{p_name}"); - let client = open_test_client(c_name); - - // register port with type name, like midi - let _p = client.register_port(p_name, MidiIn); - - // retrieve - let ports = client.ports(None, Some("midi"), PortFlags::empty()); - assert!( - ports.contains(&full_name), - "{:?} does not contain {}", - &ports, - &full_name - ); -} diff --git a/src/port/test_port.rs b/src/port/test_port.rs deleted file mode 100644 index a6b253ea2..000000000 --- a/src/port/test_port.rs +++ /dev/null @@ -1,201 +0,0 @@ -use crate::AudioIn; -use crate::AudioOut; -use crate::Client; -use crate::ClientOptions; -use crate::Port; -use crate::PortFlags; -use crate::PortSpec; -use crate::Unowned; - -fn open_test_client(name: &str) -> Client { - Client::new(name, ClientOptions::NO_START_SERVER).unwrap().0 -} - -fn open_client_with_port(client: &str, port: &str) -> (Client, Port) { - let c = open_test_client(client); - let p = c.register_port(port, AudioIn::default()).unwrap(); - (c, p) -} - -#[test] -fn port_can_be_cast_to_unowned() { - let (_c, p) = open_client_with_port("port_cwpn", "the_port_name"); - let p_alt: Port = p.clone_unowned(); - assert_eq!(p.short_name(), p_alt.short_name()); - assert_eq!(p.name(), p_alt.name()); -} - -#[test] -fn port_created_with_proper_names() { - let (_c, p) = open_client_with_port("port_cwpn", "the_port_name"); - assert_eq!(p.short_name().unwrap(), "the_port_name"); - assert_eq!(p.name().unwrap(), "port_cwpn:the_port_name"); -} - -#[test] -fn port_can_rename() { - let client_name = "port_rename"; - let original_name = "port_to_rename"; - let new_name = "port_that_was_renamed"; - - // initial port - let (_c, mut p) = open_client_with_port(client_name, original_name); - assert_eq!(p.name().unwrap(), format!("{client_name}:{original_name}")); - assert_eq!(p.short_name().unwrap(), original_name); - - // renamed port - p.set_name(new_name).unwrap(); - assert_eq!(p.name().unwrap(), format!("{client_name}:{new_name}")); - assert_eq!(p.short_name().unwrap(), new_name); -} - -#[test] -fn port_connected_count() { - let c = open_test_client("port_connected_count"); - let pa = c - .register_port("port_connected_count_a", AudioIn::default()) - .unwrap(); - let pb = c - .register_port("port_connected_count_b", AudioOut::default()) - .unwrap(); - let pc = c - .register_port("port_connected_count_c", AudioOut::default()) - .unwrap(); - let pd = c - .register_port("port_connected_count_d", AudioOut::default()) - .unwrap(); - let c = c.activate_async((), ()).unwrap(); - c.as_client().connect_ports(&pb, &pa).unwrap(); - c.as_client().connect_ports(&pc, &pa).unwrap(); - assert_eq!(pa.connected_count().unwrap(), 2); - assert_eq!(pb.connected_count().unwrap(), 1); - assert_eq!(pc.connected_count().unwrap(), 1); - assert_eq!(pd.connected_count().unwrap(), 0); -} - -#[test] -fn port_knows_connections() { - let c = open_test_client("port_knows_connections"); - let pa = c.register_port("pa", AudioIn::default()).unwrap(); - let pb = c.register_port("pb", AudioOut::default()).unwrap(); - let pc = c.register_port("pc", AudioOut::default()).unwrap(); - let pd = c.register_port("pd", AudioOut::default()).unwrap(); - let c = c.activate_async((), ()).unwrap(); - c.as_client().connect_ports(&pb, &pa).unwrap(); - c.as_client().connect_ports(&pc, &pa).unwrap(); - - // pa - assert!(pa.is_connected_to(&pb.name().unwrap()).unwrap()); - assert!(pa.is_connected_to(&pc.name().unwrap()).unwrap()); - assert!(!pa.is_connected_to(&pd.name().unwrap()).unwrap()); - - // pb - assert!(pb.is_connected_to(&pa.name().unwrap()).unwrap()); - assert!(!pb.is_connected_to(&pc.name().unwrap()).unwrap()); - assert!(!pb.is_connected_to(&pd.name().unwrap()).unwrap()); - - // pc - assert!(pc.is_connected_to(&pa.name().unwrap()).unwrap()); - assert!(!pc.is_connected_to(&pb.name().unwrap()).unwrap()); - assert!(!pc.is_connected_to(&pd.name().unwrap()).unwrap()); - - // pd - assert!(!pd.is_connected_to(&pa.name().unwrap()).unwrap()); - assert!(!pd.is_connected_to(&pb.name().unwrap()).unwrap()); - assert!(!pd.is_connected_to(&pc.name().unwrap()).unwrap()); -} - -#[test] -fn port_can_ensure_monitor() { - let (_c, p) = open_client_with_port("port_can_ensure_monitor", "maybe_monitor"); - - for should_monitor in [true, false].iter().cycle().take(10) { - p.ensure_monitor(*should_monitor).unwrap(); - assert_eq!(p.is_monitoring_input().unwrap(), *should_monitor); - } -} - -#[test] -fn port_can_request_monitor() { - let (_c, p) = open_client_with_port("port_can_ensure_monitor", "maybe_monitor"); - - for should_monitor in [true, false].iter().cycle().take(10) { - p.request_monitor(*should_monitor).unwrap(); - assert_eq!(p.is_monitoring_input().unwrap(), *should_monitor); - } -} - -#[test] -fn port_can_set_alias() { - let (_c, mut p) = open_client_with_port("port_can_set_alias", "will_get_alias"); - - // no alias - assert!(p.aliases().unwrap().is_empty()); - - // 1 alias - p.set_alias("first_alias").unwrap(); - assert_eq!(p.aliases().unwrap(), vec!["first_alias".to_string()]); - - // 2 alias - p.set_alias("second_alias").unwrap(); - assert_eq!( - p.aliases().unwrap(), - vec!["first_alias".to_string(), "second_alias".to_string()] - ); -} - -#[test] -fn port_can_unset_alias() { - let (_c, mut p) = open_client_with_port("port_can_unset_alias", "will_unset_alias"); - - // set aliases - p.set_alias("first_alias").unwrap(); - p.set_alias("second_alias").unwrap(); - assert_eq!( - p.aliases().unwrap(), - vec!["first_alias".to_string(), "second_alias".to_string()] - ); - - // unset alias - p.unset_alias("first_alias").unwrap(); - assert_eq!(p.aliases().unwrap(), vec!["second_alias".to_string()]); -} - -#[test] -fn port_unowned_no_port_type() { - assert_eq!("", Unowned.jack_port_type()); -} - -#[test] -fn port_unowned_no_port_flags() { - assert_eq!(PortFlags::empty(), Unowned.jack_flags()); -} - -#[test] -#[should_panic] -fn port_unowned_no_port_size() { - Unowned.jack_buffer_size(); -} - -#[test] -fn port_debug_printing() { - let (_c, mut p) = open_client_with_port("port_has_debug_string", "debug_info"); - p.set_alias("this_port_alias").unwrap(); - let got = format!("{p:?}"); - let parts = [ - ("name", "Ok(\"port_has_debug_string:debug_info\")"), - ("connections", "0"), - ("port_type", "Ok(\"32 bit float mono audio\")"), - ("port_flags", "PortFlags(IS_INPUT)"), - ("aliases", "[\"this_port_alias\""), - ]; - for &(k, v) in parts.iter() { - let p = format!("{k}: {v}"); - assert!( - got.contains(&p), - "Output:\n{}\nDoes not contain:\n\t{}", - got, - p - ); - } -} diff --git a/src/properties.rs b/src/properties.rs index bcf2e6eed..bb205b8f1 100644 --- a/src/properties.rs +++ b/src/properties.rs @@ -23,7 +23,6 @@ pub trait PropertyChangeHandler: Send { fn property_changed(&mut self, change: &PropertyChange); } -#[allow(dead_code)] //dead if we haven't enabled metadata pub(crate) unsafe extern "C" fn property_changed

( subject: j::jack_uuid_t, key: *const ::libc::c_char, @@ -365,7 +364,7 @@ mod metadata { #[test] fn can_set_and_get() { - let (c, _) = Client::new("dummy", ClientOptions::NO_START_SERVER).unwrap(); + let (c, _) = Client::new("dummy", ClientOptions::default()).unwrap(); let prop1 = Property::new("foo", None); assert_eq!(c.property_set(c.uuid(), "blah", &prop1), Ok(())); @@ -406,8 +405,8 @@ mod metadata { #[test] fn can_remove() { - let (c1, _) = Client::new("client1", ClientOptions::NO_START_SERVER).unwrap(); - let (c2, _) = Client::new("client2", ClientOptions::NO_START_SERVER).unwrap(); + let (c1, _) = Client::new("client1", ClientOptions::default()).unwrap(); + let (c2, _) = Client::new("client2", ClientOptions::default()).unwrap(); let prop1 = Property::new("foo", None); let prop2 = Property::new( "http://churchofrobotron.com/2084", @@ -456,7 +455,7 @@ mod metadata { #[test] fn can_property_remove_all() { - let (c, _) = Client::new("dummy", ClientOptions::NO_START_SERVER).unwrap(); + let (c, _) = Client::new("dummy", ClientOptions::default()).unwrap(); let prop = Property::new("foo", Some("bar".into())); assert_eq!(c.property_set(c.uuid(), "blah", &prop), Ok(())); @@ -489,8 +488,8 @@ mod metadata { Some("robot apocalypse".into()), ); - let (mut c1, _) = Client::new("client1", ClientOptions::NO_START_SERVER).unwrap(); - let (c2, _) = Client::new("client2", ClientOptions::NO_START_SERVER).unwrap(); + let (mut c1, _) = Client::new("client1", ClientOptions::default()).unwrap(); + let (c2, _) = Client::new("client2", ClientOptions::default()).unwrap(); let (sender, receiver): (Sender, _) = channel(); assert_eq!( Ok(()), @@ -564,7 +563,7 @@ mod metadata { #[test] #[should_panic] fn double_register() { - let (mut c, _) = Client::new("client1", ClientOptions::NO_START_SERVER).unwrap(); + let (mut c, _) = Client::new("client1", ClientOptions::default()).unwrap(); assert_eq!( Ok(()), c.register_property_change_handler(ClosurePropertyChangeHandler::new(|_| {})) diff --git a/src/ringbuffer.rs b/src/ringbuffer.rs index d27d2343f..e89352d8b 100644 --- a/src/ringbuffer.rs +++ b/src/ringbuffer.rs @@ -152,7 +152,7 @@ impl RingBufferReader { (view1, view2) } - /// Read data from the ringbuffer. Returns: the number of bytes read, which may range from 0 to + /// Read data from the ringbuffer. Returns the number of bytes read, which may range from 0 to /// buf.len(). pub fn read_buffer(&mut self, buf: &mut [u8]) -> usize { if buf.is_empty() { @@ -165,6 +165,12 @@ impl RingBufferReader { unsafe { j::jack_ringbuffer_read(self.ringbuffer_handle, bufstart, insize) } } + /// Read data from the ringbuffer. Returns the slice that was read into. This is a subset of `buf`. + pub fn read_slice<'a>(&mut self, buf: &'a mut [u8]) -> &'a [u8] { + let len = self.read_buffer(buf); + &buf[0..len] + } + /// Read data from the ringbuffer. Opposed to read_buffer() this function does not move the read /// pointer. Thus it's a convenient way to inspect data in the ringbuffer in a continous /// fashion. The price is that the data is copied into a user provided buffer. For "raw" @@ -260,7 +266,7 @@ impl RingBufferWriter { /// Return a pair of slices of the current writable space in the ringbuffer. two slices are /// needed because the space available for writing may be split across the end of the - /// ringbuffer. consider using peek_iter for convenience. + /// ringbuffer. Consider using peek_iter for convenience. pub fn get_vector(&mut self) -> (&mut [u8], &mut [u8]) { let mut vec = [ j::jack_ringbuffer_data_t::default(), @@ -322,137 +328,3 @@ impl Drop for RingBufferWriter { } } } - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn ringbuffer_can_create() { - let ringbuf = RingBuffer::new(1024); - ringbuf.unwrap(); - } - - #[test] - fn ringbuffer_can_space() { - const SIZE: usize = 1024; - const ADVANCE: usize = 5; - let ringbuf = RingBuffer::new(SIZE).unwrap(); - let (mut reader, mut writer) = ringbuf.into_reader_writer(); - - assert_eq!(writer.space(), SIZE - 1); - assert_eq!(reader.space(), 0); - - writer.advance(ADVANCE); - - assert_eq!(writer.space(), SIZE - 1 - ADVANCE); - assert_eq!(reader.space(), ADVANCE); - - reader.advance(ADVANCE); - assert_eq!(writer.space(), SIZE - 1); - assert_eq!(reader.space(), 0); - } - - #[test] - fn ringbuffer_write_read() { - let ringbuf = RingBuffer::new(1024).unwrap(); - let (mut reader, mut writer) = ringbuf.into_reader_writer(); - - let buf = [0_u8, 1, 2, 3]; - let num = writer.write_buffer(&buf); - assert_eq!(num, buf.len()); - - let mut outbuf = [0_u8; 8]; - let num = reader.read_buffer(&mut outbuf); - assert_eq!(num, buf.len()); - - assert_eq!(outbuf[..num], buf[..]); - } - - #[test] - fn ringbuffer_peek_write() { - let ringbuf = RingBuffer::new(1024).unwrap(); - let (reader, mut writer) = ringbuf.into_reader_writer(); - - let buf = [0_u8, 1, 2, 3]; - writer.write_buffer(&buf); - - let data: Vec = reader.peek_iter().copied().collect(); - - assert_eq!(data.len(), buf.len()); - assert_eq!(data[..], buf[..]); - } - - #[test] - fn ringbuffer_write_read_split() { - const BUFSIZE: usize = 10; - let ringbuf = RingBuffer::new(BUFSIZE).unwrap(); - let (mut reader, mut writer) = ringbuf.into_reader_writer(); - - let buf = [0_u8, 1, 2, 3]; - - let advancedsize = BUFSIZE / (buf.len() / 2); - writer.advance(advancedsize); - reader.advance(advancedsize); - { - let (_, v2) = writer.get_vector(); - assert_ne!(v2.len(), 0); - } - - writer.write_buffer(&buf); - - { - let (v1, _) = reader.get_vector(); - assert_ne!(v1.len(), 0); - } - - let data: Vec = reader.peek_iter().copied().collect(); - - assert_eq!(data.len(), buf.len()); - assert_eq!(data[..], buf[..]); - } - - #[test] - fn ringbuffer_peek_read() { - let ringbuf = RingBuffer::new(1024).unwrap(); - let (mut reader, mut writer) = ringbuf.into_reader_writer(); - - let buf = [0_u8, 1, 2, 3]; - for (item, bufitem) in writer.peek_iter().zip(buf.iter()) { - *item = *bufitem; - } - - writer.advance(buf.len()); - - let mut outbuf = [0_u8; 8]; - let num = reader.read_buffer(&mut outbuf); - assert_eq!(num, buf.len()); - - assert_eq!(outbuf[..num], buf[..]); - } - - #[test] - fn ringbuffer_threaded() { - use std::thread; - - let ringbuf = RingBuffer::new(1024).unwrap(); - let (mut reader, mut writer) = ringbuf.into_reader_writer(); - - let buf = [0_u8, 1, 2, 3]; - thread::spawn(move || { - for (item, bufitem) in writer.peek_iter().zip(buf.iter()) { - *item = *bufitem; - } - - writer.advance(buf.len()); - }) - .join() - .unwrap(); - - let mut outbuf = [0_u8; 8]; - let num = reader.read_buffer(&mut outbuf); - assert_eq!(num, buf.len()); - - assert_eq!(outbuf[..num], buf[..]); - } -} diff --git a/src/tests/client.rs b/src/tests/client.rs new file mode 100644 index 000000000..356fa1d96 --- /dev/null +++ b/src/tests/client.rs @@ -0,0 +1,87 @@ +#[test] +fn client_can_open() { + let (client, status) = + crate::Client::new("my new client", crate::ClientOptions::default()).unwrap(); + assert_eq!(status, crate::ClientStatus::empty()); + assert_eq!(client.name(), "my new client"); + assert_ne!(client.sample_rate(), 0); + assert_ne!(client.buffer_size(), 0); + assert_ne!(client.uuid_string(), ""); + let cpu_load = client.cpu_load(); + assert!(cpu_load > 0.0, "client.cpu_load() = {}", cpu_load); +} + +#[test] +fn time_is_montonically_increasing() { + let (client, _) = crate::Client::new("", crate::ClientOptions::empty()).unwrap(); + + let t0 = client.time(); + let frames0 = client.frames_since_cycle_start(); + let frame_time0 = client.frame_time(); + + std::thread::sleep(std::time::Duration::from_millis(50)); + assert_ne!(client.time(), t0); + assert_ne!(client.frames_since_cycle_start(), frames0); + assert_ne!(client.frame_time(), frame_time0); +} + +#[test] +fn maybe_client_can_set_buffer_size() { + let (client, _) = crate::Client::new("", crate::ClientOptions::empty()).unwrap(); + let initial_buffer_size = client.buffer_size(); + if let Err(err) = client.set_buffer_size(initial_buffer_size * 2) { + eprintln!("client does not support setting buffer size: {err}"); + return; + } + assert_eq!(client.buffer_size(), 2 * initial_buffer_size); + client.set_buffer_size(initial_buffer_size).unwrap(); +} + +#[test] +fn client_uuid_are_unique() { + let (client1, _) = crate::Client::new("", crate::ClientOptions::default()).unwrap(); + let (client2, _) = crate::Client::new("", crate::ClientOptions::default()).unwrap(); + assert_ne!(client1.uuid_string(), ""); + assert_ne!(client2.uuid_string(), ""); + assert_ne!(client1.uuid_string(), client2.uuid_string()); + assert_ne!(client1.uuid(), 0); + assert_ne!(client2.uuid(), 0); + assert_ne!(client1.uuid(), client2.uuid()); +} + +#[test] +fn uuid_can_map_to_client_name() { + let (client1, _) = + crate::Client::new("uuid-client-1", crate::ClientOptions::default()).unwrap(); + let (client2, _) = + crate::Client::new("uuid-client-2", crate::ClientOptions::default()).unwrap(); + + assert_eq!( + client1.name_by_uuid_str(&client1.uuid_string()).unwrap(), + "uuid-client-1" + ); + assert_eq!( + client1.name_by_uuid_str(&client2.uuid_string()).unwrap(), + "uuid-client-2" + ); + assert_eq!( + client1.name_by_uuid(client1.uuid()).unwrap(), + "uuid-client-1" + ); + assert_eq!( + client1.name_by_uuid(client2.uuid()).unwrap(), + "uuid-client-2" + ); +} + +#[test] +fn nonexistant_uuid_to_client_name_returns_none() { + let (client1, _) = crate::Client::new("", crate::ClientOptions::default()).unwrap(); + let (client2, _) = + crate::Client::new("dropped-client", crate::ClientOptions::default()).unwrap(); + let uuid_string = client2.uuid_string(); + let uuid = client2.uuid(); + drop(client2); + assert_eq!(client1.name_by_uuid_str(&uuid_string), None); + assert_eq!(client1.name_by_uuid(uuid), None); +} diff --git a/src/tests/log.rs b/src/tests/log.rs new file mode 100644 index 000000000..cb16752a7 --- /dev/null +++ b/src/tests/log.rs @@ -0,0 +1,32 @@ +unsafe extern "C" fn test_info_callback(_msg: *const libc::c_char) {} +unsafe extern "C" fn test_error_callback(_msg: *const libc::c_char) {} + +#[test] +fn can_set_logger() { + // TODO: This passes on JACK2 1.9.22, but not 1.9.20 which is used in the GitHub runners. + std::panic::catch_unwind(|| { + crate::set_logger(crate::LoggerType::Custom { + info: test_info_callback, + error: test_error_callback, + }); + #[cfg(feature = "dynamic_loading")] + #[allow(clippy::fn_address_comparisons)] + unsafe { + let lib = jack_sys::library().unwrap(); + type LogFn = unsafe extern "C" fn(*const libc::c_char); + assert!( + **lib.get::<*const LogFn>(b"jack_info_callback").unwrap() == test_info_callback + ); + assert!( + **lib.get::<*const LogFn>(b"jack_error_callback").unwrap() == test_error_callback + ); + } + #[cfg(not(feature = "dynamic_loading"))] + { + assert!(unsafe { crate::jack_sys::jack_info_callback } == Some(test_info_callback),); + assert!(unsafe { crate::jack_sys::jack_error_callback } == Some(test_error_callback),); + } + }) + .ok(); + super::log_to_stdio(); // Revert to enable debugging in other tests. +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 000000000..194292fe2 --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,11 @@ +mod client; +mod log; +mod processing; +mod ringbuffer; +mod time; +mod transport; + +#[ctor::ctor] +fn log_to_stdio() { + crate::set_logger(crate::LoggerType::Stdio); +} diff --git a/src/tests/processing.rs b/src/tests/processing.rs new file mode 100644 index 000000000..a964e2e43 --- /dev/null +++ b/src/tests/processing.rs @@ -0,0 +1,195 @@ +#[test] +fn panic_in_process_handler_propagates_as_error_in_deactivate() { + let (client, _) = crate::Client::new("", crate::ClientOptions::default()).unwrap(); + let (send, recv) = std::sync::mpsc::sync_channel(1); + let process_handler = crate::contrib::ClosureProcessHandler::new(move |_, _| { + send.try_send(true).ok(); + panic!("panic should convert to error!"); + }); + let ac = client.activate_async((), process_handler).unwrap(); + assert!(recv + .recv_timeout(std::time::Duration::from_secs(1)) + .unwrap()); + assert_eq!(ac.deactivate().err(), Some(crate::Error::ClientPanicked)); +} + +#[test] +fn panic_in_buffer_size_handler_propagates_as_error_in_deactivate() { + let (client, _) = crate::Client::new("", crate::ClientOptions::default()).unwrap(); + let (send, recv) = std::sync::mpsc::sync_channel(2); + let handler = crate::contrib::ClosureProcessHandler::with_state( + (), + move |_, _, _| { + send.try_send(true).unwrap(); + panic!("intentional panic here"); + }, + move |_, _, _| crate::Control::Continue, + ); + let ac = client.activate_async((), handler).unwrap(); + assert!(recv + .recv_timeout(std::time::Duration::from_secs(1)) + .unwrap()); + assert_eq!(ac.deactivate().err(), Some(crate::Error::ClientPanicked)); +} + +#[test] +fn quitting_stops_calling_process() { + let (client, _) = crate::Client::new("", crate::ClientOptions::default()).unwrap(); + let mut calls = 0; + let (send, recv) = std::sync::mpsc::sync_channel(2); + let process_handler = crate::contrib::ClosureProcessHandler::new(move |_, _| { + send.try_send(true).unwrap(); + calls += 1; + assert_eq!(calls, 1); + crate::Control::Quit + }); + let ac = client.activate_async((), process_handler).unwrap(); + assert!(recv + .recv_timeout(std::time::Duration::from_secs(1)) + .unwrap()); + ac.deactivate().unwrap(); +} + +#[test] +fn quitting_buffer_size_never_runs_process() { + let (client, _) = crate::Client::new("", crate::ClientOptions::default()).unwrap(); + let (send, recv) = std::sync::mpsc::sync_channel(2); + let handler = crate::contrib::ClosureProcessHandler::with_state( + (), + move |_, _, _| { + send.try_send(true).unwrap(); + crate::Control::Quit + }, + move |_, _, _| panic!("quit requested, this should not be called"), + ); + let ac = client.activate_async((), handler).unwrap(); + assert!(recv + .recv_timeout(std::time::Duration::from_secs(1)) + .unwrap()); + // Give the process handler some time to try to activate. + std::thread::sleep(std::time::Duration::from_millis(500)); + ac.deactivate().unwrap(); +} + +#[test] +fn buffer_size_is_called_before_process() { + let (client, _) = crate::Client::new("", crate::ClientOptions::default()).unwrap(); + let (send, recv) = std::sync::mpsc::sync_channel(2); + let process_handler = crate::contrib::ClosureProcessHandler::with_state( + "initializing", + move |state, _, _| { + assert_eq!(*state, "processing"); + send.try_send(true).ok(); + crate::Control::Continue + }, + |state, _, _| { + assert_eq!(*state, "initializing"); + *state = "processing"; + crate::Control::Continue + }, + ); + let ac = client.activate_async((), process_handler).unwrap(); + assert!(recv + .recv_timeout(std::time::Duration::from_secs(1)) + .unwrap()); + assert_eq!(ac.deactivate().unwrap().2.state, "processing"); +} + +#[test] +fn signals_in_audio_ports_are_forwarded() { + // Setup clients and ports. + let (client, _) = crate::Client::new("", crate::ClientOptions::default()).unwrap(); + let buffer_size = client.buffer_size() as usize; + assert_ne!(buffer_size, 0); + let input = client + .register_port("in", crate::AudioIn::default()) + .unwrap(); + let mut output = client + .register_port("out", crate::AudioOut::default()) + .unwrap(); + let (input_name, output_name) = (input.name().unwrap(), output.name().unwrap()); + let (send, recv) = std::sync::mpsc::sync_channel(1); + + // Setup checks. + let process_handler = crate::contrib::ClosureProcessHandler::new(move |_, ps| { + let test_val = 0.25; + output.as_mut_slice(ps).fill(test_val); + assert_eq!(output.as_mut_slice(ps).len(), buffer_size); + + assert_eq!(input.as_slice(ps).len(), buffer_size); + // We don't fail if the input is not yet ready as this depends on port connection. Port + // connection takes some time so the first few iterations may not contain the expected data. + if input.as_slice(ps).iter().all(|x| *x == test_val) { + send.try_send(true).unwrap(); + crate::Control::Quit + } else { + crate::Control::Continue + } + }); + + // Runs checks. + let ac = client.activate_async((), process_handler).unwrap(); + ac.as_client() + .connect_ports_by_name(&output_name, &input_name) + .unwrap(); + assert!(recv + .recv_timeout(std::time::Duration::from_secs(1)) + .unwrap()); + ac.deactivate().unwrap(); +} + +#[test] +fn messages_in_midi_ports_are_forwarded() { + let (client, _) = crate::Client::new("", crate::ClientOptions::default()).unwrap(); + + let buffer_size = client.buffer_size() as usize; + assert_ne!(buffer_size, 0); + let input = client + .register_port("in", crate::MidiIn::default()) + .unwrap(); + let mut output = client + .register_port("out", crate::MidiOut::default()) + .unwrap(); + let (input_name, output_name) = (input.name().unwrap(), output.name().unwrap()); + let (send, recv) = std::sync::mpsc::sync_channel(1); + let process_handler = crate::contrib::ClosureProcessHandler::new(move |_, ps| { + let mut writer = output.writer(ps); + assert_ne!(writer.max_event_size(), 0); + for time in 0..10 { + writer + .write(&crate::RawMidi { + time, + bytes: &[0, 1, 2], + }) + .unwrap(); + } + + let iter = input.iter(ps); + let ports_are_probably_connected = iter.clone().count() == 10; + if ports_are_probably_connected { + for (idx, msg) in iter.enumerate() { + assert_eq!(msg.time as usize, idx); + assert_eq!(msg.bytes, &[0, 1, 2]); + } + send.try_send(true).unwrap(); + crate::Control::Quit + } else { + crate::Control::Continue + } + }); + let ac = client.activate_async((), process_handler).unwrap(); + ac.as_client() + .connect_ports_by_name(&output_name, &input_name) + .unwrap(); + assert!(recv + .recv_timeout(std::time::Duration::from_secs(1)) + .unwrap()); + ac.deactivate().unwrap(); +} + +#[test] +fn activating_client_notifies_buffer_size_before_beginning() { + let (client, _) = crate::Client::new("", crate::ClientOptions::default()).unwrap(); + let initial_buffer_size = client.buffer_size() as usize; + assert_ne!(initial_buffer_size, 0); +} diff --git a/src/tests/ringbuffer.rs b/src/tests/ringbuffer.rs new file mode 100644 index 000000000..2dd24b69a --- /dev/null +++ b/src/tests/ringbuffer.rs @@ -0,0 +1,93 @@ +use crate::RingBuffer; + +#[test] +fn ringbuffer_new_creates_new_ringbuffer() { + RingBuffer::new(1024).unwrap(); +} + +#[test] +fn advancing_transfers_space_from_writer_to_reader() { + let ringbuf = RingBuffer::new(1024).unwrap(); + let (reader, mut writer) = ringbuf.into_reader_writer(); + + assert_eq!(writer.space(), 1023); + assert_eq!(reader.space(), 0); + + writer.advance(23); + assert_eq!(writer.space(), 1000); + assert_eq!(reader.space(), 23); +} + +#[test] +fn writing_to_writer_sends_to_reader() { + let ringbuf = RingBuffer::new(1024).unwrap(); + let (mut reader, mut writer) = ringbuf.into_reader_writer(); + + assert_eq!(writer.write_buffer(&[0, 1, 2, 3]), 4); + + let mut tmp_buffer = [0_u8; 8]; + assert_eq!(reader.read_slice(&mut tmp_buffer), &[0, 1, 2, 3]); +} + +#[test] +fn written_bytes_can_be_peaked() { + let ringbuf = RingBuffer::new(1024).unwrap(); + let (reader, mut writer) = ringbuf.into_reader_writer(); + + writer.write_buffer(&[0, 1, 2, 3]); + assert_eq!( + reader.peek_iter().copied().collect::>(), + vec![0, 1, 2, 3] + ); +} + +#[test] +fn advancing_and_writing_shifts_vector() { + let ringbuf = RingBuffer::new(8).unwrap(); + let (mut reader, mut writer) = ringbuf.into_reader_writer(); + + assert_eq!(writer.get_vector().0.len(), 7); + assert_eq!(writer.get_vector().1.len(), 0); + assert_eq!(reader.get_vector().0.len(), 0); + assert_eq!(reader.get_vector().1.len(), 0); + + writer.advance(3); + assert_eq!(writer.get_vector().0.len(), 4); + assert_eq!(writer.get_vector().1.len(), 0); + reader.advance(3); + assert_eq!(reader.get_vector().0.len(), 0); + assert_eq!(reader.get_vector().1.len(), 0); + + assert_eq!(writer.write_buffer(&[0, 1, 2]), 3); + assert_eq!(reader.get_vector().0.len(), 3); + assert_eq!(reader.get_vector().1.len(), 0); + assert_eq!(reader.peek_iter().copied().collect::>(), &[0, 1, 2]); +} + +#[test] +fn writing_and_advancing_produces_data_on_reader() { + let ringbuf = RingBuffer::new(1024).unwrap(); + let (mut reader, mut writer) = ringbuf.into_reader_writer(); + for (item, bufitem) in writer.peek_iter().zip([0, 1, 2, 3]) { + *item = bufitem; + } + assert_eq!(reader.read_slice(&mut [0; 8]), &[]); + writer.advance(4); + assert_eq!(reader.read_slice(&mut [0; 8]), &[0, 1, 2, 3]); +} + +#[test] +fn reading_and_writing_from_separate_threads_is_ok() { + let ringbuf = RingBuffer::new(1024).unwrap(); + let (mut reader, mut writer) = ringbuf.into_reader_writer(); + + std::thread::spawn(move || { + for (item, bufitem) in writer.peek_iter().zip([0, 1, 2, 3]) { + *item = bufitem; + } + writer.advance(4); + }) + .join() + .unwrap(); + assert_eq!(reader.read_slice(&mut [0; 8]), &[0, 1, 2, 3]); +} diff --git a/src/tests/time.rs b/src/tests/time.rs new file mode 100644 index 000000000..4987a3bff --- /dev/null +++ b/src/tests/time.rs @@ -0,0 +1,30 @@ +use approx::assert_abs_diff_eq; + +#[test] +fn frame_and_time_are_convertable() { + let (client, _) = crate::Client::new("", crate::ClientOptions::empty()).unwrap(); + assert_eq!(client.time_to_frames(client.frames_to_time(0)), 0); +} + +#[test] +fn one_frame_duration_is_inverse_of_sample_rate() { + let (client, _) = crate::Client::new("", crate::ClientOptions::empty()).unwrap(); + let sample_rate = client.sample_rate(); + assert_abs_diff_eq!( + (client.frames_to_time(sample_rate as _) - client.frames_to_time(0)) as f64, + 1_000_000.0, + epsilon = 1_000_000.0 * 1e-4, + ); +} + +#[test] +fn one_second_is_sample_rate_frames() { + let (client, _) = crate::Client::new("", crate::ClientOptions::empty()).unwrap(); + let t0 = client.time_to_frames(0); + let t1 = client.time_to_frames(1_000_000); + assert_abs_diff_eq!( + (t1 - t0) as f64, + client.sample_rate() as f64, + epsilon = client.sample_rate() as f64 * 1e-4 + ); +} diff --git a/src/tests/transport.rs b/src/tests/transport.rs new file mode 100644 index 000000000..99ceb4b5b --- /dev/null +++ b/src/tests/transport.rs @@ -0,0 +1,30 @@ +use std::{thread::sleep, time::Duration}; + +use crate::{Client, TransportPosition, TransportState}; + +#[test] +fn new_transport_is_not_valid() { + assert!(!TransportPosition::default().valid_bbt()); + assert!(!TransportPosition::default().valid_bbt_frame_offset()); + assert_eq!(TransportPosition::default().frame(), 0); + assert_eq!(TransportPosition::default().bbt(), None); + assert_eq!(TransportPosition::default().bbt_offset(), None); + assert_eq!(TransportPosition::default().frame_rate(), None); + assert_eq!(TransportPosition::default().usecs(), None); +} + +#[test] +fn starting_transport_sets_state_to_started() { + let (client, _) = Client::new("", Default::default()).unwrap(); + let transport = client.transport(); + + transport.stop().unwrap(); + sleep(Duration::from_millis(50)); + assert_eq!(transport.query().unwrap().state, TransportState::Stopped); + + transport.start().unwrap(); + sleep(Duration::from_millis(50)); + assert_eq!(transport.query().unwrap().state, TransportState::Rolling); + + transport.stop().unwrap(); +} diff --git a/src/transport.rs b/src/transport.rs index de9692d57..446e842ca 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -12,6 +12,10 @@ pub struct Transport { pub(crate) client_life: Weak<()>, } +//all exposed methods are realtime safe +unsafe impl Send for Transport {} +unsafe impl Sync for Transport {} + /// A structure representing the transport position. #[repr(transparent)] pub struct TransportPosition(j::jack_position_t); @@ -86,14 +90,6 @@ impl std::fmt::Display for TransportBBTValidationError { impl std::error::Error for TransportBBTValidationError {} impl Transport { - fn with_client R, R>(&self, func: F) -> Result { - if self.client_life.upgrade().is_some() { - Ok(func(self.client_ptr)) - } else { - Err(crate::Error::ClientIsNoLongerAlive) - } - } - /// Start the JACK transport rolling. /// /// # Remarks @@ -171,15 +167,6 @@ impl Transport { } } - // Helper to create generic error from jack response - fn result_from_ffi(v: Result<::libc::c_int>, r: R) -> Result { - match v { - Ok(0) => Ok(r), - Ok(error_code) => Err(crate::Error::UnknownError { error_code }), - Err(e) => Err(e), - } - } - /// Query the current transport state and position. /// /// # Remarks @@ -213,11 +200,24 @@ impl Transport { Self::state_from_ffi(unsafe { j::jack_transport_query(ptr, std::ptr::null_mut()) }) }) } -} -//all exposed methods are realtime safe -unsafe impl Send for Transport {} -unsafe impl Sync for Transport {} + fn with_client R, R>(&self, func: F) -> Result { + if self.client_life.upgrade().is_some() { + Ok(func(self.client_ptr)) + } else { + Err(crate::Error::ClientIsNoLongerAlive) + } + } + + // Helper to create generic error from jack response + fn result_from_ffi(v: Result<::libc::c_int>, r: R) -> Result { + match v { + Ok(0) => Ok(r), + Ok(error_code) => Err(crate::Error::UnknownError { error_code }), + Err(e) => Err(e), + } + } +} impl TransportPosition { /// Query to see if the BarBeatsTick data is valid. @@ -230,23 +230,6 @@ impl TransportPosition { (self.0.valid & j::JackBBTFrameOffset) != 0 } - /* - /// Query to see if the Timecode data is valid. - pub fn valid_timecode(&self) -> bool { - (self.0.valid & j::JackPositionTimecode) != 0 - } - - /// Query to see if the Audio/Video ratio is valid. - pub fn valid_avr(&self) -> bool { - (self.0.valid & j::JackAudioVideoRatio) != 0 - } - - /// Query to see if the Video frame offset is valid. - pub fn valid_video_frame_offset(&self) -> bool { - (self.0.valid & j::JackVideoFrameOffset) != 0 - } - */ - /// Get the frame number on the transport timeline. /// /// # Remarks @@ -316,7 +299,7 @@ impl TransportPosition { /// * `bbt` - The data to set in the position. `None` will invalidate the BarBeatsTick data. /// /// # Remarks - /// * If the bbt does not validate, will leave the pre-existing data intact. + /// * If `bbt` is not valid, will leave the pre-existing data intact. pub fn set_bbt( &mut self, bbt: Option, @@ -449,7 +432,7 @@ impl TransportBBT { self } - /// Validate contents. + /// Returns `self` is valid, otherwise returns an error describing what is invalid. pub fn validated(&'_ self) -> std::result::Result { if self.bar == 0 { Err(TransportBBTValidationError::BarZero) @@ -470,6 +453,7 @@ impl TransportBBT { } } + /// Returns true if valid. Use `validated` to get the exact validation results. pub fn valid(&self) -> bool { self.validated().is_ok() } @@ -496,318 +480,3 @@ impl Default for TransportBBT { } } } - -#[cfg(test)] -mod test { - mod position { - use crate::{TransportBBT, TransportPosition}; - #[test] - fn default() { - let p: TransportPosition = Default::default(); - assert!(!p.valid_bbt()); - assert!(!p.valid_bbt_frame_offset()); - assert_eq!(p.frame(), 0); - assert_eq!(p.bbt(), None); - assert_eq!(p.bbt_offset(), None); - assert_eq!(p.frame_rate(), None); - assert_eq!(p.usecs(), None); - } - - #[test] - fn usecs() { - let mut p: TransportPosition = Default::default(); - assert_eq!(p.usecs(), None); - p.0.usecs = 1; - assert_eq!(p.usecs(), Some(1)); - p.0.usecs = 0; - assert_eq!(p.usecs(), None); - p.0.usecs = 2084; - assert_eq!(p.usecs(), Some(2084)); - } - - #[test] - fn frame_rate() { - let mut p: TransportPosition = Default::default(); - assert_eq!(p.frame_rate(), None); - p.0.frame_rate = 44100; - assert_eq!(p.frame_rate(), Some(44100)); - p.0.frame_rate = 0; - assert_eq!(p.frame_rate(), None); - p.0.frame_rate = 48000; - assert_eq!(p.frame_rate(), Some(48000)); - } - - #[test] - fn bbt_invalid() { - let mut i: TransportPosition = Default::default(); - let mut v: TransportPosition = Default::default(); - let def: TransportBBT = Default::default(); - - assert!(!i.valid_bbt()); - assert_eq!(i.set_bbt(None), Ok(())); - assert!(!i.valid_bbt()); - - assert!(!v.valid_bbt()); - assert_eq!(v.set_bbt(Some(def)), Ok(())); - assert_eq!(v.bbt(), Some(def)); - - let mut t = |b| { - assert!(i.set_bbt(Some(b)).is_err()); - assert!(v.set_bbt(Some(b)).is_err()); - assert!(!i.valid_bbt()); - assert!(v.valid_bbt()); - assert_eq!(i.bbt(), None); - assert_eq!(v.bbt(), Some(def)); - }; - - let mut bbt = TransportBBT { - bar: 0, - ..Default::default() - }; - t(bbt); - - bbt = Default::default(); - bbt.beat = 0; - t(bbt); - - bbt.beat = 5; - t(bbt); - - bbt = Default::default(); - bbt.tick = 1921; - t(bbt); - bbt.tick = 1920; - t(bbt); - - bbt = Default::default(); - bbt.bpm = -1.0; - t(bbt); - - bbt = Default::default(); - bbt.ticks_per_beat = -1.0; - t(bbt); - bbt.ticks_per_beat = 0.0; - t(bbt); - - bbt = Default::default(); - bbt.sig_num = 0.0; - t(bbt); - bbt.sig_num = -1.0; - t(bbt); - - bbt = Default::default(); - bbt.sig_denom = 0.0; - t(bbt); - bbt.sig_denom = -1.0; - t(bbt); - - bbt = Default::default(); - bbt.sig_num = 7.0; - bbt.beat = 8; - t(bbt); - - bbt = Default::default(); - bbt.ticks_per_beat = 96.0; - bbt.tick = 96; - t(bbt); - } - - #[test] - fn bbt_valid() { - let mut p: TransportPosition = Default::default(); - let mut b: TransportBBT = Default::default(); - let i = TransportBBT { - beat: 5, //invalid - ..Default::default() - }; - - assert!(!i.valid()); - - assert!(!p.valid_bbt()); - assert_eq!(p.set_bbt(Some(b)), Ok(())); - assert!(p.valid_bbt()); - assert_eq!(p.bbt(), Some(b)); - - let mut t = |b: TransportBBT| { - assert!(b.valid()); - assert_eq!(p.set_bbt(Some(b)), Ok(())); - assert_eq!(p.bbt(), Some(b)); - assert!(p.set_bbt(Some(i)).is_err()); - assert_eq!(p.bbt(), Some(b)); - }; - - for i in 1..10 { - b.bar = i; - t(b); - } - - for i in 1..=4 { - b.beat = i; - t(b); - } - - b.sig_num = 7.; - for i in 1..=7 { - b.beat = i; - t(b); - } - - b.beat = 1; - b.sig_num = 4.; - b.ticks_per_beat = 96.0; - for i in 0..96 { - b.tick = i; - t(b); - } - - for i in (10..300).step_by(7) { - b.bpm = i as _; - t(b); - } - } - } - mod bbt { - use crate::{TransportBBT, TransportBBTValidationError}; - - fn approx_eq(a: f32, b: f32) -> bool { - (a - b).abs() < f32::EPSILON - } - - #[test] - fn default() { - let bbt: TransportBBT = Default::default(); - assert_eq!(bbt.bar, 1); - assert_eq!(bbt.beat, 1); - assert_eq!(bbt.tick, 0); - assert!(approx_eq(bbt.sig_num, 4.0), "{} != {}", bbt.sig_num, 4.0); - assert!( - approx_eq(bbt.sig_denom, 4.0), - "{} != {}", - bbt.sig_denom, - 4.0 - ); - assert!( - approx_eq(bbt.ticks_per_beat as f32, 1920.0), - "{} != {}", - bbt.ticks_per_beat, - 1920.0 - ); - assert!(approx_eq(bbt.bpm as f32, 120.0), "{} != {}", bbt.bpm, 120.0); - assert!( - approx_eq(bbt.bar_start_tick as f32, 0.0), - "{} != {}", - bbt.bar_start_tick, - 0.0 - ); - } - - #[test] - fn builder_valid() { - let mut bbt = TransportBBT::default(); - assert_eq!( - TransportBBT::default().with_bbt(1, 1, 0).validated(), - Ok(bbt) - ); - - bbt.bar = 100; - bbt.beat = 2; - bbt.tick = 230; - assert_eq!( - TransportBBT::default().with_bbt(100, 2, 230).validated(), - Ok(bbt) - ); - - bbt = Default::default(); - bbt.sig_num = 7.0; - bbt.sig_denom = 8.0; - assert_eq!( - TransportBBT::default().with_timesig(7.0, 8.0).validated(), - Ok(bbt) - ); - - bbt = Default::default(); - bbt.ticks_per_beat = 2000.0; - assert_eq!( - TransportBBT::default() - .with_ticks_per_beat(2000.0) - .validated(), - Ok(bbt) - ); - - bbt = Default::default(); - bbt.bar_start_tick = 1023.0; - assert_eq!( - TransportBBT::default() - .with_bar_start_tick(1023.0) - .validated(), - Ok(bbt) - ); - - bbt = Default::default(); - bbt.bar = 2; - bbt.beat = 3; - bbt.tick = 20; - bbt.bpm = 23.0; - bbt.ticks_per_beat = 96.0; - bbt.sig_num = 12.0; - bbt.sig_denom = 5.0; - bbt.bar_start_tick = 4.3; - - assert_eq!( - TransportBBT::default() - .with_bbt(bbt.bar, bbt.beat, bbt.tick) - .with_bpm(bbt.bpm) - .with_ticks_per_beat(bbt.ticks_per_beat) - .with_timesig(bbt.sig_num, bbt.sig_denom) - .with_bar_start_tick(bbt.bar_start_tick) - .validated(), - Ok(bbt) - ); - - //can simply use setters, could create invalid data.. - bbt = TransportBBT::default(); - bbt.with_bpm(120.0); - assert!( - approx_eq(bbt.bpm as f32, 120.0), - "{} != {},", - bbt.bpm, - 120.0 - ); - } - - #[test] - fn builder_invalid() { - let mut bbt = TransportBBT { - bpm: -1023.0, - ..TransportBBT::default() - }; - assert_eq!( - TransportBBT::default().with_bpm(bbt.bpm).validated(), - Err(TransportBBTValidationError::BPMRange) - ); - - bbt = Default::default(); - bbt.bar = 0; - assert_eq!( - TransportBBT::default().with_bbt(0, 1, 0).validated(), - Err(TransportBBTValidationError::BarZero) - ); - - bbt = Default::default(); - bbt.tick = bbt.ticks_per_beat as usize; - assert_eq!( - TransportBBT::default().with_bbt(1, 1, bbt.tick).validated(), - Err(TransportBBTValidationError::TickRange) - ); - - for beat in &[0, 7] { - bbt = Default::default(); - bbt.beat = *beat; - assert_eq!( - TransportBBT::default().with_bbt(1, bbt.beat, 0).validated(), - Err(TransportBBTValidationError::BeatRange) - ); - } - } - } -}