diff --git a/libherokubuildpack/src/output/background.rs b/libherokubuildpack/src/output/background.rs new file mode 100644 index 00000000..f3769cb5 --- /dev/null +++ b/libherokubuildpack/src/output/background.rs @@ -0,0 +1,213 @@ +use std::fmt::Debug; +use std::io::Write; +use std::sync::mpsc::channel; +use std::time::Duration; + +/// This module is responsible for the logic involved in the printing to output while +/// other work is being performed. Such as printing dots while a download is being performed + +/// Print dots to the given buffer at the given interval +/// +/// Returns a struct that allows for manually stopping the timer or will automatically stop +/// the timer if the guard is dropped. This functionality allows for errors that trigger +/// an exit of the function to not accidentally have a timer printing in the background +/// forever. +#[must_use] +pub(crate) fn print_interval( + mut buffer: W, + interval: Duration, + start: String, + tick: String, + end: String, +) -> state::PrintGuard +where + W: Write + Send + Debug + 'static, +{ + let (sender, receiver) = channel::<()>(); + + let join_handle = std::thread::spawn(move || { + write!(buffer, "{start}").expect("Internal error"); + buffer.flush().expect("Internal error"); + + loop { + write!(buffer, "{tick}").expect("Internal error"); + buffer.flush().expect("Internal error"); + + if receiver.recv_timeout(interval).is_ok() { + break; + } + } + + write!(buffer, "{end}").expect("Internal error"); + buffer.flush().expect("Internal error"); + + buffer + }); + + state::PrintGuard::new(join_handle, sender) +} + +pub(crate) mod state { + use std::fmt::Debug; + use std::io::Write; + use std::sync::mpsc::Sender; + use std::thread::JoinHandle; + + /// Holds the reference to the background printer + /// + /// Ensures that the dot printer is stopped in the event of an error. By signaling + /// it and joining when this struct is dropped. + /// + /// Gives access to the original io/buffer struct passed to the background writer + /// when the thread is manually stopped. + /// + /// # Panics + /// + /// Updates to this code need to take care to not introduce a panic. See + /// documentation in `PrintGuard::stop` below for more details. + #[derive(Debug)] + pub(crate) struct PrintGuard + where + W: Write + Debug, + { + /// Holds the handle to the thread printing dots + /// + /// Structs that implement `Drop` must ensure a valid internal state at + /// all times due to E0509. The handle is wrapped in an option to allow the + /// inner value to be removed while preserving internal state. + join_handle: Option>, + + /// Holds the signaling method to tell the background printer + /// to stop emitting. + stop_signal: Sender<()>, + } + + impl Drop for PrintGuard + where + W: Write + Debug, + { + fn drop(&mut self) { + // A note on correctness. It might seem that it's enough to signal the thread to + // stop, that we don't also have to join and wait for it to finish, but that's not + // the case. The printer can still emit a value after it's told to stop. + // + // When that happens the output can appear in the middle of another output, such + // as an error message if a global writer is being used such as stdout. + // As a result we have to signal AND ensure the thread is stoped before + // continuing. + if let Some(join_handle) = self.join_handle.take() { + let _ = self.stop_signal.send(()); + let _ = join_handle.join(); + } + } + } + + impl PrintGuard + where + W: Write + Debug, + { + /// Preserve internal state by ensuring the `Option` is always populated + pub(crate) fn new(join_handle: JoinHandle, sender: Sender<()>) -> Self { + let guard = PrintGuard { + join_handle: Some(join_handle), + stop_signal: sender, + }; + debug_assert!(guard.join_handle.is_some()); + + guard + } + + /// The only thing a consumer can do is stop the dots and receive + /// the original buffer. + /// + /// # Panics + /// + /// This code can panic if it encounters an unexpedcted internal state. + /// If that happens it means there is an internal bug in this logic. + /// To avoid a panic, developers modifying this file must obey the following + /// rules: + /// + /// - Always consume the struct when accessing the Option value outside of Drop + /// - Never construct this struct with a `None` option value. + /// + /// This `Option` wrapping is needed to support a implementing Drop to + /// ensure the printing is stopped. When a struct implements drop it cannot + /// remove it's internal state due to E0509: + /// + /// + /// + /// The workaround is to never allow invalid internal state by replacing the + /// inner value with a `None` when removing it. We don't want to expose this + /// implementation detail to the user, so instead we accept the panic, ensure + /// the code is exercised under test, and exhaustively document why this panic + /// exists and how developers working with this code can maintain safety. + #[allow(clippy::panic_in_result_fn)] + pub(crate) fn stop(mut self) -> std::thread::Result { + // Ignore if the channel is closed, likely means the thread died which + // we want in this case. + match self.join_handle.take() { + Some(join_handle) => { + let _ = self.stop_signal.send(()); + join_handle.join() + } + None => panic!("Internal error: Dot print internal state should never be None"), + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::fs::{File, OpenOptions}; + use tempfile::NamedTempFile; + + #[test] + fn does_stop_does_not_panic() { + let mut buffer: Vec = vec![]; + write!(buffer, "before").unwrap(); + + let dot = print_interval( + buffer, + Duration::from_millis(1), + String::from(" ."), + String::from("."), + String::from(". "), + ); + let mut writer = dot.stop().unwrap(); + + write!(writer, "after").unwrap(); + writer.flush().unwrap(); + + assert_eq!("before ... after", String::from_utf8_lossy(&writer)); + } + + #[test] + fn test_drop_stops_timer() { + let tempfile = NamedTempFile::new().unwrap(); + let mut log = File::create(tempfile.path()).unwrap(); + write!(log, "before").unwrap(); + + let dot = print_interval( + log, + Duration::from_millis(1), + String::from(" ."), + String::from("."), + String::from(". "), + ); + drop(dot); + + let mut log = OpenOptions::new() + .append(true) + .write(true) + .open(tempfile.path()) + .unwrap(); + write!(log, "after").unwrap(); + log.flush().unwrap(); + + assert_eq!( + String::from("before ... after"), + std::fs::read_to_string(tempfile.path()).unwrap() + ); + } +} diff --git a/libherokubuildpack/src/output/background_timer.rs b/libherokubuildpack/src/output/background_timer.rs deleted file mode 100644 index f3c7f187..00000000 --- a/libherokubuildpack/src/output/background_timer.rs +++ /dev/null @@ -1,157 +0,0 @@ -use std::io::Write; -use std::sync::mpsc::Sender; -use std::sync::{mpsc, Arc, Mutex}; -use std::thread::JoinHandle; -use std::time::{Duration, Instant}; - -/// This module is responsible for the logic involved in the printing to output while -/// other work is being performed. - -/// Prints a start, then a tick every second, and an end to the given `Write` value. -/// -/// Returns a struct that allows for manually stopping the timer or will automatically stop -/// the timer if the guard is dropped. This functionality allows for errors that trigger -/// an exit of the function to not accidentally have a timer printing in the background -/// forever. -pub(crate) fn start_timer( - arc_io: &Arc>, - tick_duration: Duration, - start: impl AsRef, - tick: impl AsRef, - end: impl AsRef, -) -> StopJoinGuard -where - // The 'static lifetime means as long as something holds a reference to it, nothing it references - // will go away. - // - // From https://users.rust-lang.org/t/why-does-thread-spawn-need-static-lifetime-for-generic-bounds/4541 - // - // [lifetimes] refer to the minimum possible lifetime of any borrowed references that the object contains. - T: Write + Send + Sync + 'static, -{ - let instant = Instant::now(); - let (sender, receiver) = mpsc::channel::<()>(); - let start = start.as_ref().to_string(); - let tick = tick.as_ref().to_string(); - let end = end.as_ref().to_string(); - - let arc_io = arc_io.clone(); - let handle = std::thread::spawn(move || { - let mut io = arc_io.lock().expect("Logging mutex poisoned"); - write!(&mut io, "{start}").expect("Internal error"); - io.flush().expect("Internal error"); - loop { - write!(&mut io, "{tick}").expect("Internal error"); - io.flush().expect("Internal error"); - - if receiver.recv_timeout(tick_duration).is_ok() { - write!(&mut io, "{end}").expect("Internal error"); - io.flush().expect("Internal error"); - break; - } - } - }); - - StopJoinGuard { - inner: Some(StopTimer { - handle: Some(handle), - sender: Some(sender), - instant, - }), - } -} - -/// Responsible for stopping a running timer thread -#[derive(Debug)] -pub(crate) struct StopTimer { - instant: Instant, - handle: Option>, - sender: Option>, -} - -impl StopTimer { - pub(crate) fn elapsed(&self) -> Duration { - self.instant.elapsed() - } -} - -pub(crate) trait StopJoin: std::fmt::Debug { - fn stop_join(self) -> Self; -} - -impl StopJoin for StopTimer { - fn stop_join(mut self) -> Self { - if let Some(inner) = self.sender.take() { - inner.send(()).expect("Internal error"); - } - - if let Some(inner) = self.handle.take() { - inner.join().expect("Internal error"); - } - - self - } -} - -// Guarantees that stop is called on the inner -#[derive(Debug)] -pub(crate) struct StopJoinGuard { - inner: Option, -} - -impl StopJoinGuard { - /// Since this consumes self and `stop_join` consumes - /// the inner, the option will never be empty unless - /// it was created with a None inner. - /// - /// Since inner is private we guarantee it's always Some - /// until this struct is consumed. - pub(crate) fn stop(mut self) -> T { - self.inner - .take() - .map(StopJoin::stop_join) - .expect("Internal error: Should never panic, codepath tested") - } -} - -impl Drop for StopJoinGuard { - fn drop(&mut self) { - if let Some(inner) = self.inner.take() { - inner.stop_join(); - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::output::util::ReadYourWrite; - use libcnb_test::assert_contains; - use std::thread::sleep; - - #[test] - fn does_stop_does_not_panic() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let done = start_timer(&writer.arc_io(), Duration::from_millis(1), " .", ".", ". "); - - let _ = done.stop(); - - assert_contains!(String::from_utf8_lossy(&reader.lock().unwrap()), " ... "); - } - - #[test] - fn test_drop_stops_timer() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let done = start_timer(&writer.arc_io(), Duration::from_millis(1), " .", ".", ". "); - - drop(done); - sleep(Duration::from_millis(2)); - - let before = String::from_utf8_lossy(&reader.lock().unwrap()).to_string(); - sleep(Duration::from_millis(100)); - let after = String::from_utf8_lossy(&reader.lock().unwrap()).to_string(); - assert_eq!(before, after); - } -} diff --git a/libherokubuildpack/src/output/build_log.rs b/libherokubuildpack/src/output/build_log.rs index c7564aa1..966c2de4 100644 --- a/libherokubuildpack/src/output/build_log.rs +++ b/libherokubuildpack/src/output/build_log.rs @@ -1,4 +1,4 @@ -use crate::output::background_timer::{start_timer, StopJoinGuard, StopTimer}; +use crate::output::background::{print_interval, state::PrintGuard}; #[allow(clippy::wildcard_imports)] pub use crate::output::interface::*; use crate::output::style; @@ -156,17 +156,23 @@ where } fn step_timed(self: Box, s: &str) -> Box { - let start = style::step(format!("{s}{}", style::background_timer_start())); - let tick = style::background_timer_tick(); - let end = style::background_timer_end(); + let mut io = self.io; + let data = self.data; + let timer = Instant::now(); - let arc_io = Arc::new(Mutex::new(self.io)); - let background = start_timer(&arc_io, Duration::from_secs(1), start, tick, end); + write_now(&mut io, style::step(s)); + let dot_printer = print_interval( + io, + Duration::from_secs(1), + style::background_timer_start(), + style::background_timer_tick(), + style::background_timer_end(), + ); Box::new(FinishTimedStep { - arc_io, - background, - data: self.data, + data, + timer, + dot_printer, }) } @@ -236,19 +242,6 @@ where writeln_now(&mut self.io, style::important(s.trim())); writeln_now(&mut self.io, ""); } - - fn log_warn_later_shared(&mut self, s: &str) { - let mut formatted = style::warning(s.trim()); - formatted.push('\n'); - - match crate::output::warn_later::try_push(formatted) { - Ok(()) => {} - Err(error) => { - eprintln!("[Buildpack Warning]: Cannot use the delayed warning feature due to error: {error}"); - self.log_warning_shared(s); - } - }; - } } impl ErrorLogger for AnnounceBuildLog @@ -277,15 +270,6 @@ where self } - fn warn_later( - mut self: Box, - s: &str, - ) -> Box> { - self.log_warn_later_shared(s); - - self - } - fn important( mut self: Box, s: &str, @@ -315,14 +299,6 @@ where self } - fn warn_later( - mut self: Box, - s: &str, - ) -> Box> { - self.log_warn_later_shared(s); - self - } - fn important( mut self: Box, s: &str, @@ -448,10 +424,13 @@ where /// /// Used to end a background inline timer i.e. Installing ...... (<0.1s) #[derive(Debug)] -struct FinishTimedStep { +struct FinishTimedStep +where + W: Write + Debug, +{ data: BuildData, - arc_io: Arc>, - background: StopJoinGuard, + timer: Instant, + dot_printer: PrintGuard, } impl TimedStepLogger for FinishTimedStep @@ -460,14 +439,20 @@ where { fn finish_timed_step(self: Box) -> Box { // Must stop background writing thread before retrieving IO - let duration = self.background.stop().elapsed(); - let mut io = try_unwrap_arc_io(self.arc_io); + let data = self.data; + let timer = self.timer; + + let mut io = match self.dot_printer.stop() { + Ok(io) => io, + Err(e) => std::panic::resume_unwind(e), + }; + let duration = timer.elapsed(); writeln_now(&mut io, style::details(style::time::human(&duration))); Box::new(BuildLog { io, - data: self.data, + data, state: PhantomData::, }) } @@ -499,7 +484,6 @@ mod test { use crate::command::CommandExt; use crate::output::style::{self, strip_control_codes}; use crate::output::util::{strip_trailing_whitespace, ReadYourWrite}; - use crate::output::warn_later::WarnGuard; use indoc::formatdoc; use libcnb_test::assert_contains; use pretty_assertions::assert_eq; @@ -641,47 +625,6 @@ mod test { assert_eq!(expected, strip_control_codes(reader.read_lossy().unwrap())); } - #[test] - fn warn_later_doesnt_output_newline() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - - let warn_later = WarnGuard::new(writer.clone()); - BuildLog::new(writer) - .buildpack_name("Walkin' on the Sun") - .section("So don't delay, act now, supplies are running out") - .step("Allow if you're still alive, six to eight years to arrive") - .step("And if you follow, there may be a tomorrow") - .announce() - .warn_later("And all that glitters is gold") - .warn_later("Only shooting stars break the mold") - .end_announce() - .step("But if the offer's shunned") - .step("You might as well be walking on the Sun") - .end_section() - .finish_logging(); - - drop(warn_later); - - let expected = formatdoc! {" - - # Walkin' on the Sun - - - So don't delay, act now, supplies are running out - - Allow if you're still alive, six to eight years to arrive - - And if you follow, there may be a tomorrow - - But if the offer's shunned - - You might as well be walking on the Sun - - Done (finished in < 0.1s) - - ! And all that glitters is gold - - ! Only shooting stars break the mold - "}; - - assert_eq!(expected, strip_control_codes(reader.read_lossy().unwrap())); - } - #[test] fn announce_and_exit_makes_no_whitespace() { let writer = ReadYourWrite::writer(Vec::new()); diff --git a/libherokubuildpack/src/output/interface.rs b/libherokubuildpack/src/output/interface.rs index 46dc615b..52aa1da5 100644 --- a/libherokubuildpack/src/output/interface.rs +++ b/libherokubuildpack/src/output/interface.rs @@ -34,7 +34,6 @@ pub trait AnnounceLogger: ErrorLogger + Debug { type ReturnTo; fn warning(self: Box, s: &str) -> Box>; - fn warn_later(self: Box, s: &str) -> Box>; fn important(self: Box, s: &str) -> Box>; fn end_announce(self: Box) -> Self::ReturnTo; diff --git a/libherokubuildpack/src/output/mod.rs b/libherokubuildpack/src/output/mod.rs index a862a9c9..b3b211ef 100644 --- a/libherokubuildpack/src/output/mod.rs +++ b/libherokubuildpack/src/output/mod.rs @@ -1,7 +1,6 @@ -mod background_timer; +pub(crate) mod background; pub mod build_log; pub mod interface; pub mod section_log; pub mod style; mod util; -pub mod warn_later; diff --git a/libherokubuildpack/src/output/section_log.rs b/libherokubuildpack/src/output/section_log.rs index c5781081..ac1d4e30 100644 --- a/libherokubuildpack/src/output/section_log.rs +++ b/libherokubuildpack/src/output/section_log.rs @@ -108,11 +108,6 @@ pub fn log_warning(s: impl AsRef) { logger().announce().warning(s.as_ref()); } -/// Print an warning block to the output at a later time -pub fn log_warning_later(s: impl AsRef) { - logger().announce().warn_later(s.as_ref()); -} - /// Print an important block to the output pub fn log_important(s: impl AsRef) { logger().announce().important(s.as_ref()); diff --git a/libherokubuildpack/src/output/warn_later.rs b/libherokubuildpack/src/output/warn_later.rs deleted file mode 100644 index 87395ac6..00000000 --- a/libherokubuildpack/src/output/warn_later.rs +++ /dev/null @@ -1,347 +0,0 @@ -use std::cell::RefCell; -use std::fmt::{Debug, Display}; -use std::io::Write; -use std::marker::PhantomData; -use std::rc::Rc; -use std::thread::ThreadId; - -pub type PhantomUnsync = PhantomData>; - -thread_local!(static WARN_LATER: RefCell>> = RefCell::new(None)); - -/// Queue a warning for later -/// -/// Build logs can be quite large and people don't always scroll back up to read every line. Delaying -/// a warning and emitting it right before the end of the build can increase the chances the app -/// developer will read it. -/// -/// ## Use - Setup a `WarnGuard` in your buildpack -/// -/// To ensure warnings are printed, even in the event of errors, you must create a `WarnGuard` -/// in your buildpack that will print any delayed warnings when dropped: -/// -/// ```no_run -/// // src/main.rs -/// use libherokubuildpack::output::warn_later::WarnGuard; -/// -/// // fn build(&self, context: BuildContext) -> libcnb::Result { -/// let warn_later = WarnGuard::new(std::io::stdout()); -/// // ... -/// -/// // Warnings will be emitted when the warn guard is dropped -/// drop(warn_later); -/// // } -/// ``` -/// -/// Alternatively you can manually print delayed warnings: -/// -/// ```no_run -/// use libherokubuildpack::output::warn_later::WarnGuard; -/// -/// // fn build(&self, context: BuildContext) -> libcnb::Result { -/// let warn_later = WarnGuard::new(std::io::stdout()); -/// // ... -/// -/// // Consumes the guard, prints and clears any delayed warnings. -/// warn_later.warn_now(); -/// // } -/// ``` -/// -/// ## Use - Issue a delayed warning -/// -/// Once a warn guard is in place you can queue a warning using `section_log::log_warning_later` or `build_log::*`: -/// -/// ``` -/// use libherokubuildpack::output::warn_later::WarnGuard; -/// use libherokubuildpack::output::build_log::*; -/// -/// // src/main.rs -/// let warn_later = WarnGuard::new(std::io::stdout()); -/// -/// BuildLog::new(std::io::stdout()) -/// .buildpack_name("Julius Caesar") -/// .announce() -/// .warn_later("Beware the ides of march"); -/// ``` -/// -/// ``` -/// use libherokubuildpack::output::warn_later::WarnGuard; -/// use libherokubuildpack::output::section_log::log_warning_later; -/// -/// // src/main.rs -/// let warn_later = WarnGuard::new(std::io::stdout()); -/// -/// // src/layers/greenday.rs -/// log_warning_later("WARNING: Live without warning"); -/// ``` - -/// Pushes a string to a thread local warning vec for to be emitted later -/// -/// # Errors -/// -/// If the internal `WARN_LATER` option is `None` this will emit a `WarnLaterError` because -/// the function call might not be visible to the application owner using the buildpack. -/// -/// This state can happen if no `WarnGuard` is created in the thread where the delayed warning -/// message is trying to be pushed. It can also happen if multiple `WarnGuard`-s are created in the -/// same thread and one of them "takes" the contents before the others go out of scope. -/// -/// For best practice create one and only one `WarnGuard` per thread at a time to avoid this error -/// state. -pub(crate) fn try_push(s: impl AsRef) -> Result<(), WarnLaterError> { - WARN_LATER.with(|cell| match &mut *cell.borrow_mut() { - Some(warnings) => { - warnings.push(s.as_ref().to_string()); - Ok(()) - } - None => Err(WarnLaterError::MissingGuardForThread( - std::thread::current().id(), - )), - }) -} - -/// Ensures a warning vec is present and pushes to it -/// -/// Should only ever be used within a `WarnGuard`. -/// -/// The state where the warnings are taken, but a warn guard is still present -/// can happen when more than one warn guard is created in the same thread -fn force_push(s: impl AsRef) { - WARN_LATER.with(|cell| { - let option = &mut *cell.borrow_mut(); - option - .get_or_insert(Vec::new()) - .push(s.as_ref().to_string()); - }); -} - -/// Removes all delayed warnings from current thread -/// -/// Should only execute from within a `WarnGuard` -fn take() -> Option> { - WARN_LATER.with(|cell| cell.replace(None)) -} - -#[allow(clippy::module_name_repetitions)] -#[derive(Debug)] -pub enum WarnLaterError { - MissingGuardForThread(ThreadId), -} - -impl Display for WarnLaterError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - WarnLaterError::MissingGuardForThread(id) => { - writeln!( - f, - "Cannot use warn_later unless a WarnGuard has been created\n and not yet dropped in the current thread: {id:?}", - ) - } - } - } -} - -/// Delayed Warning emitter -/// -/// To use the delayed warnings feature you'll need to first register a guard. -/// This guard will emit warnings when it goes out of scope or when you manually force it -/// to emit warnings. -/// -/// This struct allows delayed warnings to be emitted even in the even there's an error. -/// -/// See the `warn_later` module docs for usage instructions. -/// -/// The internal design of this features relies on state tied to the current thread. -/// As a result, this struct is not send or sync: -/// -/// ```compile_fail -/// // Fails to compile -/// # // Do not remove this test, it is the only thing that asserts this is not sync -/// use libherokubuildpack::output::warn_later::WarnGuard; -/// -/// fn is_sync(t: impl Sync) {} -/// -/// is_sync(WarnGuard::new(std::io::stdout())) -/// ``` -/// -/// ```compile_fail -/// // Fails to compile -/// # // Do not remove this test, it is the only thing that asserts this is not send -/// use libherokubuildpack::output::warn_later::WarnGuard; -/// -/// fn is_send(t: impl Send) {} -/// -/// is_send(WarnGuard::new(std::io::stdout())) -/// ``` -/// -/// If you are warning in multiple threads you can pass queued warnings from one thread to another. -/// -/// ```rust -/// use libherokubuildpack::output::warn_later::{WarnGuard, DelayedWarnings}; -/// -/// let main_guard = WarnGuard::new(std::io::stdout()); -/// -/// let (delayed_send, delayed_recv) = std::sync::mpsc::channel::(); -/// -/// std::thread::spawn(move || { -/// let sub_guard = WarnGuard::new(std::io::stdout()); -/// // ... -/// delayed_send -/// .send(sub_guard.consume_quiet()) -/// .unwrap(); -/// }) -/// .join(); -/// -/// main_guard -/// .extend_warnings(delayed_recv.recv().unwrap()); -/// ``` -#[derive(Debug)] -pub struct WarnGuard -where - W: Write + Debug, -{ - // Private inner to force public construction through `new()` which tracks creation state per thread. - io: W, - /// The use of WarnGuard is directly tied to the thread where it was created - /// This forces the struct to not be send or sync - /// - /// To move warn later data between threads, drain quietly, send the data to another - /// thread, and re-apply those warnings to a WarnGuard in the other thread. - unsync: PhantomUnsync, -} - -impl WarnGuard -where - W: Write + Debug, -{ - #[must_use] - #[allow(clippy::new_without_default)] - pub fn new(io: W) -> Self { - WARN_LATER.with(|cell| { - let maybe_warnings = &mut *cell.borrow_mut(); - if let Some(warnings) = maybe_warnings.take() { - let _ = maybe_warnings.insert(warnings); - eprintln!("[Buildpack warning]: Multiple `WarnGuard`-s in thread {id:?}, this may cause unexpected delayed warning output", id = std::thread::current().id()); - } else { - let _ = maybe_warnings.insert(Vec::new()); - } - }); - - Self { - io, - unsync: PhantomData, - } - } - - /// Use to move warnings from a different thread into this one - pub fn extend_warnings(&self, warnings: DelayedWarnings) { - for warning in warnings.inner { - force_push(warning.clone()); - } - } - - /// Use to move warnings out of the current thread without emitting to the UI. - pub fn consume_quiet(self) -> DelayedWarnings { - DelayedWarnings { - inner: take().unwrap_or_default(), - } - } - - /// Consumes self, prints and drains all existing delayed warnings - pub fn warn_now(self) { - drop(self); - } -} - -impl Drop for WarnGuard -where - W: Write + Debug, -{ - fn drop(&mut self) { - if let Some(warnings) = take() { - if !warnings.is_empty() { - for warning in &warnings { - writeln!(&mut self.io).expect("warn guard IO is writeable"); - write!(&mut self.io, "{warning}").expect("warn guard IO is writeable"); - } - } - } - } -} - -/// Holds warnings from a consumed `WarnGuard` -/// -/// The intended use of this struct is to pass warnings from one `WarnGuard` to another. -#[derive(Debug)] -pub struct DelayedWarnings { - // Private inner, must be constructed within a WarnGuard - inner: Vec, -} - -#[cfg(test)] -mod test { - use super::*; - use crate::output::util::ReadYourWrite; - use libcnb_test::assert_contains; - - #[test] - fn test_warn_guard_registers_itself() { - // Err when a guard is not yet created - assert!(try_push("lol").is_err()); - - // Don't err when a guard is created - let _warn_guard = WarnGuard::new(Vec::new()); - try_push("lol").unwrap(); - } - - #[test] - fn test_logging_a_warning() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let warn_guard = WarnGuard::new(writer); - drop(warn_guard); - - assert_eq!(String::new(), reader.read_lossy().unwrap()); - - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let warn_guard = WarnGuard::new(writer); - let message = - "Possessing knowledge and performing an action are two entirely different processes"; - - try_push(message).unwrap(); - drop(warn_guard); - - assert_contains!(reader.read_lossy().unwrap(), message); - - // Assert empty after calling drain - assert!(take().is_none()); - } - - #[test] - fn test_delayed_warnings_on_drop() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let guard = WarnGuard::new(writer); - - let message = "You don't have to have a reason to be tired. You don't have to earn rest or comfort. You're allowed to just be."; - try_push(message).unwrap(); - drop(guard); - - assert_contains!(reader.read_lossy().unwrap(), message); - } - - #[test] - fn does_not_double_whitespace() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let guard = WarnGuard::new(writer); - - let message = "Caution: This test is hot\n"; - try_push(message).unwrap(); - drop(guard); - - let expected = "\nCaution: This test is hot\n".to_string(); - assert_eq!(expected, reader.read_lossy().unwrap()); - } -}