use super::{CONFIG_PATH_ENV_VAR, TestPlan, TestRun};
use anyhow::{Context, bail};
use camino::{Utf8Path, Utf8PathBuf};
use guppy::graph::PackageGraph;
use nextest_filtering::ParseContext;
use nextest_runner::{
RustcCli,
cargo_config::{CargoConfigs, EnvironmentMap},
config::core::{ConfigExperimental, NextestConfig, get_num_cpus},
double_spawn::DoubleSpawnInfo,
input::InputHandlerKind,
list::{BinaryList, RustTestArtifact, TestExecuteContext, TestList},
platform::{BuildPlatforms, HostPlatform, PlatformLibdir},
reporter::{
ReporterBuilder, ReporterOutput, ShowTerminalProgress, events::FinalRunStats,
structured::StructuredReporter,
},
reuse_build::PathMapper,
run_mode::NextestRunMode,
runner::{TestRunnerBuilder, configure_handle_inheritance},
signal::SignalHandlerKind,
target_runner::TargetRunner,
test_filter::{FilterBound, RunIgnored, TestFilter},
test_output::CaptureStrategy,
};
use std::collections::{BTreeMap, BTreeSet};
use std::io::{Cursor, Write};
use std::path::Path;
use std::process::Stdio;
use std::sync::Arc;
pub(super) fn run(plan: &TestPlan) -> anyhow::Result<()> {
run_with_env(plan, &[])
}
pub fn run_with_env(plan: &TestPlan, extra_env: &[(String, String)]) -> anyhow::Result<()> {
for run in &plan.runs {
run_nextest(plan, run, extra_env)?;
}
Ok(())
}
pub(super) fn run_nextest(
plan: &TestPlan,
run: &TestRun,
extra_env: &[(String, String)],
) -> anyhow::Result<()> {
let workspace_root = utf8_path(&plan.workspace_root, "workspace root")?;
let graph_json = cargo_metadata_json(plan, run)?;
let graph = PackageGraph::from_json(&graph_json).context("failed to parse cargo metadata")?;
let build_platforms = detect_build_platforms()?;
let binary_list = Arc::new(build_binary_list(
plan,
run,
&graph,
build_platforms,
extra_env,
)?);
let cargo_config =
cargo_config_with_gears_config(&workspace_root, &plan.config_path, extra_env)?;
let cargo_env = EnvironmentMap::new(&cargo_config.configs);
let pcx = ParseContext::new(&graph);
let config = NextestConfig::from_sources(
workspace_root.clone(),
&pcx,
None,
Vec::new().iter(),
&BTreeSet::<ConfigExperimental>::new(),
)
.context("failed to load nextest config")?;
let profile = config
.profile(NextestConfig::DEFAULT_PROFILE)
.context("failed to load nextest default profile")?
.apply_build_platforms(&binary_list.rust_build_meta.build_platforms);
let double_spawn = DoubleSpawnInfo::disabled();
let target_runner = TargetRunner::empty();
let ctx = TestExecuteContext {
profile_name: profile.name(),
double_spawn: &double_spawn,
target_runner: &target_runner,
};
let path_mapper = PathMapper::noop();
let rust_build_meta = binary_list.rust_build_meta.map_paths(&path_mapper);
let test_artifacts = RustTestArtifact::from_binary_list(
&graph,
Arc::clone(&binary_list),
&rust_build_meta,
&path_mapper,
None,
)
.context("failed to create nextest test artifacts")?;
let test_filter = TestFilter::default_set(NextestRunMode::Test, RunIgnored::Default);
let test_list = TestList::new(
&ctx,
test_artifacts,
rust_build_meta,
&test_filter,
None,
workspace_root,
cargo_env,
&profile,
FilterBound::DefaultSet,
get_num_cpus(),
)
.context("failed to list tests with nextest")?;
let mut runner_builder = TestRunnerBuilder::default();
runner_builder.set_capture_strategy(CaptureStrategy::Split);
let runner = runner_builder
.build(
&test_list,
&profile,
nextest_cli_args(run),
SignalHandlerKind::Standard,
InputHandlerKind::Standard,
double_spawn,
target_runner,
)
.context("failed to build nextest runner")?;
let mut reporter_builder = ReporterBuilder::default();
reporter_builder.set_colorize(false);
let mut reporter = reporter_builder.build(
&test_list,
&profile,
ShowTerminalProgress::No,
ReporterOutput::Terminal,
StructuredReporter::new(),
);
configure_handle_inheritance(false)
.context("failed to configure nextest handle inheritance")?;
let run_stats = runner
.try_execute(|event| reporter.report_event(event))
.context("nextest failed to execute tests")?;
reporter.finish();
match run_stats.summarize_final() {
FinalRunStats::Success => Ok(()),
FinalRunStats::NoTestsRun => bail!("nextest found no tests to run"),
FinalRunStats::Cancelled { kind, .. } => bail!("nextest run cancelled: {kind:?}"),
FinalRunStats::Failed { kind } => bail!("nextest run failed: {kind:?}"),
}
}
fn cargo_metadata_json(plan: &TestPlan, run: &TestRun) -> anyhow::Result<String> {
let mut args = vec!["metadata".to_owned(), "--format-version=1".to_owned()];
run.append_cargo_metadata_args(&mut args);
let output = crate::common::cargo_cmd()?
.args(args)
.current_dir(&plan.workspace_root)
.output()
.context("failed to run cargo metadata for nextest")?;
if !output.status.success() {
bail!("cargo metadata for nextest exited with {}", output.status);
}
String::from_utf8(output.stdout).context("cargo metadata output was not UTF-8")
}
fn build_binary_list(
plan: &TestPlan,
run: &TestRun,
graph: &PackageGraph,
build_platforms: BuildPlatforms,
extra_env: &[(String, String)],
) -> anyhow::Result<BinaryList> {
let mut args = vec![
"test".to_owned(),
"--no-run".to_owned(),
"--message-format".to_owned(),
"json-render-diagnostics".to_owned(),
];
run.append_cargo_args(&mut args);
let output = crate::common::cargo_cmd()?
.args(args)
.current_dir(&plan.workspace_root)
.envs(extra_env.iter().map(|(key, value)| (key, value)))
.env(CONFIG_PATH_ENV_VAR, &plan.config_path)
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.output()
.context("failed to build test binaries for nextest")?;
if !output.status.success() {
bail!(
"building test binaries for nextest exited with {}",
output.status
);
}
BinaryList::from_messages(Cursor::new(output.stdout), graph, build_platforms)
.context("failed to parse nextest binary list from Cargo messages")
}
fn detect_build_platforms() -> anyhow::Result<BuildPlatforms> {
let host = HostPlatform::detect(PlatformLibdir::from_rustc_stdout(
RustcCli::print_host_libdir().read(),
))?;
Ok(BuildPlatforms { host, target: None })
}
struct CargoConfigGuard {
configs: CargoConfigs,
_file: tempfile::NamedTempFile,
}
fn cargo_config_with_gears_config(
workspace_root: &Utf8Path,
config_path: &Path,
extra_env: &[(String, String)],
) -> anyhow::Result<CargoConfigGuard> {
let mut file = tempfile::NamedTempFile::new()
.context("failed to create temporary Cargo config for nextest")?;
let mut env = extra_env.iter().cloned().collect::<BTreeMap<_, _>>();
env.insert(
CONFIG_PATH_ENV_VAR.to_owned(),
config_path.to_string_lossy().to_string(),
);
for (key, value) in env {
write_cargo_env_config(&mut file, &key, &value)
.context("failed to write temporary Cargo config for nextest")?;
}
let config_path = utf8_path(file.path(), "temporary Cargo config")?;
let configs = CargoConfigs::new_with_isolation(
[config_path.as_str()],
workspace_root,
Utf8Path::new("/"),
Vec::new(),
)
.context("failed to discover Cargo config for nextest")?;
Ok(CargoConfigGuard {
configs,
_file: file,
})
}
fn write_cargo_env_config(
file: &mut tempfile::NamedTempFile,
key: &str,
value: &str,
) -> anyhow::Result<()> {
let encoded_key = toml_edit::Value::from(key).to_string();
let encoded_value = toml_edit::Value::from(value).to_string();
write!(
file,
"[env.{encoded_key}]\nvalue = {encoded_value}\nforce = true\n"
)?;
Ok(())
}
fn utf8_path(path: &Path, label: &str) -> anyhow::Result<Utf8PathBuf> {
Utf8PathBuf::from_path_buf(path.to_path_buf())
.map_err(|path| anyhow::anyhow!("{label} is not valid UTF-8: {}", path.display()))
}
fn nextest_cli_args(run: &TestRun) -> Vec<String> {
let mut args = vec!["cargo".to_owned(), "nextest".to_owned(), "run".to_owned()];
run.append_cargo_args(&mut args);
args
}