diff --git a/vdev/src/app.rs b/vdev/src/app.rs index 74ed674cae0ce..2178f72105b06 100644 --- a/vdev/src/app.rs +++ b/vdev/src/app.rs @@ -1,4 +1,5 @@ use std::ffi::{OsStr, OsString}; +use std::io::{PipeReader, pipe}; use std::{ borrow::Cow, env, io::Read, path::PathBuf, process::Command, process::ExitStatus, process::Stdio, sync::LazyLock, sync::OnceLock, time::Duration, @@ -6,6 +7,7 @@ use std::{ use anyhow::{Context as _, Result, bail}; use indicatif::{ProgressBar, ProgressStyle}; +use itertools::Itertools; use log::LevelFilter; use crate::{config::Config, git, platform, util}; @@ -74,119 +76,213 @@ pub fn version() -> Result { Ok(version) } -/// Overlay some extra helper functions onto `std::process::Command` -pub trait CommandExt { - fn script(script: &str) -> Self; - fn in_repo(&mut self) -> &mut Self; - fn check_output(&mut self) -> Result; - fn check_run(&mut self) -> Result<()>; - fn run(&mut self) -> Result; - fn wait(&mut self, message: impl Into>) -> Result<()>; - fn pre_exec(&self); - fn features(&mut self, features: &[String]) -> &mut Self; +pub struct VDevCommand { + pub inner: Command, } -impl CommandExt for Command { +impl VDevCommand { + #[must_use] + pub fn new>(program: S) -> Self { + Self { + inner: Command::new(program.as_ref()), + } + } + /// Create a new command to execute the named script in the repository `scripts` directory. fn script(script: &str) -> Self { let path: PathBuf = [path(), "scripts", script].into_iter().collect(); if cfg!(windows) { // On Windows, all scripts must be run through an explicit interpreter. - let mut command = Command::new(&*SHELL); - command.arg(path); - command + Self::new(&*SHELL).arg(path) } else { // On all other systems, we can run scripts directly. - Command::new(path) + Self::new(path) } } +} + +impl From for VDevCommand { + fn from(command: Command) -> Self { + Self { inner: command } + } +} + +impl VDevCommand { + #[must_use] + pub fn arg>(mut self, arg: S) -> Self { + self.inner.arg(arg); + self + } + + #[must_use] + pub fn args(mut self, args: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + self.inner.args(args); + self + } + + #[must_use] + pub fn features(mut self, features: &[String]) -> Self { + self = self.arg("--no-default-features").arg("--features"); + if features.is_empty() { + self = self.arg(platform::default_features()); + } else { + self = self.arg(features.join(",")); + } + self + } + + #[must_use] + pub fn in_repo(mut self) -> Self { + self.inner.current_dir(path()); + self + } + + #[must_use] + pub fn env(mut self, key: K, val: V) -> Self + where + K: AsRef, + V: AsRef, + { + self.inner.env(key, val); + self + } + + #[must_use] + pub fn envs(mut self, vars: I) -> Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + self.inner.envs(vars); + self + } + + /// Run the command and capture its output. + pub fn check_output(self) -> Result { + self.check_output_inner().map(|(_, output)| output) + } - /// Set the command's working directory to the repository directory. - fn in_repo(&mut self) -> &mut Self { - self.current_dir(path()) + /// Set up the command's stdout/stderr to be piped to the reader + fn setup_output(&mut self) -> Result { + let (reader, writer) = pipe()?; + let writer_clone = writer.try_clone()?; + + self.inner.stdout(Stdio::from(writer)); + self.inner.stderr(Stdio::from(writer_clone)); + Ok(reader) } /// Run the command and capture its output. - fn check_output(&mut self) -> Result { - // Set up the command's stdout to be piped, so we can capture it + fn check_output_inner(mut self) -> Result<(ExitStatus, String)> { + let error_info = format!( + "\"{}\" {}", + self.inner.get_program().to_string_lossy(), + self.inner + .get_args() + .map(|arg| format!("\"{}\"", arg.to_string_lossy())) + .join(" ") + ); + self.pre_exec(); - self.stdout(Stdio::piped()); - // Spawn the process - let mut child = self.spawn()?; + let mut reader = self.setup_output()?; - // Read the output from child.stdout into a buffer - let mut buffer = Vec::new(); - child.stdout.take().unwrap().read_to_end(&mut buffer)?; + // Spawn the process + let mut child = self + .inner + .spawn() + .with_context(|| format!("Failed to spawn process {error_info}"))?; // Catch the exit code let status = child.wait()?; // There are commands that might fail with stdout, but we probably do not // want to capture - // If the exit code is non-zero, return an error with the command, exit code, and stderr output + // If the exit code is non-zero, return an error with the command, exit code, and full output + drop(self.inner); // Drop inner to prevent deadlock when reading + + let mut buffer = Vec::new(); + reader.read_to_end(&mut buffer).unwrap(); + let output = String::from_utf8_lossy(&buffer); + if !status.success() { - let stdout = String::from_utf8_lossy(&buffer); bail!( - "Command: {:?}\nfailed with exit code: {}\n\noutput:\n{}", - self, + "Command: {error_info}\nfailed with exit code: {}\n\noutput:\n{output}", status.code().unwrap(), - stdout ); } // If the command exits successfully, return the output as a string - Ok(String::from_utf8(buffer)?) + Ok((status, output.into_owned())) } /// Run the command and catch its exit code. - fn run(&mut self) -> Result { + pub fn run(self) -> Result { self.pre_exec(); - self.status().map_err(Into::into) + self.check_output_inner().map(|(status, _)| status) } - fn check_run(&mut self) -> Result<()> { - let status = self.run()?; - if status.success() { - Ok(()) - } else { - let exit = status.code().unwrap(); - bail!("command: {self:?}\n failed with exit code: {exit}") - } + pub fn check_run(self) -> Result<()> { + self.run().map(|_| ()) } /// Run the command, capture its output, and display a progress bar while it's /// executing. Intended to be used for long-running processes with little interaction. - fn wait(&mut self, message: impl Into>) -> Result<()> { + pub fn wait(&mut self, message: impl Into>) -> Result<()> { + let error_info = format!( + "\"{}\" {}", + self.inner.get_program().to_string_lossy(), + self.inner + .get_args() + .map(|arg| format!("\"{}\"", arg.to_string_lossy())) + .join(" ") + ); + self.pre_exec(); + let mut reader = self.setup_output()?; + let progress_bar = get_progress_bar()?; progress_bar.set_message(message); - let result = self.output(); - progress_bar.finish_and_clear(); + // Spawn the process + let child = self + .inner + .spawn() + .with_context(|| format!("Failed to spawn process {error_info}")); - let Ok(output) = result else { - bail!("could not run command") - }; + if child.is_err() { + progress_bar.finish_and_clear(); + } + let status = child?.wait(); + if status.is_err() { + progress_bar.finish_and_clear(); + } + let status = status?; + + if !status.success() { + let mut buffer = Vec::new(); + reader.read_to_end(&mut buffer).unwrap(); + let output = String::from_utf8_lossy(&buffer); - if output.status.success() { - Ok(()) - } else { bail!( - "{}\nfailed with exit code: {}", - String::from_utf8(output.stdout)?, - output.status.code().unwrap() - ) + "Command: {error_info}\nfailed with exit code: {}\n\noutput:\n{output}", + status.code().unwrap(), + ); } + Ok(()) } /// Print out a pre-execution debug message. fn pre_exec(&self) { - debug!("Executing: {self:?}"); - if let Some(cwd) = self.get_current_dir() { + if let Some(cwd) = self.inner.get_current_dir() { debug!(" in working directory {cwd:?}"); } - for (key, value) in self.get_envs() { + for (key, value) in self.inner.get_envs() { let key = key.to_string_lossy(); if let Some(value) = value { debug!(" ${key}={:?}", value.to_string_lossy()); @@ -195,17 +291,6 @@ impl CommandExt for Command { } } } - - fn features(&mut self, features: &[String]) -> &mut Self { - self.arg("--no-default-features"); - self.arg("--features"); - if features.is_empty() { - self.arg(platform::default_features()); - } else { - self.arg(features.join(",")); - } - self - } } /// Short-cut wrapper to create a new command, feed in the args, set the working directory, and then @@ -216,12 +301,12 @@ pub fn exec>( in_repo: bool, ) -> Result<()> { let mut command = match program.strip_prefix("scripts/") { - Some(script) => Command::script(script), - None => Command::new(program), - }; - command.args(args); + Some(script) => VDevCommand::script(script), + None => VDevCommand::new(program), + } + .args(args); if in_repo { - command.in_repo(); + command = command.in_repo(); } command.check_run() } diff --git a/vdev/src/commands/build/vector.rs b/vdev/src/commands/build/vector.rs index 100357386b2f3..96e57d00a98c4 100644 --- a/vdev/src/commands/build/vector.rs +++ b/vdev/src/commands/build/vector.rs @@ -1,9 +1,7 @@ -use std::process::Command; - use anyhow::Result; use clap::Args; -use crate::{app::CommandExt as _, platform}; +use crate::{app::VDevCommand, platform}; /// Build the `vector` executable. #[derive(Args, Debug)] @@ -23,22 +21,17 @@ pub struct Cli { impl Cli { pub fn exec(self) -> Result<()> { - let mut command = Command::new("cargo"); - command.in_repo(); - command.arg("build"); + let mut command = VDevCommand::new("cargo").in_repo().arg("build"); if self.release { - command.arg("--release"); + command = command.arg("--release"); } - command.features(&self.feature); - let target = self.target.unwrap_or_else(platform::default_target); - command.args(["--target", &target]); + command = command.features(&self.feature).args(["--target", &target]); waiting!("Building Vector"); - command.check_run()?; - Ok(()) + command.check_run() } } diff --git a/vdev/src/commands/crate_versions.rs b/vdev/src/commands/crate_versions.rs index f12442967effa..ab6831ae29e15 100644 --- a/vdev/src/commands/crate_versions.rs +++ b/vdev/src/commands/crate_versions.rs @@ -1,11 +1,11 @@ -use std::{collections::HashMap, collections::HashSet, process::Command}; +use std::{collections::HashMap, collections::HashSet}; use anyhow::Result; use clap::Args; use itertools::Itertools as _; use regex::Regex; -use crate::{app::CommandExt as _, util}; +use crate::{app::VDevCommand, util}; /// Show information about crates versions pulled in by all dependencies #[derive(Args, Debug)] @@ -25,7 +25,7 @@ impl Cli { let re_crate = Regex::new(r" (\S+) v([0-9.]+)").unwrap(); let mut versions: HashMap> = HashMap::default(); - for line in Command::new("cargo") + for line in VDevCommand::new("cargo") .arg("tree") .features(&self.feature) .check_output()? diff --git a/vdev/src/commands/exec.rs b/vdev/src/commands/exec.rs index f7e6a11f6af6d..196732aac7a9e 100644 --- a/vdev/src/commands/exec.rs +++ b/vdev/src/commands/exec.rs @@ -1,9 +1,7 @@ -use std::process::Command; - use anyhow::Result; use clap::Args; -use crate::app::CommandExt as _; +use crate::app::VDevCommand; /// Execute a command within the repository #[derive(Args, Debug)] @@ -15,7 +13,7 @@ pub struct Cli { impl Cli { pub fn exec(self) -> Result<()> { - let status = Command::new(&self.args[0]) + let status = VDevCommand::new(&self.args[0]) .in_repo() .args(&self.args[1..]) .run()?; diff --git a/vdev/src/commands/release/github.rs b/vdev/src/commands/release/github.rs index c529b80635c46..00d5f05511aa4 100644 --- a/vdev/src/commands/release/github.rs +++ b/vdev/src/commands/release/github.rs @@ -1,8 +1,7 @@ -use crate::app::CommandExt as _; +use crate::app::VDevCommand; use crate::util; -use anyhow::{anyhow, Ok, Result}; +use anyhow::{Result, anyhow}; use glob::glob; -use std::process::Command; /// Uploads target/artifacts to GitHub releases #[derive(clap::Args, Debug)] @@ -21,25 +20,24 @@ impl Cli { .map_err(|e| anyhow!("failed to turn path into string: {:?}", e))?; let version = util::get_version()?; - let mut command = Command::new("gh"); - command.in_repo(); - command.args( - [ - "release", - "--repo", - "vectordotdev/vector", - "create", - &format!("v{version}"), - "--title", - &format!("v{version}"), - "--notes", - &format!("[View release notes](https://vector.dev/releases/{version})"), - ] - .map(String::from) - .into_iter() - .chain(artifacts), - ); - command.check_run()?; - Ok(()) + VDevCommand::new("gh") + .in_repo() + .args( + [ + "release", + "--repo", + "vectordotdev/vector", + "create", + &format!("v{version}"), + "--title", + &format!("v{version}"), + "--notes", + &format!("[View release notes](https://vector.dev/releases/{version})"), + ] + .map(String::from) + .into_iter() + .chain(artifacts), + ) + .check_run() } } diff --git a/vdev/src/commands/run.rs b/vdev/src/commands/run.rs index f259134b28991..aada6d4f60eb7 100644 --- a/vdev/src/commands/run.rs +++ b/vdev/src/commands/run.rs @@ -1,9 +1,9 @@ -use std::{path::PathBuf, process::Command}; +use std::path::PathBuf; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use clap::Args; -use crate::{app::CommandExt as _, features}; +use crate::{app::VDevCommand, features}; /// Run `vector` with the minimum set of features required by the config file #[derive(Args, Debug)] @@ -37,17 +37,22 @@ impl Cli { let mut features = features::load_and_extract(&self.config)?; features.extend(self.feature); let features = features.join(","); - let mut command = Command::new("cargo"); - command.args(["run", "--no-default-features", "--features", &features]); + let mut command = VDevCommand::new("cargo").args([ + "run", + "--no-default-features", + "--features", + &features, + ]); if self.release { - command.arg("--release"); + command = command.arg("--release"); } - command.args([ - "--", - "--config", - self.config.to_str().expect("Invalid config file name"), - ]); - command.args(self.args); - command.check_run() + command + .args([ + "--", + "--config", + self.config.to_str().expect("Invalid config file name"), + ]) + .args(self.args) + .check_run() } } diff --git a/vdev/src/environment.rs b/vdev/src/environment.rs index 57edfc97844e0..7fd36dfaab382 100644 --- a/vdev/src/environment.rs +++ b/vdev/src/environment.rs @@ -1,6 +1,7 @@ use cfg_if::cfg_if; use std::collections::BTreeMap; -use std::process::Command; + +use crate::app::VDevCommand; cfg_if! { if #[cfg(unix)] { @@ -30,14 +31,18 @@ pub(crate) fn extract_present(environment: &Environment) -> BTreeMap VDevCommand { for (key, value) in environment { - command.arg("--env"); + command = command.arg("--env"); match value { - Some(value) => command.arg(format!("{key}={value}")), - None => command.arg(key), - }; + Some(value) => command = command.arg(format!("{key}={value}")), + None => command = command.arg(key), + } } + command } cfg_if! { diff --git a/vdev/src/git.rs b/vdev/src/git.rs index d4804674f1aa2..e79b0e4a9515e 100644 --- a/vdev/src/git.rs +++ b/vdev/src/git.rs @@ -1,7 +1,7 @@ -use crate::app::CommandExt as _; +use crate::app::VDevCommand; use anyhow::{Result, anyhow, bail}; use git2::{BranchType, ErrorCode, Repository}; -use std::{collections::HashSet, process::Command}; +use std::collections::HashSet; pub fn current_branch() -> Result { let output = run_and_check_output(&["rev-parse", "--abbrev-ref", "HEAD"])?; @@ -95,46 +95,46 @@ pub fn get_modified_files() -> Result> { } pub fn set_config_value(key: &str, value: &str) -> Result { - Command::new("git") + VDevCommand::new("git") .args(["config", key, value]) - .stdout(std::process::Stdio::null()) .check_output() } /// Checks if the current directory's repo is clean pub fn check_git_repository_clean() -> Result { - Ok(Command::new("git") + VDevCommand::new("git") .args(["diff-index", "--quiet", "HEAD"]) - .stdout(std::process::Stdio::null()) - .status() - .map(|status| status.success())?) + .run() + .map(|status| status.success()) } pub fn add_files_in_current_dir() -> Result { - Command::new("git").args(["add", "."]).check_output() + VDevCommand::new("git").args(["add", "."]).check_output() } /// Commits changes from the current repo pub fn commit(commit_message: &str) -> Result { - Command::new("git") + VDevCommand::new("git") .args(["commit", "--all", "--message", commit_message]) .check_output() } /// Pushes changes from the current repo pub fn push() -> Result { - Command::new("git").args(["push"]).check_output() + VDevCommand::new("git").args(["push"]).check_output() } pub fn push_and_set_upstream(branch_name: &str) -> Result { - Command::new("git") + VDevCommand::new("git") .args(["push", "-u", "origin", branch_name]) .check_output() } pub fn clone(repo_url: &str) -> Result { // We cannot use capture_output since this will need to run in the CWD - Command::new("git").args(["clone", repo_url]).check_output() + VDevCommand::new("git") + .args(["clone", repo_url]) + .check_output() } /// Walks up from the current working directory until it finds a `.git` @@ -189,7 +189,7 @@ pub fn create_branch(branch_name: &str) -> Result<()> { } pub fn run_and_check_output(args: &[&str]) -> Result { - Command::new("git").in_repo().args(args).check_output() + VDevCommand::new("git").in_repo().args(args).check_output() } fn is_warning_line(line: &str) -> bool { diff --git a/vdev/src/testing/build.rs b/vdev/src/testing/build.rs index ac3f9ea95864a..ace0c246b99c0 100644 --- a/vdev/src/testing/build.rs +++ b/vdev/src/testing/build.rs @@ -1,12 +1,11 @@ -use crate::app; -use crate::app::CommandExt; +use crate::app::{self, VDevCommand}; use crate::environment::{Environment, extract_present}; use crate::testing::config::RustToolchainConfig; use crate::testing::docker::docker_command; use crate::util::IS_A_TTY; use anyhow::Result; +use std::path::Path; use std::path::PathBuf; -use std::{path::Path, process::Command}; pub const ALL_INTEGRATIONS_FEATURE_FLAG: &str = "all-integration-tests"; @@ -19,37 +18,33 @@ pub fn prepare_build_command( dockerfile: &Path, features: Option<&[String]>, config_environment_variables: &Environment, -) -> Command { +) -> VDevCommand { // Start with `docker build` - let mut command = docker_command(["build"]); - // Ensure we run from the repo root (so `.` context is correct) - command.current_dir(app::path()); + let mut command = docker_command(["build"]).in_repo(); // If we're attached to a TTY, show fancy progress if *IS_A_TTY { - command.args(["--progress", "tty"]); + command = command.args(["--progress", "tty"]); } // Add all of the flags in one go - command.args([ - "--pull", - "--tag", - image, - "--file", - dockerfile.to_str().unwrap(), - "--label", - "vector-test-runner=true", - "--build-arg", - &format!("RUST_VERSION={}", RustToolchainConfig::rust_version()), - "--build-arg", - &format!("FEATURES={}", features.unwrap_or(&[]).join(",")), - ]); - - command.envs(extract_present(config_environment_variables)); - - command.args(["."]); command + .args([ + "--pull", + "--tag", + image, + "--file", + dockerfile.to_str().unwrap(), + "--label", + "vector-test-runner=true", + "--build-arg", + &format!("RUST_VERSION={}", RustToolchainConfig::rust_version()), + "--build-arg", + &format!("FEATURES={}", features.unwrap_or(&[]).join(",")), + ]) + .envs(extract_present(config_environment_variables)) + .args(["."]) } #[allow(dead_code)] @@ -59,7 +54,7 @@ pub fn build_integration_image() -> Result<()> { .iter() .collect(); let image = format!("vector-test-runner-{}", RustToolchainConfig::rust_version()); - let mut cmd = prepare_build_command( + let cmd = prepare_build_command( &image, &dockerfile, Some(&[ALL_INTEGRATIONS_FEATURE_FLAG.to_string()]), diff --git a/vdev/src/testing/docker.rs b/vdev/src/testing/docker.rs index 6aca2a4b9ed6d..539690892055e 100644 --- a/vdev/src/testing/docker.rs +++ b/vdev/src/testing/docker.rs @@ -1,28 +1,24 @@ use std::env; use std::ffi::{OsStr, OsString}; use std::path::PathBuf; -use std::process::{Command, Stdio}; use std::sync::LazyLock; +use crate::app::VDevCommand; + pub static CONTAINER_TOOL: LazyLock = LazyLock::new(|| env::var_os("CONTAINER_TOOL").unwrap_or_else(detect_container_tool)); pub(super) static DOCKER_SOCKET: LazyLock = LazyLock::new(detect_docker_socket); -pub fn docker_command>(args: impl IntoIterator) -> Command { - let mut command = Command::new(&*CONTAINER_TOOL); - command.args(args); - command +pub fn docker_command>(args: impl IntoIterator) -> VDevCommand { + VDevCommand::new(&*CONTAINER_TOOL).args(args) } fn detect_container_tool() -> OsString { for tool in ["docker", "podman"] { - if Command::new(tool) + if VDevCommand::new(tool) .arg("version") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .and_then(|mut child| child.wait()) + .run() .is_ok_and(|status| status.success()) { return OsString::from(String::from(tool)); diff --git a/vdev/src/testing/integration.rs b/vdev/src/testing/integration.rs index 7d255e973b383..38e37cf3614b5 100644 --- a/vdev/src/testing/integration.rs +++ b/vdev/src/testing/integration.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, fs, path::Path, path::PathBuf, process::Command}; +use std::{collections::BTreeMap, fs, path::Path, path::PathBuf}; use anyhow::{Context, Result, bail}; use tempfile::{Builder, NamedTempFile}; @@ -8,7 +8,7 @@ use super::config::{ }; use super::runner::{ContainerTestRunner as _, IntegrationTestRunner, TestRunner as _}; use super::state::EnvsDir; -use crate::app::CommandExt as _; +use crate::app::VDevCommand; use crate::environment::{Environment, extract_present, rename_environment_keys}; use crate::testing::build::ALL_INTEGRATIONS_FEATURE_FLAG; use crate::testing::docker::{CONTAINER_TOOL, DOCKER_SOCKET}; @@ -300,38 +300,39 @@ impl Compose { args: &[&'static str], environment: Option<&Environment>, ) -> Result<()> { - let mut command = Command::new(CONTAINER_TOOL.clone()); - command.arg("compose"); - // When the integration test environment is already active, the tempfile path does not - // exist because `Compose::new()` has not been called. In this case, the `stop` command - // needs to use the calculated path from the integration name instead of the nonexistent - // tempfile path. This is because `stop` doesn't go through the same logic as `start` - // and doesn't create a new tempfile before calling docker compose. - // If stop command needs to use some of the injected bits then we need to rebuild it - command.arg("--file"); - if self.temp_file.path().exists() { - command.arg(self.temp_file.path()); + let mut command = VDevCommand::new(CONTAINER_TOOL.clone()) + .arg("compose") + // When the integration test environment is already active, the tempfile path does not + // exist because `Compose::new()` has not been called. In this case, the `stop` command + // needs to use the calculated path from the integration name instead of the nonexistent + // tempfile path. This is because `stop` doesn't go through the same logic as `start` + // and doesn't create a new tempfile before calling docker compose. + // If stop command needs to use some of the injected bits then we need to rebuild it + .arg("--file"); + + let path = if self.temp_file.path().exists() { + self.temp_file.path() } else { - command.arg(&self.original_path); - } - - command.args(args); + &self.original_path + }; - command.current_dir(&self.test_dir); + command = command.arg(path).args(args); - command.env("DOCKER_SOCKET", &*DOCKER_SOCKET); - command.env(NETWORK_ENV_VAR, &self.network); + command.inner.current_dir(&self.test_dir); - // some services require this in order to build Vector - command.env("RUST_VERSION", RustToolchainConfig::rust_version()); + command = command + .env("DOCKER_SOCKET", &*DOCKER_SOCKET) + .env(NETWORK_ENV_VAR, &self.network) + // some services require this in order to build Vector + .env("RUST_VERSION", RustToolchainConfig::rust_version()); for (key, value) in &self.env { if let Some(value) = value { - command.env(key, value); + command = command.env(key, value); } } if let Some(environment) = environment { - command.envs(extract_present(environment)); + command = command.envs(extract_present(environment)); } waiting!("{action} service environment"); diff --git a/vdev/src/testing/runner.rs b/vdev/src/testing/runner.rs index 0a18190677eb6..5e877d01889f5 100644 --- a/vdev/src/testing/runner.rs +++ b/vdev/src/testing/runner.rs @@ -1,11 +1,10 @@ use std::collections::HashSet; -use std::process::Command; use std::{env, path::PathBuf}; use anyhow::Result; use super::config::{IntegrationRunnerConfig, RustToolchainConfig}; -use crate::app::{self, CommandExt as _}; +use crate::app::{self, VDevCommand}; use crate::environment::{Environment, append_environment_variables}; use crate::testing::build::prepare_build_command; use crate::testing::docker::{DOCKER_SOCKET, docker_command}; @@ -70,7 +69,7 @@ pub trait ContainerTestRunner: TestRunner { fn volumes(&self) -> Vec; fn state(&self) -> Result { - let mut command = docker_command(["ps", "-a", "--format", "{{.Names}} {{.State}}"]); + let command = docker_command(["ps", "-a", "--format", "{{.Names}} {{.State}}"]); let container_name = self.container_name(); for line in command.check_output()?.lines() { @@ -125,7 +124,7 @@ pub trait ContainerTestRunner: TestRunner { } fn ensure_volumes(&self) -> Result<()> { - let mut command = docker_command(["volume", "ls", "--format", "{{.Name}}"]); + let command = docker_command(["volume", "ls", "--format", "{{.Name}}"]); let mut volumes = HashSet::new(); volumes.insert(VOLUME_TARGET); @@ -153,7 +152,7 @@ pub trait ContainerTestRunner: TestRunner { .iter() .collect(); - let mut command = + let command = prepare_build_command(&self.image_name(), &dockerfile, features, config_env_vars); waiting!("Building image {}", self.image_name()); command.check_run() @@ -238,24 +237,25 @@ where let mut command = docker_command(["exec"]); if *IS_A_TTY { - command.arg("--tty"); + command = command.arg("--tty"); } - command.args(["--env", "RUST_BACKTRACE=1"]); - command.args(["--env", &format!("CARGO_BUILD_TARGET_DIR={TARGET_PATH}")]); + command = command + .args(["--env", "RUST_BACKTRACE=1"]) + .args(["--env", &format!("CARGO_BUILD_TARGET_DIR={TARGET_PATH}")]); for (key, value) in outer_env { if let Some(value) = value { - command.env(key, value); + command = command.env(key, value); } - command.args(["--env", key]); + command = command.args(["--env", key]); } - append_environment_variables(&mut command, config_environment_variables); + command = append_environment_variables(command, config_environment_variables); - command.arg(self.container_name()); - command.args(TEST_COMMAND); - command.args(args); - - command.check_run() + command + .arg(self.container_name()) + .args(TEST_COMMAND) + .args(args) + .check_run() } } @@ -288,7 +288,7 @@ impl IntegrationTestRunner { pub(super) fn ensure_network(&self) -> Result<()> { if let Some(network_name) = &self.network { - let mut command = docker_command(["network", "ls", "--format", "{{.Name}}"]); + let command = docker_command(["network", "ls", "--format", "{{.Name}}"]); if command .check_output()? @@ -370,21 +370,19 @@ impl TestRunner for LocalTestRunner { args: &[String], _directory: &str, ) -> Result<()> { - let mut command = Command::new(TEST_COMMAND[0]); - command.args(&TEST_COMMAND[1..]); - command.args(args); - - for (key, value) in outer_env { - if let Some(value) = value { - command.env(key, value); - } - } - for (key, value) in inner_env { - if let Some(value) = value { - command.env(key, value); - } - } - - command.check_run() + VDevCommand::new(TEST_COMMAND[0]) + .args(&TEST_COMMAND[1..]) + .args(args) + .envs( + outer_env + .iter() + .filter_map(|(key, value)| value.as_ref().map(|v| (key, v))), + ) + .envs( + inner_env + .iter() + .filter_map(|(key, value)| value.as_ref().map(|v| (key, v))), + ) + .check_run() } }