use crate::error::RunnerError;
use std::time::Duration;
use super::CommandSpec;
#[derive(Debug, Clone)]
pub struct ProcessOutput {
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub exit_code: Option<i32>,
pub timed_out: bool,
}
impl ProcessOutput {
#[must_use]
pub fn new(stdout: Vec<u8>, stderr: Vec<u8>, exit_code: Option<i32>, timed_out: bool) -> Self {
Self {
stdout,
stderr,
exit_code,
timed_out,
}
}
#[must_use]
pub fn stdout_string(&self) -> String {
String::from_utf8_lossy(&self.stdout).to_string()
}
#[must_use]
pub fn stderr_string(&self) -> String {
String::from_utf8_lossy(&self.stderr).to_string()
}
#[must_use]
pub fn success(&self) -> bool {
self.exit_code == Some(0) && !self.timed_out
}
}
pub trait ProcessRunner {
fn run(&self, cmd: &CommandSpec, timeout: Duration) -> Result<ProcessOutput, RunnerError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_output_new() {
let output = ProcessOutput::new(
b"stdout content".to_vec(),
b"stderr content".to_vec(),
Some(0),
false,
);
assert_eq!(output.stdout, b"stdout content");
assert_eq!(output.stderr, b"stderr content");
assert_eq!(output.exit_code, Some(0));
assert!(!output.timed_out);
}
#[test]
fn test_process_output_stdout_string() {
let output = ProcessOutput::new(b"hello world".to_vec(), Vec::new(), Some(0), false);
assert_eq!(output.stdout_string(), "hello world");
}
#[test]
fn test_process_output_stderr_string() {
let output = ProcessOutput::new(Vec::new(), b"error message".to_vec(), Some(1), false);
assert_eq!(output.stderr_string(), "error message");
}
#[test]
fn test_process_output_success() {
let success = ProcessOutput::new(Vec::new(), Vec::new(), Some(0), false);
assert!(success.success());
let failure = ProcessOutput::new(Vec::new(), Vec::new(), Some(1), false);
assert!(!failure.success());
let timeout = ProcessOutput::new(Vec::new(), Vec::new(), Some(0), true);
assert!(!timeout.success());
let killed = ProcessOutput::new(Vec::new(), Vec::new(), None, false);
assert!(!killed.success());
}
#[test]
fn test_process_output_clone() {
let output = ProcessOutput::new(b"stdout".to_vec(), b"stderr".to_vec(), Some(42), true);
let cloned = output.clone();
assert_eq!(cloned.stdout, output.stdout);
assert_eq!(cloned.stderr, output.stderr);
assert_eq!(cloned.exit_code, output.exit_code);
assert_eq!(cloned.timed_out, output.timed_out);
}
#[test]
fn test_process_output_lossy_utf8() {
let invalid_utf8 = vec![0xff, 0xfe, 0x00, 0x01];
let output = ProcessOutput::new(invalid_utf8.clone(), invalid_utf8, Some(0), false);
let stdout = output.stdout_string();
let stderr = output.stderr_string();
assert!(!stdout.is_empty());
assert!(!stderr.is_empty());
}
struct MockRunner {
expected_output: ProcessOutput,
}
impl ProcessRunner for MockRunner {
fn run(
&self,
_cmd: &CommandSpec,
_timeout: Duration,
) -> Result<ProcessOutput, RunnerError> {
Ok(self.expected_output.clone())
}
}
#[test]
fn test_process_runner_trait_implementation() {
let mock = MockRunner {
expected_output: ProcessOutput::new(
b"mock stdout".to_vec(),
b"mock stderr".to_vec(),
Some(0),
false,
),
};
let cmd = CommandSpec::new("test").arg("--flag");
let result = mock.run(&cmd, Duration::from_secs(30));
assert!(result.is_ok());
let output = result.unwrap();
assert_eq!(output.stdout_string(), "mock stdout");
assert_eq!(output.stderr_string(), "mock stderr");
assert!(output.success());
}
#[test]
fn test_process_runner_with_error() {
struct ErrorRunner;
impl ProcessRunner for ErrorRunner {
fn run(
&self,
_cmd: &CommandSpec,
_timeout: Duration,
) -> Result<ProcessOutput, RunnerError> {
Err(RunnerError::NativeExecutionFailed {
reason: "mock error".to_string(),
})
}
}
let runner = ErrorRunner;
let cmd = CommandSpec::new("test");
let result = runner.run(&cmd, Duration::from_secs(30));
assert!(result.is_err());
match result {
Err(RunnerError::NativeExecutionFailed { reason }) => {
assert_eq!(reason, "mock error");
}
_ => panic!("Expected NativeExecutionFailed error"),
}
}
#[test]
fn test_process_runner_with_timeout_error() {
struct TimeoutRunner;
impl ProcessRunner for TimeoutRunner {
fn run(
&self,
_cmd: &CommandSpec,
timeout: Duration,
) -> Result<ProcessOutput, RunnerError> {
Err(RunnerError::Timeout {
timeout_seconds: timeout.as_secs(),
})
}
}
let runner = TimeoutRunner;
let cmd = CommandSpec::new("test");
let result = runner.run(&cmd, Duration::from_secs(60));
assert!(result.is_err());
match result {
Err(RunnerError::Timeout { timeout_seconds }) => {
assert_eq!(timeout_seconds, 60);
}
_ => panic!("Expected Timeout error"),
}
}
}