use std::process::{Command, Output};
use crate::error::{Error, Result};
pub fn run(program: &str, args: &[&str], context: &str) -> Result<String> {
let output = Command::new(program)
.args(args)
.output()
.map_err(|e| Error::other(format!("Failed to run {}: {}", context, e)))?;
if !output.status.success() {
return Err(Error::other(format!("{} failed: {}", context, error_text(&output))));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
pub fn run_in(dir: &str, program: &str, args: &[&str], context: &str) -> Result<String> {
let output = Command::new(program)
.args(args)
.current_dir(dir)
.output()
.map_err(|e| Error::other(format!("Failed to run {}: {}", context, e)))?;
if !output.status.success() {
return Err(Error::other(format!("{} failed: {}", context, error_text(&output))));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
pub fn run_in_optional(dir: &str, program: &str, args: &[&str]) -> Option<String> {
let output = Command::new(program)
.args(args)
.current_dir(dir)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
if stdout.is_empty() {
None
} else {
Some(stdout)
}
}
pub fn error_text(output: &Output) -> String {
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.trim().is_empty() {
stderr.trim().to_string()
} else {
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
}
pub fn succeeded(program: &str, args: &[&str]) -> bool {
Command::new(program)
.args(args)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn succeeded_in(dir: &str, program: &str, args: &[&str]) -> bool {
Command::new(program)
.args(args)
.current_dir(dir)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn require_success(success: bool, stderr: &str, operation: &str) -> Result<()> {
if success {
Ok(())
} else {
Err(Error::other(format!("{}_FAILED: {}", operation, stderr)))
}
}
use serde::Serialize;
#[derive(Debug, Clone, Default, Serialize)]
pub struct CapturedOutput {
#[serde(skip_serializing_if = "String::is_empty")]
pub stdout: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub stderr: String,
}
impl CapturedOutput {
pub fn new(stdout: String, stderr: String) -> Self {
Self { stdout, stderr }
}
pub fn is_empty(&self) -> bool {
self.stdout.is_empty() && self.stderr.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_succeeds_with_valid_command() {
let result = run("echo", &["hello"], "echo test");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "hello");
}
#[test]
fn run_fails_with_invalid_command() {
let result = run("nonexistent_command_xyz", &[], "test");
assert!(result.is_err());
}
#[test]
fn succeeded_returns_true_for_valid_command() {
assert!(succeeded("true", &[]));
}
#[test]
fn succeeded_returns_false_for_failing_command() {
assert!(!succeeded("false", &[]));
}
#[test]
fn run_in_optional_returns_none_on_failure() {
let result = run_in_optional("/tmp", "false", &[]);
assert!(result.is_none());
}
#[test]
fn error_text_prefers_stderr() {
let output = Output {
status: std::process::ExitStatus::default(),
stdout: b"stdout content".to_vec(),
stderr: b"stderr content".to_vec(),
};
assert_eq!(error_text(&output), "stderr content");
}
#[test]
fn error_text_falls_back_to_stdout() {
let output = Output {
status: std::process::ExitStatus::default(),
stdout: b"stdout content".to_vec(),
stderr: b"".to_vec(),
};
assert_eq!(error_text(&output), "stdout content");
}
#[test]
fn require_success_passes_when_successful() {
let result = require_success(true, "", "TEST");
assert!(result.is_ok());
}
#[test]
fn require_success_fails_with_error_message() {
let result = require_success(false, "Something went wrong", "LIST");
assert!(result.is_err());
}
}