#[path = "../../src/command_error.rs"]
mod command_error;
#[path = "../../src/command_output.rs"]
mod command_output;
#[path = "../../src/output_stream.rs"]
mod output_stream;
pub(crate) mod command_runner {
pub(crate) mod captured_output {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/command_runner/captured_output.rs"
));
}
pub(crate) mod command_io {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/command_runner/command_io.rs"
));
}
pub(crate) mod error_mapping {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/command_runner/error_mapping.rs"
));
}
pub(crate) mod finished_command {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/command_runner/finished_command.rs"
));
}
pub(crate) mod managed_child_process {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/command_runner/managed_child_process.rs"
));
}
pub(crate) mod output_capture_error {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/command_runner/output_capture_error.rs"
));
}
pub(crate) mod output_capture_options {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/command_runner/output_capture_options.rs"
));
}
pub(crate) mod output_collector {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/command_runner/output_collector.rs"
));
}
pub(crate) mod output_reader {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/command_runner/output_reader.rs"
));
}
pub(crate) mod output_tee {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/command_runner/output_tee.rs"
));
}
pub(crate) mod running_command {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/command_runner/running_command.rs"
));
}
pub(crate) mod stdin_pipe {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/command_runner/stdin_pipe.rs"
));
}
pub(crate) mod stdin_writer {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/command_runner/stdin_writer.rs"
));
}
pub(crate) mod wait_policy {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/command_runner/wait_policy.rs"
));
}
}
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
#[cfg(windows)]
use std::os::windows::process::ExitStatusExt;
use std::{
io::{
self,
Read,
Write,
},
panic,
path::PathBuf,
process::{
ChildStderr,
ChildStdin,
ChildStdout,
Command as ProcessCommand,
ExitStatus,
Stdio,
},
thread,
time::Duration,
};
pub use command_error::CommandError;
pub use command_output::CommandOutput;
use command_runner::{
captured_output::CapturedOutput,
command_io::CommandIo,
error_mapping::{
kill_failed,
output_pipe_error,
spawn_failed,
wait_failed,
},
managed_child_process::ManagedChildProcess,
output_capture_error::OutputCaptureError,
output_capture_options::OutputCaptureOptions,
output_collector::{
collect_output,
join_output_reader,
read_output,
read_output_stream,
},
output_reader::OutputReader,
output_tee::OutputTee,
running_command::RunningCommand,
stdin_pipe::{
join_stdin_writer,
write_stdin_bytes,
},
stdin_writer::StdinWriter,
};
pub use output_stream::OutputStream;
use process_wrap::std::ChildWrapper;
use command_runner::finished_command::FinishedCommand;
mod coverage_support_subject {
use std::{
cell::Cell,
ffi::OsStr,
};
use super::TestChild;
use crate::{
OutputStream,
command_runner::managed_child_process::ManagedChildProcess,
};
thread_local! {
static FAKE_CHILDREN_ENABLED: Cell<bool> = const { Cell::new(false) };
}
struct FakeChildGuard {
previous: bool,
}
impl Drop for FakeChildGuard {
fn drop(&mut self) {
FAKE_CHILDREN_ENABLED.set(self.previous);
}
}
pub fn with_fake_children_enabled<T>(operation: impl FnOnce() -> T) -> T {
let _guard = enable_fake_children();
operation()
}
pub(crate) fn fake_children_enabled() -> bool {
FAKE_CHILDREN_ENABLED.get()
}
fn enable_fake_children() -> FakeChildGuard {
let previous = fake_children_enabled();
FAKE_CHILDREN_ENABLED.set(true);
FakeChildGuard { previous }
}
pub(crate) fn fake_child_for(program: &OsStr) -> Option<ManagedChildProcess> {
match program.to_string_lossy().as_ref() {
"__qubit_command_missing_stdout__" => Some(Box::new(TestChild::default())),
"__qubit_command_collect_output_error__" => Some(Box::new(TestChild::default())),
_ => None,
}
}
pub(crate) fn forced_collect_output_error(command: &str) -> Option<OutputStream> {
if command.contains("__qubit_command_collect_output_error__")
|| command.contains("__qubit_command_timeout_collect_output_error__")
{
Some(OutputStream::Stdout)
} else {
None
}
}
}
#[path = "defensive_paths_tests.rs"]
mod defensive_paths_tests;
#[path = "failing_flush_tests.rs"]
mod failing_flush_tests;
#[path = "failing_reader_tests.rs"]
mod failing_reader_tests;
#[path = "failing_write_tests.rs"]
mod failing_write_tests;
#[path = "fake_child_guard_tests.rs"]
mod fake_child_guard_tests;
#[path = "no_stdin_child_tests.rs"]
mod no_stdin_child_tests;
#[path = "synthetic_children_tests.rs"]
mod synthetic_children_tests;
struct FailingReader;
impl Read for FailingReader {
fn read(&mut self, _buffer: &mut [u8]) -> io::Result<usize> {
Err(io::Error::other("read failed"))
}
}
struct FailingWrite;
impl Write for FailingWrite {
fn write(&mut self, _buffer: &[u8]) -> io::Result<usize> {
Err(io::Error::other("write failed"))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
struct FailingFlush;
impl Write for FailingFlush {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
Ok(buffer.len())
}
fn flush(&mut self) -> io::Result<()> {
Err(io::Error::other("flush failed"))
}
}
#[derive(Debug, Default)]
struct TestChild {
stdin: Option<ChildStdin>,
stdout: Option<ChildStdout>,
stderr: Option<ChildStderr>,
try_wait_error: Option<&'static str>,
clear_try_wait_error_after_first: bool,
pending_checks: usize,
pending: bool,
exited_after_kill_attempt: bool,
kill_attempted: bool,
kill_error: Option<&'static str>,
wait_error: Option<&'static str>,
}
impl ChildWrapper for TestChild {
fn inner(&self) -> &dyn ChildWrapper {
self
}
fn inner_mut(&mut self) -> &mut dyn ChildWrapper {
self
}
fn into_inner(self: Box<Self>) -> Box<dyn ChildWrapper> {
self
}
fn stdin(&mut self) -> &mut Option<ChildStdin> {
&mut self.stdin
}
fn stdout(&mut self) -> &mut Option<ChildStdout> {
&mut self.stdout
}
fn stderr(&mut self) -> &mut Option<ChildStderr> {
&mut self.stderr
}
fn id(&self) -> u32 {
0
}
fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
if let Some(message) = self.try_wait_error {
if self.clear_try_wait_error_after_first {
self.try_wait_error = None;
}
Err(io::Error::other(message))
} else if self.pending_checks > 0 {
self.pending_checks -= 1;
Ok(None)
} else if self.pending && !(self.kill_attempted && self.exited_after_kill_attempt) {
Ok(None)
} else {
Ok(Some(success_status()))
}
}
fn wait(&mut self) -> io::Result<ExitStatus> {
if let Some(message) = self.wait_error {
Err(io::Error::other(message))
} else {
Ok(success_status())
}
}
fn start_kill(&mut self) -> io::Result<()> {
self.kill_attempted = true;
if let Some(message) = self.kill_error {
Err(io::Error::other(message))
} else {
Ok(())
}
}
}
fn success_status() -> ExitStatus {
ExitStatus::from_raw(0)
}
fn reader_ok(bytes: Vec<u8>) -> OutputReader {
thread::spawn(move || {
Ok(CapturedOutput {
bytes,
truncated: false,
})
})
}
fn reader_read_error(message: &'static str) -> OutputReader {
thread::spawn(move || Err(OutputCaptureError::Read(io::Error::other(message))))
}
fn reader_write_error(message: &'static str) -> OutputReader {
thread::spawn(move || {
Err(OutputCaptureError::Write {
path: PathBuf::from("reader-output.txt"),
source: io::Error::other(message),
})
})
}
fn reader_panic() -> OutputReader {
thread::spawn(move || -> Result<CapturedOutput, OutputCaptureError> {
panic!("output reader panic");
})
}
fn stdin_writer_error(message: &'static str) -> StdinWriter {
Some(thread::spawn(move || Err(io::Error::other(message))))
}
fn stdin_writer_broken_pipe() -> StdinWriter {
Some(thread::spawn(move || {
Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"stdin broken pipe",
))
}))
}
fn stdin_writer_panic() -> StdinWriter {
Some(thread::spawn(move || -> io::Result<()> {
panic!("stdin writer panic");
}))
}
fn empty_command_io() -> CommandIo {
CommandIo::new(reader_ok(Vec::new()), reader_ok(Vec::new()), None)
}
fn failing_stdout_command_io() -> CommandIo {
CommandIo::new(
reader_read_error("stdout collection failed"),
reader_ok(Vec::new()),
None,
)
}
fn failing_stdin_command_io() -> CommandIo {
CommandIo::new(
reader_ok(Vec::new()),
reader_ok(Vec::new()),
stdin_writer_error("stdin collection failed"),
)
}
fn boxed_child(child: TestChild) -> ManagedChildProcess {
Box::new(child)
}
fn empty_stdout() -> ChildStdout {
let mut child = empty_process_command()
.stdout(Stdio::piped())
.spawn()
.expect("empty stdout child should spawn");
let stdout = child.stdout.take().expect("stdout should be piped");
child.wait().expect("empty stdout child should finish");
stdout
}
fn empty_stderr() -> ChildStderr {
let mut child = empty_process_command()
.stderr(Stdio::piped())
.spawn()
.expect("empty stderr child should spawn");
let stderr = child.stderr.take().expect("stderr should be piped");
child.wait().expect("empty stderr child should finish");
stderr
}
fn empty_process_command() -> ProcessCommand {
#[cfg(not(windows))]
{
let mut command = ProcessCommand::new("sh");
command.arg("-c").arg(":");
command
}
#[cfg(windows)]
{
let mut command = ProcessCommand::new("cmd");
command.arg("/C").arg("exit /B 0");
command
}
}
fn capture_with_writer(writer: Box<dyn Write + Send>, path: &str) -> OutputCaptureOptions {
OutputCaptureOptions {
max_bytes: None,
tee: Some(OutputTee {
writer,
path: PathBuf::from(path),
}),
}
}
fn expect_command_error(
result: Result<FinishedCommand, CommandError>,
message: &str,
) -> CommandError {
match result {
Ok(_) => panic!("{message}"),
Err(error) => error,
}
}
#[test]
fn test_error_mapping_builds_all_error_variants() {
assert!(matches!(
spawn_failed("spawn", io::Error::other("spawn failed")),
CommandError::SpawnFailed { .. }
));
assert!(matches!(
wait_failed("wait", io::Error::other("wait failed")),
CommandError::WaitFailed { .. }
));
assert!(matches!(
kill_failed(
"kill".to_owned(),
Duration::from_millis(1),
io::Error::other("kill failed"),
),
CommandError::KillFailed { .. }
));
assert!(matches!(
output_pipe_error("pipe", OutputStream::Stdout),
CommandError::ReadOutputFailed {
stream: OutputStream::Stdout,
..
}
));
assert!(matches!(
output_pipe_error("pipe", OutputStream::Stderr),
CommandError::ReadOutputFailed {
stream: OutputStream::Stderr,
..
}
));
}
#[test]
fn test_read_output_reports_read_write_and_flush_failures() {
let read_error = read_output(
&mut FailingReader,
OutputCaptureOptions::new(None, None, None),
)
.expect_err("failing reader should report read error");
assert!(matches!(read_error, OutputCaptureError::Read(_)));
let write_error = read_output(
&mut io::Cursor::new(b"write".to_vec()),
capture_with_writer(Box::new(FailingWrite), "stdout.txt"),
)
.expect_err("failing writer should report write error");
assert!(matches!(write_error, OutputCaptureError::Write { .. }));
let flush_error = read_output(
&mut io::Cursor::new(b"flush".to_vec()),
capture_with_writer(Box::new(FailingFlush), "stderr.txt"),
)
.expect_err("failing flush should report write error");
assert!(matches!(flush_error, OutputCaptureError::Write { .. }));
}
#[test]
fn test_read_output_stream_drains_reader_thread() {
let reader = read_output_stream(
Box::new(io::Cursor::new(b"threaded".to_vec())),
OutputCaptureOptions::new(None, None, None),
);
let output = reader
.join()
.expect("reader thread should not panic")
.expect("reader should succeed");
assert_eq!(output.bytes, b"threaded");
assert!(!output.truncated);
}
#[test]
fn test_read_output_covers_successful_limited_and_tee_paths() {
let output = read_output(
&mut io::Cursor::new(b"abc".to_vec()),
OutputCaptureOptions::new(Some(8), None, None),
)
.expect("limited read should succeed without truncation");
assert_eq!(output.bytes, b"abc");
assert!(!output.truncated);
let output = read_output(
&mut io::Cursor::new(b"abcdef".to_vec()),
OutputCaptureOptions::new(Some(3), None, None),
)
.expect("limited read should report truncation");
assert_eq!(output.bytes, b"abc");
assert!(output.truncated);
let output = read_output(
&mut io::Cursor::new(b"tee".to_vec()),
capture_with_writer(Box::new(io::sink()), "sink.txt"),
)
.expect("successful tee writer should flush cleanly");
assert_eq!(output.bytes, b"tee");
assert!(!output.truncated);
}
#[test]
fn test_collect_output_maps_reader_and_stdin_errors() {
let error = collect_output(
"collect-stdout",
success_status(),
Duration::ZERO,
reader_read_error("collect stdout failed"),
reader_ok(Vec::new()),
None,
)
.expect_err("stdout reader error should be mapped");
assert!(matches!(
error,
CommandError::ReadOutputFailed {
stream: OutputStream::Stdout,
..
}
));
let error = collect_output(
"collect-stderr",
success_status(),
Duration::ZERO,
reader_ok(Vec::new()),
reader_read_error("collect stderr failed"),
None,
)
.expect_err("stderr reader error should be mapped");
assert!(matches!(
error,
CommandError::ReadOutputFailed {
stream: OutputStream::Stderr,
..
}
));
let error = collect_output(
"collect-stdin",
success_status(),
Duration::ZERO,
reader_ok(Vec::new()),
reader_ok(Vec::new()),
stdin_writer_error("collect stdin failed"),
)
.expect_err("stdin writer error should be mapped");
assert!(matches!(error, CommandError::WriteInputFailed { .. }));
}
#[test]
fn test_join_output_reader_maps_write_error_and_panic() {
let error = join_output_reader(
"writer",
OutputStream::Stdout,
reader_write_error("reader write failed"),
)
.expect_err("writer error should be mapped");
assert!(matches!(error, CommandError::WriteOutputFailed { .. }));
let previous_hook = panic::take_hook();
panic::set_hook(Box::new(|_| {}));
let panic_error = join_output_reader("panic", OutputStream::Stderr, reader_panic())
.expect_err("reader panic should be mapped");
panic::set_hook(previous_hook);
assert!(matches!(
panic_error,
CommandError::ReadOutputFailed {
stream: OutputStream::Stderr,
..
}
));
}
#[test]
fn test_stdin_pipe_maps_missing_pipe_write_error_and_panic() {
let mut missing_stdin_child = TestChild::default();
let error = write_stdin_bytes(
"missing-stdin",
&mut missing_stdin_child,
Some(b"x".to_vec()),
)
.expect_err("missing stdin pipe should be reported");
assert!(matches!(error, CommandError::WriteInputFailed { .. }));
let mut no_input_child = TestChild::default();
let writer = write_stdin_bytes("no-stdin", &mut no_input_child, None)
.expect("missing configured bytes should not require stdin");
assert!(writer.is_none());
let error = join_stdin_writer("stdin-write", stdin_writer_error("stdin write failed"))
.expect_err("stdin write error should be mapped");
assert!(matches!(error, CommandError::WriteInputFailed { .. }));
join_stdin_writer("stdin-broken-pipe", stdin_writer_broken_pipe())
.expect("broken pipe should be ignored after process exit");
let previous_hook = panic::take_hook();
panic::set_hook(Box::new(|_| {}));
let panic_error = join_stdin_writer("stdin-panic", stdin_writer_panic())
.expect_err("stdin writer panic should be mapped");
panic::set_hook(previous_hook);
assert!(matches!(panic_error, CommandError::WriteInputFailed { .. }));
}
#[test]
#[cfg(not(windows))]
fn test_stdin_pipe_writes_to_real_child_stdin() {
let mut child = ProcessCommand::new("sh")
.arg("-c")
.arg("cat >/dev/null")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("stdin test child should spawn");
let writer = write_stdin_bytes("stdin-success", &mut child, Some(b"input".to_vec()))
.expect("stdin writer should start");
join_stdin_writer("stdin-success", writer).expect("stdin writer should finish");
child.wait().expect("stdin test child should exit");
}
#[test]
fn test_running_command_maps_wait_error_with_exited_cleanup() {
let child = TestChild {
stdout: Some(empty_stdout()),
stderr: Some(empty_stderr()),
try_wait_error: Some("try wait failed"),
clear_try_wait_error_after_first: true,
pending: true,
exited_after_kill_attempt: true,
..TestChild::default()
};
let error = expect_command_error(
RunningCommand::new(
"wait-error".to_owned(),
boxed_child(child),
empty_command_io(),
)
.wait_for_completion(None),
"try-wait failure should be reported",
);
assert!(matches!(error, CommandError::WaitFailed { .. }));
}
#[test]
fn test_running_command_completes_after_pending_poll() {
let child = TestChild {
stdout: Some(empty_stdout()),
stderr: Some(empty_stderr()),
pending_checks: 1,
..TestChild::default()
};
let finished = RunningCommand::new(
"pending-then-success".to_owned(),
boxed_child(child),
empty_command_io(),
)
.wait_for_completion(Some(Duration::from_millis(100)))
.expect("child should complete after one pending poll");
assert_eq!(finished.command_text, "pending-then-success");
}
#[test]
fn test_running_command_completes_after_pending_poll_without_timeout() {
let child = TestChild {
stdout: Some(empty_stdout()),
stderr: Some(empty_stderr()),
pending_checks: 1,
..TestChild::default()
};
let finished = RunningCommand::new(
"pending-without-timeout".to_owned(),
boxed_child(child),
empty_command_io(),
)
.wait_for_completion(None)
.expect("child should complete after one pending poll");
assert_eq!(finished.command_text, "pending-without-timeout");
}
#[test]
fn test_running_command_propagates_collection_error_on_normal_exit() {
let child = TestChild {
stdout: Some(empty_stdout()),
stderr: Some(empty_stderr()),
..TestChild::default()
};
let error = expect_command_error(
RunningCommand::new(
"normal-collection-error".to_owned(),
boxed_child(child),
failing_stdout_command_io(),
)
.wait_for_completion(None),
"output collection error should be reported",
);
assert!(matches!(
error,
CommandError::ReadOutputFailed {
stream: OutputStream::Stdout,
..
}
));
}
#[test]
fn test_running_command_preserves_wait_error_when_child_stays_pending() {
let child = TestChild {
stdout: Some(empty_stdout()),
stderr: Some(empty_stderr()),
try_wait_error: Some("try wait failed"),
clear_try_wait_error_after_first: true,
pending: true,
..TestChild::default()
};
let error = expect_command_error(
RunningCommand::new(
"pending-wait-error".to_owned(),
boxed_child(child),
empty_command_io(),
)
.wait_for_completion(None),
"try-wait failure should be reported",
);
assert!(matches!(error, CommandError::WaitFailed { .. }));
}
#[test]
fn test_running_command_propagates_collection_error_after_timeout() {
let child = TestChild {
stdout: Some(empty_stdout()),
stderr: Some(empty_stderr()),
pending: true,
exited_after_kill_attempt: true,
..TestChild::default()
};
let error = expect_command_error(
RunningCommand::new(
"timeout-collection-error".to_owned(),
boxed_child(child),
failing_stdin_command_io(),
)
.wait_for_completion(Some(Duration::ZERO)),
"timeout collection error should be reported before timeout output",
);
assert!(matches!(error, CommandError::WriteInputFailed { .. }));
}
#[test]
fn test_running_command_returns_timeout_after_successful_kill_and_wait() {
let child = TestChild {
stdout: Some(empty_stdout()),
stderr: Some(empty_stderr()),
pending: true,
exited_after_kill_attempt: true,
..TestChild::default()
};
let error = expect_command_error(
RunningCommand::new(
"timeout-success".to_owned(),
boxed_child(child),
empty_command_io(),
)
.wait_for_completion(Some(Duration::ZERO)),
"successful timeout cleanup should report timeout",
);
assert!(matches!(error, CommandError::TimedOut { .. }));
}
#[test]
fn test_running_command_maps_timeout_kill_and_wait_errors() {
let kill_error_child = TestChild {
stdout: Some(empty_stdout()),
stderr: Some(empty_stderr()),
pending: true,
kill_error: Some("kill failed"),
exited_after_kill_attempt: true,
..TestChild::default()
};
let error = expect_command_error(
RunningCommand::new(
"kill-error".to_owned(),
boxed_child(kill_error_child),
empty_command_io(),
)
.wait_for_completion(Some(Duration::ZERO)),
"kill failure should be reported",
);
assert!(matches!(error, CommandError::KillFailed { .. }));
let wait_error_child = TestChild {
stdout: Some(empty_stdout()),
stderr: Some(empty_stderr()),
pending: true,
wait_error: Some("wait after kill failed"),
exited_after_kill_attempt: true,
..TestChild::default()
};
let error = expect_command_error(
RunningCommand::new(
"wait-after-kill-error".to_owned(),
boxed_child(wait_error_child),
empty_command_io(),
)
.wait_for_completion(Some(Duration::ZERO)),
"wait-after-kill failure should be reported",
);
assert!(matches!(error, CommandError::WaitFailed { .. }));
}