use std::process::Command;
use camino::{Utf8Path, Utf8PathBuf};
use serde_json::Value;
use super::test_binary::TestBinary;
use super::workspace::Workspace;
use crate::error::error;
use crate::{HashMap, Result};
const MAX_FILTERSET_BYTES: usize = 8 * 1024;
#[derive(Debug, Clone)]
pub(super) struct Harness {
binaries: Utf8PathBuf,
metadata: Utf8PathBuf,
ids: HashMap<Utf8PathBuf, String>,
}
impl Harness {
pub(super) fn prepare(work: &Workspace, binaries: &[TestBinary]) -> Result<Self> {
let listing = work.capture_nextest_list(binaries)?;
let ids = binary_ids(&listing);
let paths = work.write_scratch("nextest-binaries.json", &listing)?;
let metadata = work.write_scratch("nextest-metadata.json", &work.capture_cargo_metadata()?)?;
let harness = Self {
binaries: paths,
metadata,
ids,
};
for binary in binaries {
let _id = harness.id(&binary.path)?;
}
Ok(harness)
}
pub(super) fn command(&self, work: &Workspace, binary: &TestBinary, only: &[&str]) -> Result<Command> {
let id = self.id(&binary.path)?;
let mut command = Command::new(nextest_binary());
let _ = command.current_dir(work.root().as_std_path());
let _ = command.args([
"nextest",
"run",
"--binaries-metadata",
self.binaries.as_str(),
"--cargo-metadata",
self.metadata.as_str(),
]);
for filterset in filtersets(&id, only) {
let _ = command.args(["-E", &filterset]);
}
let _ = command.args(["--failure-output", "immediate-final"]);
if only.is_empty() {
let _ = command.args(["--no-tests", "pass"]);
}
if !work.test_arguments().is_empty() {
let _ = command.arg("--");
let _ = command.args(work.test_arguments());
}
Ok(command)
}
#[cfg(test)]
pub(super) fn fake(ids: &[(&str, &str)]) -> Self {
Self {
binaries: Utf8PathBuf::from("/t/binaries.json"),
metadata: Utf8PathBuf::from("/t/metadata.json"),
ids: ids.iter().map(|(path, id)| (Utf8PathBuf::from(*path), (*id).to_owned())).collect(),
}
}
fn id(&self, path: &Utf8Path) -> Result<String> {
self.ids.get(path).cloned().ok_or_else(|| {
error!(
"`cargo nextest` does not know the test binary `{path}` that this run built.\n\
Run without `--nextest` to run the binaries directly instead."
)
})
}
}
fn binary_ids(listing: &str) -> HashMap<Utf8PathBuf, String> {
let mut ids = HashMap::default();
let Ok(message) = serde_json::from_str::<Value>(listing) else {
return ids;
};
let Some(binaries) = message.get("rust-binaries").and_then(Value::as_object) else {
return ids;
};
for entry in binaries.values() {
let path = entry.get("binary-path").and_then(Value::as_str);
let id = entry.get("binary-id").and_then(Value::as_str);
if let (Some(path), Some(id)) = (path, id) {
let _replaced = ids.insert(Utf8PathBuf::from(path), id.to_owned());
}
}
ids
}
fn matcher(id: &str) -> String {
let escaped = id.replace('\\', "\\\\").replace(')', "\\)").replace(',', "\\,");
format!("={escaped}")
}
fn filtersets(id: &str, only: &[&str]) -> Vec<String> {
let binary = format!("binary_id({})", matcher(id));
if only.is_empty() {
return vec![binary];
}
let tests = only
.iter()
.map(|name| format!("test({})", matcher(name)))
.collect::<Vec<_>>()
.join(" or ");
let narrowed = format!("{binary} and ({tests})");
if narrowed.len() > MAX_FILTERSET_BYTES {
vec![binary]
} else {
vec![narrowed]
}
}
fn nextest_binary() -> String {
"cargo-nextest".to_owned()
}
pub(super) const TEST_RUN_FAILED: i32 = 100;
pub(super) const TEST_LIST_CREATION_FAILED: i32 = 104;
pub(super) const NO_TESTS_RUN: i32 = 4;
pub(super) fn first_failure(output: &str) -> Option<&str> {
for line in output.lines() {
let trimmed = line.trim_start();
let Some(rest) = trimmed.strip_prefix("FAIL ").map(str::trim_start) else {
continue;
};
let after_time = rest.split_once(']').map_or(rest, |(_before, after)| after).trim_start();
let after_count = after_time.split_once(')').map_or(after_time, |(_before, after)| after).trim_start();
let name = after_count.split_once(' ').map_or(after_count, |(_binary, test)| test);
if !name.is_empty() {
return Some(name.trim());
}
}
None
}
#[cfg(all(test, not(miri)))]
mod fuzz {
use super::first_failure;
use crate::testing::{spliced, token};
#[test]
fn a_nextest_failure_is_never_lost_among_arbitrary_output() {
bolero::check!()
.with_type::<(Vec<String>, String, String, usize)>()
.for_each(|(noise, binary, name, at)| {
let line = format!(" FAIL [ 0.024s] (1/2) {} {}", token(binary), token(name));
let output = spliced(noise, &line, *at);
assert!(first_failure(&output).is_some(), "the failure was lost in {output:?}");
});
}
#[test]
fn arbitrary_output_is_read_without_panicking() {
bolero::check!().with_type::<String>().for_each(|output| {
if let Some(name) = first_failure(output) {
assert!(!name.is_empty(), "an empty test name was reported for {output:?}");
assert!(!name.contains('\n'), "a test name spans lines: {name:?}");
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_listing_yields_the_id_each_binary_is_known_by() {
let listing = r#"{"rust-binaries":{"nxspike":{"binary-id":"nxspike","binary-path":"/t/deps/nxspike-abc","kind":"lib"}}}"#;
let ids = binary_ids(listing);
assert_eq!(ids.get(Utf8Path::new("/t/deps/nxspike-abc")).map(String::as_str), Some("nxspike"));
}
#[test]
fn a_listing_that_says_nothing_yields_no_ids() {
assert!(binary_ids("not json").is_empty());
assert!(binary_ids("{}").is_empty());
assert!(binary_ids(r#"{"rust-binaries":{"x":{"binary-id":"x"}}}"#).is_empty());
}
#[test]
fn a_binary_id_becomes_an_exact_filterset_atom() {
assert_eq!(matcher("crate::bin/name"), "=crate::bin/name");
assert_eq!(matcher("plain"), "=plain");
}
#[test]
fn a_character_that_would_end_the_atom_is_escaped() {
assert_eq!(matcher("odd)name"), "=odd\\)name");
assert_eq!(matcher("odd,name"), "=odd\\,name");
assert_eq!(matcher("back\\slash"), "=back\\\\slash");
}
#[test]
fn a_long_test_selection_runs_the_whole_binary() {
let long = "x".repeat(MAX_FILTERSET_BYTES);
assert_eq!(filtersets("nxspike", &[&long]), ["binary_id(=nxspike)"]);
}
#[test]
fn selected_tests_form_one_parenthesized_union() {
assert_eq!(
filtersets("nxspike", &["tests::one", "tests::two"]),
["binary_id(=nxspike) and (test(=tests::one) or test(=tests::two))"]
);
}
#[test]
fn the_first_failing_test_is_named_from_nextest_output() {
let output = " FAIL [ 0.024s] (1/2) nxspike tests::fails_when_asked\n\
FAIL [ 0.030s] (2/2) nxspike tests::also_fails\n";
assert_eq!(first_failure(output), Some("tests::fails_when_asked"));
}
#[test]
fn output_with_no_failure_line_names_nothing() {
assert_eq!(first_failure(" PASS [ 0.012s] (1/1) nxspike tests::works"), None);
assert_eq!(first_failure(""), None);
}
#[test]
fn a_failure_is_found_past_lines_that_are_not_failures() {
let output = " Starting 2 tests across 1 binary\n\
PASS [ 0.010s] (1/2) nxspike tests::works\n\
FAIL [ 0.024s] (2/2) nxspike tests::fails_when_asked\n";
assert_eq!(first_failure(output), Some("tests::fails_when_asked"));
}
#[test]
fn a_known_binary_resolves_to_the_id_nextest_gave_it() {
let harness = Harness::fake(&[("/t/deps/nxspike-abc", "nxspike")]);
assert_eq!(harness.id(Utf8Path::new("/t/deps/nxspike-abc")).unwrap(), "nxspike");
}
#[test]
fn a_binary_nextest_never_listed_is_an_error_that_names_it() {
let harness = Harness::fake(&[("/t/deps/nxspike-abc", "nxspike")]);
let failure = harness.id(Utf8Path::new("/t/deps/stranger-def")).unwrap_err().to_string();
assert!(failure.contains("/t/deps/stranger-def"), "{failure}");
assert!(failure.contains("without `--nextest`"), "{failure}");
}
#[test]
#[cfg(unix)]
fn the_command_names_the_metadata_and_this_binary_alone() {
let (_scratch, mut work) = crate::testing::shell_workspace("nextest-command", "exit 0");
work.set_test_args(vec!["--nocapture".to_owned()]);
let harness = Harness::fake(&[("/t/deps/nxspike-abc", "nxspike")]);
let command = harness
.command(&work, &crate::testing::test_binary("/t/deps/nxspike-abc"), &[])
.expect("a known binary yields a command");
let args: Vec<_> = command.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect();
assert_eq!(command.get_program(), "cargo-nextest");
assert_eq!(
command.get_current_dir().map(std::path::Path::to_path_buf),
Some(work.root().as_std_path().to_path_buf())
);
assert_eq!(
args,
vec![
"nextest",
"run",
"--binaries-metadata",
"/t/binaries.json",
"--cargo-metadata",
"/t/metadata.json",
"-E",
"binary_id(=nxspike)",
"--failure-output",
"immediate-final",
"--no-tests",
"pass",
"--",
"--nocapture",
]
);
}
#[test]
#[cfg(unix)]
fn a_command_with_no_test_arguments_ends_at_the_filterset() {
let (_scratch, mut work) = crate::testing::shell_workspace("nextest-bare", "exit 0");
work.set_test_args(Vec::new());
let harness = Harness::fake(&[("/t/deps/nxspike-abc", "nxspike")]);
let command = harness
.command(&work, &crate::testing::test_binary("/t/deps/nxspike-abc"), &[])
.expect("a known binary yields a command");
let args: Vec<_> = command.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect();
assert_eq!(args.last().map(String::as_str), Some("pass"));
assert!(!args.iter().any(|arg| arg == "--"), "{args:?}");
}
#[test]
#[cfg(unix)]
fn a_command_for_a_binary_nextest_never_listed_is_refused() {
let (_scratch, work) = crate::testing::shell_workspace("nextest-stranger", "exit 0");
let harness = Harness::fake(&[("/t/deps/nxspike-abc", "nxspike")]);
let failure = harness
.command(&work, &crate::testing::test_binary("/t/deps/stranger-def"), &[])
.unwrap_err()
.to_string();
assert!(failure.contains("/t/deps/stranger-def"), "{failure}");
}
#[test]
#[cfg(unix)]
fn a_command_narrowed_to_one_test_filters_on_the_binary_and_the_test() {
let (_scratch, mut work) = crate::testing::shell_workspace("nextest-filtered", "exit 0");
work.set_test_args(Vec::new());
let harness = Harness::fake(&[("/t/deps/nxspike-abc", "nxspike")]);
let command = harness
.command(&work, &crate::testing::test_binary("/t/deps/nxspike-abc"), &["tests::parses"])
.expect("a known binary yields a command");
let args: Vec<_> = command.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect();
assert!(
args.contains(&"binary_id(=nxspike) and (test(=tests::parses))".to_owned()),
"{args:?}"
);
}
#[test]
#[cfg(unix)]
fn a_narrowed_command_does_not_suppress_the_empty_run_exit_code() {
let (_scratch, mut work) = crate::testing::shell_workspace("nextest-narrowed-no-tests", "exit 0");
work.set_test_args(Vec::new());
let harness = Harness::fake(&[("/t/deps/nxspike-abc", "nxspike")]);
let command = harness
.command(&work, &crate::testing::test_binary("/t/deps/nxspike-abc"), &["tests::parses"])
.expect("a known binary yields a command");
let args: Vec<_> = command.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect();
assert!(!args.iter().any(|arg| arg == "--no-tests"), "{args:?}");
let whole = harness
.command(&work, &crate::testing::test_binary("/t/deps/nxspike-abc"), &[])
.expect("a known binary yields a command");
let whole: Vec<_> = whole.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect();
assert!(whole.windows(2).any(|pair| pair == ["--no-tests", "pass"]), "{whole:?}");
}
#[test]
#[cfg(unix)]
fn a_command_narrowed_to_several_tests_asks_for_any_of_them_within_the_one_binary() {
let (_scratch, mut work) = crate::testing::shell_workspace("nextest-several", "exit 0");
work.set_test_args(Vec::new());
let harness = Harness::fake(&[("/t/deps/nxspike-abc", "nxspike")]);
let command = harness
.command(
&work,
&crate::testing::test_binary("/t/deps/nxspike-abc"),
&["tests::parses", "tests::rejects"],
)
.expect("a known binary yields a command");
let args: Vec<_> = command.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect();
assert!(
args.contains(&"binary_id(=nxspike) and (test(=tests::parses) or test(=tests::rejects))".to_owned()),
"{args:?}"
);
}
#[test]
#[cfg(unix)]
fn a_test_name_holding_filterset_syntax_is_escaped() {
assert_eq!(matcher("odd)name,here"), "=odd\\)name\\,here");
}
}