concurrent_shell_commands 0.4.0

runs commands concurrently and display their name and output, with configurable filtering of output
Documentation
#![allow(clippy::panic)]
use cucumber::gherkin::Step;
use cucumber::{World as _, given, then, when};
use tokio::process::Command;

#[derive(Debug, Default, cucumber::World)]
struct World {
    workspace: Option<tempfile::TempDir>,
    output: Option<std::process::Output>,
    want_stdout: Vec<String>,
    want_stderr: Vec<String>,
}

#[given("I'm in an empty folder")]
fn in_empty_folder(world: &mut World) {
    world.workspace = Some(tempfile::tempdir().unwrap());
}

#[when(expr = "I run {string}")]
async fn i_run(world: &mut World, command: String) {
    let mut args = shellwords::split(&command).unwrap().into_iter();
    let mut executable = args.next().unwrap();
    if executable == "conc" {
        let cwd = std::env::current_dir().unwrap();
        let conc_path = cwd.join("target/debug/conc");
        executable = conc_path.to_string_lossy().to_string();
    }
    let mut command = Command::new(executable);
    command.args(args);
    if let Some(workspace) = &world.workspace {
        command.current_dir(workspace);
    }
    world.output = Some(command.output().await.unwrap());
}

#[then(expr = "the exit code is {int}")]
fn the_exit_code_is(world: &mut World, expected: i32) {
    let Some(output) = world.output.as_ref() else {
        panic!("No command ran");
    };
    assert_eq!(output.status.code().unwrap(), expected);
}

#[then("STDOUT contains:")]
fn stdout_contains(world: &mut World, step: &Step) {
    let want_block = step.docstring().unwrap().trim();
    world.want_stdout.push(want_block.to_owned());
}

#[then("STDERR contains:")]
fn stderr_contains(world: &mut World, step: &Step) {
    let want_block = step.docstring().unwrap().trim();
    world.want_stderr.push(want_block.to_owned());
}

#[then("the output is empty")]
fn the_output_is_empty(world: &mut World) {
    let Some(output) = world.output.as_ref() else {
        panic!("No command ran");
    };
    assert!(output.stdout.is_empty());
    assert!(output.stderr.is_empty());
}

#[then("STDERR is empty")]
fn stderr_is_empty(world: &mut World) {
    let Some(output) = world.output.as_ref() else {
        panic!("No command ran");
    };
    assert!(
        output.stderr.is_empty(),
        "Expected stderr to be empty but got: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    World::cucumber()
        .after(|_feature, _rule, _scenario, _ev, world| {
            Box::pin(async move {
                let Some(world) = world else {
                    panic!("No world");
                };
                let Some(output) = world.output.as_ref() else {
                    panic!("No command ran");
                };
                let stdout = String::from_utf8_lossy(&output.stdout).to_string();
                test_helpers::verify_output("stdout", stdout, &world.want_stdout);
                let stderr = String::from_utf8_lossy(&output.stderr).to_string();
                test_helpers::verify_output("stderr", stderr, &world.want_stderr);
            })
        })
        .run_and_exit("features")
        .await;
}