use crate::error::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProcessResult<T> {
program: String,
stdout: T,
stderr: String,
exit_code: i32,
timed_out: bool,
}
impl<T> ProcessResult<T> {
pub(crate) fn new(
program: String,
stdout: T,
stderr: String,
exit_code: i32,
timed_out: bool,
) -> Self {
Self {
program,
stdout,
stderr,
exit_code,
timed_out,
}
}
pub fn stdout(&self) -> &T {
&self.stdout
}
pub fn into_stdout(self) -> T {
self.stdout
}
pub fn stderr(&self) -> &str {
&self.stderr
}
pub fn exit_code(&self) -> i32 {
self.exit_code
}
pub fn timed_out(&self) -> bool {
self.timed_out
}
pub fn is_success(&self) -> bool {
self.exit_code == 0
}
pub fn ensure_success(self) -> Result<Self, Error> {
if self.is_success() {
return Ok(self);
}
Err(Error::Exit {
program: self.program.clone(),
code: self.exit_code,
stderr: truncate_stderr(&self.stderr),
})
}
}
impl ProcessResult<String> {
pub fn combined(&self) -> String {
format!("{}{}", self.stdout(), self.stderr())
}
}
fn truncate_stderr(stderr: &str) -> String {
const MAX: usize = 4 * 1024;
if stderr.len() <= MAX {
return stderr.to_owned();
}
let mut end = MAX;
while !stderr.is_char_boundary(end) {
end -= 1;
}
format!("{}… (truncated)", &stderr[..end])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn success_is_code_zero() {
let ok = ProcessResult::new("git".into(), "out".to_owned(), String::new(), 0, false);
assert!(ok.is_success());
assert!(ok.ensure_success().is_ok());
}
#[test]
fn nonzero_exit_turns_into_error() {
let bad = ProcessResult::new("git".into(), "out".to_owned(), "boom".to_owned(), 2, false);
assert!(!bad.is_success());
let err = bad.ensure_success().unwrap_err();
match err {
Error::Exit {
program,
code,
stderr,
} => {
assert_eq!(program, "git");
assert_eq!(code, 2);
assert_eq!(stderr, "boom");
}
other => panic!("expected Exit, got {other:?}"),
}
}
#[test]
fn combined_concatenates_stdout_then_stderr() {
let r = ProcessResult::new("p".into(), "out".to_owned(), "err".to_owned(), 0, false);
assert_eq!(r.combined(), "outerr");
}
#[test]
fn stderr_is_truncated_in_error_only() {
let big = "x".repeat(10_000);
let bad = ProcessResult::new("p".into(), Vec::<u8>::new(), big.clone(), 1, false);
let full = bad.stderr().to_owned();
assert_eq!(full.len(), 10_000);
let Error::Exit { stderr, .. } = bad.ensure_success().unwrap_err() else {
panic!("expected Exit");
};
assert!(stderr.len() < 10_000);
assert!(stderr.ends_with("… (truncated)"));
}
}