Skip to content
This repository has been archived by the owner on Dec 29, 2021. It is now read-only.

WIP: Holistic CLI test harness experiments #98

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ build = "build.rs"
[[bin]]
name = "assert_fixture"

[features]
default = ["tempdir"]
tempdir = ["tempfile", "globwalk"]

[dependencies]
colored = "1.5"
difference = "2.0"
failure = "0.1"
failure_derive = "0.1"
serde_json = "1.0"
environment = "0.1"
tempfile = { version="3.0", optional=true }
globwalk = { version="0.1", optional=true }

[build-dependencies]
skeptic = "0.13"
Expand Down
367 changes: 367 additions & 0 deletions src/cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,367 @@
use std::ffi;
use std::fmt;
use std::io::Write;
use std::io;
use std::process;
use std::str;

use failure;

/// Extend `Command` with helpers for running the current crate's binaries.
pub trait CommandCargoExt {
/// Create a `Command` to run the crate's main binary.
///
/// # Examples
///
/// ```rust,ignore
/// extern crate assert_cli;
/// use std::process::Command;
/// use assert_cli::cmd::*;
///
/// Command::main_binary()
/// .output()
/// .unwrap();
/// ```
fn main_binary() -> Self;

/// Create a `Command` Run a specific binary of the current crate.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Run/to run

///
/// # Examples
///
/// ```rust,ignore
/// extern crate assert_cli;
/// use std::process::Command;
/// use assert_cli::cmd::*;
///
/// Command::cargo_binary("assert_fixture")
/// .output()
/// .unwrap();
/// ```
fn cargo_binary<S: AsRef<ffi::OsStr>>(name: S) -> Self;
}

impl CommandCargoExt for process::Command {
fn main_binary() -> Self {
let mut cmd = process::Command::new("carg");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

carg -> cargo?
Same in line 51

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Obviously very little testing has been done which might be clear considering this is here :). This is a rough sketch to get high level feedback before actually getting the details right.

cmd.arg("run").arg("--quit").arg("--");
cmd
}

fn cargo_binary<S: AsRef<ffi::OsStr>>(name: S) -> Self {
let mut cmd = process::Command::new("carg");
cmd.arg("run")
.arg("--quit")
.arg("--bin")
.arg(name.as_ref())
.arg("--");
cmd
}
}

/// Extend `Command` with a helper to pass a buffer to `stdin`
pub trait CommandStdInExt {
/// Write `buffer` to `stdin` when the command is run.
///
/// # Examples
///
/// ```rust,ignore
/// extern crate assert_cli;
/// use std::process::Command;
/// use assert_cli::cmd::*;
///
/// Command::new("cat")
/// .with_stdin("42")
/// .unwrap();
/// ```
fn with_stdin<S>(self, buffer: S) -> StdInCommand
where
S: Into<Vec<u8>>;
}

impl CommandStdInExt for process::Command {
fn with_stdin<S>(self, buffer: S) -> StdInCommand
where
S: Into<Vec<u8>>,
{
StdInCommand {
cmd: self,
stdin: buffer.into(),
}
}
}

/// `std::process::Command` with a `stdin` buffer.
pub struct StdInCommand {
cmd: process::Command,
stdin: Vec<u8>,
}

impl StdInCommand {
/// Executes the command as a child process, waiting for it to finish and collecting all of its
/// output.
///
/// By default, stdout and stderr are captured (and used to provide the resulting output).
/// Stdin is not inherited from the parent and any attempt by the child process to read from
/// the stdin stream will result in the stream immediately closing.
///
/// *(mirrors `std::process::Command::output`**
pub fn output(&mut self) -> io::Result<process::Output> {
self.spawn()?.wait_with_output()
}

/// Executes the command as a child process, returning a handle to it.
///
/// By default, stdin, stdout and stderr are inherited from the parent.
///
/// *(mirrors `std::process::Command::spawn`**
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/**/)*

fn spawn(&mut self) -> io::Result<process::Child> {
// stdout/stderr should only be piped for `output` according to `process::Command::new`.
self.cmd.stdin(process::Stdio::piped());
self.cmd.stdout(process::Stdio::piped());
self.cmd.stderr(process::Stdio::piped());

let mut spawned = self.cmd.spawn()?;

spawned
.stdin
.as_mut()
.expect("Couldn't get mut ref to command stdin")
.write_all(&self.stdin)?;
Ok(spawned)
}
}

/// `std::process::Output` represented as a `Result`.
pub type OutputResult = Result<process::Output, OutputError>;

/// Extends `std::process::Output` with methods to to convert it to an `OutputResult`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/to to/to

pub trait OutputOkExt
where
Self: ::std::marker::Sized,
{
/// Convert an `std::process::Output` into an `OutputResult`.
///
/// # Examples
///
/// ```rust,ignore
/// extern crate assert_cli;
/// use std::process::Command;
/// use assert_cli::cmd::*;
///
/// Command::new("echo")
/// .args(&["42"])
/// .output()
/// .ok()
/// .unwrap();
/// ```
fn ok(self) -> OutputResult;

/// Unwrap a `std::process::Output` but with a prettier message than `.ok().unwrap()`.
///
/// # Examples
///
/// ```rust,ignore
/// extern crate assert_cli;
/// use std::process::Command;
/// use assert_cli::cmd::*;
///
/// Command::new("echo")
/// .args(&["42"])
/// .output()
/// .unwrap();
/// ```
fn unwrap(self) {
if let Err(err) = self.ok() {
panic!("{}", err);
}
}
}

impl OutputOkExt for process::Output {
/// Convert an `std::process::Output` into an `OutputResult`.
///
/// # Examples
///
/// ```rust,ignore
/// extern crate assert_cli;
/// use std::process::Command;
/// use assert_cli::cmd::*;
///
/// Command::new("echo")
/// .args(&["42"])
/// .output()
/// .ok()
/// .unwrap();
/// ```
fn ok(self) -> OutputResult {
if self.status.success() {
Ok(self)
} else {
let error = OutputError::new(self);
Err(error)
}
}
}

impl<'c> OutputOkExt for &'c mut process::Command {
/// Convert an `std::process::Command` into an `OutputResult`.
///
/// # Examples
///
/// ```rust,ignore
/// extern crate assert_cli;
/// use std::process::Command;
/// use assert_cli::cmd::*;
///
/// Command::new("echo")
/// .args(&["42"])
/// .ok()
/// .unwrap();
/// ```
fn ok(self) -> OutputResult {
let output = self.output().map_err(|e| OutputError::with_cause(e))?;
if output.status.success() {
Ok(output)
} else {
let error = OutputError::new(output).set_cmd(format!("{:?}", self));
Err(error)
}
}
}

impl<'c> OutputOkExt for &'c mut StdInCommand {
/// Convert an `std::process::Command` into an `OutputResult`.
///
/// # Examples
///
/// ```rust,ignore
/// extern crate assert_cli;
/// use std::process::Command;
/// use assert_cli::cmd::*;
///
/// Command::new("cat")
/// .with_stdin("42")
/// .ok()
/// .unwrap();
/// ```
fn ok(self) -> OutputResult {
let output = self.output().map_err(|e| OutputError::with_cause(e))?;
if output.status.success() {
Ok(output)
} else {
let error = OutputError::new(output)
.set_cmd(format!("{:?}", self.cmd))
.set_stdin(self.stdin.clone());
Err(error)
}
}
}

#[derive(Fail, Debug)]
struct Output {
output: process::Output,
}

impl fmt::Display for Output {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(code) = self.output.status.code() {
writeln!(f, "code={}", code)?;
} else {
writeln!(f, "code=<interrupted>")?;
}
if let Ok(stdout) = str::from_utf8(&self.output.stdout) {
writeln!(f, "stdout=```{}```", stdout)?;
} else {
writeln!(f, "stdout=```{:?}```", self.output.stdout)?;
}
if let Ok(stderr) = str::from_utf8(&self.output.stderr) {
writeln!(f, "stderr=```{}```", stderr)?;
} else {
writeln!(f, "stderr=```{:?}```", self.output.stderr)?;
}

Ok(())
}
}

#[derive(Debug)]
enum OutputCause {
Expected(Output),
Unexpected(failure::Error),
}

impl fmt::Display for OutputCause {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
OutputCause::Expected(ref e) => write!(f, "{}", e),
OutputCause::Unexpected(ref e) => write!(f, "{}", e),
}
}
}

/// `std::process::Output` as a `Fail`.
#[derive(Fail, Debug)]
pub struct OutputError {
cmd: Option<String>,
stdin: Option<Vec<u8>>,
cause: OutputCause,
}

impl OutputError {
/// Convert `std::process::Output` into a `Fail`.
pub fn new(output: process::Output) -> Self {
Self {
cmd: None,
stdin: None,
cause: OutputCause::Expected(Output { output }),
}
}

/// For errors that happen in creating a `std::process::Output`.
pub fn with_cause<E>(cause: E) -> Self
where
E: Into<failure::Error>,
{
Self {
cmd: None,
stdin: None,
cause: OutputCause::Unexpected(cause.into()),
}
}

/// Add the command line for additional context.
pub fn set_cmd(mut self, cmd: String) -> Self {
self.cmd = Some(cmd);
self
}

/// Add the `stdn` for additional context.
pub fn set_stdin(mut self, stdin: Vec<u8>) -> Self {
self.stdin = Some(stdin);
self
}

/// Access the contained `std::process::Output`.
pub fn as_output(&self) -> Option<&process::Output> {
match self.cause {
OutputCause::Expected(ref e) => Some(&e.output),
OutputCause::Unexpected(_) => None,
}
}
}

impl fmt::Display for OutputError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(ref cmd) = self.cmd {
writeln!(f, "command=`{}`", cmd)?;
}
if let Some(ref stdin) = self.stdin {
if let Ok(stdin) = str::from_utf8(&stdin) {
writeln!(f, "stdin=```{}```", stdin)?;
} else {
writeln!(f, "stdin=```{:?}```", stdin)?;
}
}
write!(f, "{}", self.cause)
}
}
Loading