use super::test::TestArgs;
use super::test_runner::DiscoveredBinaries;
use crate::cli::ui::Ui;
use crate::core::action::BuildPlan;
use std::collections::{BTreeSet, HashMap};
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Mutex;
use std::time::Instant;
pub fn jobs_for(requested: usize) -> usize {
if requested > 0 {
return requested;
}
std::thread::available_parallelism().map_or(2, |n| n.get())
}
pub fn parse_list(output: &str) -> BTreeSet<String> {
output
.lines()
.filter_map(|line| line.strip_suffix(": test"))
.map(str::to_owned)
.collect()
}
pub fn selected(name: &str, filter: Option<&str>, exact: bool) -> bool {
match filter {
None => true,
Some(f) if exact => name == f,
Some(f) => name.contains(f),
}
}
struct Target {
label: String,
path: PathBuf,
env: HashMap<String, String>,
work_dir: PathBuf,
whole: bool,
}
type Listing = (BTreeSet<String>, BTreeSet<String>);
struct Job {
target: usize,
name: Option<String>,
ignored: bool,
}
struct Context<'a> {
binaries: &'a DiscoveredBinaries,
plan: &'a BuildPlan,
release: bool,
targets: Vec<Target>,
extra_env: Vec<(String, std::ffi::OsString)>,
}
impl Context<'_> {
fn command(&self, target: &Target) -> Command {
let mut cmd = Command::new(&target.path);
cmd.current_dir(&target.work_dir);
crate::adapter::rust::unit_env::apply_runtime_execution_env(
&mut cmd,
&target.work_dir,
&target.env,
self.plan,
self.release,
);
if !target.env.contains_key("CARGO_PKG_NAME") {
for (name, file) in &self.binaries.cli_binaries {
cmd.env(format!("CARGO_BIN_EXE_{name}"), file);
}
}
for (key, value) in &self.extra_env {
cmd.env(key, value);
}
cmd
}
fn list(&self, target: &Target, ignored_only: bool) -> Option<BTreeSet<String>> {
let mut cmd = self.command(target);
cmd.arg("--list");
if ignored_only {
cmd.arg("--ignored");
}
let output = cmd.output().ok()?;
output
.status
.success()
.then(|| parse_list(&String::from_utf8_lossy(&output.stdout)))
}
}
fn parallel_for<T: Sync>(items: &[T], jobs: usize, work: impl Fn(usize, &T) + Sync) {
let next = AtomicUsize::new(0);
std::thread::scope(|scope| {
for _ in 0..jobs.clamp(1, items.len().max(1)) {
scope.spawn(|| loop {
let i = next.fetch_add(1, Ordering::Relaxed);
if i >= items.len() {
break;
}
work(i, &items[i]);
});
}
});
}
#[allow(clippy::too_many_arguments)]
pub fn run(
binaries: &DiscoveredBinaries,
args: &TestArgs,
root_dir: &std::path::Path,
plan: &BuildPlan,
profile: &str,
start: Instant,
requested_jobs: usize,
extra_env: &[(String, std::ffi::OsString)],
) -> i32 {
let jobs = jobs_for(requested_jobs);
let targets: Vec<Target> = binaries
.test_binaries
.iter()
.map(|(description, path, env)| {
let manifest_dir = env
.get("CARGO_MANIFEST_DIR")
.filter(|d| !d.is_empty() && d.as_str() != ".");
let work_dir = match manifest_dir {
Some(dir) if std::path::Path::new(dir).is_absolute() => PathBuf::from(dir),
Some(dir) => root_dir.join(dir),
None => root_dir.to_path_buf(),
};
Target {
label: path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| description.clone()),
path: if path.is_absolute() {
path.clone()
} else {
root_dir.join(path)
},
env: env.clone(),
work_dir,
whole: description.contains("::custom-test:"),
}
})
.collect();
let context = Context {
binaries,
plan,
release: args.release,
targets,
extra_env: extra_env.to_vec(),
};
let listed: Mutex<Vec<Option<Listing>>> = Mutex::new(vec![None; context.targets.len()]);
parallel_for(&context.targets, jobs, |i, target| {
if target.whole {
return;
}
if let (Some(all), Some(ignored)) =
(context.list(target, false), context.list(target, true))
{
listed.lock().unwrap()[i] = Some((all, ignored));
}
});
let listed = listed.into_inner().unwrap();
let mut queue: Vec<Job> = Vec::new();
let mut skipped = 0usize;
for (i, listing) in listed.iter().enumerate() {
match listing {
Some((all, ignored)) => {
for name in all {
if !selected(name, args.filter.as_deref(), args.exact) {
continue;
}
let is_ignored = ignored.contains(name);
if is_ignored != args.ignored {
skipped += 1;
continue;
}
queue.push(Job {
target: i,
name: Some(name.clone()),
ignored: is_ignored,
});
}
}
None => queue.push(Job {
target: i,
name: None,
ignored: false,
}),
}
}
if queue.is_empty() {
println!("mitts test: no tests matched ({skipped} skipped)");
return 0;
}
Ui::stage_start(
"🧪",
&format!("Running {} tests in {jobs} processes", queue.len()),
);
let stop = AtomicBool::new(false);
let print = Mutex::new(());
let outcome = Mutex::new((0usize, Vec::<String>::new()));
let run_started = Instant::now();
parallel_for(&queue, jobs, |_, job| {
if stop.load(Ordering::Relaxed) {
return;
}
let target = &context.targets[job.target];
let mut cmd = context.command(target);
let shown = match &job.name {
Some(name) => {
cmd.args(["--exact", name, "--nocapture"]);
if job.ignored {
cmd.arg("--ignored");
}
format!("{} {name}", target.label)
}
None => {
if let Some(filter) = &args.filter {
cmd.arg(filter);
}
target.label.clone()
}
};
let started = Instant::now();
let result = cmd.output();
let seconds = started.elapsed().as_secs_f64();
let (ok, captured) = match result {
Ok(output) => (
output.status.success(),
format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
),
),
Err(err) => (
false,
format!("failed to start {}: {err}", target.path.display()),
),
};
let _guard = print.lock().unwrap();
if ok {
println!(" PASS [{seconds:8.3}s] {shown}");
outcome.lock().unwrap().0 += 1;
} else {
println!(" FAIL [{seconds:8.3}s] {shown}");
for line in captured.trim_end().lines() {
println!(" {line}");
}
outcome.lock().unwrap().1.push(shown);
if !args.no_fail_fast {
stop.store(true, Ordering::Relaxed);
}
}
});
let (passed, failed) = outcome.into_inner().unwrap();
let not_run = queue.len() - passed - failed.len();
let elapsed = format!("{:.2}s", start.elapsed().as_secs_f64());
println!(
" Summary [{:8.3}s] {} tests run: {passed} passed, {} failed, {skipped} skipped{}",
run_started.elapsed().as_secs_f64(),
passed + failed.len(),
failed.len(),
if not_run > 0 {
format!(", {not_run} not run after a failure")
} else {
String::new()
}
);
if failed.is_empty() {
Ui::finish(profile, &elapsed, &format!("{passed} tests passed"));
0
} else {
for name in &failed {
println!(" FAIL {name}");
}
Ui::finish_failed(profile, &elapsed, &format!("{} tests failed", failed.len()));
101
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_the_cores_by_default_and_the_requested_number_otherwise() {
let cores = std::thread::available_parallelism().map_or(2, |n| n.get());
assert_eq!(jobs_for(0), cores);
assert!(jobs_for(0) >= 1);
assert_eq!(jobs_for(7), 7);
assert_eq!(jobs_for(1), 1);
}
#[test]
fn a_list_gives_the_tests_and_not_the_benchmarks_or_the_summary() {
let names = parse_list(
"alpha::one: test\nalpha::two: test\nspeed: benchmark\n\n3 tests, 1 benchmarks\n",
);
assert_eq!(
names.into_iter().collect::<Vec<_>>(),
["alpha::one", "alpha::two"]
);
}
#[test]
fn the_filter_matches_a_part_of_the_name_unless_exact() {
assert!(selected("a::b::c", None, false));
assert!(selected("a::b::c", Some("b::"), false));
assert!(!selected("a::b::c", Some("x"), false));
assert!(selected("a::b::c", Some("a::b::c"), true));
assert!(!selected("a::b::c", Some("b"), true));
}
}