use std::io::{ErrorKind, Write};
use std::sync::{Mutex, PoisonError};
use isothrum_runner::{Event, Events};
use crate::error::{Error, Result};
use crate::text::human;
pub(crate) struct Term {
out: Mutex<Box<dyn Write + Send>>,
err: Mutex<Box<dyn Write + Send>>,
failed: Mutex<Option<std::io::Error>>,
}
impl Term {
pub(crate) fn std() -> Term {
Term::new(Box::new(std::io::stdout()), Box::new(std::io::stderr()))
}
pub(crate) fn new(out: Box<dyn Write + Send>, err: Box<dyn Write + Send>) -> Term {
Term {
out: Mutex::new(out),
err: Mutex::new(err),
failed: Mutex::new(None),
}
}
pub(crate) fn out(&self, line: &str) {
self.write(&self.out, line);
}
pub(crate) fn err(&self, line: &str) {
self.write(&self.err, line);
}
fn write(&self, stream: &Mutex<Box<dyn Write + Send>>, line: &str) {
let mut w = stream.lock().unwrap_or_else(PoisonError::into_inner);
if let Err(e) = writeln!(w, "{line}").and_then(|()| w.flush())
&& e.kind() != ErrorKind::BrokenPipe
{
self.failed.lock().unwrap_or_else(PoisonError::into_inner).get_or_insert(e);
}
}
pub(crate) fn finish(&self) -> Result<()> {
match self.failed.lock().unwrap_or_else(PoisonError::into_inner).take() {
Some(e) => Err(Error::Output(e)),
None => Ok(()),
}
}
}
pub(crate) struct Json<'a>(pub(crate) &'a Term);
impl Events for Json<'_> {
fn emit(&self, event: &Event) {
match serde_json::to_string(event) {
Ok(text) => self.0.out(&text),
Err(e) => self.0.err(&format!("warning: an event couldn't be written as JSON: {e}")),
}
}
}
pub(crate) struct Human<'a>(pub(crate) &'a Term);
impl Events for Human<'_> {
fn emit(&self, event: &Event) {
if let Some(line) = human(&event.kind) {
self.0.err(&line);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::Buf;
struct Failing(ErrorKind);
impl Write for Failing {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::from(self.0))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[test]
fn a_reader_that_left_is_no_error_and_a_full_disk_is_said_once_done() {
let closed = Term::new(Box::new(Failing(ErrorKind::BrokenPipe)), Box::new(std::io::sink()));
closed.out("a line");
assert!(closed.finish().is_ok());
let full = Term::new(Box::new(Failing(ErrorKind::StorageFull)), Box::new(std::io::sink()));
full.out("a line");
full.out("another");
assert!(matches!(full.finish(), Err(Error::Output(e)) if e.kind() == ErrorKind::StorageFull));
assert!(full.finish().is_ok(), "said once");
}
#[test]
fn output_and_notes_go_to_their_own_streams() {
let (out, err) = (Buf::default(), Buf::default());
let term = Term::new(Box::new(out.clone()), Box::new(err.clone()));
term.out("answer");
term.err("note");
assert_eq!(out.take(), "answer\n");
assert_eq!(err.take(), "note\n");
}
}