geam-cli 0.2.3

Standalone command implementation for Geam
Documentation
use crate::error::CliError;
use std::fmt::Arguments;
use std::io::Write;

pub(crate) enum Progress<'a> {
    Hidden,
    Visible(&'a mut (dyn Write + Send)),
}

impl Progress<'_> {
    pub(crate) fn report(&mut self, message: Arguments<'_>) -> Result<(), CliError> {
        match self {
            Self::Hidden => Ok(()),
            Self::Visible(writer) => writeln!(writer, "geam: {message}")
                .and_then(|()| writer.flush())
                .map_err(CliError::PreparationProgressIo),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::Progress;
    use crate::error::CliError;
    use std::io::{self, Write};

    #[derive(Default)]
    struct RecordedOutput {
        bytes: Vec<u8>,
        flushes: Vec<usize>,
    }

    impl Write for RecordedOutput {
        fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
            self.bytes.extend_from_slice(bytes);
            Ok(bytes.len())
        }

        fn flush(&mut self) -> io::Result<()> {
            self.flushes.push(self.bytes.len());
            Ok(())
        }
    }

    #[test]
    fn appends_and_flushes_each_geam_phase_without_terminal_controls() {
        let mut output = RecordedOutput::default();
        let mut progress = Progress::Visible(&mut output);
        progress
            .report(format_args!("Preparing app"))
            .expect("first phase");
        progress
            .report(format_args!("Prepared app"))
            .expect("completed phase");

        assert_eq!(output.bytes, b"geam: Preparing app\ngeam: Prepared app\n");
        assert_eq!(output.flushes, [20, 39]);
        Progress::Hidden
            .report(format_args!("hidden"))
            .expect("hidden output");
    }

    struct FailedOutput {
        fail_write: bool,
    }

    impl Write for FailedOutput {
        fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
            if self.fail_write {
                Err(io::Error::new(io::ErrorKind::BrokenPipe, "closed output"))
            } else {
                Ok(bytes.len())
            }
        }

        fn flush(&mut self) -> io::Result<()> {
            Err(io::Error::new(io::ErrorKind::BrokenPipe, "closed output"))
        }
    }

    #[test]
    fn preserves_phase_write_and_flush_failures() {
        for fail_write in [true, false] {
            let mut output = FailedOutput { fail_write };
            let error = Progress::Visible(&mut output)
                .report(format_args!("Preparing app"))
                .expect_err("closed output must fail");
            assert_eq!(error.to_string(), "failed to write preparation progress");
            assert!(matches!(error, CliError::PreparationProgressIo(error)
                if error.kind() == io::ErrorKind::BrokenPipe));
        }
    }
}