#![allow(clippy::missing_panics_doc)]
#[cfg(unix)]
use std::time::Duration;
use kawauso_process::Invocation;
use kawauso_process::Run;
use kawauso_process::error::RunCommandError;
use kawauso_process::run::Stream;
#[cfg(unix)]
use tokio::time::sleep;
#[cfg(unix)]
use tokio::time::timeout;
#[cfg(unix)]
const FLOOD: usize = 256 * 1024;
async fn lines(mut run: Run) -> Result<Vec<(Stream, String)>, RunCommandError> {
let mut lines = Vec::new();
while let Some(line) = run.next_line().await? {
lines.push((line.stream(), line.text().get().to_owned()));
}
Ok(lines)
}
fn shell(commands: &[&str]) -> Invocation {
if cfg!(windows) {
Invocation::new("cmd").arg("/C").arg(commands.join(" & "))
} else {
Invocation::new("sh").arg("-c").arg(commands.join("; "))
}
}
#[cfg(unix)]
#[tokio::test]
async fn handle_that_the_caller_drops_ends_the_command() {
let directory = tempfile::tempdir().unwrap();
let marker = directory.path().join("marker");
let command = format!("sleep 1; : > \"{}\"", marker.display());
drop(shell(&[&command]).start().unwrap());
sleep(Duration::from_secs(2)).await;
assert!(!marker.exists());
}
#[cfg(unix)]
#[tokio::test]
async fn id_after_a_wait_for_the_end_names_the_command_that_ran() {
let mut run = shell(&["echo $$"]).start().unwrap();
let line = run.next_line().await.unwrap().unwrap();
run.wait_for_end().await.unwrap();
let id = run.id().unwrap();
assert_eq!(id.get().to_string(), line.text().get());
}
#[cfg(unix)]
#[tokio::test]
async fn id_with_a_command_that_runs_names_the_command() {
let mut run = shell(&["echo $$", "sleep 5"]).start().unwrap();
let line = run.next_line().await.unwrap().unwrap();
let id = run.id().unwrap();
assert_eq!(id.get().to_string(), line.text().get());
}
#[cfg(unix)]
#[tokio::test]
async fn next_line_with_a_byte_that_is_no_character_replaces_the_byte() {
let run = shell(&[r"printf '\377\n'"]).start().unwrap();
let lines = lines(run).await.unwrap();
assert_eq!(lines, [(Stream::StandardOutput, "\u{fffd}".to_owned())]);
}
#[cfg(unix)]
#[tokio::test]
async fn next_line_with_a_command_that_writes_before_it_ends_reports_the_line() {
let mut run = shell(&["echo hello", "sleep 5"]).start().unwrap();
let line = timeout(Duration::from_secs(2), run.next_line())
.await
.unwrap()
.unwrap();
assert_eq!(line.unwrap().text().get(), "hello");
}
#[tokio::test]
async fn next_line_with_a_command_that_writes_to_both_streams_names_the_stream_of_each_line() {
let run = shell(&["echo hello", "echo oops 1>&2"]).start().unwrap();
let mut lines = lines(run).await.unwrap();
lines.sort();
assert_eq!(
lines,
[
(Stream::StandardError, "oops".to_owned()),
(Stream::StandardOutput, "hello".to_owned()),
]
);
}
#[cfg(unix)]
#[tokio::test]
async fn next_line_with_a_future_that_the_caller_dropped_reports_the_whole_line() {
let mut run = shell(&[r"printf 'hel'", "sleep 2", r"printf 'lo\n'"])
.start()
.unwrap();
let dropped = timeout(Duration::from_millis(500), run.next_line()).await;
let lines = lines(run).await.unwrap();
assert_eq!(
(dropped.is_err(), lines),
(true, vec![(Stream::StandardOutput, "hello".to_owned())])
);
}
#[cfg(unix)]
#[tokio::test]
async fn next_line_with_a_last_line_that_has_no_end_reports_the_line() {
let run = shell(&[r"printf 'hello'"]).start().unwrap();
let lines = lines(run).await.unwrap();
assert_eq!(lines, [(Stream::StandardOutput, "hello".to_owned())]);
}
#[cfg(unix)]
#[tokio::test]
async fn next_line_with_a_line_that_ends_with_a_carriage_return_removes_it() {
let run = shell(&[r"printf 'one\r\ntwo\r\n'"]).start().unwrap();
let lines = lines(run).await.unwrap();
assert_eq!(
lines,
[
(Stream::StandardOutput, "one".to_owned()),
(Stream::StandardOutput, "two".to_owned()),
]
);
}
#[tokio::test]
async fn next_line_with_lines_reports_them_in_the_order_of_the_command() {
let run = shell(&["echo one", "echo two", "echo three"])
.start()
.unwrap();
let lines = lines(run).await.unwrap();
assert_eq!(
lines,
[
(Stream::StandardOutput, "one".to_owned()),
(Stream::StandardOutput, "two".to_owned()),
(Stream::StandardOutput, "three".to_owned()),
]
);
}
#[cfg(unix)]
#[tokio::test]
async fn wait_with_a_byte_that_is_no_character_keeps_the_byte_in_the_capture() {
let run = shell(&[r"printf '\377\n'"]).start().unwrap();
let execution = run.wait().await.unwrap();
assert_eq!(execution.stdout().get(), b"\xff\n");
}
#[cfg(unix)]
#[tokio::test]
async fn wait_for_end_with_a_command_that_closed_its_streams_waits_for_the_command() {
let mut run = shell(&["exec 1>&- 2>&-", "sleep 5"]).start().unwrap();
let ended = timeout(Duration::from_millis(500), run.wait_for_end()).await;
assert!(ended.is_err());
}
#[tokio::test]
async fn wait_for_end_with_a_command_that_ended_leaves_the_result_with_the_handle() {
let mut run = shell(&["echo hello", "exit 3"]).start().unwrap();
run.wait_for_end().await.unwrap();
let execution = run.wait().await.unwrap();
assert_eq!(
(
execution.status().code(),
execution.stdout().to_string_lossy().trim().to_owned()
),
(Some(3), "hello".to_owned())
);
}
#[cfg(unix)]
#[tokio::test]
async fn wait_for_end_with_a_future_that_the_caller_dropped_leaves_the_command_running() {
let mut run = shell(&["sleep 1", "exit 3"]).start().unwrap();
let dropped = timeout(Duration::from_millis(100), run.wait_for_end()).await;
let execution = run.wait().await.unwrap();
assert_eq!(
(dropped.is_err(), execution.status().code()),
(true, Some(3))
);
}
#[tokio::test]
async fn wait_with_a_command_that_failed_reports_the_exit_code() {
let run = shell(&["exit 3"]).start().unwrap();
let execution = run.wait().await.unwrap();
assert_eq!(execution.status().code(), Some(3));
}
#[cfg(unix)]
#[tokio::test]
async fn wait_with_a_command_that_fills_both_pipes_captures_everything() {
let run = shell(&[
&format!("head -c {FLOOD} /dev/zero"),
&format!("head -c {FLOOD} /dev/zero >&2"),
])
.start()
.unwrap();
let execution = run.wait().await.unwrap();
assert_eq!(
(
execution.stdout().get().len(),
execution.stderr().get().len()
),
(FLOOD, FLOOD)
);
}
#[cfg(unix)]
#[tokio::test]
async fn wait_with_a_command_that_writes_its_identifier_reports_the_same_identifier() {
let run = shell(&["echo $$"]).start().unwrap();
let execution = run.wait().await.unwrap();
assert_eq!(
execution.id().unwrap().get().to_string(),
execution.stdout().to_string_lossy().trim()
);
}
#[tokio::test]
async fn wait_without_a_line_that_the_caller_read_returns_the_whole_capture() {
let run = shell(&["echo hello", "echo oops 1>&2"]).start().unwrap();
let execution = run.wait().await.unwrap();
assert_eq!(
(
execution.stdout().to_string_lossy().trim(),
execution.stderr().to_string_lossy().trim()
),
("hello", "oops")
);
}