use command_stream::{create, exec, run, ProcessRunner, RunOptions, StdinOption};
use std::collections::HashMap;
#[cfg(unix)]
use std::time::Duration;
use tempfile::TempDir;
#[tokio::test]
async fn test_simple_echo() {
let result = run("echo hello").await.unwrap();
assert!(result.is_success());
assert!(result.stdout.contains("hello"));
}
#[tokio::test]
async fn test_echo_with_multiple_words() {
let result = run("echo hello world").await.unwrap();
assert!(result.is_success());
assert!(result.stdout.contains("hello world"));
}
#[tokio::test]
async fn test_command_with_arguments() {
let result = run("echo -n test").await.unwrap();
assert!(result.is_success());
assert_eq!(result.stdout, "test");
}
#[tokio::test]
async fn test_real_shell_preserves_missing_final_newlines() {
#[cfg(unix)]
let command = "printf stdout; printf stderr >&2";
#[cfg(windows)]
let command = "<nul set /p x=stdout&<nul 1>&2 set /p x=stderr&exit /b 0";
let result = exec(
command,
RunOptions {
mirror: false,
..Default::default()
},
)
.await
.unwrap();
assert!(result.is_success());
assert_eq!(result.stdout, "stdout");
assert_eq!(result.stderr, "stderr");
}
#[cfg(unix)]
#[tokio::test]
async fn test_real_shell_drains_full_stderr_pipe_before_stdout() {
const CHUNK: &str = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
const REPEATS: usize = 4096;
let command = format!(
"i=0; while [ \"$i\" -lt {REPEATS} ]; do printf '{CHUNK}'; i=$((i + 1)); done >&2; printf stdout"
);
let result = tokio::time::timeout(
Duration::from_secs(10),
exec(
command,
RunOptions {
mirror: false,
..Default::default()
},
),
)
.await
.expect("process deadlocked while stderr exceeded pipe capacity")
.unwrap();
assert!(result.is_success());
assert_eq!(result.stdout, "stdout");
assert_eq!(result.stderr.len(), CHUNK.len() * REPEATS);
assert!(result.stderr.bytes().all(|byte| byte == b'e'));
}
#[tokio::test]
async fn test_false_command_returns_nonzero() {
let result = run("false").await.unwrap();
assert!(!result.is_success());
assert_eq!(result.code, 1);
}
#[tokio::test]
async fn test_true_command_returns_zero() {
let result = run("true").await.unwrap();
assert!(result.is_success());
assert_eq!(result.code, 0);
}
#[tokio::test]
async fn test_process_runner_basic() {
let mut runner = ProcessRunner::new(
"echo hello",
RunOptions {
mirror: false,
..Default::default()
},
);
let result = runner.run().await.unwrap();
assert!(result.is_success());
assert!(result.stdout.contains("hello"));
}
#[tokio::test]
async fn test_process_runner_with_capture() {
let mut runner = ProcessRunner::new(
"echo captured",
RunOptions {
mirror: false,
capture: true,
..Default::default()
},
);
let result = runner.run().await.unwrap();
assert!(result.stdout.contains("captured"));
}
#[tokio::test]
async fn test_process_runner_is_finished() {
let mut runner = ProcessRunner::new("true", RunOptions::default());
assert!(!runner.is_finished());
let _ = runner.run().await;
assert!(runner.is_finished());
}
#[tokio::test]
async fn test_process_runner_result() {
let mut runner = ProcessRunner::new("echo test", RunOptions::default());
let _ = runner.run().await;
let result = runner.result();
assert!(result.is_some());
assert!(result.unwrap().is_success());
}
#[tokio::test]
async fn test_custom_working_directory() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("test.txt"), "content").unwrap();
let mut runner = ProcessRunner::new(
"ls",
RunOptions {
mirror: false,
cwd: Some(dir.path().to_path_buf()),
..Default::default()
},
);
let result = runner.run().await.unwrap();
assert!(result.is_success());
assert!(result.stdout.contains("test.txt"));
}
#[tokio::test]
async fn test_custom_environment_variable() {
let mut env_vars = HashMap::new();
env_vars.insert("MY_TEST_VAR".to_string(), "test_value".to_string());
let mut runner = ProcessRunner::new(
"printenv MY_TEST_VAR",
RunOptions {
mirror: false,
env: Some(env_vars),
..Default::default()
},
);
let result = runner.run().await.unwrap();
assert!(result.is_success());
assert!(result.stdout.contains("test_value"));
}
#[cfg(unix)]
#[tokio::test]
async fn test_process_runner_exports_directory_environment_safely() {
let oldpwd = "old dir ' $(printf injected) `printf injected`; end";
let env_vars = HashMap::from([
(
"PWD".to_string(),
std::env::current_dir()
.unwrap()
.to_string_lossy()
.to_string(),
),
("OLDPWD".to_string(), oldpwd.to_string()),
]);
let mut runner = ProcessRunner::new(
"/usr/bin/env",
RunOptions {
mirror: false,
env: Some(env_vars),
..Default::default()
},
);
let result = runner.run().await.unwrap();
assert!(result.is_success(), "child failed: {}", result.stderr);
assert!(
result
.stdout
.lines()
.any(|line| line == format!("OLDPWD={oldpwd}")),
"child environment did not preserve OLDPWD: {}",
result.stdout
);
}
#[tokio::test]
async fn test_stdin_content() {
let mut runner = ProcessRunner::new(
"cat",
RunOptions {
mirror: false,
stdin: StdinOption::Content("hello from stdin".to_string()),
..Default::default()
},
);
let result = runner.run().await.unwrap();
assert!(result.is_success());
assert!(result.stdout.contains("hello from stdin"));
}
#[tokio::test]
async fn test_exec_with_options() {
let result = exec(
"echo test",
RunOptions {
mirror: false,
..Default::default()
},
)
.await
.unwrap();
assert!(result.is_success());
assert!(result.stdout.contains("test"));
}
#[tokio::test]
async fn test_create_and_run() {
let mut runner = create("echo created", RunOptions::default());
let result = runner.run().await.unwrap();
assert!(result.is_success());
assert!(result.stdout.contains("created"));
}
#[tokio::test]
async fn test_virtual_echo() {
let result = run("echo virtual echo").await.unwrap();
assert!(result.is_success());
assert!(result.stdout.contains("virtual echo"));
}
#[tokio::test]
async fn test_virtual_pwd() {
let result = run("pwd").await.unwrap();
assert!(result.is_success());
assert!(!result.stdout.is_empty());
}
#[tokio::test]
async fn test_virtual_true() {
let result = run("true").await.unwrap();
assert!(result.is_success());
assert_eq!(result.code, 0);
}
#[tokio::test]
async fn test_virtual_false() {
let result = run("false").await.unwrap();
assert!(!result.is_success());
assert_eq!(result.code, 1);
}
#[tokio::test]
async fn test_virtual_sleep() {
let start = std::time::Instant::now();
let result = run("sleep 0.05").await.unwrap();
let elapsed = start.elapsed();
assert!(result.is_success());
assert!(elapsed.as_millis() >= 40);
}
#[tokio::test]
async fn test_stderr_capture() {
let result = run("cat nonexistent_file_12345").await.unwrap();
assert!(!result.is_success());
assert!(!result.stderr.is_empty());
}
#[tokio::test]
async fn test_exit_code_zero() {
let result = run("exit 0").await.unwrap();
assert_eq!(result.code, 0);
}
#[tokio::test]
async fn test_exit_code_nonzero() {
let result = run("exit 42").await.unwrap();
assert_eq!(result.code, 42);
}
#[tokio::test]
async fn test_kill_process() {
let mut runner = ProcessRunner::new(
"sleep 10",
RunOptions {
mirror: false,
..Default::default()
},
);
runner.start().await.unwrap();
let kill_result = runner.kill();
assert!(kill_result.is_ok());
}