use crate::{
errors::{CreateTestListError, FromMessagesError, WriteTestListError},
helpers::{dylib_path, dylib_path_envvar, write_test_name},
list::{BinaryList, OutputFormat, RustBuildMeta, Styles, TestListState},
reuse_build::PathMapper,
target_runner::{PlatformRunner, TargetRunner},
test_filter::TestFilterBuilder,
};
use camino::{Utf8Path, Utf8PathBuf};
use futures::prelude::*;
use guppy::{
graph::{PackageGraph, PackageMetadata},
PackageId,
};
use nextest_metadata::{
BuildPlatform, RustNonTestBinaryKind, RustTestBinaryKind, RustTestBinarySummary,
RustTestCaseSummary, RustTestSuiteStatusSummary, RustTestSuiteSummary, TestListSummary,
};
use once_cell::sync::{Lazy, OnceCell};
use owo_colors::OwoColorize;
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
ffi::{OsStr, OsString},
io,
io::Write,
path::PathBuf,
sync::Arc,
};
use tokio::runtime::Runtime;
#[derive(Clone, Debug)]
pub struct RustTestArtifact<'g> {
pub binary_id: String,
pub package: PackageMetadata<'g>,
pub binary_path: Utf8PathBuf,
pub binary_name: String,
pub kind: RustTestBinaryKind,
pub non_test_binaries: BTreeSet<(String, Utf8PathBuf)>,
pub cwd: Utf8PathBuf,
pub build_platform: BuildPlatform,
}
impl<'g> RustTestArtifact<'g> {
pub fn from_binary_list(
graph: &'g PackageGraph,
binary_list: Arc<BinaryList>,
rust_build_meta: &RustBuildMeta<TestListState>,
path_mapper: &PathMapper,
platform_filter: Option<BuildPlatform>,
) -> Result<Vec<Self>, FromMessagesError> {
let mut binaries = vec![];
for binary in &binary_list.rust_binaries {
if platform_filter.is_some() && platform_filter != Some(binary.build_platform) {
continue;
}
let package_id = PackageId::new(binary.package_id.clone());
let package = graph
.metadata(&package_id)
.map_err(FromMessagesError::PackageGraph)?;
let cwd = package
.manifest_path()
.parent()
.unwrap_or_else(|| {
panic!(
"manifest path {} doesn't have a parent",
package.manifest_path()
)
})
.to_path_buf();
let binary_path = path_mapper.map_binary(binary.path.clone());
let cwd = path_mapper.map_cwd(cwd);
let non_test_binaries = if binary.kind == RustTestBinaryKind::TEST
|| binary.kind == RustTestBinaryKind::BENCH
{
match rust_build_meta.non_test_binaries.get(package_id.repr()) {
Some(binaries) => binaries
.iter()
.filter_map(|binary| {
(binary.kind == RustNonTestBinaryKind::BIN_EXE).then(|| {
let abs_path = rust_build_meta.target_directory.join(&binary.path);
(binary.name.clone(), abs_path)
})
})
.collect(),
None => BTreeSet::new(),
}
} else {
BTreeSet::new()
};
binaries.push(RustTestArtifact {
binary_id: binary.id.clone(),
package,
binary_path,
binary_name: binary.name.clone(),
kind: binary.kind.clone(),
cwd,
non_test_binaries,
build_platform: binary.build_platform,
})
}
Ok(binaries)
}
fn into_test_suite(self, status: RustTestSuiteStatus) -> (Utf8PathBuf, RustTestSuite<'g>) {
let Self {
binary_id,
package,
binary_path,
binary_name,
kind,
non_test_binaries,
cwd,
build_platform,
} = self;
(
binary_path,
RustTestSuite {
binary_id,
package,
binary_name,
kind,
non_test_binaries,
cwd,
build_platform,
status,
},
)
}
}
#[derive(Clone, Debug)]
pub struct TestList<'g> {
test_count: usize,
rust_build_meta: RustBuildMeta<TestListState>,
rust_suites: BTreeMap<Utf8PathBuf, RustTestSuite<'g>>,
updated_dylib_path: OsString,
skip_count: OnceCell<usize>,
}
impl<'g> TestList<'g> {
pub fn new<I>(
test_artifacts: I,
rust_build_meta: RustBuildMeta<TestListState>,
filter: &TestFilterBuilder,
runner: &TargetRunner,
list_threads: usize,
) -> Result<Self, CreateTestListError>
where
I: IntoIterator<Item = RustTestArtifact<'g>>,
I::IntoIter: Send,
{
let updated_dylib_path = Self::create_dylib_path(&rust_build_meta)?;
log::debug!(
"updated {}: {}",
dylib_path_envvar(),
updated_dylib_path.to_string_lossy(),
);
let runtime = Runtime::new().map_err(CreateTestListError::TokioRuntimeCreate)?;
let stream = futures::stream::iter(test_artifacts.into_iter()).map(|test_binary| {
async {
if filter.should_obtain_test_list_from_binary(&test_binary) {
let (non_ignored, ignored) =
test_binary.exec(&updated_dylib_path, runner).await?;
let (bin, info) = Self::process_output(
test_binary,
filter,
non_ignored.as_str(),
ignored.as_str(),
)?;
Ok::<_, CreateTestListError>((bin, info))
} else {
Ok(Self::process_skipped(test_binary))
}
}
});
let fut = stream.buffer_unordered(list_threads).try_collect();
let rust_suites: BTreeMap<_, _> = runtime.block_on(fut)?;
let test_count = rust_suites
.values()
.map(|suite| suite.status.test_count())
.sum();
Ok(Self {
rust_suites,
rust_build_meta,
updated_dylib_path,
test_count,
skip_count: OnceCell::new(),
})
}
#[cfg(test)]
fn new_with_outputs(
test_bin_outputs: impl IntoIterator<
Item = (RustTestArtifact<'g>, impl AsRef<str>, impl AsRef<str>),
>,
rust_build_meta: RustBuildMeta<TestListState>,
filter: &TestFilterBuilder,
) -> Result<Self, CreateTestListError> {
let mut test_count = 0;
let updated_dylib_path = Self::create_dylib_path(&rust_build_meta)?;
let test_artifacts = test_bin_outputs
.into_iter()
.map(|(test_binary, non_ignored, ignored)| {
if filter.should_obtain_test_list_from_binary(&test_binary) {
let (bin, info) = Self::process_output(
test_binary,
filter,
non_ignored.as_ref(),
ignored.as_ref(),
)?;
test_count += info.status.test_count();
Ok((bin, info))
} else {
Ok(Self::process_skipped(test_binary))
}
})
.collect::<Result<BTreeMap<_, _>, _>>()?;
Ok(Self {
rust_suites: test_artifacts,
rust_build_meta,
updated_dylib_path,
test_count,
skip_count: OnceCell::new(),
})
}
pub fn test_count(&self) -> usize {
self.test_count
}
pub fn rust_build_meta(&self) -> &RustBuildMeta<TestListState> {
&self.rust_build_meta
}
pub fn skip_count(&self) -> usize {
*self.skip_count.get_or_init(|| {
self.iter_tests()
.filter(|instance| !instance.test_info.filter_match.is_match())
.count()
})
}
pub fn run_count(&self) -> usize {
self.test_count - self.skip_count()
}
pub fn binary_count(&self) -> usize {
self.rust_suites.len()
}
pub fn get(&self, test_bin: impl AsRef<Utf8Path>) -> Option<&RustTestSuite> {
self.rust_suites.get(test_bin.as_ref())
}
pub fn updated_dylib_path(&self) -> &OsStr {
&self.updated_dylib_path
}
pub fn to_summary(&self) -> TestListSummary {
let rust_suites = self
.rust_suites
.iter()
.map(|(binary_path, info)| {
let (status, test_cases) = info.status.to_summary();
let testsuite = RustTestSuiteSummary {
package_name: info.package.name().to_owned(),
binary: RustTestBinarySummary {
binary_name: info.binary_name.clone(),
package_id: info.package.id().repr().to_owned(),
kind: info.kind.clone(),
binary_path: binary_path.clone(),
binary_id: info.binary_id.clone(),
build_platform: info.build_platform,
},
cwd: info.cwd.clone(),
status,
test_cases,
};
(info.binary_id.clone(), testsuite)
})
.collect();
let mut summary = TestListSummary::new(self.rust_build_meta.to_summary());
summary.test_count = self.test_count;
summary.rust_suites = rust_suites;
summary
}
pub fn write(
&self,
output_format: OutputFormat,
writer: impl Write,
colorize: bool,
) -> Result<(), WriteTestListError> {
match output_format {
OutputFormat::Human { verbose } => self
.write_human(writer, verbose, colorize)
.map_err(WriteTestListError::Io),
OutputFormat::Serializable(format) => format
.to_writer(&self.to_summary(), writer)
.map_err(WriteTestListError::Json),
}
}
pub fn iter(&self) -> impl Iterator<Item = (&Utf8Path, &RustTestSuite)> + '_ {
self.rust_suites
.iter()
.map(|(path, info)| (path.as_path(), info))
}
pub fn iter_tests(&self) -> impl Iterator<Item = TestInstance<'_>> + '_ {
self.rust_suites.iter().flat_map(|(test_bin, test_suite)| {
test_suite
.status
.test_cases()
.map(move |(name, test_info)| {
TestInstance::new(name, test_bin, test_suite, test_info)
})
})
}
pub fn to_string(&self, output_format: OutputFormat) -> Result<String, WriteTestListError> {
let mut buf = Vec::with_capacity(1024);
self.write(output_format, &mut buf, false)?;
Ok(String::from_utf8(buf).expect("buffer is valid UTF-8"))
}
#[cfg(test)]
pub(crate) fn empty() -> Self {
Self {
test_count: 0,
rust_build_meta: RustBuildMeta::empty(),
updated_dylib_path: OsString::new(),
rust_suites: BTreeMap::new(),
skip_count: OnceCell::new(),
}
}
pub(crate) fn create_dylib_path(
rust_build_meta: &RustBuildMeta<TestListState>,
) -> Result<OsString, CreateTestListError> {
let dylib_path = dylib_path();
let dylib_path_is_empty = dylib_path.is_empty();
let new_paths = rust_build_meta.dylib_paths();
let mut updated_dylib_path: Vec<PathBuf> =
Vec::with_capacity(dylib_path.len() + new_paths.len());
updated_dylib_path.extend(
new_paths
.iter()
.map(|path| path.clone().into_std_path_buf()),
);
updated_dylib_path.extend(dylib_path);
if cfg!(target_os = "macos") && dylib_path_is_empty {
if let Some(home) = home::home_dir() {
updated_dylib_path.push(home.join("lib"));
}
updated_dylib_path.push("/usr/local/lib".into());
updated_dylib_path.push("/usr/lib".into());
}
std::env::join_paths(updated_dylib_path)
.map_err(move |error| CreateTestListError::dylib_join_paths(new_paths, error))
}
fn process_output(
test_binary: RustTestArtifact<'g>,
filter: &TestFilterBuilder,
non_ignored: impl AsRef<str>,
ignored: impl AsRef<str>,
) -> Result<(Utf8PathBuf, RustTestSuite<'g>), CreateTestListError> {
let mut test_cases = BTreeMap::new();
let mut non_ignored_filter = filter.build();
for test_name in Self::parse(&test_binary.binary_id, non_ignored.as_ref())? {
test_cases.insert(
test_name.into(),
RustTestCaseSummary {
ignored: false,
filter_match: non_ignored_filter.filter_match(&test_binary, test_name, false),
},
);
}
let mut ignored_filter = filter.build();
for test_name in Self::parse(&test_binary.binary_id, ignored.as_ref())? {
test_cases.insert(
test_name.into(),
RustTestCaseSummary {
ignored: true,
filter_match: ignored_filter.filter_match(&test_binary, test_name, true),
},
);
}
Ok(test_binary.into_test_suite(RustTestSuiteStatus::Listed { test_cases }))
}
fn process_skipped(test_binary: RustTestArtifact<'g>) -> (Utf8PathBuf, RustTestSuite<'g>) {
test_binary.into_test_suite(RustTestSuiteStatus::Skipped)
}
fn parse<'a>(
binary_id: &'a str,
list_output: &'a str,
) -> Result<Vec<&'a str>, CreateTestListError> {
let mut list = Self::parse_impl(binary_id, list_output).collect::<Result<Vec<_>, _>>()?;
list.sort_unstable();
Ok(list)
}
fn parse_impl<'a>(
binary_id: &'a str,
list_output: &'a str,
) -> impl Iterator<Item = Result<&'a str, CreateTestListError>> + 'a {
list_output.lines().map(move |line| {
line.strip_suffix(": test")
.or_else(|| line.strip_suffix(": benchmark"))
.ok_or_else(|| {
CreateTestListError::parse_line(
binary_id,
format!(
"line '{}' did not end with the string ': test' or ': benchmark'",
line
),
list_output,
)
})
})
}
fn write_human(&self, mut writer: impl Write, verbose: bool, colorize: bool) -> io::Result<()> {
let mut styles = Styles::default();
if colorize {
styles.colorize();
}
for (test_bin, info) in &self.rust_suites {
if !verbose
&& info
.status
.test_cases()
.all(|(_, test_case)| !test_case.filter_match.is_match())
{
continue;
}
writeln!(writer, "{}:", info.binary_id.style(styles.binary_id))?;
if verbose {
writeln!(writer, " {} {}", "bin:".style(styles.field), test_bin)?;
writeln!(writer, " {} {}", "cwd:".style(styles.field), info.cwd)?;
writeln!(
writer,
" {} {}",
"build platform:".style(styles.field),
info.build_platform,
)?;
}
let mut indented = indent_write::io::IndentWriter::new(" ", &mut writer);
match &info.status {
RustTestSuiteStatus::Listed { test_cases } => {
if test_cases.is_empty() {
writeln!(indented, "(no tests)")?;
} else {
for (name, info) in test_cases {
match (verbose, info.filter_match.is_match()) {
(_, true) => {
write_test_name(name, &styles, &mut indented)?;
writeln!(indented)?;
}
(true, false) => {
write_test_name(name, &styles, &mut indented)?;
writeln!(indented, " (skipped)")?;
}
(false, false) => {
}
}
}
}
}
RustTestSuiteStatus::Skipped => {
writeln!(
indented,
"(test binary did not match filter expressions, skipped)"
)?;
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RustTestSuite<'g> {
pub binary_id: String,
pub package: PackageMetadata<'g>,
pub binary_name: String,
pub kind: RustTestBinaryKind,
pub cwd: Utf8PathBuf,
pub build_platform: BuildPlatform,
pub non_test_binaries: BTreeSet<(String, Utf8PathBuf)>,
pub status: RustTestSuiteStatus,
}
impl<'g> RustTestArtifact<'g> {
async fn exec(
&self,
dylib_path: &OsStr,
runner: &TargetRunner,
) -> Result<(String, String), CreateTestListError> {
if !self.cwd.is_dir() {
return Err(CreateTestListError::CwdIsNotDir {
binary_id: self.binary_id.clone(),
cwd: self.cwd.clone(),
});
}
let platform_runner = runner.for_build_platform(self.build_platform);
let non_ignored = self.exec_single(false, dylib_path, platform_runner);
let ignored = self.exec_single(true, dylib_path, platform_runner);
let (non_ignored_out, ignored_out) = futures::future::join(non_ignored, ignored).await;
Ok((non_ignored_out?, ignored_out?))
}
async fn exec_single(
&self,
ignored: bool,
dylib_path: &OsStr,
runner: Option<&PlatformRunner>,
) -> Result<String, CreateTestListError> {
let mut argv = Vec::new();
let program: String = if let Some(runner) = runner {
argv.extend(runner.args());
argv.push(self.binary_path.as_str());
runner.binary().into()
} else {
debug_assert!(
self.binary_path.is_absolute(),
"binary path {} is absolute",
self.binary_path
);
self.binary_path.clone().into()
};
argv.extend(["--list", "--format", "terse"]);
if ignored {
argv.push("--ignored");
}
let cmd = make_test_command(
program.clone(),
&argv,
&self.cwd,
&self.package,
dylib_path,
&self.non_test_binaries,
);
let mut cmd = tokio::process::Command::from(cmd);
match cmd.output().await {
Ok(output) => {
if output.status.success() {
String::from_utf8(output.stdout).map_err(|err| {
CreateTestListError::CommandNonUtf8 {
binary_id: self.binary_id.clone(),
command: std::iter::once(program)
.chain(argv.iter().map(|&s| s.to_owned()))
.collect(),
stdout: err.into_bytes(),
stderr: output.stderr,
}
})
} else {
Err(CreateTestListError::CommandFail {
binary_id: self.binary_id.clone(),
command: std::iter::once(program)
.chain(argv.iter().map(|&s| s.to_owned()))
.collect(),
exit_status: output.status,
stdout: output.stdout,
stderr: output.stderr,
})
}
}
Err(error) => Err(CreateTestListError::CommandExecFail {
binary_id: self.binary_id.clone(),
command: std::iter::once(program)
.chain(argv.iter().map(|&s| s.to_owned()))
.collect(),
error,
}),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RustTestSuiteStatus {
Listed {
test_cases: BTreeMap<String, RustTestCaseSummary>,
},
Skipped,
}
static EMPTY_TEST_CASE_MAP: Lazy<BTreeMap<String, RustTestCaseSummary>> = Lazy::new(BTreeMap::new);
impl RustTestSuiteStatus {
pub fn test_count(&self) -> usize {
match self {
RustTestSuiteStatus::Listed { test_cases } => test_cases.len(),
RustTestSuiteStatus::Skipped => 0,
}
}
pub fn test_cases(&self) -> impl Iterator<Item = (&str, &RustTestCaseSummary)> + '_ {
match self {
RustTestSuiteStatus::Listed { test_cases } => test_cases.iter(),
RustTestSuiteStatus::Skipped => {
EMPTY_TEST_CASE_MAP.iter()
}
}
.map(|(name, case)| (name.as_str(), case))
}
pub fn to_summary(
&self,
) -> (
RustTestSuiteStatusSummary,
BTreeMap<String, RustTestCaseSummary>,
) {
match self {
Self::Listed { test_cases } => (RustTestSuiteStatusSummary::LISTED, test_cases.clone()),
Self::Skipped => (RustTestSuiteStatusSummary::SKIPPED, BTreeMap::new()),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TestInstance<'a> {
pub name: &'a str,
pub binary: &'a Utf8Path,
pub bin_info: &'a RustTestSuite<'a>,
pub test_info: &'a RustTestCaseSummary,
}
impl<'a> TestInstance<'a> {
pub(crate) fn new(
name: &'a (impl AsRef<str> + ?Sized),
binary: &'a (impl AsRef<Utf8Path> + ?Sized),
bin_info: &'a RustTestSuite,
test_info: &'a RustTestCaseSummary,
) -> Self {
Self {
name: name.as_ref(),
binary: binary.as_ref(),
bin_info,
test_info,
}
}
#[inline]
pub(crate) fn sort_key(&self) -> (&'a str, &'a str) {
(&self.bin_info.binary_id, self.name)
}
pub(crate) fn make_expression(
&self,
test_list: &TestList<'_>,
target_runner: &TargetRunner,
) -> std::process::Command {
let platform_runner = target_runner.for_build_platform(self.bin_info.build_platform);
let mut args = Vec::new();
let program: String = match platform_runner {
Some(runner) => {
args.extend(runner.args());
args.push(self.binary.as_str());
runner.binary().into()
}
None => self.binary.to_owned().into(),
};
args.extend(["--exact", self.name, "--nocapture"]);
if self.test_info.ignored {
args.push("--ignored");
}
make_test_command(
program,
args,
&self.bin_info.cwd,
&self.bin_info.package,
test_list.updated_dylib_path(),
&self.bin_info.non_test_binaries,
)
}
}
pub(crate) fn make_test_command(
program: String,
args: impl IntoIterator<Item = impl AsRef<OsStr>>,
cwd: &Utf8PathBuf,
package: &PackageMetadata<'_>,
dylib_path: &OsStr,
non_test_binaries: &BTreeSet<(String, Utf8PathBuf)>,
) -> std::process::Command {
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(|| (k, v)),
Err(_) => None,
})
.collect()
});
let mut cmd = std::process::Command::new(program);
cmd.args(args)
.current_dir(cwd)
.env("NEXTEST", "1")
.env("NEXTEST_EXECUTION_MODE", "process-per-test")
.env(
"CARGO_MANIFEST_DIR",
cwd,
)
.env(
"__NEXTEST_ORIGINAL_CARGO_MANIFEST_DIR",
package.manifest_path().parent().unwrap(),
)
.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(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);
}
for (name, path) in non_test_binaries {
cmd.env(format!("NEXTEST_BIN_EXE_{}", name), &path);
}
cmd
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{list::SerializableFormat, test_filter::RunIgnored};
use guppy::CargoMetadata;
use indoc::indoc;
use maplit::btreemap;
use nextest_filtering::FilteringExpr;
use nextest_metadata::{FilterMatch, MismatchReason};
use once_cell::sync::Lazy;
use pretty_assertions::assert_eq;
use std::iter;
#[test]
fn test_parse_test_list() {
let non_ignored_output = indoc! {"
tests::foo::test_bar: test
tests::baz::test_quux: test
benches::bench_foo: benchmark
"};
let ignored_output = indoc! {"
tests::ignored::test_bar: test
tests::baz::test_ignored: test
benches::ignored_bench_foo: benchmark
"};
let test_filter = TestFilterBuilder::new(
RunIgnored::Default,
None,
iter::empty::<String>(),
vec![FilteringExpr::parse("platform(target)", &*PACKAGE_GRAPH_FIXTURE).unwrap()],
);
let fake_cwd: Utf8PathBuf = "/fake/cwd".into();
let fake_binary_name = "fake-binary".to_owned();
let fake_binary_id = "fake-package::fake-binary".to_owned();
let test_binary = RustTestArtifact {
binary_path: "/fake/binary".into(),
cwd: fake_cwd.clone(),
package: package_metadata(),
binary_name: fake_binary_name.clone(),
binary_id: fake_binary_id.clone(),
kind: RustTestBinaryKind::LIB,
non_test_binaries: BTreeSet::new(),
build_platform: BuildPlatform::Target,
};
let skipped_binary_name = "skipped-binary".to_owned();
let skipped_binary_id = "fake-package::skipped-binary".to_owned();
let skipped_binary = RustTestArtifact {
binary_path: "/fake/skipped-binary".into(),
cwd: fake_cwd.clone(),
package: package_metadata(),
binary_name: skipped_binary_name.clone(),
binary_id: skipped_binary_id.clone(),
kind: RustTestBinaryKind::PROC_MACRO,
non_test_binaries: BTreeSet::new(),
build_platform: BuildPlatform::Host,
};
let rust_build_meta = RustBuildMeta::new("/fake").map_paths(&PathMapper::noop());
let test_list = TestList::new_with_outputs(
[
(test_binary, &non_ignored_output, &ignored_output),
(
skipped_binary,
&"should-not-show-up-stdout",
&"should-not-show-up-stderr",
),
],
rust_build_meta,
&test_filter,
)
.expect("valid output");
assert_eq!(
test_list.rust_suites,
btreemap! {
"/fake/binary".into() => RustTestSuite {
status: RustTestSuiteStatus::Listed {
test_cases: btreemap! {
"tests::foo::test_bar".to_owned() => RustTestCaseSummary {
ignored: false,
filter_match: FilterMatch::Matches,
},
"tests::baz::test_quux".to_owned() => RustTestCaseSummary {
ignored: false,
filter_match: FilterMatch::Matches,
},
"benches::bench_foo".to_owned() => RustTestCaseSummary {
ignored: false,
filter_match: FilterMatch::Matches,
},
"tests::ignored::test_bar".to_owned() => RustTestCaseSummary {
ignored: true,
filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
},
"tests::baz::test_ignored".to_owned() => RustTestCaseSummary {
ignored: true,
filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
},
"benches::ignored_bench_foo".to_owned() => RustTestCaseSummary {
ignored: true,
filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
},
},
},
cwd: fake_cwd.clone(),
build_platform: BuildPlatform::Target,
package: package_metadata(),
binary_name: fake_binary_name,
binary_id: fake_binary_id,
kind: RustTestBinaryKind::LIB,
non_test_binaries: BTreeSet::new(),
},
"/fake/skipped-binary".into() => RustTestSuite {
status: RustTestSuiteStatus::Skipped,
cwd: fake_cwd,
build_platform: BuildPlatform::Host,
package: package_metadata(),
binary_name: skipped_binary_name,
binary_id: skipped_binary_id,
kind: RustTestBinaryKind::PROC_MACRO,
non_test_binaries: BTreeSet::new(),
},
}
);
static EXPECTED_HUMAN: &str = indoc! {"
fake-package::fake-binary:
benches::bench_foo
tests::baz::test_quux
tests::foo::test_bar
"};
static EXPECTED_HUMAN_VERBOSE: &str = indoc! {"
fake-package::fake-binary:
bin: /fake/binary
cwd: /fake/cwd
build platform: target
benches::bench_foo
benches::ignored_bench_foo (skipped)
tests::baz::test_ignored (skipped)
tests::baz::test_quux
tests::foo::test_bar
tests::ignored::test_bar (skipped)
fake-package::skipped-binary:
bin: /fake/skipped-binary
cwd: /fake/cwd
build platform: host
(test binary did not match filter expressions, skipped)
"};
static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
{
"rust-build-meta": {
"target-directory": "/fake",
"base-output-directories": [],
"non-test-binaries": {},
"linked-paths": []
},
"test-count": 6,
"rust-suites": {
"fake-package::fake-binary": {
"package-name": "metadata-helper",
"binary-id": "fake-package::fake-binary",
"binary-name": "fake-binary",
"package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
"kind": "lib",
"binary-path": "/fake/binary",
"build-platform": "target",
"cwd": "/fake/cwd",
"status": "listed",
"testcases": {
"benches::bench_foo": {
"ignored": false,
"filter-match": {
"status": "matches"
}
},
"benches::ignored_bench_foo": {
"ignored": true,
"filter-match": {
"status": "mismatch",
"reason": "ignored"
}
},
"tests::baz::test_ignored": {
"ignored": true,
"filter-match": {
"status": "mismatch",
"reason": "ignored"
}
},
"tests::baz::test_quux": {
"ignored": false,
"filter-match": {
"status": "matches"
}
},
"tests::foo::test_bar": {
"ignored": false,
"filter-match": {
"status": "matches"
}
},
"tests::ignored::test_bar": {
"ignored": true,
"filter-match": {
"status": "mismatch",
"reason": "ignored"
}
}
}
},
"fake-package::skipped-binary": {
"package-name": "metadata-helper",
"binary-id": "fake-package::skipped-binary",
"binary-name": "skipped-binary",
"package-id": "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)",
"kind": "proc-macro",
"binary-path": "/fake/skipped-binary",
"build-platform": "host",
"cwd": "/fake/cwd",
"status": "skipped",
"testcases": {}
}
}
}"#};
assert_eq!(
test_list
.to_string(OutputFormat::Human { verbose: false })
.expect("human succeeded"),
EXPECTED_HUMAN
);
assert_eq!(
test_list
.to_string(OutputFormat::Human { verbose: true })
.expect("human succeeded"),
EXPECTED_HUMAN_VERBOSE
);
println!(
"{}",
test_list
.to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
.expect("json-pretty succeeded")
);
assert_eq!(
test_list
.to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
.expect("json-pretty succeeded"),
EXPECTED_JSON_PRETTY
);
}
static PACKAGE_GRAPH_FIXTURE: Lazy<PackageGraph> = Lazy::new(|| {
static FIXTURE_JSON: &str = include_str!("../../../fixtures/cargo-metadata.json");
let metadata = CargoMetadata::parse_json(FIXTURE_JSON).expect("fixture is valid JSON");
metadata
.build_graph()
.expect("fixture is valid PackageGraph")
});
static PACKAGE_METADATA_ID: &str = "metadata-helper 0.1.0 (path+file:///Users/fakeuser/local/testcrates/metadata/metadata-helper)";
fn package_metadata() -> PackageMetadata<'static> {
PACKAGE_GRAPH_FIXTURE
.metadata(&PackageId::new(PACKAGE_METADATA_ID))
.expect("package ID is valid")
}
}