diff --git a/CHANGELOG.md b/CHANGELOG.md index ea2eaaa..36efefb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add: New function `global::with_locked_writer` is introduced to allow consistently capturing write output. This function is designed for use in testing output or in other non-reentrant capture cases. This blocks all threads using this function but one from executing so that a deterministic and consistent output is captured. Previously tests could be written with a thread_local writer, however there's a subtle race condition in that approach if the output relies on "paragraph" style text (https://github.com/heroku-buildpacks/bullet_stream/pull/43). + ## v0.9.0 2025/06/05 - Change: Result of `global::sub_start_timer(...).done()` is no longer "must use". This means it no longer needs `let _ =` for clippy. (https://github.com/heroku-buildpacks/bullet_stream/pull/38) diff --git a/src/docs/global_done_one.rs b/src/docs/global_done_one.rs deleted file mode 100644 index 0ab50c4..0000000 --- a/src/docs/global_done_one.rs +++ /dev/null @@ -1,3 +0,0 @@ - -# let output = bullet_stream::strip_ansi(std::fs::read_to_string(&path).unwrap()); -assert_eq!(r" diff --git a/src/docs/global_done_two.rs b/src/docs/global_done_two.rs deleted file mode 100644 index b7e1d78..0000000 --- a/src/docs/global_done_two.rs +++ /dev/null @@ -1 +0,0 @@ -".trim_start(), output.trim_start()); diff --git a/src/docs/global_setup.rs b/src/docs/global_setup.rs deleted file mode 100644 index d96a9d9..0000000 --- a/src/docs/global_setup.rs +++ /dev/null @@ -1,7 +0,0 @@ - -use bullet_stream::global::print; -# use pretty_assertions::{assert_eq, assert_ne}; -# -# let temp = tempfile::tempdir().unwrap(); -# let path = temp.path().join("log"); -# bullet_stream::global::set_writer(std::fs::File::create(&path).unwrap()); diff --git a/src/global.rs b/src/global.rs index af095db..038b447 100644 --- a/src/global.rs +++ b/src/global.rs @@ -1,7 +1,12 @@ use crate::util::ParagraphInspectWrite; use crate::util::TrailingParagraph; use crate::util::TrailingParagraphSend; +use std::any::Any; +use std::cell::Cell; use std::io::Write; +use std::panic::catch_unwind; +use std::panic::resume_unwind; +use std::panic::AssertUnwindSafe; use std::sync::LazyLock; use std::sync::Mutex; @@ -25,12 +30,16 @@ pub type _GlobalWriter = GlobalWriter; impl Write for GlobalWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { - let mut w = WRITER.lock().unwrap(); + let mut w = WRITER.lock().map_err(|_| { + std::io::Error::other("GlobalWriter lock poisoned - cannot guarantee data consistency") + })?; w.write(buf) } fn flush(&mut self) -> std::io::Result<()> { - let mut w = WRITER.lock().unwrap(); + let mut w = WRITER.lock().map_err(|_| { + std::io::Error::other("GlobalWriter lock poisoned - cannot guarantee data consistency") + })?; w.flush() } } @@ -55,17 +64,155 @@ impl TrailingParagraph for GlobalWriter { /// /// # Panics /// -/// If you try to pass in a `_GlobalWriter` +/// - If you try to pass in a `GlobalWriter` +/// - If you try to call `set_writer` inside of `with_locked_writer` pub fn set_writer(new_writer: W) where W: Write + Send + 'static, { if std::any::Any::type_id(&new_writer) == std::any::TypeId::of::() { - panic!("Cannot set the global writer to _GlobalWriter"); - } else { - let mut writer = WRITER.lock().unwrap(); - *writer = Box::new(ParagraphInspectWrite::new(new_writer)); + panic!("Cannot set the global writer to GlobalWriter"); } + + let _global_lock = WITH_WRITER_GLOBAL_LOCK + .try_lock() + .expect("Cannot call `set_writer` inside of `with_locked_writer`"); + + let mut writer = WRITER + .lock() + .expect("Global writer lock poisoned - cannot guarantee data consistency"); + *writer = Box::new(ParagraphInspectWrite::new(new_writer)); +} + +static WITH_WRITER_GLOBAL_LOCK: LazyLock> = LazyLock::new(|| ().into()); +thread_local! { + static WITH_WRITER_REENTRANT_CHECK: Cell = const { Cell::new(false) }; +} + +/// RAII guard for preventing reentrant calls to `with_locked_writer` +/// +/// This guard automatically resets the reentrant check flag when dropped, +/// ensuring proper cleanup even if the guarded code panics. +struct ReentrantGuard; + +impl ReentrantGuard { + /// Creates a new guard, panicking if already set (indicating reentrant call) + fn new() -> Self { + WITH_WRITER_REENTRANT_CHECK.with(|only_once| { + if only_once.get() { + panic!("Cannot call this function recursively!"); + } + only_once.set(true); + }); + ReentrantGuard + } +} + +impl Drop for ReentrantGuard { + fn drop(&mut self) { + WITH_WRITER_REENTRANT_CHECK.with(|only_once| { + only_once.set(false); + }); + } +} + +/// Sets the global writer for the duration of the provided closure +/// +/// This is meant to be used in tests where the order of writes are important. +/// +/// ```rust +/// use bullet_stream::global::{self, print}; +/// +/// let out = global::with_locked_writer(Vec::::new(), || { +/// print::bullet("Hello world"); +/// }); +/// assert_eq!("- Hello world\n".to_string(), String::from_utf8_lossy(&out)); +/// +/// let out = global::with_locked_writer(Vec::::new(), || { +/// print::bullet("Knock, knock, Neo"); +/// }); +/// assert_eq!("- Knock, knock, Neo\n".to_string(), String::from_utf8_lossy(&out)); +/// ``` +/// +/// Guarantees that only one invocation of this is called at a time. Returns the provided +/// writer on completion. Panics if called recursively in the same thread. +/// +/// # Panics +/// +/// - If you mutate the global writer via another mechanism (such as calling `global::set_writer`) +/// from within this thread. +/// +/// ```should_panic +/// use bullet_stream::global; +/// +/// global::with_locked_writer(Vec::::new(), || { +/// global::set_writer(Vec::::new()); +/// }); +/// ``` +/// +/// - If you try to call the function recursively in the same thread: +/// +/// ```should_panic +/// use bullet_stream::global; +/// +/// global::with_locked_writer(Vec::::new(), || { +/// global::with_locked_writer(Vec::::new(), || { +/// // +/// }); +/// }); +/// ``` +/// +/// - If you try to pass in a `GlobalWriter` +/// +/// ```should_panic +/// use bullet_stream::global; +/// +/// global::with_locked_writer(global::GlobalWriter, || { +/// // +/// }); +/// ``` +pub fn with_locked_writer(new_writer: W, f: F) -> W +where + W: Write + Send + Any + 'static, + F: FnOnce(), +{ + if std::any::Any::type_id(&new_writer) == std::any::TypeId::of::() { + panic!("Cannot set the global writer to GlobalWriter"); + } + // Ensure all locks are dropped on panic, this supports test assertion failures + // without poisoning locks + let writer_or_panic = { + // Panic if called recursively, must come before lock to prevent deadlock + let _reentrant_guard = ReentrantGuard::new(); + let _global_lock = WITH_WRITER_GLOBAL_LOCK + .lock() + .expect("Global writer coordination lock poisoned - cannot guarantee thread safety"); + let old_writer = { + let mut write_lock = WRITER + .lock() + .expect("Global writer lock poisoned - cannot guarantee data consistency"); + std::mem::replace( + &mut *write_lock, + Box::new(ParagraphInspectWrite::new(new_writer)), + ) + }; + + let f_panic = catch_unwind(AssertUnwindSafe(f)); + + let new_writer = { + let mut write_lock = WRITER + .lock() + .expect("Global writer lock poisoned - cannot guarantee data consistency"); + std::mem::replace(&mut *write_lock, old_writer) + }; + + if let Ok(original) = (new_writer as Box).downcast::>() { + f_panic.map(|_| original.inner) + } else { + panic!("Could not downcast to original type. Writer was mutated unexpectedly. This indicates a bug in with_locked_writer implementation.") + } + }; + writer_or_panic.unwrap_or_else(|payload| resume_unwind(payload)) } #[cfg(feature = "global_functions")] @@ -99,18 +246,24 @@ pub mod print { /// Output a h1 header to the global writer without state /// /// ``` - #[doc = include_str!("./docs/global_setup.rs")] + /// use bullet_stream::global::print; + /// # use pretty_assertions::assert_eq; + /// # + /// # let output = bullet_stream::global::with_locked_writer(Vec::::new(), ||{ /// - /// print::h1("I am a top level header"); /// let duration = std::time::Instant::now(); - /// // ... + /// print::h1("I am a top level header"); + /// /// print::all_done(&Some(duration)); + /// # }); + /// + /// let expected = indoc::formatdoc!{" /// - #[doc = include_str!("./docs/global_done_one.rs")] - /// ## I am a top level header + /// ## I am a top level header /// - /// - Done (finished in < 0.1s) - #[doc = include_str!("./docs/global_done_two.rs")] + /// - Done (finished in < 0.1s) + /// "}; + /// assert_eq!(expected, bullet_stream::strip_ansi(String::from_utf8_lossy(&output))); /// ``` pub fn h1(s: impl AsRef) { write::h1(&mut GlobalWriter, s); @@ -119,21 +272,27 @@ pub mod print { /// Output a h2 header to the global writer without state /// /// ``` - #[doc = include_str!("./docs/global_setup.rs")] + /// use bullet_stream::global::print; + /// # use pretty_assertions::assert_eq; + /// # + /// # let output = bullet_stream::global::with_locked_writer(Vec::::new(), ||{ /// /// print::h1("I am a top level header"); /// print::h2("I am an h2 header"); /// let duration = std::time::Instant::now(); /// // ... /// print::all_done(&Some(duration)); + /// # }); /// - #[doc = include_str!("./docs/global_done_one.rs")] - /// ## I am a top level header + /// let expected = indoc::formatdoc!{" /// - /// ### I am an h2 header + /// ## I am a top level header /// - /// - Done (finished in < 0.1s) - #[doc = include_str!("./docs/global_done_two.rs")] + /// ### I am an h2 header + /// + /// - Done (finished in < 0.1s) + /// "}; + /// assert_eq!(expected, bullet_stream::strip_ansi(String::from_utf8_lossy(&output))); /// ``` pub fn h2(s: impl AsRef) { write::h2(&mut GlobalWriter, s); @@ -142,23 +301,30 @@ pub mod print { /// Output a h3 header to the global writer without state /// /// ``` - #[doc = include_str!("./docs/global_setup.rs")] + /// use bullet_stream::global::print; + /// # use pretty_assertions::assert_eq; + /// # + /// # let output = bullet_stream::global::with_locked_writer(Vec::::new(), ||{ + /// /// print::h1("I am a top level header"); /// print::h2("I am an h2 header"); /// print::h3("I am an h3 header"); /// let duration = std::time::Instant::now(); /// // ... /// print::all_done(&Some(duration)); + /// # }); /// - #[doc = include_str!("./docs/global_done_one.rs")] - /// ## I am a top level header + /// let expected = indoc::formatdoc!{" /// - /// ### I am an h2 header + /// ## I am a top level header /// - /// #### I am an h3 header + /// ### I am an h2 header /// - /// - Done (finished in < 0.1s) - #[doc = include_str!("./docs/global_done_two.rs")] + /// #### I am an h3 header + /// + /// - Done (finished in < 0.1s) + /// "}; + /// assert_eq!(expected, bullet_stream::strip_ansi(String::from_utf8_lossy(&output))); /// ``` pub fn h3(s: impl AsRef) { write::h3(&mut GlobalWriter, s); @@ -170,15 +336,22 @@ pub mod print { /// writer. /// /// ``` - #[doc = include_str!("./docs/global_setup.rs")] + /// use bullet_stream::global::print; + /// # use pretty_assertions::assert_eq; + /// # + /// # let output = bullet_stream::global::with_locked_writer(Vec::::new(), ||{ + /// /// print::plain("This almost seems silly."); /// print::plain("But it auto-flushes IO."); /// print::plain("Which is nice."); - #[doc = include_str!("./docs/global_done_one.rs")] - /// This almost seems silly. - /// But it auto-flushes IO. - /// Which is nice. - #[doc = include_str!("./docs/global_done_two.rs")] + /// # }); + /// + /// let expected = indoc::formatdoc!{" + /// This almost seems silly. + /// But it auto-flushes IO. + /// Which is nice. + /// "}; + /// assert_eq!(expected, bullet_stream::strip_ansi(String::from_utf8_lossy(&output))); /// ``` pub fn plain(s: impl AsRef) { write::plain(&mut GlobalWriter, s) @@ -188,16 +361,24 @@ pub mod print { /// /// Use together with [all_done] /// ``` - #[doc = include_str!("./docs/global_setup.rs")] + /// use bullet_stream::global::print; + /// # use pretty_assertions::assert_eq; + /// # + /// # let output = bullet_stream::global::with_locked_writer(Vec::::new(), ||{ + /// /// let started = print::buildpack("Heroku Awesome Buildpack"); /// print::bullet("Just add awesome."); /// print::all_done(&Some(started)); - #[doc = include_str!("./docs/global_done_one.rs")] - /// ### Heroku Awesome Buildpack + /// # }); + /// + /// let expected = indoc::formatdoc!{" + /// + /// ### Heroku Awesome Buildpack /// - /// - Just add awesome. - /// - Done (finished in < 0.1s) - #[doc = include_str!("./docs/global_done_two.rs")] + /// - Just add awesome. + /// - Done (finished in < 0.1s) + /// "}; + /// assert_eq!(expected, bullet_stream::strip_ansi(String::from_utf8_lossy(&output))); /// ``` pub fn buildpack(s: impl AsRef) -> Instant { write::h2(&mut GlobalWriter, s); @@ -207,7 +388,10 @@ pub mod print { /// Header to break up subsections in a buildpack's output /// /// ``` - #[doc = include_str!("./docs/global_setup.rs")] + /// use bullet_stream::global::print; + /// # use pretty_assertions::assert_eq; + /// # + /// # let output = bullet_stream::global::with_locked_writer(Vec::::new(), ||{ /// /// let started = print::buildpack("FYEO Buildpack"); /// @@ -226,26 +410,30 @@ pub mod print { /// print::sub_bullet("2023"); /// /// print::all_done(&Some(started)); - #[doc = include_str!("./docs/global_done_one.rs")] - /// ### FYEO Buildpack - /// - /// #### the branches bending low - /// - /// - Tracks - /// - all the windows are glowing - /// - looking in between those long reeds - /// - Released - /// - 2024 - /// - /// #### failed book plots - /// - /// - Tracks - /// - the stream at new river beach - /// - a line that is broad - /// - Released - /// - 2023 - /// - Done (finished in < 0.1s) - #[doc = include_str!("./docs/global_done_two.rs")] + /// # }); + /// + /// let expected = indoc::formatdoc!{" + /// + /// ### FYEO Buildpack + /// + /// #### the branches bending low + /// + /// - Tracks + /// - all the windows are glowing + /// - looking in between those long reeds + /// - Released + /// - 2024 + /// + /// #### failed book plots + /// + /// - Tracks + /// - the stream at new river beach + /// - a line that is broad + /// - Released + /// - 2023 + /// - Done (finished in < 0.1s) + /// "}; + /// assert_eq!(expected, bullet_stream::strip_ansi(String::from_utf8_lossy(&output))); /// ``` pub fn header(s: impl AsRef) { write::h3(&mut GlobalWriter, s); @@ -254,12 +442,18 @@ pub mod print { /// Output a bullet point to the global writer without state /// /// ``` - #[doc = include_str!("./docs/global_setup.rs")] + /// use bullet_stream::global::print; + /// # use pretty_assertions::assert_eq; + /// # + /// # let output = bullet_stream::global::with_locked_writer(Vec::::new(), ||{ /// /// print::bullet("Good point!"); - #[doc = include_str!("./docs/global_done_one.rs")] - /// - Good point! - #[doc = include_str!("./docs/global_done_two.rs")] + /// # }); + /// + /// let expected = indoc::formatdoc!{" + /// - Good point! + /// "}; + /// assert_eq!(expected, bullet_stream::strip_ansi(String::from_utf8_lossy(&output))); /// ``` pub fn bullet(s: impl AsRef) { write::bullet(&mut GlobalWriter, s) @@ -268,15 +462,20 @@ pub mod print { /// Output a sub-bullet point to the global writer without state /// /// ``` - #[doc = include_str!("./docs/global_setup.rs")] + /// use bullet_stream::global::print; + /// # use pretty_assertions::assert_eq; + /// # + /// # let output = bullet_stream::global::with_locked_writer(Vec::::new(), ||{ /// /// print::bullet("Good point!"); /// print::sub_bullet("Another good point!"); + /// # }); /// - #[doc = include_str!("./docs/global_done_one.rs")] - /// - Good point! - /// - Another good point! - #[doc = include_str!("./docs/global_done_two.rs")] + /// let expected = indoc::formatdoc!{" + /// - Good point! + /// - Another good point! + /// "}; + /// assert_eq!(expected, bullet_stream::strip_ansi(String::from_utf8_lossy(&output))); /// ``` pub fn sub_bullet(s: impl AsRef) { write::sub_bullet(&mut GlobalWriter, s); @@ -285,7 +484,7 @@ pub mod print { /// Print a sub-bullet and stream a command to the global writer without state /// /// ```no_run - #[doc = include_str!("./docs/global_setup.rs")] + /// use bullet_stream::global::print; /// use fun_run::CommandWithName; /// /// let mut cmd = std::process::Command::new("bash"); @@ -308,7 +507,7 @@ pub mod print { /// This provieds convienence and standardization over [sub_stream_with] /// /// ```no_run - #[doc = include_str!("./docs/global_setup.rs")] + /// use bullet_stream::global::print; /// use fun_run::CommandWithName; /// /// print::sub_stream_cmd( @@ -326,16 +525,22 @@ pub mod print { /// Print a sub-bullet and then emmit dots to the global writer without state /// /// ``` - #[doc = include_str!("./docs/global_setup.rs")] + /// use bullet_stream::global::print; + /// # use pretty_assertions::assert_eq; + /// # + /// # let output = bullet_stream::global::with_locked_writer(Vec::::new(), ||{ /// /// print::bullet("Ruby"); /// let timer = print::sub_start_timer("Installing"); /// // ... /// timer.done(); - #[doc = include_str!("./docs/global_done_one.rs")] - /// - Ruby - /// - Installing ... (< 0.1s) - #[doc = include_str!("./docs/global_done_two.rs")] + /// # }); + /// + /// let expected = indoc::formatdoc!{" + /// - Ruby + /// - Installing ... (< 0.1s) + /// "}; + /// assert_eq!(expected, bullet_stream::strip_ansi(String::from_utf8_lossy(&output))); /// ``` pub fn sub_start_timer(s: impl AsRef) -> Print> { write::sub_start_timer(ParagraphInspectWrite::new(GlobalWriter), Instant::now(), s) @@ -348,7 +553,7 @@ pub mod print { /// Provides convience and standardization over [sub_start_timer]. /// /// ```no_run - #[doc = include_str!("./docs/global_setup.rs")] + /// use bullet_stream::global::print; /// use fun_run::CommandWithName; /// /// print::sub_time_cmd( @@ -367,18 +572,24 @@ pub mod print { /// Print an all done message with timing info to the UI /// /// ``` - #[doc = include_str!("./docs/global_setup.rs")] + /// use bullet_stream::global::print; + /// # use pretty_assertions::assert_eq; + /// # + /// # let output = bullet_stream::global::with_locked_writer(Vec::::new(), ||{ /// /// print::h2("I am an h2 header"); /// let duration = std::time::Instant::now(); /// // ... /// print::all_done(&Some(duration)); + /// # }); + /// + /// let expected = indoc::formatdoc!{" /// - #[doc = include_str!("./docs/global_done_one.rs")] - /// ### I am an h2 header + /// ### I am an h2 header /// - /// - Done (finished in < 0.1s) - #[doc = include_str!("./docs/global_done_two.rs")] + /// - Done (finished in < 0.1s) + /// "}; + /// assert_eq!(expected, bullet_stream::strip_ansi(String::from_utf8_lossy(&output))); /// ``` pub fn all_done(started: &Option) { write::all_done(&mut GlobalWriter, started); @@ -387,16 +598,22 @@ pub mod print { /// Print a warning to the global writer without state /// /// ``` - #[doc = include_str!("./docs/global_setup.rs")] + /// use bullet_stream::global::print; + /// # use pretty_assertions::assert_eq; + /// # + /// # let output = bullet_stream::global::with_locked_writer(Vec::::new(), ||{ /// /// print::warning("This town ain't\nbig enough\nfor the both of us"); - #[doc = include_str!("./docs/global_done_one.rs")] + /// # }); /// - /// ! This town ain't - /// ! big enough - /// ! for the both of us + /// let expected = indoc::formatdoc!{" /// - #[doc = include_str!("./docs/global_done_two.rs")] + /// ! This town ain't + /// ! big enough + /// ! for the both of us + /// + /// "}; + /// assert_eq!(expected, bullet_stream::strip_ansi(String::from_utf8_lossy(&output))); /// ``` pub fn warning(s: impl AsRef) { write::warning(&mut GlobalWriter, s); @@ -405,7 +622,10 @@ pub mod print { /// Print an error to the global writer without state /// /// ``` - #[doc = include_str!("./docs/global_setup.rs")] + /// use bullet_stream::global::print; + /// # use pretty_assertions::assert_eq; + /// # + /// # let output = bullet_stream::global::with_locked_writer(Vec::::new(), ||{ /// use indoc::formatdoc; /// /// print::error(formatdoc! {" @@ -414,17 +634,72 @@ pub mod print { /// in deep space that I really wish I'd listened to what my mother told /// me when I was young /// "}); + /// # }); /// - #[doc = include_str!("./docs/global_done_one.rs")] + /// let expected = indoc::formatdoc!{" /// - /// ! It's at times like this, when I'm trapped in a Vogon - /// ! airlock with a man from Betelgeuse, and about to die of asphyxiation - /// ! in deep space that I really wish I'd listened to what my mother told - /// ! me when I was young + /// ! It's at times like this, when I'm trapped in a Vogon + /// ! airlock with a man from Betelgeuse, and about to die of asphyxiation + /// ! in deep space that I really wish I'd listened to what my mother told + /// ! me when I was young /// - #[doc = include_str!("./docs/global_done_two.rs")] + /// "}; + /// assert_eq!(expected, bullet_stream::strip_ansi(String::from_utf8_lossy(&output))); /// ``` pub fn error(s: impl AsRef) { write::error(&mut GlobalWriter, s); } } + +#[cfg(test)] +mod test { + use super::*; + use crate::strip_ansi; + use indoc::formatdoc; + use pretty_assertions::assert_eq; + use std::panic; + use std::thread; + + #[test] + fn with_locked_writer_handles_panics_across_threads() { + let handle1 = thread::spawn(|| { + panic::catch_unwind(|| { + with_locked_writer(Vec::new(), || { + print::bullet("About to panic"); + panic!("Intentional panic for testing"); + }); + }) + }); + + let result = handle1 + .join() + .expect("First thread should complete successfully"); + + assert!(result.is_err(), "Expected panic to be caught {:?}", result); + + let handle2 = thread::spawn(|| { + let output = with_locked_writer(Vec::new(), || { + print::bullet("This should work fine"); + print::sub_bullet("Even after another thread panicked"); + }); + + let expected = formatdoc! {" + - This should work fine + - Even after another thread panicked + "}; + + assert_eq!(expected, strip_ansi(String::from_utf8_lossy(&output))); + }); + + handle2 + .join() + .expect("Second thread should complete successfully"); + + let output = with_locked_writer(Vec::new(), || { + print::bullet("Main thread still works"); + }); + + let expected = "- Main thread still works\n"; + assert_eq!(expected, strip_ansi(String::from_utf8_lossy(&output))); + } +} diff --git a/src/lib.rs b/src/lib.rs index 7299328..4401f09 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -746,7 +746,7 @@ mod test { use indoc::formatdoc; use libcnb_test::assert_contains; use pretty_assertions::assert_eq; - use std::{cell::RefCell, fs::File, process::Command}; + use std::{fs::File, process::Command}; #[test] fn double_h2_h2_newlines() { @@ -1102,41 +1102,20 @@ mod test { assert_eq!(expected, strip_ansi(String::from_utf8_lossy(&io))); } - thread_local! { - static THREAD_LOCAL_WRITER: RefCell> = const { RefCell::new(Vec::new()) }; - } - - struct V8ThreadedWriter; - impl V8ThreadedWriter { - fn take() -> Vec { - THREAD_LOCAL_WRITER.take() - } - } - impl Write for V8ThreadedWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - THREAD_LOCAL_WRITER.with_borrow_mut(|writer| writer.write(buf)) - } - - fn flush(&mut self) -> std::io::Result<()> { - THREAD_LOCAL_WRITER.with_borrow_mut(|writer| writer.flush()) - } - } - #[test] fn global_preserves_newline() { - global::set_writer(V8ThreadedWriter); - - Print::global() - .h1("Genuine Joes") - .bullet("Dodge") - .sub_bullet("A ball") - .error("A wrench"); + let output = global::with_locked_writer(Vec::new(), || { + Print::global() + .h1("Genuine Joes") + .bullet("Dodge") + .sub_bullet("A ball") + .error("A wrench"); + + Print::global() + .without_header() + .error("It's a bold strategy, Cotton.\nLet's see if it pays off for 'em."); + }); - Print::global() - .without_header() - .error("It's a bold strategy, Cotton.\nLet's see if it pays off for 'em."); - - let io = V8ThreadedWriter::take(); let expected = formatdoc! {" # Genuine Joes @@ -1151,7 +1130,7 @@ mod test { "}; - assert_eq!(expected, strip_ansi(String::from_utf8_lossy(&io))); + assert_eq!(expected, strip_ansi(String::from_utf8_lossy(&output))); } #[test] diff --git a/src/util.rs b/src/util.rs index d0559c0..2ccf4c4 100644 --- a/src/util.rs +++ b/src/util.rs @@ -76,8 +76,8 @@ pub(crate) trait TrailingParagraph: Write { fn trailing_newline_count(&self) -> usize; } -pub(crate) trait TrailingParagraphSend: TrailingParagraph + Send {} -impl TrailingParagraphSend for T where T: TrailingParagraph + Send {} +pub(crate) trait TrailingParagraphSend: TrailingParagraph + Any + Send {} +impl TrailingParagraphSend for T where T: TrailingParagraph + Any + Send {} impl TrailingParagraph for ParagraphInspectWrite where