diff --git a/Cargo.lock b/Cargo.lock index dd179380..02f3878d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,12 +33,56 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -159,6 +203,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -167,11 +212,25 @@ version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", "terminal_size", ] +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_lex" version = "1.1.0" @@ -193,6 +252,12 @@ dependencies = [ "cc", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "combine" version = "4.6.7" @@ -436,6 +501,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "escape8259" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6" + [[package]] name = "extended" version = "0.1.0" @@ -625,6 +696,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.18" @@ -732,6 +809,18 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1028,6 +1117,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "opusic-sys" version = "0.6.0" @@ -1271,6 +1366,7 @@ dependencies = [ "hound", "inquire", "lewton", + "libtest-mimic", "minimp3_fixed", "num-rational", "quickcheck", @@ -1332,6 +1428,13 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7204ed6420f698836b76d4d5c2ec5dec7585fd5c3a788fd1cde855d1de598239" +[[package]] +name = "rtsan_tests" +version = "0.0.0" +dependencies = [ + "rodio", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -1503,6 +1606,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "symphonia" version = "0.5.5" @@ -1901,6 +2010,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 7a1d5d25..d8a93a83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,13 @@ +[workspace] +members = ["tests/rtsan_tests"] +resolver = "2" + +[workspace.package] +edition = "2021" + +[workspace.dependencies] +rodio = {path = "."} + [package] name = "rodio" version = "0.22.2" @@ -110,6 +120,9 @@ hound = ["dep:hound"] # WAV minimp3 = ["dep:minimp3_fixed"] # MP3 lewton = ["dep:lewton"] # Ogg Vorbis +# enable the realtime sanitizer on the nightly channel +rtsan = [] + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] @@ -135,6 +148,7 @@ rtrb = { version = "0.3.2", optional = true } num-rational = "0.4.2" symphonia-adapter-libopus = { version = "0.2", optional = true } +libtest-mimic = "0.8.2" [dev-dependencies] quickcheck = "1" @@ -272,3 +286,7 @@ required-features = ["playback", "vorbis"] [[example]] name = "third_party_codec" required-features = ["playback", "symphonia", "symphonia-isomp4"] + +[[test]] +harness = false +name = "rtsan" diff --git a/src/lib.rs b/src/lib.rs index fbca45f5..9664c477 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -184,6 +184,9 @@ allow(unreachable_code) )] #![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(feature = "rtsan", feature(sanitize))] +// used to overwrite the `Iterator::next` function to be sanitized by rtsan +#![cfg_attr(feature = "rtsan", feature(supertrait_item_shadowing))] #[cfg(feature = "playback")] pub use cpal::{ diff --git a/src/source/mod.rs b/src/source/mod.rs index 63d5233e..fbc08536 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -199,6 +199,12 @@ pub trait Source: Iterator { /// to determine how many samples remain in the iterator. fn current_span_len(&self) -> Option; + #[cfg(feature = "rtsan")] + #[cfg_attr(feature = "rtsan", sanitize(realtime = "nonblocking"))] + fn next(&mut self) -> Option { + ::next(self) + } + /// Returns true if the source is exhausted (has no more samples available). #[inline] fn is_exhausted(&self) -> bool { diff --git a/src/speakers/builder.rs b/src/speakers/builder.rs index 0ad89165..1f821f8f 100644 --- a/src/speakers/builder.rs +++ b/src/speakers/builder.rs @@ -597,6 +597,7 @@ where $( cpal::SampleFormat::$sample_format => device.build_output_stream::<$generic, _, _>( cpal_config2, + #[cfg_attr(feature = "rtsan", sanitize(realtime = "nonblocking"))] move |data, _| { data.iter_mut().for_each(|d| { *d = source diff --git a/tests/rtsan.rs b/tests/rtsan.rs new file mode 100644 index 00000000..ccafa653 --- /dev/null +++ b/tests/rtsan.rs @@ -0,0 +1,74 @@ +extern crate libtest_mimic; + +use libtest_mimic::{Arguments, Failed, Trial}; +use std::{ + fs::read_dir, + process::{Command, ExitCode, Stdio}, +}; + +fn main() -> ExitCode { + let args = Arguments::from_args(); + + let mut tests = Vec::new(); + + let host_tuple = { + let output = Command::new("rustc") + .args(["--print", "host-tuple"]) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + assert!(output.status.success()); + String::from_utf8(output.stdout).unwrap() + }; + + // collect test cases + for file in read_dir("tests/rtsan_tests/src/bin").unwrap() { + let file = file.unwrap(); + + assert!(file.metadata().unwrap().is_file()); + + let name = file + .file_name() + .to_str() + .unwrap() + .strip_suffix(".rs") + .unwrap() + .to_owned(); + let host_tuple = host_tuple.clone(); + + let test = Trial::test(name.clone(), move || { + let process = Command::new("cargo") + .args([ + "+nightly", + "run", + "-p", + "rtsan_tests", + "--bin", + &name, + // this puts cargo in "cross compilation mode", so it doesn't try to compile the build scripts with sanitizers, + "--target", + &host_tuple, + ]) + .env("RUSTFLAGS", "-Zsanitizer=realtime") + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + let output = process.wait_with_output().unwrap(); + if output.status.success() { + Ok(()) + } else { + Err(Failed::from( + String::from("realtime violation detected. Output: \n") + + &String::from_utf8_lossy(&output.stderr), + )) + } + }); + tests.push(test); + } + + libtest_mimic::run(&args, tests).exit_code() +} diff --git a/tests/rtsan_tests/Cargo.toml b/tests/rtsan_tests/Cargo.toml new file mode 100644 index 00000000..c0a9a3bb --- /dev/null +++ b/tests/rtsan_tests/Cargo.toml @@ -0,0 +1,7 @@ +[package] +edition.workspace = true +name = "rtsan_tests" +publish = false + +[dependencies] +rodio = { workspace = true, features = ["rtsan"]} diff --git a/tests/rtsan_tests/src/bin/stereo.rs b/tests/rtsan_tests/src/bin/stereo.rs new file mode 100644 index 00000000..2c2c3d4a --- /dev/null +++ b/tests/rtsan_tests/src/bin/stereo.rs @@ -0,0 +1,16 @@ +//! Plays a tone alternating between right and left ears, with right being first. + +use rodio::Source; +use std::error::Error; + +fn main() -> Result<(), Box> { + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); + + let file = std::fs::File::open("assets/RL.ogg")?; + player.append(rodio::Decoder::try_from(file)?.amplify(0.2)); + + player.sleep_until_end(); + + Ok(()) +}