diff --git a/libherokubuildpack/src/buildpack_output/inline_output.rs b/libherokubuildpack/src/buildpack_output/inline_output.rs index 0ab8cf15..4272bfd1 100644 --- a/libherokubuildpack/src/buildpack_output/inline_output.rs +++ b/libherokubuildpack/src/buildpack_output/inline_output.rs @@ -20,7 +20,7 @@ //! //! inline_output::step("Clearing the cache") //! ``` -use crate::buildpack_output::{state, BuildpackOutput, Stream}; +use crate::buildpack_output::{state, BuildpackOutput, ParagraphInspectWrite, Stream}; use std::io::Stdout; use std::time::Instant; @@ -76,7 +76,7 @@ pub fn important(s: impl AsRef) { fn build_buildpack_output() -> BuildpackOutput { BuildpackOutput:: { - io: std::io::stdout(), + io: ParagraphInspectWrite::new(std::io::stdout()), // Be careful not to do anything that might access this state // as it's ephemeral data (i.e. not passed in from the start of the build) started: Some(Instant::now()), diff --git a/libherokubuildpack/src/buildpack_output/mod.rs b/libherokubuildpack/src/buildpack_output/mod.rs index f3c3895e..827b0fe9 100644 --- a/libherokubuildpack/src/buildpack_output/mod.rs +++ b/libherokubuildpack/src/buildpack_output/mod.rs @@ -15,6 +15,7 @@ //! output.finish(); //! ``` //! +use crate::buildpack_output::util::ParagraphInspectWrite; use std::fmt::Debug; use std::io::Write; use std::sync::{Arc, Mutex}; @@ -28,7 +29,7 @@ mod util; #[allow(clippy::module_name_repetitions)] #[derive(Debug)] pub struct BuildpackOutput { - pub(crate) io: W, + pub(crate) io: ParagraphInspectWrite, pub(crate) started: Option, pub(crate) state: T, } @@ -138,7 +139,7 @@ where { pub fn new(io: W) -> Self { Self { - io, + io: ParagraphInspectWrite::new(io), state: state::NotStarted, started: None, } @@ -186,7 +187,7 @@ where writeln_now(&mut self.io, style::section("Done")); } - self.io + self.io.inner } } @@ -265,9 +266,9 @@ where /// Mostly used for outputting a running command. #[derive(Debug)] #[doc(hidden)] -pub struct Stream { +pub struct Stream { buildpack_output_started: Option, - arc_io: Arc>, + arc_io: Arc>>, started: Instant, } diff --git a/libherokubuildpack/src/buildpack_output/util.rs b/libherokubuildpack/src/buildpack_output/util.rs index b7921266..33b3aa66 100644 --- a/libherokubuildpack/src/buildpack_output/util.rs +++ b/libherokubuildpack/src/buildpack_output/util.rs @@ -1,3 +1,6 @@ +use std::fmt::Debug; +use std::io::Write; + /// Iterator yielding every line in a string. The line includes newline character(s). /// /// @@ -35,6 +38,41 @@ impl<'a> Iterator for LinesWithEndings<'a> { } } +#[derive(Debug)] +pub(crate) struct ParagraphInspectWrite { + pub(crate) inner: W, + pub(crate) was_paragraph: bool, +} + +impl ParagraphInspectWrite +where + W: Debug, +{ + pub(crate) fn new(io: W) -> Self { + Self { + inner: io, + was_paragraph: false, + } + } +} + +impl Write for ParagraphInspectWrite { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + // Only modify `was_paragraph` if we write anything + if !buf.is_empty() { + // TODO: This will not work with Windows line endings + self.was_paragraph = + buf.len() >= 2 && buf[buf.len() - 2] == b'\n' && buf[buf.len() - 1] == b'\n'; + } + + self.inner.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.inner.flush() + } +} + #[cfg(test)] pub(crate) mod test_helpers { use super::*; @@ -84,4 +122,24 @@ mod test { assert_eq!("zfoo\nzbar\n", actual); } + + #[test] + #[allow(clippy::write_with_newline)] + fn test_paragraph_inspect_write() { + use std::io::Write; + + let buffer: Vec = vec![]; + let mut inspect_write = ParagraphInspectWrite::new(buffer); + + assert!(!inspect_write.was_paragraph); + + write!(&mut inspect_write, "Hello World!\n").unwrap(); + assert!(!inspect_write.was_paragraph); + + write!(&mut inspect_write, "Hello World!\n\n").unwrap(); + assert!(inspect_write.was_paragraph); + + write!(&mut inspect_write, "End.\n").unwrap(); + assert!(!inspect_write.was_paragraph); + } }