use super::{DETACHED_EXIT, OpEntry, PIPED_UNOBSERVED};
const SURFACE_STDERR_LINES: usize = 3;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpRow {
pub ts: String,
pub argv: String,
pub cwd: String,
pub exit: i32,
pub stdout: String,
pub stderr: String,
}
impl OpRow {
pub fn has_output(&self) -> bool {
!self.stdout.is_empty() || !self.stderr.is_empty()
}
pub fn failed(&self) -> bool {
match self.exit {
0 | PIPED_UNOBSERVED => false,
DETACHED_EXIT => !self.stderr.is_empty(),
_ => true,
}
}
}
impl From<&OpEntry> for OpRow {
fn from(entry: &OpEntry) -> Self {
Self {
ts: entry.ts.clone(),
argv: entry.argv.join(" "),
cwd: entry.cwd.clone(),
exit: entry.exit,
stdout: entry.stdout.clone(),
stderr: entry.stderr.clone(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SurfaceFailure {
pub argv: String,
pub stderr_tail: String,
}
impl From<&OpRow> for SurfaceFailure {
fn from(row: &OpRow) -> Self {
Self {
argv: row.argv.clone(),
stderr_tail: stderr_tail(&row.stderr),
}
}
}
fn stderr_tail(stderr: &str) -> String {
let lines: Vec<&str> = stderr.trim_end_matches('\n').lines().collect();
let start = lines.len().saturating_sub(SURFACE_STDERR_LINES);
lines.get(start..).unwrap_or_default().join("\n")
}
#[cfg(test)]
mod tests {
use super::super::SYNTHETIC_EXIT;
use super::*;
fn entry(exit: i32, stderr: &str) -> OpEntry {
OpEntry {
ts: "TS".into(),
argv: vec!["bl".into(), "close".into(), "bl-4db6".into()],
cwd: "/proj".into(),
exit,
stdout: "out".into(),
stderr: stderr.into(),
}
}
#[test]
fn op_row_carries_the_full_entry_and_joins_argv() {
let row = OpRow::from(&entry(0, ""));
assert_eq!(row.ts, "TS");
assert_eq!(row.argv, "bl close bl-4db6");
assert_eq!(row.cwd, "/proj");
assert_eq!(row.exit, 0);
assert_eq!(row.stdout, "out");
assert_eq!(row.stderr, "");
}
#[test]
fn has_output_reflects_either_stream() {
assert!(OpRow::from(&entry(0, "")).has_output()); let neither = OpEntry {
stdout: String::new(),
..entry(0, "")
};
assert!(!OpRow::from(&neither).has_output());
let only_err = OpEntry {
stdout: String::new(),
..entry(1, "boom")
};
assert!(OpRow::from(&only_err).has_output());
}
#[test]
fn failed_classifies_every_exit_class() {
assert!(!OpRow::from(&entry(0, "")).failed()); assert!(!OpRow::from(&entry(PIPED_UNOBSERVED, "")).failed()); assert!(!OpRow::from(&entry(DETACHED_EXIT, "")).failed()); assert!(OpRow::from(&entry(DETACHED_EXIT, "no such file")).failed()); assert!(OpRow::from(&entry(1, "gate")).failed()); assert!(OpRow::from(&entry(SYNTHETIC_EXIT, "missing binary")).failed()); }
#[test]
fn surface_failure_carries_argv_and_stderr_tail() {
let row = OpRow::from(&entry(2, "line1\nline2"));
let f = SurfaceFailure::from(&row);
assert_eq!(f.argv, "bl close bl-4db6");
assert_eq!(f.stderr_tail, "line1\nline2");
}
#[test]
fn stderr_tail_keeps_only_the_last_lines() {
assert_eq!(stderr_tail(""), "");
assert_eq!(stderr_tail("only\n"), "only");
assert_eq!(stderr_tail("a\nb\nc\nd\ne\n"), "c\nd\ne");
}
}