#[cfg(feature = "color")]
use anstream::panic;
use crate::IntoData;
#[derive(Debug)]
pub struct Command {
cmd: std::process::Command,
stdin: Option<crate::Data>,
timeout: Option<std::time::Duration>,
_stderr_to_stdout: bool,
config: crate::Assert,
}
impl Command {
pub fn cargo_bin(name: &str) -> Self {
Self::new(cargo_bin(name))
}
pub fn new(program: impl AsRef<std::ffi::OsStr>) -> Self {
Self {
cmd: std::process::Command::new(program),
stdin: None,
timeout: None,
_stderr_to_stdout: false,
config: crate::Assert::new().action_env(crate::assert::DEFAULT_ACTION_ENV),
}
}
pub fn from_std(cmd: std::process::Command) -> Self {
Self {
cmd,
stdin: None,
timeout: None,
_stderr_to_stdout: false,
config: crate::Assert::new().action_env(crate::assert::DEFAULT_ACTION_ENV),
}
}
pub fn with_assert(mut self, config: crate::Assert) -> Self {
self.config = config;
self
}
pub fn arg(mut self, arg: impl AsRef<std::ffi::OsStr>) -> Self {
self.cmd.arg(arg);
self
}
pub fn args(mut self, args: impl IntoIterator<Item = impl AsRef<std::ffi::OsStr>>) -> Self {
self.cmd.args(args);
self
}
pub fn env(
mut self,
key: impl AsRef<std::ffi::OsStr>,
value: impl AsRef<std::ffi::OsStr>,
) -> Self {
self.cmd.env(key, value);
self
}
pub fn envs(
mut self,
vars: impl IntoIterator<Item = (impl AsRef<std::ffi::OsStr>, impl AsRef<std::ffi::OsStr>)>,
) -> Self {
self.cmd.envs(vars);
self
}
pub fn env_remove(mut self, key: impl AsRef<std::ffi::OsStr>) -> Self {
self.cmd.env_remove(key);
self
}
pub fn env_clear(mut self) -> Self {
self.cmd.env_clear();
self
}
pub fn current_dir(mut self, dir: impl AsRef<std::path::Path>) -> Self {
self.cmd.current_dir(dir);
self
}
pub fn stdin(mut self, stream: impl IntoData) -> Self {
self.stdin = Some(stream.into_data());
self
}
#[cfg(feature = "cmd")]
pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
self.timeout = Some(timeout);
self
}
#[cfg(feature = "cmd")]
pub fn stderr_to_stdout(mut self) -> Self {
self._stderr_to_stdout = true;
self
}
}
impl Command {
#[track_caller]
#[must_use]
pub fn assert(self) -> OutputAssert {
let config = self.config.clone();
match self.output() {
Ok(output) => OutputAssert::new(output).with_assert(config),
Err(err) => {
panic!("Failed to spawn: {}", err)
}
}
}
#[cfg(feature = "cmd")]
pub fn output(self) -> Result<std::process::Output, std::io::Error> {
if self._stderr_to_stdout {
self.single_output()
} else {
self.split_output()
}
}
#[cfg(not(feature = "cmd"))]
pub fn output(self) -> Result<std::process::Output, std::io::Error> {
self.split_output()
}
#[cfg(feature = "cmd")]
fn single_output(mut self) -> Result<std::process::Output, std::io::Error> {
self.cmd.stdin(std::process::Stdio::piped());
let (reader, writer) = os_pipe::pipe()?;
let writer_clone = writer.try_clone()?;
self.cmd.stdout(writer);
self.cmd.stderr(writer_clone);
let mut child = self.cmd.spawn()?;
drop(self.cmd);
let stdin = self
.stdin
.as_ref()
.map(|d| d.to_bytes())
.transpose()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
let stdout = process_single_io(&mut child, reader, stdin)?;
let status = wait(child, self.timeout)?;
let stdout = stdout.join().unwrap().ok().unwrap_or_default();
Ok(std::process::Output {
status,
stdout,
stderr: Default::default(),
})
}
fn split_output(mut self) -> Result<std::process::Output, std::io::Error> {
self.cmd.stdin(std::process::Stdio::piped());
self.cmd.stdout(std::process::Stdio::piped());
self.cmd.stderr(std::process::Stdio::piped());
let mut child = self.cmd.spawn()?;
let stdin = self
.stdin
.as_ref()
.map(|d| d.to_bytes())
.transpose()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
let (stdout, stderr) = process_split_io(&mut child, stdin)?;
let status = wait(child, self.timeout)?;
let stdout = stdout
.and_then(|t| t.join().unwrap().ok())
.unwrap_or_default();
let stderr = stderr
.and_then(|t| t.join().unwrap().ok())
.unwrap_or_default();
Ok(std::process::Output {
status,
stdout,
stderr,
})
}
}
fn process_split_io(
child: &mut std::process::Child,
input: Option<Vec<u8>>,
) -> std::io::Result<(Option<Stream>, Option<Stream>)> {
use std::io::Write;
let stdin = input.and_then(|i| {
child
.stdin
.take()
.map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i)))
});
let stdout = child.stdout.take().map(threaded_read);
let stderr = child.stderr.take().map(threaded_read);
stdin.and_then(|t| t.join().unwrap().ok());
Ok((stdout, stderr))
}
#[cfg(feature = "cmd")]
fn process_single_io(
child: &mut std::process::Child,
stdout: os_pipe::PipeReader,
input: Option<Vec<u8>>,
) -> std::io::Result<Stream> {
use std::io::Write;
let stdin = input.and_then(|i| {
child
.stdin
.take()
.map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i)))
});
let stdout = threaded_read(stdout);
debug_assert!(child.stdout.is_none());
debug_assert!(child.stderr.is_none());
stdin.and_then(|t| t.join().unwrap().ok());
Ok(stdout)
}
type Stream = std::thread::JoinHandle<Result<Vec<u8>, std::io::Error>>;
fn threaded_read<R>(mut input: R) -> Stream
where
R: std::io::Read + Send + 'static,
{
std::thread::spawn(move || {
let mut ret = Vec::new();
input.read_to_end(&mut ret).map(|_| ret)
})
}
impl From<std::process::Command> for Command {
fn from(cmd: std::process::Command) -> Self {
Self::from_std(cmd)
}
}
pub struct OutputAssert {
output: std::process::Output,
config: crate::Assert,
}
impl OutputAssert {
pub fn new(output: std::process::Output) -> Self {
Self {
output,
config: crate::Assert::new().action_env(crate::assert::DEFAULT_ACTION_ENV),
}
}
pub fn with_assert(mut self, config: crate::Assert) -> Self {
self.config = config;
self
}
pub fn get_output(&self) -> &std::process::Output {
&self.output
}
#[track_caller]
pub fn success(self) -> Self {
if !self.output.status.success() {
let desc = format!(
"Expected {}, was {}",
self.config.palette.info("success"),
self.config
.palette
.error(display_exit_status(self.output.status))
);
use std::fmt::Write;
let mut buf = String::new();
writeln!(&mut buf, "{desc}").unwrap();
self.write_stdout(&mut buf).unwrap();
self.write_stderr(&mut buf).unwrap();
panic!("{}", buf);
}
self
}
#[track_caller]
pub fn failure(self) -> Self {
if self.output.status.success() {
let desc = format!(
"Expected {}, was {}",
self.config.palette.info("failure"),
self.config.palette.error("success")
);
use std::fmt::Write;
let mut buf = String::new();
writeln!(&mut buf, "{desc}").unwrap();
self.write_stdout(&mut buf).unwrap();
self.write_stderr(&mut buf).unwrap();
panic!("{}", buf);
}
self
}
#[track_caller]
pub fn interrupted(self) -> Self {
if self.output.status.code().is_some() {
let desc = format!(
"Expected {}, was {}",
self.config.palette.info("interrupted"),
self.config
.palette
.error(display_exit_status(self.output.status))
);
use std::fmt::Write;
let mut buf = String::new();
writeln!(&mut buf, "{desc}").unwrap();
self.write_stdout(&mut buf).unwrap();
self.write_stderr(&mut buf).unwrap();
panic!("{}", buf);
}
self
}
#[track_caller]
pub fn code(self, expected: i32) -> Self {
if self.output.status.code() != Some(expected) {
let desc = format!(
"Expected {}, was {}",
self.config.palette.info(expected),
self.config
.palette
.error(display_exit_status(self.output.status))
);
use std::fmt::Write;
let mut buf = String::new();
writeln!(&mut buf, "{desc}").unwrap();
self.write_stdout(&mut buf).unwrap();
self.write_stderr(&mut buf).unwrap();
panic!("{}", buf);
}
self
}
#[track_caller]
pub fn stdout_eq(self, expected: impl IntoData) -> Self {
let expected = expected.into_data();
self.stdout_eq_inner(expected)
}
#[track_caller]
#[deprecated(since = "0.6.0", note = "Replaced with `OutputAssert::stdout_eq`")]
pub fn stdout_eq_(self, expected: impl IntoData) -> Self {
self.stdout_eq(expected)
}
#[track_caller]
fn stdout_eq_inner(self, expected: crate::Data) -> Self {
let actual = self.output.stdout.as_slice().into_data();
if let Err(err) = self.config.try_eq(Some(&"stdout"), actual, expected) {
err.panic();
}
self
}
#[track_caller]
pub fn stderr_eq(self, expected: impl IntoData) -> Self {
let expected = expected.into_data();
self.stderr_eq_inner(expected)
}
#[track_caller]
#[deprecated(since = "0.6.0", note = "Replaced with `OutputAssert::stderr_eq`")]
pub fn stderr_eq_(self, expected: impl IntoData) -> Self {
self.stderr_eq(expected)
}
#[track_caller]
fn stderr_eq_inner(self, expected: crate::Data) -> Self {
let actual = self.output.stderr.as_slice().into_data();
if let Err(err) = self.config.try_eq(Some(&"stderr"), actual, expected) {
err.panic();
}
self
}
fn write_stdout(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> {
if !self.output.stdout.is_empty() {
writeln!(writer, "stdout:")?;
writeln!(writer, "```")?;
writeln!(writer, "{}", String::from_utf8_lossy(&self.output.stdout))?;
writeln!(writer, "```")?;
}
Ok(())
}
fn write_stderr(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> {
if !self.output.stderr.is_empty() {
writeln!(writer, "stderr:")?;
writeln!(writer, "```")?;
writeln!(writer, "{}", String::from_utf8_lossy(&self.output.stderr))?;
writeln!(writer, "```")?;
}
Ok(())
}
}
#[cfg(not(feature = "cmd"))]
pub fn display_exit_status(status: std::process::ExitStatus) -> String {
basic_exit_status(status)
}
#[cfg(feature = "cmd")]
pub fn display_exit_status(status: std::process::ExitStatus) -> String {
#[cfg(unix)]
fn detailed_exit_status(status: std::process::ExitStatus) -> Option<String> {
use std::os::unix::process::ExitStatusExt;
let signal = status.signal()?;
let name = match signal as libc::c_int {
libc::SIGABRT => ", SIGABRT: process abort signal",
libc::SIGALRM => ", SIGALRM: alarm clock",
libc::SIGFPE => ", SIGFPE: erroneous arithmetic operation",
libc::SIGHUP => ", SIGHUP: hangup",
libc::SIGILL => ", SIGILL: illegal instruction",
libc::SIGINT => ", SIGINT: terminal interrupt signal",
libc::SIGKILL => ", SIGKILL: kill",
libc::SIGPIPE => ", SIGPIPE: write on a pipe with no one to read",
libc::SIGQUIT => ", SIGQUIT: terminal quit signal",
libc::SIGSEGV => ", SIGSEGV: invalid memory reference",
libc::SIGTERM => ", SIGTERM: termination signal",
libc::SIGBUS => ", SIGBUS: access to undefined memory",
#[cfg(not(target_os = "haiku"))]
libc::SIGSYS => ", SIGSYS: bad system call",
libc::SIGTRAP => ", SIGTRAP: trace/breakpoint trap",
_ => "",
};
Some(format!("signal: {signal}{name}"))
}
#[cfg(windows)]
fn detailed_exit_status(status: std::process::ExitStatus) -> Option<String> {
use windows_sys::Win32::Foundation::*;
let extra = match status.code().unwrap() as NTSTATUS {
STATUS_ACCESS_VIOLATION => "STATUS_ACCESS_VIOLATION",
STATUS_IN_PAGE_ERROR => "STATUS_IN_PAGE_ERROR",
STATUS_INVALID_HANDLE => "STATUS_INVALID_HANDLE",
STATUS_INVALID_PARAMETER => "STATUS_INVALID_PARAMETER",
STATUS_NO_MEMORY => "STATUS_NO_MEMORY",
STATUS_ILLEGAL_INSTRUCTION => "STATUS_ILLEGAL_INSTRUCTION",
STATUS_NONCONTINUABLE_EXCEPTION => "STATUS_NONCONTINUABLE_EXCEPTION",
STATUS_INVALID_DISPOSITION => "STATUS_INVALID_DISPOSITION",
STATUS_ARRAY_BOUNDS_EXCEEDED => "STATUS_ARRAY_BOUNDS_EXCEEDED",
STATUS_FLOAT_DENORMAL_OPERAND => "STATUS_FLOAT_DENORMAL_OPERAND",
STATUS_FLOAT_DIVIDE_BY_ZERO => "STATUS_FLOAT_DIVIDE_BY_ZERO",
STATUS_FLOAT_INEXACT_RESULT => "STATUS_FLOAT_INEXACT_RESULT",
STATUS_FLOAT_INVALID_OPERATION => "STATUS_FLOAT_INVALID_OPERATION",
STATUS_FLOAT_OVERFLOW => "STATUS_FLOAT_OVERFLOW",
STATUS_FLOAT_STACK_CHECK => "STATUS_FLOAT_STACK_CHECK",
STATUS_FLOAT_UNDERFLOW => "STATUS_FLOAT_UNDERFLOW",
STATUS_INTEGER_DIVIDE_BY_ZERO => "STATUS_INTEGER_DIVIDE_BY_ZERO",
STATUS_INTEGER_OVERFLOW => "STATUS_INTEGER_OVERFLOW",
STATUS_PRIVILEGED_INSTRUCTION => "STATUS_PRIVILEGED_INSTRUCTION",
STATUS_STACK_OVERFLOW => "STATUS_STACK_OVERFLOW",
STATUS_DLL_NOT_FOUND => "STATUS_DLL_NOT_FOUND",
STATUS_ORDINAL_NOT_FOUND => "STATUS_ORDINAL_NOT_FOUND",
STATUS_ENTRYPOINT_NOT_FOUND => "STATUS_ENTRYPOINT_NOT_FOUND",
STATUS_CONTROL_C_EXIT => "STATUS_CONTROL_C_EXIT",
STATUS_DLL_INIT_FAILED => "STATUS_DLL_INIT_FAILED",
STATUS_FLOAT_MULTIPLE_FAULTS => "STATUS_FLOAT_MULTIPLE_FAULTS",
STATUS_FLOAT_MULTIPLE_TRAPS => "STATUS_FLOAT_MULTIPLE_TRAPS",
STATUS_REG_NAT_CONSUMPTION => "STATUS_REG_NAT_CONSUMPTION",
STATUS_HEAP_CORRUPTION => "STATUS_HEAP_CORRUPTION",
STATUS_STACK_BUFFER_OVERRUN => "STATUS_STACK_BUFFER_OVERRUN",
STATUS_ASSERTION_FAILURE => "STATUS_ASSERTION_FAILURE",
_ => return None,
};
Some(extra.to_owned())
}
if let Some(extra) = detailed_exit_status(status) {
format!("{} ({})", basic_exit_status(status), extra)
} else {
basic_exit_status(status)
}
}
fn basic_exit_status(status: std::process::ExitStatus) -> String {
if let Some(code) = status.code() {
code.to_string()
} else {
"interrupted".to_owned()
}
}
#[cfg(feature = "cmd")]
fn wait(
mut child: std::process::Child,
timeout: Option<std::time::Duration>,
) -> std::io::Result<std::process::ExitStatus> {
if let Some(timeout) = timeout {
wait_timeout::ChildExt::wait_timeout(&mut child, timeout)
.transpose()
.unwrap_or_else(|| {
let _ = child.kill();
child.wait()
})
} else {
child.wait()
}
}
#[cfg(not(feature = "cmd"))]
fn wait(
mut child: std::process::Child,
_timeout: Option<std::time::Duration>,
) -> std::io::Result<std::process::ExitStatus> {
child.wait()
}
#[doc(inline)]
pub use crate::cargo_bin;
pub fn cargo_bin(name: &str) -> std::path::PathBuf {
cargo_bin_opt(name).unwrap_or_else(|| missing_cargo_bin(name))
}
pub fn cargo_bin_opt(name: &str) -> Option<std::path::PathBuf> {
let env_var = format!("{CARGO_BIN_EXE_}{name}");
std::env::var_os(env_var)
.map(|p| p.into())
.or_else(|| legacy_cargo_bin(name))
}
pub fn cargo_bins() -> impl Iterator<Item = (String, std::path::PathBuf)> {
std::env::vars_os()
.filter_map(|(k, v)| {
k.into_string()
.ok()
.map(|k| (k, std::path::PathBuf::from(v)))
})
.filter_map(|(k, v)| k.strip_prefix(CARGO_BIN_EXE_).map(|s| (s.to_owned(), v)))
}
const CARGO_BIN_EXE_: &str = "CARGO_BIN_EXE_";
fn missing_cargo_bin(name: &str) -> ! {
let possible_names: Vec<_> = cargo_bins().map(|(k, _)| k).collect();
if possible_names.is_empty() {
panic!("`CARGO_BIN_EXE_{name}` is unset
help: if this is running within a unit test, move it to an integration test to gain access to `CARGO_BIN_EXE_{name}`")
} else {
let mut names = String::new();
for (i, name) in possible_names.iter().enumerate() {
use std::fmt::Write as _;
if i != 0 {
let _ = write!(&mut names, ", ");
}
let _ = write!(&mut names, "\"{name}\"");
}
panic!(
"`CARGO_BIN_EXE_{name}` is unset
help: available binary names are {names}"
)
}
}
fn legacy_cargo_bin(name: &str) -> Option<std::path::PathBuf> {
let target_dir = target_dir()?;
let bin_path = target_dir.join(format!("{}{}", name, std::env::consts::EXE_SUFFIX));
if !bin_path.exists() {
return None;
}
Some(bin_path)
}
fn target_dir() -> Option<std::path::PathBuf> {
let mut path = std::env::current_exe().ok()?;
let _test_bin_name = path.pop();
if path.ends_with("deps") {
let _deps = path.pop();
}
Some(path)
}
#[cfg(feature = "examples")]
pub use examples::{compile_example, compile_examples};
#[cfg(feature = "examples")]
pub(crate) mod examples {
#[cfg(feature = "examples")]
pub fn compile_example<'a>(
target_name: &str,
args: impl IntoIterator<Item = &'a str>,
) -> crate::assert::Result<std::path::PathBuf> {
crate::debug!("Compiling example {}", target_name);
let messages = escargot::CargoBuild::new()
.current_target()
.current_release()
.example(target_name)
.args(args)
.exec()
.map_err(|e| crate::assert::Error::new(e.to_string()))?;
for message in messages {
let message = message.map_err(|e| crate::assert::Error::new(e.to_string()))?;
let message = message
.decode()
.map_err(|e| crate::assert::Error::new(e.to_string()))?;
crate::debug!("Message: {:?}", message);
if let Some(bin) = decode_example_message(&message) {
let (name, bin) = bin?;
assert_eq!(target_name, name);
return bin;
}
}
Err(crate::assert::Error::new(format!(
"Unknown error building example {target_name}"
)))
}
#[cfg(feature = "examples")]
pub fn compile_examples<'a>(
args: impl IntoIterator<Item = &'a str>,
) -> crate::assert::Result<
impl Iterator<Item = (String, crate::assert::Result<std::path::PathBuf>)>,
> {
crate::debug!("Compiling examples");
let mut examples = std::collections::BTreeMap::new();
let messages = escargot::CargoBuild::new()
.current_target()
.current_release()
.examples()
.args(args)
.exec()
.map_err(|e| crate::assert::Error::new(e.to_string()))?;
for message in messages {
let message = message.map_err(|e| crate::assert::Error::new(e.to_string()))?;
let message = message
.decode()
.map_err(|e| crate::assert::Error::new(e.to_string()))?;
crate::debug!("Message: {:?}", message);
if let Some(bin) = decode_example_message(&message) {
let (name, bin) = bin?;
examples.insert(name.to_owned(), bin);
}
}
Ok(examples.into_iter())
}
#[allow(clippy::type_complexity)]
fn decode_example_message<'m>(
message: &'m escargot::format::Message<'_>,
) -> Option<crate::assert::Result<(&'m str, crate::assert::Result<std::path::PathBuf>)>> {
match message {
escargot::format::Message::CompilerMessage(msg) => {
let level = msg.message.level;
if level == escargot::format::diagnostic::DiagnosticLevel::Ice
|| level == escargot::format::diagnostic::DiagnosticLevel::Error
{
let output = msg
.message
.rendered
.as_deref()
.unwrap_or_else(|| msg.message.message.as_ref())
.to_owned();
if is_example_target(&msg.target) {
let bin = Err(crate::assert::Error::new(output));
Some(Ok((msg.target.name.as_ref(), bin)))
} else {
Some(Err(crate::assert::Error::new(output)))
}
} else {
None
}
}
escargot::format::Message::CompilerArtifact(artifact) => {
if !artifact.profile.test && is_example_target(&artifact.target) {
let path = artifact
.executable
.clone()
.expect("cargo is new enough for this to be present");
let bin = Ok(path.into_owned());
Some(Ok((artifact.target.name.as_ref(), bin)))
} else {
None
}
}
_ => None,
}
}
fn is_example_target(target: &escargot::format::Target<'_>) -> bool {
target.crate_types == ["bin"] && target.kind == ["example"]
}
}
#[test]
#[should_panic = "`CARGO_BIN_EXE_non-existent` is unset
help: if this is running within a unit test, move it to an integration test to gain access to `CARGO_BIN_EXE_non-existent`"]
fn cargo_bin_in_unit_test() {
cargo_bin("non-existent");
}