use std::io::{BufRead, Write};
use anyhow::Result;
#[derive(Debug, PartialEq, Eq)]
pub enum GuardOutcome {
Proceed,
Cancelled,
DryRun,
}
pub struct GuardOptions<'a> {
pub prompt: &'a str,
pub dry_run_message: &'a str,
pub force: bool,
pub dry_run: bool,
}
pub fn guard_destructive_with_io(
opts: &GuardOptions<'_>,
reader: &mut dyn BufRead,
writer: &mut dyn Write,
) -> Result<GuardOutcome> {
if opts.dry_run {
writeln!(writer, "{}", opts.dry_run_message)?;
return Ok(GuardOutcome::DryRun);
}
if opts.force {
return Ok(GuardOutcome::Proceed);
}
write!(writer, "{}", opts.prompt)?;
writer.flush()?;
let mut answer = String::new();
reader.read_line(&mut answer)?;
if answer.trim().eq_ignore_ascii_case("y") {
Ok(GuardOutcome::Proceed)
} else {
writeln!(writer, "Cancelled.")?;
Ok(GuardOutcome::Cancelled)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::io::Cursor;
fn opts<'a>(prompt: &'a str, dry_run_message: &'a str) -> GuardOptions<'a> {
GuardOptions {
prompt,
dry_run_message,
force: false,
dry_run: false,
}
}
#[test]
fn dry_run_returns_dry_run_and_prints_preview() {
let mut input = Cursor::new(Vec::<u8>::new());
let mut output = Vec::<u8>::new();
let mut o = opts("Delete? ", "Would delete X.");
o.dry_run = true;
let outcome = guard_destructive_with_io(&o, &mut input, &mut output).unwrap();
assert_eq!(outcome, GuardOutcome::DryRun);
assert_eq!(String::from_utf8(output).unwrap(), "Would delete X.\n");
}
#[test]
fn dry_run_wins_over_force() {
let mut input = Cursor::new(Vec::<u8>::new());
let mut output = Vec::<u8>::new();
let mut o = opts("Delete? ", "Would delete X.");
o.force = true;
o.dry_run = true;
let outcome = guard_destructive_with_io(&o, &mut input, &mut output).unwrap();
assert_eq!(outcome, GuardOutcome::DryRun);
assert!(String::from_utf8(output)
.unwrap()
.contains("Would delete X."));
}
#[test]
fn force_returns_proceed_without_io() {
let mut input = Cursor::new(Vec::<u8>::new());
let mut output = Vec::<u8>::new();
let mut o = opts("Delete? ", "Would delete X.");
o.force = true;
let outcome = guard_destructive_with_io(&o, &mut input, &mut output).unwrap();
assert_eq!(outcome, GuardOutcome::Proceed);
assert!(output.is_empty());
}
#[test]
fn yes_lowercase_proceeds() {
let mut input = Cursor::new(b"y\n".to_vec());
let mut output = Vec::<u8>::new();
let outcome =
guard_destructive_with_io(&opts("Delete? ", "Would delete."), &mut input, &mut output)
.unwrap();
assert_eq!(outcome, GuardOutcome::Proceed);
assert_eq!(String::from_utf8(output).unwrap(), "Delete? ");
}
#[test]
fn yes_uppercase_proceeds() {
let mut input = Cursor::new(b"Y\n".to_vec());
let mut output = Vec::<u8>::new();
let outcome =
guard_destructive_with_io(&opts("Delete? ", "Would delete."), &mut input, &mut output)
.unwrap();
assert_eq!(outcome, GuardOutcome::Proceed);
}
#[test]
fn yes_with_whitespace_proceeds() {
let mut input = Cursor::new(b" y \n".to_vec());
let mut output = Vec::<u8>::new();
let outcome =
guard_destructive_with_io(&opts("Delete? ", "Would delete."), &mut input, &mut output)
.unwrap();
assert_eq!(outcome, GuardOutcome::Proceed);
}
#[test]
fn no_cancels_and_prints_notice() {
let mut input = Cursor::new(b"n\n".to_vec());
let mut output = Vec::<u8>::new();
let outcome =
guard_destructive_with_io(&opts("Delete? ", "Would delete."), &mut input, &mut output)
.unwrap();
assert_eq!(outcome, GuardOutcome::Cancelled);
assert!(String::from_utf8(output).unwrap().contains("Cancelled."));
}
#[test]
fn empty_answer_cancels() {
let mut input = Cursor::new(b"\n".to_vec());
let mut output = Vec::<u8>::new();
let outcome =
guard_destructive_with_io(&opts("Delete? ", "Would delete."), &mut input, &mut output)
.unwrap();
assert_eq!(outcome, GuardOutcome::Cancelled);
}
#[test]
fn random_text_cancels() {
let mut input = Cursor::new(b"maybe\n".to_vec());
let mut output = Vec::<u8>::new();
let outcome =
guard_destructive_with_io(&opts("Delete? ", "Would delete."), &mut input, &mut output)
.unwrap();
assert_eq!(outcome, GuardOutcome::Cancelled);
}
struct FailingWriter;
impl std::io::Write for FailingWriter {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("simulated write failure"))
}
fn flush(&mut self) -> std::io::Result<()> {
Err(std::io::Error::other("simulated flush failure"))
}
}
struct FailingReader;
impl std::io::Read for FailingReader {
fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("simulated read failure"))
}
}
impl std::io::BufRead for FailingReader {
fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
Err(std::io::Error::other("simulated read failure"))
}
fn consume(&mut self, _: usize) {}
}
#[test]
fn failing_writer_flush_returns_error() {
let mut writer = FailingWriter;
let err = std::io::Write::flush(&mut writer).unwrap_err();
assert!(err.to_string().contains("simulated flush failure"));
}
#[test]
fn failing_reader_read_returns_error() {
let mut reader = FailingReader;
let mut buf = [0u8; 1];
let err = std::io::Read::read(&mut reader, &mut buf).unwrap_err();
assert!(err.to_string().contains("simulated read failure"));
}
#[test]
fn failing_reader_consume_is_a_noop() {
let mut reader = FailingReader;
std::io::BufRead::consume(&mut reader, 0);
std::io::BufRead::consume(&mut reader, 5);
}
#[test]
fn dry_run_propagates_writer_error() {
let mut input = Cursor::new(Vec::<u8>::new());
let mut writer = FailingWriter;
let mut o = opts("Delete? ", "Would delete.");
o.dry_run = true;
let err = guard_destructive_with_io(&o, &mut input, &mut writer).unwrap_err();
assert!(err.to_string().contains("simulated write failure"));
}
#[test]
fn prompt_propagates_writer_error() {
let mut input = Cursor::new(b"y\n".to_vec());
let mut writer = FailingWriter;
let err =
guard_destructive_with_io(&opts("Delete? ", "Would delete."), &mut input, &mut writer)
.unwrap_err();
assert!(err.to_string().contains("simulated"));
}
#[test]
fn prompt_propagates_reader_error() {
let mut reader = FailingReader;
let mut output = Vec::<u8>::new();
let err =
guard_destructive_with_io(&opts("Delete? ", "Would delete."), &mut reader, &mut output)
.unwrap_err();
assert!(err.to_string().contains("simulated read failure"));
}
#[test]
fn prompt_propagates_flush_error() {
struct FlushFailsWriter;
impl std::io::Write for FlushFailsWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Err(std::io::Error::other("simulated flush failure"))
}
}
let mut input = Cursor::new(b"y\n".to_vec());
let mut writer = FlushFailsWriter;
let err =
guard_destructive_with_io(&opts("Delete? ", "Would delete."), &mut input, &mut writer)
.unwrap_err();
assert!(err.to_string().contains("simulated flush failure"));
}
#[test]
fn cancellation_notice_propagates_writer_error() {
struct WriterFailsAfter {
successes_remaining: usize,
}
impl std::io::Write for WriterFailsAfter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if self.successes_remaining == 0 {
Err(std::io::Error::other(
"simulated cancellation write failure",
))
} else {
self.successes_remaining -= 1;
Ok(buf.len())
}
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut input = Cursor::new(b"n\n".to_vec());
let mut writer = WriterFailsAfter {
successes_remaining: 1,
};
let err =
guard_destructive_with_io(&opts("Delete? ", "Would delete."), &mut input, &mut writer)
.unwrap_err();
assert!(err
.to_string()
.contains("simulated cancellation write failure"));
}
}