use std::io::{ErrorKind, Write};
use std::sync::{Mutex, PoisonError};
use isothrum_runner::{ErrorCode, Event, EventKind, Events, Op};
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 json(&self, event: &Event) {
match serde_json::to_string(event) {
Ok(text) => self.out(&text),
Err(e) => self.err(&format!("warning: an event couldn't be written as JSON: {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) fn failure(code: ErrorCode, message: String) -> EventKind {
EventKind::Error {
code,
message,
step: None,
timeout: None,
}
}
pub(crate) struct Json<'a>(pub(crate) &'a Term);
impl Events for Json<'_> {
fn emit(&self, event: &Event) {
self.0.json(event);
}
}
#[derive(Debug, Default)]
pub(crate) struct Trail(Mutex<Option<Last>>);
#[derive(Debug, Clone)]
struct Last {
seq: u64,
run: Option<String>,
op: Option<Op>,
error: bool,
}
impl Trail {
fn keep(&self, event: &Event) {
let last = Last {
seq: event.seq,
run: event.run.clone(),
op: event.op,
error: matches!(event.kind, EventKind::Error { .. }),
};
*self.0.lock().unwrap_or_else(PoisonError::into_inner) = Some(last);
}
fn last(&self) -> Option<Last> {
self.0.lock().unwrap_or_else(PoisonError::into_inner).clone()
}
pub(crate) fn ended_in_error(&self) -> bool {
self.last().is_some_and(|l| l.error)
}
pub(crate) fn next(&self, run: Option<String>, op: Option<Op>, kind: EventKind) -> Event {
match self.last() {
Some(last) => Event::new(last.seq + 1, last.run, last.op, kind),
None => Event::new(1, run, op, kind),
}
}
pub(crate) fn follow(&self, run: Option<String>, op: Option<Op>, kind: EventKind) -> Event {
let event = self.next(run, op, kind);
self.keep(&event);
event
}
}
pub(crate) struct Traced<'a> {
pub(crate) sink: &'a dyn Events,
pub(crate) trail: &'a Trail,
}
impl Events for Traced<'_> {
fn emit(&self, event: &Event) {
self.trail.keep(event);
self.sink.emit(event);
}
}
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");
}
}