use crate::{
cargo_config::EnvironmentMap,
double_spawn::{DoubleSpawnContext, DoubleSpawnInfo},
helpers::dylib_path_envvar,
list::{RustBuildMeta, TestListState},
test_output::CaptureStrategy,
};
use camino::{Utf8Path, Utf8PathBuf};
use guppy::graph::PackageMetadata;
use once_cell::sync::Lazy;
use std::{
collections::{BTreeSet, HashMap},
ffi::{OsStr, OsString},
fs::File,
io::{BufRead, BufReader},
};
use tracing::warn;
mod imp;
pub(crate) use imp::{Child, ChildAccumulator, ChildFds};
#[derive(Clone, Debug)]
pub(crate) struct LocalExecuteContext<'a> {
pub(crate) rust_build_meta: &'a RustBuildMeta<TestListState>,
pub(crate) double_spawn: &'a DoubleSpawnInfo,
pub(crate) dylib_path: &'a OsStr,
pub(crate) env: &'a EnvironmentMap,
}
pub(crate) struct TestCommand {
command: std::process::Command,
double_spawn: Option<DoubleSpawnContext>,
}
impl TestCommand {
pub(crate) fn new(
lctx: &LocalExecuteContext<'_>,
program: String,
args: &[&str],
cwd: &Utf8Path,
package: &PackageMetadata<'_>,
non_test_binaries: &BTreeSet<(String, Utf8PathBuf)>,
) -> Self {
let mut cmd = create_command(program, args, lctx.double_spawn);
lctx.env.apply_env(&mut cmd);
if let Some(out_dir) = lctx
.rust_build_meta
.build_script_out_dirs
.get(package.id().repr())
{
let out_dir = lctx.rust_build_meta.target_directory.join(out_dir);
cmd.env("OUT_DIR", &out_dir);
apply_build_script_env(&mut cmd, &out_dir);
}
cmd.current_dir(cwd)
.env("NEXTEST", "1")
.env("NEXTEST_EXECUTION_MODE", "process-per-test")
.env(
"CARGO_MANIFEST_DIR",
cwd,
);
apply_package_env(&mut cmd, package);
apply_ld_dyld_env(&mut cmd, lctx.dylib_path);
for (name, path) in non_test_binaries {
cmd.env(format!("NEXTEST_BIN_EXE_{name}"), path);
}
let double_spawn = lctx.double_spawn.spawn_context();
Self {
command: cmd,
double_spawn,
}
}
#[inline]
pub(crate) fn command_mut(&mut self) -> &mut std::process::Command {
&mut self.command
}
pub(crate) fn spawn(self, capture_strategy: CaptureStrategy) -> std::io::Result<imp::Child> {
let res = imp::spawn(self.command, capture_strategy);
if let Some(ctx) = self.double_spawn {
ctx.finish();
}
res
}
pub(crate) async fn wait_with_output(self) -> std::io::Result<std::process::Output> {
let mut cmd = self.command;
cmd.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let res = tokio::process::Command::from(cmd).spawn();
if let Some(ctx) = self.double_spawn {
ctx.finish();
}
res?.wait_with_output().await
}
}
pub(crate) fn create_command<I, S>(
program: String,
args: I,
double_spawn: &DoubleSpawnInfo,
) -> std::process::Command
where
I: IntoIterator<Item = S>,
S: AsRef<str> + AsRef<OsStr>,
{
let cmd = if let Some(current_exe) = double_spawn.current_exe() {
let mut cmd = std::process::Command::new(current_exe);
cmd.args([DoubleSpawnInfo::SUBCOMMAND_NAME, "--", program.as_str()]);
cmd.arg(shell_words::join(args));
cmd
} else {
let mut cmd = std::process::Command::new(program);
cmd.args(args);
cmd
};
cmd
}
fn apply_package_env(cmd: &mut std::process::Command, package: &PackageMetadata<'_>) {
cmd.env("CARGO_PKG_VERSION", format!("{}", package.version()))
.env(
"CARGO_PKG_VERSION_MAJOR",
format!("{}", package.version().major),
)
.env(
"CARGO_PKG_VERSION_MINOR",
format!("{}", package.version().minor),
)
.env(
"CARGO_PKG_VERSION_PATCH",
format!("{}", package.version().patch),
)
.env(
"CARGO_PKG_VERSION_PRE",
format!("{}", package.version().pre),
)
.env("CARGO_PKG_AUTHORS", package.authors().join(":"))
.env("CARGO_PKG_NAME", package.name())
.env(
"CARGO_PKG_DESCRIPTION",
package.description().unwrap_or_default(),
)
.env("CARGO_PKG_HOMEPAGE", package.homepage().unwrap_or_default())
.env("CARGO_PKG_LICENSE", package.license().unwrap_or_default())
.env(
"CARGO_PKG_LICENSE_FILE",
package.license_file().unwrap_or_else(|| "".as_ref()),
)
.env(
"CARGO_PKG_REPOSITORY",
package.repository().unwrap_or_default(),
)
.env(
"CARGO_PKG_RUST_VERSION",
package
.minimum_rust_version()
.map_or(String::new(), |v| v.to_string()),
);
}
fn apply_build_script_env(cmd: &mut std::process::Command, out_dir: &Utf8Path) {
let Some(out_dir_parent) = out_dir.parent() else {
warn!("could not determine parent directory of output directory {out_dir}");
return;
};
let Ok(out_file) = File::open(out_dir_parent.join("output")) else {
warn!("could not find build script output file at {out_dir_parent}/output");
return;
};
parse_build_script_output(
BufReader::new(out_file),
&out_dir_parent.join("output"),
|key, val| {
cmd.env(key, val);
},
);
}
fn parse_build_script_output<R, Cb>(out_file: R, out_file_path: &Utf8Path, mut callback: Cb)
where
R: BufRead,
Cb: FnMut(&str, &str),
{
for line in out_file.lines() {
let Ok(line) = line else {
warn!("in build script output `{out_file_path}`, found line with invalid UTF-8");
continue;
};
let Some(key_val) = line
.strip_prefix("cargo::rustc-env=")
.or_else(|| line.strip_prefix("cargo:rustc-env="))
else {
continue;
};
let Some((k, v)) = key_val.split_once('=') else {
warn!("rustc-env variable '{key_val}' has no value in {out_file_path}, skipping");
continue;
};
callback(k, v);
}
}
pub(crate) fn apply_ld_dyld_env(cmd: &mut std::process::Command, dylib_path: &OsStr) {
fn is_sip_sanitized(var: &str) -> bool {
var.starts_with("LD_") || var.starts_with("DYLD_")
}
static LD_DYLD_ENV_VARS: Lazy<HashMap<String, OsString>> = Lazy::new(|| {
std::env::vars_os()
.filter_map(|(k, v)| match k.into_string() {
Ok(k) => is_sip_sanitized(&k).then_some((k, v)),
Err(_) => None,
})
.collect()
});
cmd.env(dylib_path_envvar(), dylib_path);
for (k, v) in &*LD_DYLD_ENV_VARS {
if k != dylib_path_envvar() {
cmd.env("NEXTEST_".to_owned() + k, v);
}
}
if is_sip_sanitized(dylib_path_envvar()) {
cmd.env("NEXTEST_".to_owned() + dylib_path_envvar(), dylib_path);
}
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
#[test]
fn parse_build_script() {
let out_file = indoc! {"
some_other_line
cargo::rustc-env=NEW_VAR=new_val
cargo:rustc-env=OLD_VAR=old_val
cargo::rustc-env=NEW_MISSING_VALUE
cargo:rustc-env=OLD_MISSING_VALUE
cargo:rustc-env=NEW_EMPTY_VALUE=
"};
let mut key_vals = Vec::new();
parse_build_script_output(
BufReader::new(std::io::Cursor::new(out_file)),
Utf8Path::new("<test input>"),
|key, val| key_vals.push((key.to_owned(), val.to_owned())),
);
assert_eq!(
key_vals,
vec![
("NEW_VAR".to_owned(), "new_val".to_owned()),
("OLD_VAR".to_owned(), "old_val".to_owned()),
("NEW_EMPTY_VALUE".to_owned(), "".to_owned()),
],
"parsed key-value pairs match"
);
}
}