use bstr::ByteSlice;
use std::error::Error;
use std::fmt;
use std::process;
pub trait OutputOkExt
where
Self: ::std::marker::Sized,
{
fn ok(self) -> OutputResult;
fn unwrap(self) -> process::Output {
match self.ok() {
Ok(output) => output,
Err(err) => panic!("{}", err),
}
}
fn unwrap_err(self) -> OutputError {
match self.ok() {
Ok(output) => panic!(
"Command completed successfully\nstdout=```{}```",
DebugBytes::new(&output.stdout)
),
Err(err) => err,
}
}
}
impl OutputOkExt for process::Output {
fn ok(self) -> OutputResult {
if self.status.success() {
Ok(self)
} else {
let error = OutputError::new(self);
Err(error)
}
}
}
impl<'c> OutputOkExt for &'c mut process::Command {
fn ok(self) -> OutputResult {
let output = self.output().map_err(OutputError::with_cause)?;
if output.status.success() {
Ok(output)
} else {
let error = OutputError::new(output).set_cmd(format!("{:?}", self));
Err(error)
}
}
fn unwrap_err(self) -> OutputError {
match self.ok() {
Ok(output) => panic!(
"Completed successfully:\ncommand=`{:?}`\nstdout=```{}```",
self,
DebugBytes::new(&output.stdout)
),
Err(err) => err,
}
}
}
pub type OutputResult = Result<process::Output, OutputError>;
#[derive(Debug)]
pub struct OutputError {
cmd: Option<String>,
stdin: Option<bstr::BString>,
cause: OutputCause,
}
impl OutputError {
pub fn new(output: process::Output) -> Self {
Self {
cmd: None,
stdin: None,
cause: OutputCause::Expected(Output { output }),
}
}
pub fn with_cause<E>(cause: E) -> Self
where
E: Error + Send + Sync + 'static,
{
Self {
cmd: None,
stdin: None,
cause: OutputCause::Unexpected(Box::new(cause)),
}
}
pub fn set_cmd(mut self, cmd: String) -> Self {
self.cmd = Some(cmd);
self
}
pub fn set_stdin(mut self, stdin: Vec<u8>) -> Self {
self.stdin = Some(bstr::BString::from(stdin));
self
}
pub fn as_output(&self) -> Option<&process::Output> {
match self.cause {
OutputCause::Expected(ref e) => Some(&e.output),
OutputCause::Unexpected(_) => None,
}
}
}
impl Error for OutputError {}
impl fmt::Display for OutputError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let palette = crate::Palette::current();
if let Some(ref cmd) = self.cmd {
writeln!(
f,
"{}={}",
palette.key.paint("command"),
palette.value.paint(cmd)
)?;
}
if let Some(ref stdin) = self.stdin {
writeln!(
f,
"{}={}",
palette.key.paint("stdin"),
palette.value.paint(DebugBytes::new(stdin))
)?;
}
write!(f, "{}", self.cause)
}
}
#[derive(Debug)]
enum OutputCause {
Expected(Output),
Unexpected(Box<dyn Error + Send + Sync + 'static>),
}
impl fmt::Display for OutputCause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
OutputCause::Expected(ref e) => write!(f, "{}", e),
OutputCause::Unexpected(ref e) => write!(f, "{}", e),
}
}
}
#[derive(Debug)]
struct Output {
output: process::Output,
}
impl fmt::Display for Output {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
output_fmt(&self.output, f)
}
}
pub(crate) fn output_fmt(output: &process::Output, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let palette = crate::Palette::current();
if let Some(code) = output.status.code() {
writeln!(
f,
"{}={}",
palette.key.paint("code"),
palette.value.paint(code)
)?;
} else {
writeln!(
f,
"{}={}",
palette.key.paint("code"),
palette.value.paint("<interrupted>")
)?;
}
write!(
f,
"{}={}\n{}={}\n",
palette.key.paint("stdout"),
palette.value.paint(DebugBytes::new(&output.stdout)),
palette.key.paint("stderr"),
palette.value.paint(DebugBytes::new(&output.stderr)),
)?;
Ok(())
}
#[derive(Debug)]
pub(crate) struct DebugBytes<'a> {
bytes: &'a [u8],
}
impl<'a> DebugBytes<'a> {
pub(crate) fn new(bytes: &'a [u8]) -> Self {
DebugBytes { bytes }
}
}
impl<'a> fmt::Display for DebugBytes<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
format_bytes(self.bytes, f)
}
}
#[derive(Debug)]
pub(crate) struct DebugBuffer {
buffer: bstr::BString,
}
impl DebugBuffer {
pub(crate) fn new(buffer: Vec<u8>) -> Self {
DebugBuffer {
buffer: buffer.into(),
}
}
}
impl fmt::Display for DebugBuffer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
format_bytes(&self.buffer, f)
}
}
fn format_bytes(data: &[u8], f: &mut impl fmt::Write) -> fmt::Result {
#![allow(clippy::assertions_on_constants)]
const LINES_MIN_OVERFLOW: usize = 80;
const LINES_MAX_START: usize = 20;
const LINES_MAX_END: usize = 40;
const LINES_MAX_PRINTED: usize = LINES_MAX_START + LINES_MAX_END;
assert!(LINES_MAX_PRINTED < LINES_MIN_OVERFLOW);
const BYTES_MIN_OVERFLOW: usize = 8192;
const BYTES_MAX_START: usize = 2048;
const BYTES_MAX_END: usize = 2048;
const BYTES_MAX_PRINTED: usize = BYTES_MAX_START + BYTES_MAX_END;
assert!(BYTES_MAX_PRINTED < BYTES_MIN_OVERFLOW);
let lines_total = data.as_bstr().lines_with_terminator().count();
let multiline = 1 < lines_total;
if LINES_MIN_OVERFLOW <= lines_total {
let lines_omitted = lines_total - LINES_MAX_PRINTED;
let start_lines = data.as_bstr().lines_with_terminator().take(LINES_MAX_START);
let end_lines = data
.as_bstr()
.lines_with_terminator()
.skip(LINES_MAX_START + lines_omitted);
writeln!(f, "<{} lines total>", lines_total)?;
write_debug_bstrs(f, true, start_lines)?;
writeln!(f, "<{} lines omitted>", lines_omitted)?;
write_debug_bstrs(f, true, end_lines)
} else if BYTES_MIN_OVERFLOW <= data.len() {
write!(
f,
"<{} bytes total>{}",
data.len(),
if multiline { "\n" } else { "" }
)?;
write_debug_bstrs(
f,
multiline,
data[..BYTES_MAX_START].lines_with_terminator(),
)?;
write!(
f,
"<{} bytes omitted>{}",
data.len() - BYTES_MAX_PRINTED,
if multiline { "\n" } else { "" }
)?;
write_debug_bstrs(
f,
multiline,
data[data.len() - BYTES_MAX_END..].lines_with_terminator(),
)
} else {
write_debug_bstrs(f, multiline, data.lines_with_terminator())
}
}
fn write_debug_bstrs<'a>(
f: &mut impl std::fmt::Write,
multiline: bool,
mut lines: impl Iterator<Item = &'a [u8]>,
) -> std::fmt::Result {
if multiline {
writeln!(f, "```")?;
for mut line in lines {
let mut newline = false;
if line.last() == Some(&b'\n') {
line = &line[..line.len() - 1];
newline = true;
}
let s = format!("{:?}", line.as_bstr());
write!(
f,
"{}{}",
&s[1..s.len() - 1],
if newline { "\n" } else { "" }
)?;
}
writeln!(f, "```")
} else {
write!(f, "{:?}", lines.next().unwrap_or(&[]).as_bstr())
}
}
#[cfg(test)]
mod test {
#[test]
fn format_bytes() {
let mut s = String::new();
for i in 0..80 {
s.push_str(&format!("{}\n", i));
}
let mut buf = String::new();
super::format_bytes(s.as_bytes(), &mut buf).unwrap();
assert_eq!(
"<80 lines total>
```
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
```
<20 lines omitted>
```
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
```
",
buf
);
}
#[test]
fn no_trailing_newline() {
let s = "no\ntrailing\nnewline";
let mut buf = String::new();
super::format_bytes(s.as_bytes(), &mut buf).unwrap();
assert_eq!(
"```
no
trailing
newline```
",
buf
);
}
}