use super::{DisplayFilterMatcher, TestListDisplayFilter};
use crate::{
cargo_config::EnvironmentMap,
config::{
core::EvaluatableProfile,
overrides::{ListSettings, TestSettings, group_membership::PrecomputedGroupMembership},
scripts::{ScriptCommandEnvMap, WrapperScriptConfig, WrapperScriptTargetRunner},
},
double_spawn::DoubleSpawnInfo,
errors::{
CreateTestListError, FromMessagesError, TestListFromSummaryError, WriteTestListError,
},
helpers::{convert_build_platform, dylib_path, dylib_path_envvar, write_test_name},
indenter::indented,
list::{BinaryList, OutputFormat, RustBuildMeta, Styles, TestListState},
partition::{Partitioner, PartitionerBuilder, PartitionerScope},
reuse_build::PathMapper,
run_mode::NextestRunMode,
runner::Interceptor,
target_runner::{PlatformRunner, TargetRunner},
test_command::{LocalExecuteContext, TestCommand, TestCommandPhase},
test_filter::{BinaryMismatchReason, FilterBinaryMatch, FilterBound, TestFilter},
write_str::WriteStr,
};
use camino::{Utf8Path, Utf8PathBuf};
use debug_ignore::DebugIgnore;
use futures::prelude::*;
use guppy::{
PackageId,
graph::{PackageGraph, PackageMetadata},
};
use iddqd::{IdOrdItem, IdOrdMap, id_upcast};
use nextest_filtering::{BinaryQuery, EvalContext, GroupLookup, TestQuery};
use nextest_metadata::{
BuildPlatform, FilterMatch, MismatchReason, RustBinaryId, RustNonTestBinaryKind,
RustTestBinaryKind, RustTestBinarySummary, RustTestCaseSummary, RustTestKind,
RustTestSuiteStatusSummary, RustTestSuiteSummary, TestCaseName, TestListSummary,
};
use owo_colors::OwoColorize;
use quick_junit::ReportUuid;
use serde::{Deserialize, Serialize};
use std::{
borrow::{Borrow, Cow},
collections::{BTreeMap, BTreeSet},
ffi::{OsStr, OsString},
fmt,
hash::{Hash, Hasher},
io,
path::PathBuf,
sync::{Arc, OnceLock},
};
use swrite::{SWrite, swrite};
use tokio::runtime::Runtime;
use tracing::debug;
#[derive(Clone, Debug)]
pub struct RustTestArtifact<'g> {
pub binary_id: RustBinaryId,
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_build_path(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(|binary| {
binary.kind == RustNonTestBinaryKind::BIN_EXE
})
.map(|binary| {
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)
}
pub fn to_binary_query(&self) -> BinaryQuery<'_> {
BinaryQuery {
package_id: self.package.id(),
binary_id: &self.binary_id,
kind: &self.kind,
binary_name: &self.binary_name,
platform: convert_build_platform(self.build_platform),
}
}
fn into_test_suite(self, status: RustTestSuiteStatus) -> RustTestSuite<'g> {
let Self {
binary_id,
package,
binary_path,
binary_name,
kind,
non_test_binaries,
cwd,
build_platform,
} = self;
RustTestSuite {
binary_id,
binary_path,
package,
binary_name,
kind,
non_test_binaries,
cwd,
build_platform,
status,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SkipCounts {
pub skipped_tests: usize,
pub skipped_tests_rerun: usize,
pub skipped_tests_non_benchmark: usize,
pub skipped_tests_default_filter: usize,
pub skipped_binaries: usize,
pub skipped_binaries_default_filter: usize,
}
#[derive(Clone, Debug)]
pub struct TestList<'g> {
test_count: usize,
mode: NextestRunMode,
rust_build_meta: RustBuildMeta<TestListState>,
rust_suites: IdOrdMap<RustTestSuite<'g>>,
workspace_root: Utf8PathBuf,
env: EnvironmentMap,
updated_dylib_path: OsString,
skip_counts: OnceLock<SkipCounts>,
}
impl<'g> TestList<'g> {
#[expect(clippy::too_many_arguments)]
pub fn new<I>(
ctx: &TestExecuteContext<'_>,
test_artifacts: I,
rust_build_meta: RustBuildMeta<TestListState>,
filter: &TestFilter,
partitioner_builder: Option<&PartitionerBuilder>,
workspace_root: Utf8PathBuf,
env: EnvironmentMap,
profile: &impl ListProfile,
bound: FilterBound,
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)?;
debug!(
"updated {}: {}",
dylib_path_envvar(),
updated_dylib_path.to_string_lossy(),
);
let lctx = LocalExecuteContext {
phase: TestCommandPhase::List,
workspace_root: &workspace_root,
rust_build_meta: &rust_build_meta,
double_spawn: ctx.double_spawn,
dylib_path: &updated_dylib_path,
profile_name: ctx.profile_name,
env: &env,
};
let ecx = profile.filterset_ecx();
let runtime = Runtime::new().map_err(CreateTestListError::TokioRuntimeCreate)?;
let stream = futures::stream::iter(test_artifacts).map(|test_binary| {
async {
let binary_query = test_binary.to_binary_query();
let binary_match = filter.filter_binary_match(&binary_query, &ecx, bound);
match binary_match {
FilterBinaryMatch::Definite | FilterBinaryMatch::Possible => {
debug!(
"executing test binary to obtain test list \
(match result is {binary_match:?}): {}",
test_binary.binary_id,
);
let list_settings = profile.list_settings_for(&binary_query);
let (non_ignored, ignored) = test_binary
.exec(&lctx, &list_settings, ctx.target_runner)
.await?;
let parsed = Self::parse_output(
test_binary,
non_ignored.as_str(),
ignored.as_str(),
)?;
Ok::<_, CreateTestListError>(parsed)
}
FilterBinaryMatch::Mismatch { reason } => {
debug!("skipping test binary: {reason}: {}", test_binary.binary_id,);
Ok(Self::make_skipped(test_binary, reason))
}
}
}
});
let fut = stream
.buffer_unordered(list_threads)
.try_collect::<Vec<_>>();
let parsed_binaries: Vec<ParsedTestBinary<'g>> = runtime.block_on(fut)?;
runtime.shutdown_background();
let group_membership = if filter.has_group_predicates() {
let test_queries = Self::collect_test_queries_from_parsed(&parsed_binaries);
Some(profile.precompute_group_memberships(test_queries.into_iter()))
} else {
None
};
let groups = group_membership.as_ref().map(|g| g as &dyn GroupLookup);
let mut rust_suites = Self::build_suites(parsed_binaries, filter, &ecx, bound, groups);
Self::apply_partitioning(&mut rust_suites, partitioner_builder);
let test_count = rust_suites
.iter()
.map(|suite| suite.status.test_count())
.sum();
Ok(Self {
rust_suites,
mode: filter.mode(),
workspace_root,
env,
rust_build_meta,
updated_dylib_path,
test_count,
skip_counts: OnceLock::new(),
})
}
#[cfg(test)]
#[expect(clippy::too_many_arguments)]
pub(crate) fn new_with_outputs(
test_bin_outputs: impl IntoIterator<
Item = (RustTestArtifact<'g>, impl AsRef<str>, impl AsRef<str>),
>,
workspace_root: Utf8PathBuf,
rust_build_meta: RustBuildMeta<TestListState>,
filter: &TestFilter,
partitioner_builder: Option<&PartitionerBuilder>,
env: EnvironmentMap,
ecx: &EvalContext<'_>,
bound: FilterBound,
) -> Result<Self, CreateTestListError> {
let updated_dylib_path = Self::create_dylib_path(&rust_build_meta)?;
let parsed_binaries = test_bin_outputs
.into_iter()
.map(|(test_binary, non_ignored, ignored)| {
let binary_query = test_binary.to_binary_query();
let binary_match = filter.filter_binary_match(&binary_query, ecx, bound);
match binary_match {
FilterBinaryMatch::Definite | FilterBinaryMatch::Possible => {
debug!(
"processing output for binary \
(match result is {binary_match:?}): {}",
test_binary.binary_id,
);
Self::parse_output(test_binary, non_ignored.as_ref(), ignored.as_ref())
}
FilterBinaryMatch::Mismatch { reason } => {
debug!("skipping test binary: {reason}: {}", test_binary.binary_id,);
Ok(Self::make_skipped(test_binary, reason))
}
}
})
.collect::<Result<Vec<_>, _>>()?;
let mut rust_suites = Self::build_suites(parsed_binaries, filter, ecx, bound, None);
Self::apply_partitioning(&mut rust_suites, partitioner_builder);
let test_count = rust_suites
.iter()
.map(|suite| suite.status.test_count())
.sum();
Ok(Self {
rust_suites,
mode: filter.mode(),
workspace_root,
env,
rust_build_meta,
updated_dylib_path,
test_count,
skip_counts: OnceLock::new(),
})
}
pub fn from_summary(
graph: &'g PackageGraph,
summary: &TestListSummary,
mode: NextestRunMode,
) -> Result<Self, TestListFromSummaryError> {
let rust_build_meta = RustBuildMeta::from_summary(summary.rust_build_meta.clone())
.map_err(TestListFromSummaryError::RustBuildMeta)?;
let workspace_root = graph.workspace().root().to_path_buf();
let env = EnvironmentMap::empty();
let updated_dylib_path = OsString::new();
let mut rust_suites = IdOrdMap::new();
let mut test_count = 0;
for (binary_id, suite_summary) in &summary.rust_suites {
let package_id = PackageId::new(suite_summary.binary.package_id.clone());
let package = graph.metadata(&package_id).map_err(|_| {
TestListFromSummaryError::PackageNotFound {
name: suite_summary.package_name.clone(),
package_id: suite_summary.binary.package_id.clone(),
}
})?;
let status = if suite_summary.status == RustTestSuiteStatusSummary::SKIPPED {
RustTestSuiteStatus::Skipped {
reason: BinaryMismatchReason::Expression,
}
} else if suite_summary.status == RustTestSuiteStatusSummary::SKIPPED_DEFAULT_FILTER {
RustTestSuiteStatus::Skipped {
reason: BinaryMismatchReason::DefaultSet,
}
} else {
let test_cases: IdOrdMap<RustTestCase> = suite_summary
.test_cases
.iter()
.map(|(name, info)| RustTestCase {
name: name.clone(),
test_info: info.clone(),
})
.collect();
test_count += test_cases.len();
RustTestSuiteStatus::Listed {
test_cases: DebugIgnore(test_cases),
}
};
let suite = RustTestSuite {
binary_id: binary_id.clone(),
binary_path: suite_summary.binary.binary_path.clone(),
package,
binary_name: suite_summary.binary.binary_name.clone(),
kind: suite_summary.binary.kind.clone(),
non_test_binaries: BTreeSet::new(), cwd: suite_summary.cwd.clone(),
build_platform: suite_summary.binary.build_platform,
status,
};
let _ = rust_suites.insert_unique(suite);
}
Ok(Self {
rust_suites,
mode,
workspace_root,
env,
rust_build_meta,
updated_dylib_path,
test_count,
skip_counts: OnceLock::new(),
})
}
pub fn test_count(&self) -> usize {
self.test_count
}
pub fn mode(&self) -> NextestRunMode {
self.mode
}
pub fn rust_build_meta(&self) -> &RustBuildMeta<TestListState> {
&self.rust_build_meta
}
pub fn skip_counts(&self) -> &SkipCounts {
self.skip_counts.get_or_init(|| {
let mut skipped_tests_rerun = 0;
let mut skipped_tests_non_benchmark = 0;
let mut skipped_tests_default_filter = 0;
let skipped_tests = self
.iter_tests()
.filter(|instance| match instance.test_info.filter_match {
FilterMatch::Mismatch {
reason: MismatchReason::RerunAlreadyPassed,
} => {
skipped_tests_rerun += 1;
true
}
FilterMatch::Mismatch {
reason: MismatchReason::NotBenchmark,
} => {
skipped_tests_non_benchmark += 1;
true
}
FilterMatch::Mismatch {
reason: MismatchReason::DefaultFilter,
} => {
skipped_tests_default_filter += 1;
true
}
FilterMatch::Mismatch { .. } => true,
FilterMatch::Matches => false,
})
.count();
let mut skipped_binaries_default_filter = 0;
let skipped_binaries = self
.rust_suites
.iter()
.filter(|suite| match suite.status {
RustTestSuiteStatus::Skipped {
reason: BinaryMismatchReason::DefaultSet,
} => {
skipped_binaries_default_filter += 1;
true
}
RustTestSuiteStatus::Skipped { .. } => true,
RustTestSuiteStatus::Listed { .. } => false,
})
.count();
SkipCounts {
skipped_tests,
skipped_tests_rerun,
skipped_tests_non_benchmark,
skipped_tests_default_filter,
skipped_binaries,
skipped_binaries_default_filter,
}
})
}
pub fn run_count(&self) -> usize {
self.test_count - self.skip_counts().skipped_tests
}
pub fn binary_count(&self) -> usize {
self.rust_suites.len()
}
pub fn listed_binary_count(&self) -> usize {
self.binary_count() - self.skip_counts().skipped_binaries
}
pub fn workspace_root(&self) -> &Utf8Path {
&self.workspace_root
}
pub fn cargo_env(&self) -> &EnvironmentMap {
&self.env
}
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(|test_suite| {
let (status, test_cases) = test_suite.status.to_summary();
let testsuite = RustTestSuiteSummary {
package_name: test_suite.package.name().to_owned(),
binary: RustTestBinarySummary {
binary_name: test_suite.binary_name.clone(),
package_id: test_suite.package.id().repr().to_owned(),
kind: test_suite.kind.clone(),
binary_path: test_suite.binary_path.clone(),
binary_id: test_suite.binary_id.clone(),
build_platform: test_suite.build_platform,
},
cwd: test_suite.cwd.clone(),
status,
test_cases,
};
(test_suite.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: &mut dyn WriteStr,
colorize: bool,
) -> Result<(), WriteTestListError> {
match output_format {
OutputFormat::Human { verbose } => self
.write_human(writer, verbose, colorize)
.map_err(WriteTestListError::Io),
OutputFormat::Oneline { verbose } => self
.write_oneline(writer, verbose, colorize)
.map_err(WriteTestListError::Io),
OutputFormat::Serializable(format) => format.to_writer(&self.to_summary(), writer),
}
}
pub fn iter(&self) -> impl Iterator<Item = &RustTestSuite<'_>> + '_ {
self.rust_suites.iter()
}
pub fn get_suite(&self, binary_id: &RustBinaryId) -> Option<&RustTestSuite<'_>> {
self.rust_suites.get(binary_id)
}
pub fn iter_tests(&self) -> impl Iterator<Item = TestInstance<'_>> + '_ {
self.rust_suites.iter().flat_map(|test_suite| {
test_suite
.status
.test_cases()
.map(move |case| TestInstance::new(case, test_suite))
})
}
pub fn to_priority_queue(
&'g self,
profile: &'g EvaluatableProfile<'g>,
) -> TestPriorityQueue<'g> {
TestPriorityQueue::new(self, profile)
}
pub fn to_string(&self, output_format: OutputFormat) -> Result<String, WriteTestListError> {
let mut s = String::with_capacity(1024);
self.write(output_format, &mut s, false)?;
Ok(s)
}
pub fn empty() -> Self {
Self {
test_count: 0,
mode: NextestRunMode::Test,
workspace_root: Utf8PathBuf::new(),
rust_build_meta: RustBuildMeta::empty(),
env: EnvironmentMap::empty(),
updated_dylib_path: OsString::new(),
rust_suites: IdOrdMap::new(),
skip_counts: OnceLock::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 parse_output(
test_binary: RustTestArtifact<'g>,
non_ignored: impl AsRef<str>,
ignored: impl AsRef<str>,
) -> Result<ParsedTestBinary<'g>, CreateTestListError> {
let mut test_cases = Vec::new();
for (test_name, kind) in Self::parse(&test_binary.binary_id, non_ignored.as_ref())? {
test_cases.push(ParsedTestCase {
name: TestCaseName::new(test_name),
kind,
ignored: false,
});
}
for (test_name, kind) in Self::parse(&test_binary.binary_id, ignored.as_ref())? {
test_cases.push(ParsedTestCase {
name: TestCaseName::new(test_name),
kind,
ignored: true,
});
}
Ok(ParsedTestBinary::Listed {
artifact: test_binary,
test_cases,
})
}
fn build_suites(
parsed: impl IntoIterator<Item = ParsedTestBinary<'g>>,
filter: &TestFilter,
ecx: &EvalContext<'_>,
bound: FilterBound,
groups: Option<&dyn GroupLookup>,
) -> IdOrdMap<RustTestSuite<'g>> {
parsed
.into_iter()
.map(|binary| match binary {
ParsedTestBinary::Listed {
artifact,
test_cases,
} => {
let filtered = {
let query = artifact.to_binary_query();
let mut map = IdOrdMap::new();
for tc in test_cases {
let filter_match = filter.filter_match(
query, &tc.name, &tc.kind, ecx, bound, tc.ignored, groups,
);
map.insert_overwrite(RustTestCase {
name: tc.name,
test_info: RustTestCaseSummary {
kind: Some(tc.kind),
ignored: tc.ignored,
filter_match,
},
});
}
map
};
artifact.into_test_suite(RustTestSuiteStatus::Listed {
test_cases: filtered.into(),
})
}
ParsedTestBinary::Skipped { artifact, reason } => {
artifact.into_test_suite(RustTestSuiteStatus::Skipped { reason })
}
})
.collect()
}
fn make_skipped(
test_binary: RustTestArtifact<'g>,
reason: BinaryMismatchReason,
) -> ParsedTestBinary<'g> {
ParsedTestBinary::Skipped {
artifact: test_binary,
reason,
}
}
fn collect_test_queries_from_parsed<'a>(
parsed_binaries: &'a [ParsedTestBinary<'g>],
) -> Vec<TestQuery<'a>> {
parsed_binaries
.iter()
.filter_map(|binary| match binary {
ParsedTestBinary::Listed {
artifact,
test_cases,
} => Some((artifact, test_cases)),
ParsedTestBinary::Skipped { .. } => None,
})
.flat_map(|(artifact, test_cases)| {
let binary_query = artifact.to_binary_query();
test_cases.iter().map(move |tc| TestQuery {
binary_query,
test_name: &tc.name,
})
})
.collect()
}
fn apply_partitioning(
rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
partitioner_builder: Option<&PartitionerBuilder>,
) {
let Some(partitioner_builder) = partitioner_builder else {
return;
};
match partitioner_builder.scope() {
PartitionerScope::PerBinary => {
Self::apply_per_binary_partitioning(rust_suites, partitioner_builder);
}
PartitionerScope::CrossBinary => {
Self::apply_cross_binary_partitioning(rust_suites, partitioner_builder);
}
}
}
fn apply_per_binary_partitioning(
rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
partitioner_builder: &PartitionerBuilder,
) {
for mut suite in rust_suites.iter_mut() {
let RustTestSuiteStatus::Listed { test_cases } = &mut suite.status else {
continue;
};
let mut non_ignored_partitioner = partitioner_builder.build();
apply_partitioner_to_tests(test_cases, &mut *non_ignored_partitioner, false);
let mut ignored_partitioner = partitioner_builder.build();
apply_partitioner_to_tests(test_cases, &mut *ignored_partitioner, true);
}
}
fn apply_cross_binary_partitioning(
rust_suites: &mut IdOrdMap<RustTestSuite<'_>>,
partitioner_builder: &PartitionerBuilder,
) {
let mut non_ignored_partitioner = partitioner_builder.build();
for mut suite in rust_suites.iter_mut() {
let RustTestSuiteStatus::Listed { test_cases } = &mut suite.status else {
continue;
};
apply_partitioner_to_tests(test_cases, &mut *non_ignored_partitioner, false);
}
let mut ignored_partitioner = partitioner_builder.build();
for mut suite in rust_suites.iter_mut() {
let RustTestSuiteStatus::Listed { test_cases } = &mut suite.status else {
continue;
};
apply_partitioner_to_tests(test_cases, &mut *ignored_partitioner, true);
}
}
fn parse<'a>(
binary_id: &'a RustBinaryId,
list_output: &'a str,
) -> Result<Vec<(&'a str, RustTestKind)>, CreateTestListError> {
let mut list = parse_list_lines(binary_id, list_output).collect::<Result<Vec<_>, _>>()?;
list.sort_unstable();
Ok(list)
}
pub fn write_human(
&self,
writer: &mut dyn WriteStr,
verbose: bool,
colorize: bool,
) -> io::Result<()> {
self.write_human_impl(None, writer, verbose, colorize)
}
pub(crate) fn write_human_with_filter(
&self,
filter: &TestListDisplayFilter<'_>,
writer: &mut dyn WriteStr,
verbose: bool,
colorize: bool,
) -> io::Result<()> {
self.write_human_impl(Some(filter), writer, verbose, colorize)
}
fn write_human_impl(
&self,
filter: Option<&TestListDisplayFilter<'_>>,
mut writer: &mut dyn WriteStr,
verbose: bool,
colorize: bool,
) -> io::Result<()> {
let mut styles = Styles::default();
if colorize {
styles.colorize();
}
for info in &self.rust_suites {
let matcher = match filter {
Some(filter) => match filter.matcher_for(&info.binary_id) {
Some(matcher) => matcher,
None => continue,
},
None => DisplayFilterMatcher::All,
};
if !verbose
&& info
.status
.test_cases()
.all(|case| !case.test_info.filter_match.is_match())
{
continue;
}
writeln!(writer, "{}:", info.binary_id.style(styles.binary_id))?;
if verbose {
writeln!(
writer,
" {} {}",
"bin:".style(styles.field),
info.binary_path
)?;
writeln!(writer, " {} {}", "cwd:".style(styles.field), info.cwd)?;
writeln!(
writer,
" {} {}",
"build platform:".style(styles.field),
info.build_platform,
)?;
}
let mut indented = indented(writer).with_str(" ");
match &info.status {
RustTestSuiteStatus::Listed { test_cases } => {
let matching_tests: Vec<_> = test_cases
.iter()
.filter(|case| matcher.is_match(&case.name))
.collect();
if matching_tests.is_empty() {
writeln!(indented, "(no tests)")?;
} else {
for case in matching_tests {
match (verbose, case.test_info.filter_match.is_match()) {
(_, true) => {
write_test_name(&case.name, &styles, &mut indented)?;
writeln!(indented)?;
}
(true, false) => {
write_test_name(&case.name, &styles, &mut indented)?;
writeln!(indented, " (skipped)")?;
}
(false, false) => {
}
}
}
}
}
RustTestSuiteStatus::Skipped { reason } => {
writeln!(indented, "(test binary {reason}, skipped)")?;
}
}
writer = indented.into_inner();
}
Ok(())
}
pub fn write_oneline(
&self,
writer: &mut dyn WriteStr,
verbose: bool,
colorize: bool,
) -> io::Result<()> {
let mut styles = Styles::default();
if colorize {
styles.colorize();
}
for info in &self.rust_suites {
match &info.status {
RustTestSuiteStatus::Listed { test_cases } => {
for case in test_cases.iter() {
let is_match = case.test_info.filter_match.is_match();
if !verbose && !is_match {
continue;
}
write!(writer, "{} ", info.binary_id.style(styles.binary_id))?;
write_test_name(&case.name, &styles, writer)?;
if verbose {
write!(
writer,
" [{}{}] [{}{}] [{}{}]{}",
"bin: ".style(styles.field),
info.binary_path,
"cwd: ".style(styles.field),
info.cwd,
"build platform: ".style(styles.field),
info.build_platform,
if is_match { "" } else { " (skipped)" },
)?;
}
writeln!(writer)?;
}
}
RustTestSuiteStatus::Skipped { .. } => {
}
}
}
Ok(())
}
}
fn apply_partitioner_to_tests(
test_cases: &mut IdOrdMap<RustTestCase>,
partitioner: &mut dyn Partitioner,
ignored: bool,
) {
for mut test_case in test_cases.iter_mut() {
if test_case.test_info.ignored == ignored {
apply_partition_to_test(&mut test_case, partitioner);
}
}
}
fn apply_partition_to_test(test_case: &mut RustTestCase, partitioner: &mut dyn Partitioner) {
match test_case.test_info.filter_match {
FilterMatch::Matches => {
if !partitioner.test_matches(test_case.name.as_str()) {
test_case.test_info.filter_match = FilterMatch::Mismatch {
reason: MismatchReason::Partition,
};
}
}
FilterMatch::Mismatch {
reason: MismatchReason::RerunAlreadyPassed,
} => {
let _ = partitioner.test_matches(test_case.name.as_str());
}
FilterMatch::Mismatch { .. } => {
}
}
}
fn parse_list_lines<'a>(
binary_id: &'a RustBinaryId,
list_output: &'a str,
) -> impl Iterator<Item = Result<(&'a str, RustTestKind), CreateTestListError>> + 'a + use<'a> {
list_output
.lines()
.map(move |line| match line.strip_suffix(": test") {
Some(test_name) => Ok((test_name, RustTestKind::TEST)),
None => match line.strip_suffix(": benchmark") {
Some(test_name) => Ok((test_name, RustTestKind::BENCH)),
None => Err(CreateTestListError::parse_line(
binary_id.clone(),
format!(
"line {line:?} did not end with the string \": test\" or \": benchmark\""
),
list_output,
)),
},
})
}
pub trait ListProfile {
fn filterset_ecx(&self) -> EvalContext<'_>;
fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_>;
fn precompute_group_memberships<'a>(
&self,
_tests: impl Iterator<Item = TestQuery<'a>>,
) -> PrecomputedGroupMembership;
}
impl<'g> ListProfile for EvaluatableProfile<'g> {
fn filterset_ecx(&self) -> EvalContext<'_> {
self.filterset_ecx()
}
fn list_settings_for(&self, query: &BinaryQuery<'_>) -> ListSettings<'_> {
self.list_settings_for(query)
}
fn precompute_group_memberships<'a>(
&self,
tests: impl Iterator<Item = TestQuery<'a>>,
) -> PrecomputedGroupMembership {
EvaluatableProfile::precompute_group_memberships(self, tests)
}
}
pub struct TestPriorityQueue<'a> {
tests: Vec<TestInstanceWithSettings<'a>>,
}
impl<'a> TestPriorityQueue<'a> {
fn new(test_list: &'a TestList<'a>, profile: &'a EvaluatableProfile<'a>) -> Self {
let mode = test_list.mode();
let mut tests = test_list
.iter_tests()
.map(|instance| {
let settings = profile.settings_for(mode, &instance.to_test_query());
TestInstanceWithSettings { instance, settings }
})
.collect::<Vec<_>>();
tests.sort_by_key(|test| test.settings.priority());
Self { tests }
}
}
impl<'a> IntoIterator for TestPriorityQueue<'a> {
type Item = TestInstanceWithSettings<'a>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.tests.into_iter()
}
}
#[derive(Debug)]
pub struct TestInstanceWithSettings<'a> {
pub instance: TestInstance<'a>,
pub settings: TestSettings<'a>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RustTestSuite<'g> {
pub binary_id: RustBinaryId,
pub binary_path: Utf8PathBuf,
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> RustTestSuite<'g> {
pub fn to_binary_query(&self) -> BinaryQuery<'_> {
BinaryQuery {
package_id: self.package.id(),
binary_id: &self.binary_id,
kind: &self.kind,
binary_name: &self.binary_name,
platform: convert_build_platform(self.build_platform),
}
}
}
impl IdOrdItem for RustTestSuite<'_> {
type Key<'a>
= &'a RustBinaryId
where
Self: 'a;
fn key(&self) -> Self::Key<'_> {
&self.binary_id
}
id_upcast!();
}
impl RustTestArtifact<'_> {
async fn exec(
&self,
lctx: &LocalExecuteContext<'_>,
list_settings: &ListSettings<'_>,
target_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 = target_runner.for_build_platform(self.build_platform);
let non_ignored = self.exec_single(false, lctx, list_settings, platform_runner);
let ignored = self.exec_single(true, lctx, list_settings, 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,
lctx: &LocalExecuteContext<'_>,
list_settings: &ListSettings<'_>,
runner: Option<&PlatformRunner>,
) -> Result<String, CreateTestListError> {
let mut cli = TestCommandCli::default();
cli.apply_wrappers(
list_settings.list_wrapper(),
runner,
lctx.workspace_root,
&lctx.rust_build_meta.target_directory,
);
cli.push(self.binary_path.as_str());
cli.extend(["--list", "--format", "terse"]);
if ignored {
cli.push("--ignored");
}
let cmd = TestCommand::new(
lctx,
cli.program
.clone()
.expect("at least one argument passed in")
.into_owned(),
&cli.args,
cli.env,
&self.cwd,
&self.package,
&self.non_test_binaries,
&Interceptor::None, );
let output =
cmd.wait_with_output()
.await
.map_err(|error| CreateTestListError::CommandExecFail {
binary_id: self.binary_id.clone(),
command: cli.to_owned_cli(),
error,
})?;
if output.status.success() {
String::from_utf8(output.stdout).map_err(|err| CreateTestListError::CommandNonUtf8 {
binary_id: self.binary_id.clone(),
command: cli.to_owned_cli(),
stdout: err.into_bytes(),
stderr: output.stderr,
})
} else {
Err(CreateTestListError::CommandFail {
binary_id: self.binary_id.clone(),
command: cli.to_owned_cli(),
exit_status: output.status,
stdout: output.stdout,
stderr: output.stderr,
})
}
}
}
enum ParsedTestBinary<'g> {
Listed {
artifact: RustTestArtifact<'g>,
test_cases: Vec<ParsedTestCase>,
},
Skipped {
artifact: RustTestArtifact<'g>,
reason: BinaryMismatchReason,
},
}
struct ParsedTestCase {
name: TestCaseName,
kind: RustTestKind,
ignored: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RustTestSuiteStatus {
Listed {
test_cases: DebugIgnore<IdOrdMap<RustTestCase>>,
},
Skipped {
reason: BinaryMismatchReason,
},
}
static EMPTY_TEST_CASE_MAP: IdOrdMap<RustTestCase> = IdOrdMap::new();
impl RustTestSuiteStatus {
pub fn test_count(&self) -> usize {
match self {
RustTestSuiteStatus::Listed { test_cases } => test_cases.len(),
RustTestSuiteStatus::Skipped { .. } => 0,
}
}
pub fn get(&self, name: &TestCaseName) -> Option<&RustTestCase> {
match self {
RustTestSuiteStatus::Listed { test_cases } => test_cases.get(name),
RustTestSuiteStatus::Skipped { .. } => None,
}
}
pub fn test_cases(&self) -> impl Iterator<Item = &RustTestCase> + '_ {
match self {
RustTestSuiteStatus::Listed { test_cases } => test_cases.iter(),
RustTestSuiteStatus::Skipped { .. } => {
EMPTY_TEST_CASE_MAP.iter()
}
}
}
pub fn to_summary(
&self,
) -> (
RustTestSuiteStatusSummary,
BTreeMap<TestCaseName, RustTestCaseSummary>,
) {
match self {
Self::Listed { test_cases } => (
RustTestSuiteStatusSummary::LISTED,
test_cases
.iter()
.cloned()
.map(|case| (case.name, case.test_info))
.collect(),
),
Self::Skipped {
reason: BinaryMismatchReason::Expression,
} => (RustTestSuiteStatusSummary::SKIPPED, BTreeMap::new()),
Self::Skipped {
reason: BinaryMismatchReason::DefaultSet,
} => (
RustTestSuiteStatusSummary::SKIPPED_DEFAULT_FILTER,
BTreeMap::new(),
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RustTestCase {
pub name: TestCaseName,
pub test_info: RustTestCaseSummary,
}
impl IdOrdItem for RustTestCase {
type Key<'a> = &'a TestCaseName;
fn key(&self) -> Self::Key<'_> {
&self.name
}
id_upcast!();
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TestInstance<'a> {
pub name: &'a TestCaseName,
pub suite_info: &'a RustTestSuite<'a>,
pub test_info: &'a RustTestCaseSummary,
}
impl<'a> TestInstance<'a> {
pub(crate) fn new(case: &'a RustTestCase, suite_info: &'a RustTestSuite) -> Self {
Self {
name: &case.name,
suite_info,
test_info: &case.test_info,
}
}
#[inline]
pub fn id(&self) -> TestInstanceId<'a> {
TestInstanceId {
binary_id: &self.suite_info.binary_id,
test_name: self.name,
}
}
pub fn to_test_query(&self) -> TestQuery<'a> {
TestQuery {
binary_query: BinaryQuery {
package_id: self.suite_info.package.id(),
binary_id: &self.suite_info.binary_id,
kind: &self.suite_info.kind,
binary_name: &self.suite_info.binary_name,
platform: convert_build_platform(self.suite_info.build_platform),
},
test_name: self.name,
}
}
pub(crate) fn make_command(
&self,
ctx: &TestExecuteContext<'_>,
test_list: &TestList<'_>,
wrapper_script: Option<&WrapperScriptConfig>,
extra_args: &[String],
interceptor: &Interceptor,
) -> TestCommand {
let cli = self.compute_cli(ctx, test_list, wrapper_script, extra_args);
let lctx = LocalExecuteContext {
phase: TestCommandPhase::Run,
workspace_root: test_list.workspace_root(),
rust_build_meta: &test_list.rust_build_meta,
double_spawn: ctx.double_spawn,
dylib_path: test_list.updated_dylib_path(),
profile_name: ctx.profile_name,
env: &test_list.env,
};
TestCommand::new(
&lctx,
cli.program
.expect("at least one argument is guaranteed")
.into_owned(),
&cli.args,
cli.env,
&self.suite_info.cwd,
&self.suite_info.package,
&self.suite_info.non_test_binaries,
interceptor,
)
}
pub(crate) fn command_line(
&self,
ctx: &TestExecuteContext<'_>,
test_list: &TestList<'_>,
wrapper_script: Option<&WrapperScriptConfig>,
extra_args: &[String],
) -> Vec<String> {
self.compute_cli(ctx, test_list, wrapper_script, extra_args)
.to_owned_cli()
}
fn compute_cli(
&self,
ctx: &'a TestExecuteContext<'_>,
test_list: &TestList<'_>,
wrapper_script: Option<&'a WrapperScriptConfig>,
extra_args: &'a [String],
) -> TestCommandCli<'a> {
let platform_runner = ctx
.target_runner
.for_build_platform(self.suite_info.build_platform);
let mut cli = TestCommandCli::default();
cli.apply_wrappers(
wrapper_script,
platform_runner,
test_list.workspace_root(),
&test_list.rust_build_meta().target_directory,
);
cli.push(self.suite_info.binary_path.as_str());
cli.extend(["--exact", self.name.as_str(), "--nocapture"]);
if self.test_info.ignored {
cli.push("--ignored");
}
match test_list.mode() {
NextestRunMode::Test => {}
NextestRunMode::Benchmark => {
cli.push("--bench");
}
}
cli.extend(extra_args.iter().map(String::as_str));
cli
}
}
#[derive(Clone, Debug, Default)]
struct TestCommandCli<'a> {
program: Option<Cow<'a, str>>,
args: Vec<Cow<'a, str>>,
env: Option<&'a ScriptCommandEnvMap>,
}
impl<'a> TestCommandCli<'a> {
fn apply_wrappers(
&mut self,
wrapper_script: Option<&'a WrapperScriptConfig>,
platform_runner: Option<&'a PlatformRunner>,
workspace_root: &Utf8Path,
target_dir: &Utf8Path,
) {
if let Some(wrapper) = wrapper_script {
match wrapper.target_runner {
WrapperScriptTargetRunner::Ignore => {
self.env = Some(&wrapper.command.env);
self.push(wrapper.command.program(workspace_root, target_dir));
self.extend(wrapper.command.args.iter().map(String::as_str));
}
WrapperScriptTargetRunner::AroundWrapper => {
self.env = Some(&wrapper.command.env);
if let Some(runner) = platform_runner {
self.push(runner.binary());
self.extend(runner.args());
}
self.push(wrapper.command.program(workspace_root, target_dir));
self.extend(wrapper.command.args.iter().map(String::as_str));
}
WrapperScriptTargetRunner::WithinWrapper => {
self.env = Some(&wrapper.command.env);
self.push(wrapper.command.program(workspace_root, target_dir));
self.extend(wrapper.command.args.iter().map(String::as_str));
if let Some(runner) = platform_runner {
self.push(runner.binary());
self.extend(runner.args());
}
}
WrapperScriptTargetRunner::OverridesWrapper => {
if let Some(runner) = platform_runner {
self.push(runner.binary());
self.extend(runner.args());
} else {
self.env = Some(&wrapper.command.env);
self.push(wrapper.command.program(workspace_root, target_dir));
self.extend(wrapper.command.args.iter().map(String::as_str));
}
}
}
} else {
if let Some(runner) = platform_runner {
self.push(runner.binary());
self.extend(runner.args());
}
}
}
fn push(&mut self, arg: impl Into<Cow<'a, str>>) {
if self.program.is_none() {
self.program = Some(arg.into());
} else {
self.args.push(arg.into());
}
}
fn extend(&mut self, args: impl IntoIterator<Item = &'a str>) {
for arg in args {
self.push(arg);
}
}
fn to_owned_cli(&self) -> Vec<String> {
let mut owned_cli = Vec::new();
if let Some(program) = &self.program {
owned_cli.push(program.to_string());
}
owned_cli.extend(self.args.iter().map(|arg| arg.to_string()));
owned_cli
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize)]
pub struct TestInstanceId<'a> {
pub binary_id: &'a RustBinaryId,
pub test_name: &'a TestCaseName,
}
impl TestInstanceId<'_> {
pub fn attempt_id(
&self,
run_id: ReportUuid,
stress_index: Option<u32>,
attempt: u32,
) -> String {
let mut out = String::new();
swrite!(out, "{run_id}:{}", self.binary_id);
if let Some(stress_index) = stress_index {
swrite!(out, "@stress-{}", stress_index);
}
swrite!(out, "${}", self.test_name);
if attempt > 1 {
swrite!(out, "#{attempt}");
}
out
}
}
impl fmt::Display for TestInstanceId<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.binary_id, self.test_name)
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
pub struct OwnedTestInstanceId {
pub binary_id: RustBinaryId,
#[serde(rename = "name")]
pub test_name: TestCaseName,
}
impl OwnedTestInstanceId {
pub fn as_ref(&self) -> TestInstanceId<'_> {
TestInstanceId {
binary_id: &self.binary_id,
test_name: &self.test_name,
}
}
}
impl TestInstanceId<'_> {
pub fn to_owned(&self) -> OwnedTestInstanceId {
OwnedTestInstanceId {
binary_id: self.binary_id.clone(),
test_name: self.test_name.clone(),
}
}
}
pub trait TestInstanceIdKey {
fn key<'k>(&'k self) -> TestInstanceId<'k>;
}
impl TestInstanceIdKey for OwnedTestInstanceId {
fn key<'k>(&'k self) -> TestInstanceId<'k> {
TestInstanceId {
binary_id: &self.binary_id,
test_name: &self.test_name,
}
}
}
impl<'a> TestInstanceIdKey for TestInstanceId<'a> {
fn key<'k>(&'k self) -> TestInstanceId<'k> {
*self
}
}
impl<'a> Borrow<dyn TestInstanceIdKey + 'a> for OwnedTestInstanceId {
fn borrow(&self) -> &(dyn TestInstanceIdKey + 'a) {
self
}
}
impl<'a> PartialEq for dyn TestInstanceIdKey + 'a {
fn eq(&self, other: &(dyn TestInstanceIdKey + 'a)) -> bool {
self.key() == other.key()
}
}
impl<'a> Eq for dyn TestInstanceIdKey + 'a {}
impl<'a> PartialOrd for dyn TestInstanceIdKey + 'a {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<'a> Ord for dyn TestInstanceIdKey + 'a {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.key().cmp(&other.key())
}
}
impl<'a> Hash for dyn TestInstanceIdKey + 'a {
fn hash<H: Hasher>(&self, state: &mut H) {
self.key().hash(state);
}
}
#[derive(Clone, Debug)]
pub struct TestExecuteContext<'a> {
pub profile_name: &'a str,
pub double_spawn: &'a DoubleSpawnInfo,
pub target_runner: &'a TargetRunner,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
config::scripts::{ScriptCommand, ScriptCommandEnvMap, ScriptCommandRelativeTo},
list::{
SerializableFormat,
test_helpers::{PACKAGE_GRAPH_FIXTURE, package_metadata},
},
platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform},
target_runner::PlatformRunnerSource,
test_filter::{RunIgnored, TestFilterPatterns},
};
use iddqd::id_ord_map;
use indoc::indoc;
use nextest_filtering::{CompiledExpr, Filterset, FiltersetKind, KnownGroups, ParseContext};
use nextest_metadata::{FilterMatch, MismatchReason, PlatformLibdirUnavailable, RustTestKind};
use pretty_assertions::assert_eq;
use std::{
collections::{BTreeMap, HashSet},
hash::DefaultHasher,
};
use target_spec::Platform;
use test_strategy::proptest;
#[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 cx = ParseContext::new(&PACKAGE_GRAPH_FIXTURE);
let test_filter = TestFilter::new(
NextestRunMode::Test,
RunIgnored::Default,
TestFilterPatterns::default(),
vec![
Filterset::parse(
"platform(target)".to_owned(),
&cx,
FiltersetKind::Test,
&KnownGroups::Known {
custom_groups: HashSet::new(),
},
)
.unwrap(),
],
)
.unwrap();
let fake_cwd: Utf8PathBuf = "/fake/cwd".into();
let fake_binary_name = "fake-binary".to_owned();
let fake_binary_id = RustBinaryId::new("fake-package::fake-binary");
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 = RustBinaryId::new("fake-package::skipped-binary");
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 fake_triple = TargetTriple {
platform: Platform::new(
"aarch64-unknown-linux-gnu",
target_spec::TargetFeatures::Unknown,
)
.unwrap(),
source: TargetTripleSource::CliOption,
location: TargetDefinitionLocation::Builtin,
};
let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
let build_platforms = BuildPlatforms {
host: HostPlatform {
platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
libdir: PlatformLibdir::Available(fake_host_libdir.into()),
},
target: Some(TargetPlatform {
triple: fake_triple,
libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::new_const("test")),
}),
};
let fake_env = EnvironmentMap::empty();
let rust_build_meta =
RustBuildMeta::new("/fake", "/fake", build_platforms).map_paths(&PathMapper::noop());
let ecx = EvalContext {
default_filter: &CompiledExpr::ALL,
};
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",
),
],
Utf8PathBuf::from("/fake/path"),
rust_build_meta,
&test_filter,
None,
fake_env,
&ecx,
FilterBound::All,
)
.expect("valid output");
assert_eq!(
test_list.rust_suites,
id_ord_map! {
RustTestSuite {
status: RustTestSuiteStatus::Listed {
test_cases: id_ord_map! {
RustTestCase {
name: TestCaseName::new("tests::foo::test_bar"),
test_info: RustTestCaseSummary {
kind: Some(RustTestKind::TEST),
ignored: false,
filter_match: FilterMatch::Matches,
},
},
RustTestCase {
name: TestCaseName::new("tests::baz::test_quux"),
test_info: RustTestCaseSummary {
kind: Some(RustTestKind::TEST),
ignored: false,
filter_match: FilterMatch::Matches,
},
},
RustTestCase {
name: TestCaseName::new("benches::bench_foo"),
test_info: RustTestCaseSummary {
kind: Some(RustTestKind::BENCH),
ignored: false,
filter_match: FilterMatch::Matches,
},
},
RustTestCase {
name: TestCaseName::new("tests::ignored::test_bar"),
test_info: RustTestCaseSummary {
kind: Some(RustTestKind::TEST),
ignored: true,
filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
},
},
RustTestCase {
name: TestCaseName::new("tests::baz::test_ignored"),
test_info: RustTestCaseSummary {
kind: Some(RustTestKind::TEST),
ignored: true,
filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
},
},
RustTestCase {
name: TestCaseName::new("benches::ignored_bench_foo"),
test_info: RustTestCaseSummary {
kind: Some(RustTestKind::BENCH),
ignored: true,
filter_match: FilterMatch::Mismatch { reason: MismatchReason::Ignored },
},
},
}.into(),
},
cwd: fake_cwd.clone(),
build_platform: BuildPlatform::Target,
package: package_metadata(),
binary_name: fake_binary_name,
binary_id: fake_binary_id,
binary_path: "/fake/binary".into(),
kind: RustTestBinaryKind::LIB,
non_test_binaries: BTreeSet::new(),
},
RustTestSuite {
status: RustTestSuiteStatus::Skipped {
reason: BinaryMismatchReason::Expression,
},
cwd: fake_cwd,
build_platform: BuildPlatform::Host,
package: package_metadata(),
binary_name: skipped_binary_name,
binary_id: skipped_binary_id,
binary_path: "/fake/skipped-binary".into(),
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 didn't match filtersets, skipped)
"};
static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
{
"rust-build-meta": {
"target-directory": "/fake",
"build-directory": "/fake",
"base-output-directories": [],
"non-test-binaries": {},
"build-script-out-dirs": {},
"build-script-info": {},
"linked-paths": [],
"platforms": {
"host": {
"platform": {
"triple": "x86_64-unknown-linux-gnu",
"target-features": "unknown"
},
"libdir": {
"status": "available",
"path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
}
},
"targets": [
{
"platform": {
"triple": "aarch64-unknown-linux-gnu",
"target-features": "unknown"
},
"libdir": {
"status": "unavailable",
"reason": "test"
}
}
]
},
"target-platforms": [
{
"triple": "aarch64-unknown-linux-gnu",
"target-features": "unknown"
}
],
"target-platform": "aarch64-unknown-linux-gnu"
},
"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": {
"kind": "bench",
"ignored": false,
"filter-match": {
"status": "matches"
}
},
"benches::ignored_bench_foo": {
"kind": "bench",
"ignored": true,
"filter-match": {
"status": "mismatch",
"reason": "ignored"
}
},
"tests::baz::test_ignored": {
"kind": "test",
"ignored": true,
"filter-match": {
"status": "mismatch",
"reason": "ignored"
}
},
"tests::baz::test_quux": {
"kind": "test",
"ignored": false,
"filter-match": {
"status": "matches"
}
},
"tests::foo::test_bar": {
"kind": "test",
"ignored": false,
"filter-match": {
"status": "matches"
}
},
"tests::ignored::test_bar": {
"kind": "test",
"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": {}
}
}
}"#};
static EXPECTED_ONELINE: &str = indoc! {"
fake-package::fake-binary benches::bench_foo
fake-package::fake-binary tests::baz::test_quux
fake-package::fake-binary tests::foo::test_bar
"};
static EXPECTED_ONELINE_VERBOSE: &str = indoc! {"
fake-package::fake-binary benches::bench_foo [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
fake-package::fake-binary benches::ignored_bench_foo [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
fake-package::fake-binary tests::baz::test_ignored [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
fake-package::fake-binary tests::baz::test_quux [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
fake-package::fake-binary tests::foo::test_bar [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target]
fake-package::fake-binary tests::ignored::test_bar [bin: /fake/binary] [cwd: /fake/cwd] [build platform: target] (skipped)
"};
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
);
assert_eq!(
test_list
.to_string(OutputFormat::Oneline { verbose: false })
.expect("oneline succeeded"),
EXPECTED_ONELINE
);
assert_eq!(
test_list
.to_string(OutputFormat::Oneline { verbose: true })
.expect("oneline verbose succeeded"),
EXPECTED_ONELINE_VERBOSE
);
}
#[test]
fn test_ignored_overrides_non_ignored() {
let non_ignored_output = indoc! {"
tests::unique_non_ignored: test
tests::overlap_test: test
"};
let ignored_output = indoc! {"
tests::unique_ignored: test
tests::overlap_test: test
"};
let test_filter = TestFilter::new(
NextestRunMode::Test,
RunIgnored::All,
TestFilterPatterns::default(),
Vec::new(),
)
.unwrap();
let fake_cwd: Utf8PathBuf = "/fake/cwd".into();
let fake_binary_id = RustBinaryId::new("fake-package::overlap-binary");
let test_binary = RustTestArtifact {
binary_path: "/fake/binary".into(),
cwd: fake_cwd.clone(),
package: package_metadata(),
binary_name: "overlap-binary".to_owned(),
binary_id: fake_binary_id.clone(),
kind: RustTestBinaryKind::LIB,
non_test_binaries: BTreeSet::new(),
build_platform: BuildPlatform::Target,
};
let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
let build_platforms = BuildPlatforms {
host: HostPlatform {
platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
libdir: PlatformLibdir::Available(fake_host_libdir.into()),
},
target: None,
};
let fake_env = EnvironmentMap::empty();
let rust_build_meta =
RustBuildMeta::new("/fake", "/fake", build_platforms).map_paths(&PathMapper::noop());
let ecx = EvalContext {
default_filter: &CompiledExpr::ALL,
};
let test_list = TestList::new_with_outputs(
[(test_binary, &non_ignored_output, &ignored_output)],
Utf8PathBuf::from("/fake/path"),
rust_build_meta,
&test_filter,
None,
fake_env,
&ecx,
FilterBound::All,
)
.expect("valid output");
let suite = test_list
.rust_suites
.get(&fake_binary_id)
.expect("suite exists");
match &suite.status {
RustTestSuiteStatus::Listed { test_cases } => {
let overlap = test_cases
.get(&TestCaseName::new("tests::overlap_test"))
.expect("overlap_test exists");
assert!(
overlap.test_info.ignored,
"overlapping test should be marked ignored"
);
}
other => panic!("expected Listed status, got {other:?}"),
}
}
#[test]
fn apply_wrappers_examples() {
cfg_if::cfg_if! {
if #[cfg(windows)]
{
let workspace_root = Utf8Path::new("D:\\workspace\\root");
let target_dir = Utf8Path::new("C:\\foo\\bar");
} else {
let workspace_root = Utf8Path::new("/workspace/root");
let target_dir = Utf8Path::new("/foo/bar");
}
};
{
let mut cli_no_wrappers = TestCommandCli::default();
cli_no_wrappers.apply_wrappers(None, None, workspace_root, target_dir);
cli_no_wrappers.extend(["binary", "arg"]);
assert!(cli_no_wrappers.env.is_none());
assert_eq!(cli_no_wrappers.to_owned_cli(), vec!["binary", "arg"]);
}
{
let runner = PlatformRunner::debug_new(
"runner".into(),
Vec::new(),
PlatformRunnerSource::Env("fake".to_owned()),
);
let mut cli_runner_only = TestCommandCli::default();
cli_runner_only.apply_wrappers(None, Some(&runner), workspace_root, target_dir);
cli_runner_only.extend(["binary", "arg"]);
assert!(cli_runner_only.env.is_none());
assert_eq!(
cli_runner_only.to_owned_cli(),
vec!["runner", "binary", "arg"],
);
}
{
let runner = PlatformRunner::debug_new(
"runner".into(),
Vec::new(),
PlatformRunnerSource::Env("fake".to_owned()),
);
let wrapper_ignore = WrapperScriptConfig {
command: ScriptCommand {
program: "wrapper".into(),
args: Vec::new(),
env: ScriptCommandEnvMap::default(),
relative_to: ScriptCommandRelativeTo::None,
},
target_runner: WrapperScriptTargetRunner::Ignore,
};
let mut cli_wrapper_ignore = TestCommandCli::default();
cli_wrapper_ignore.apply_wrappers(
Some(&wrapper_ignore),
Some(&runner),
workspace_root,
target_dir,
);
cli_wrapper_ignore.extend(["binary", "arg"]);
assert_eq!(
cli_wrapper_ignore.env,
Some(&ScriptCommandEnvMap::default())
);
assert_eq!(
cli_wrapper_ignore.to_owned_cli(),
vec!["wrapper", "binary", "arg"],
);
}
{
let runner = PlatformRunner::debug_new(
"runner".into(),
Vec::new(),
PlatformRunnerSource::Env("fake".to_owned()),
);
let env = ScriptCommandEnvMap::new(BTreeMap::from([(
String::from("MSG"),
String::from("hello world"),
)]))
.expect("valid env var keys");
let wrapper_around = WrapperScriptConfig {
command: ScriptCommand {
program: "wrapper".into(),
args: Vec::new(),
env: env.clone(),
relative_to: ScriptCommandRelativeTo::None,
},
target_runner: WrapperScriptTargetRunner::AroundWrapper,
};
let mut cli_wrapper_around = TestCommandCli::default();
cli_wrapper_around.apply_wrappers(
Some(&wrapper_around),
Some(&runner),
workspace_root,
target_dir,
);
cli_wrapper_around.extend(["binary", "arg"]);
assert_eq!(cli_wrapper_around.env, Some(&env));
assert_eq!(
cli_wrapper_around.to_owned_cli(),
vec!["runner", "wrapper", "binary", "arg"],
);
}
{
let runner = PlatformRunner::debug_new(
"runner".into(),
Vec::new(),
PlatformRunnerSource::Env("fake".to_owned()),
);
let wrapper_within = WrapperScriptConfig {
command: ScriptCommand {
program: "wrapper".into(),
args: Vec::new(),
env: ScriptCommandEnvMap::default(),
relative_to: ScriptCommandRelativeTo::None,
},
target_runner: WrapperScriptTargetRunner::WithinWrapper,
};
let mut cli_wrapper_within = TestCommandCli::default();
cli_wrapper_within.apply_wrappers(
Some(&wrapper_within),
Some(&runner),
workspace_root,
target_dir,
);
cli_wrapper_within.extend(["binary", "arg"]);
assert_eq!(
cli_wrapper_within.env,
Some(&ScriptCommandEnvMap::default())
);
assert_eq!(
cli_wrapper_within.to_owned_cli(),
vec!["wrapper", "runner", "binary", "arg"],
);
}
{
let runner = PlatformRunner::debug_new(
"runner".into(),
Vec::new(),
PlatformRunnerSource::Env("fake".to_owned()),
);
let wrapper_overrides = WrapperScriptConfig {
command: ScriptCommand {
program: "wrapper".into(),
args: Vec::new(),
env: ScriptCommandEnvMap::default(),
relative_to: ScriptCommandRelativeTo::None,
},
target_runner: WrapperScriptTargetRunner::OverridesWrapper,
};
let mut cli_wrapper_overrides = TestCommandCli::default();
cli_wrapper_overrides.apply_wrappers(
Some(&wrapper_overrides),
Some(&runner),
workspace_root,
target_dir,
);
cli_wrapper_overrides.extend(["binary", "arg"]);
assert!(
cli_wrapper_overrides.env.is_none(),
"overrides-wrapper with runner should not apply wrapper env"
);
assert_eq!(
cli_wrapper_overrides.to_owned_cli(),
vec!["runner", "binary", "arg"],
);
}
{
let wrapper_overrides = WrapperScriptConfig {
command: ScriptCommand {
program: "wrapper".into(),
args: Vec::new(),
env: ScriptCommandEnvMap::default(),
relative_to: ScriptCommandRelativeTo::None,
},
target_runner: WrapperScriptTargetRunner::OverridesWrapper,
};
let mut cli_wrapper_overrides_no_runner = TestCommandCli::default();
cli_wrapper_overrides_no_runner.apply_wrappers(
Some(&wrapper_overrides),
None,
workspace_root,
target_dir,
);
cli_wrapper_overrides_no_runner.extend(["binary", "arg"]);
assert_eq!(
cli_wrapper_overrides_no_runner.env,
Some(&ScriptCommandEnvMap::default()),
"overrides-wrapper without runner should apply wrapper env"
);
assert_eq!(
cli_wrapper_overrides_no_runner.to_owned_cli(),
vec!["wrapper", "binary", "arg"],
);
}
{
let wrapper_with_args = WrapperScriptConfig {
command: ScriptCommand {
program: "wrapper".into(),
args: vec!["--flag".to_string(), "value".to_string()],
env: ScriptCommandEnvMap::default(),
relative_to: ScriptCommandRelativeTo::None,
},
target_runner: WrapperScriptTargetRunner::Ignore,
};
let mut cli_wrapper_args = TestCommandCli::default();
cli_wrapper_args.apply_wrappers(
Some(&wrapper_with_args),
None,
workspace_root,
target_dir,
);
cli_wrapper_args.extend(["binary", "arg"]);
assert_eq!(cli_wrapper_args.env, Some(&ScriptCommandEnvMap::default()));
assert_eq!(
cli_wrapper_args.to_owned_cli(),
vec!["wrapper", "--flag", "value", "binary", "arg"],
);
}
{
let runner_with_args = PlatformRunner::debug_new(
"runner".into(),
vec!["--runner-flag".into(), "value".into()],
PlatformRunnerSource::Env("fake".to_owned()),
);
let mut cli_runner_args = TestCommandCli::default();
cli_runner_args.apply_wrappers(
None,
Some(&runner_with_args),
workspace_root,
target_dir,
);
cli_runner_args.extend(["binary", "arg"]);
assert!(cli_runner_args.env.is_none());
assert_eq!(
cli_runner_args.to_owned_cli(),
vec!["runner", "--runner-flag", "value", "binary", "arg"],
);
}
{
let wrapper_relative_to_workspace_root = WrapperScriptConfig {
command: ScriptCommand {
program: "abc/def/my-wrapper".into(),
args: vec!["--verbose".to_string()],
env: ScriptCommandEnvMap::default(),
relative_to: ScriptCommandRelativeTo::WorkspaceRoot,
},
target_runner: WrapperScriptTargetRunner::Ignore,
};
let mut cli_wrapper_relative = TestCommandCli::default();
cli_wrapper_relative.apply_wrappers(
Some(&wrapper_relative_to_workspace_root),
None,
workspace_root,
target_dir,
);
cli_wrapper_relative.extend(["binary", "arg"]);
cfg_if::cfg_if! {
if #[cfg(windows)] {
let wrapper_path = "D:\\workspace\\root\\abc\\def\\my-wrapper";
} else {
let wrapper_path = "/workspace/root/abc/def/my-wrapper";
}
}
assert_eq!(
cli_wrapper_relative.env,
Some(&ScriptCommandEnvMap::default())
);
assert_eq!(
cli_wrapper_relative.to_owned_cli(),
vec![wrapper_path, "--verbose", "binary", "arg"],
);
}
{
let wrapper_relative_to_target = WrapperScriptConfig {
command: ScriptCommand {
program: "abc/def/my-wrapper".into(),
args: vec!["--verbose".to_string()],
env: ScriptCommandEnvMap::default(),
relative_to: ScriptCommandRelativeTo::Target,
},
target_runner: WrapperScriptTargetRunner::Ignore,
};
let mut cli_wrapper_relative = TestCommandCli::default();
cli_wrapper_relative.apply_wrappers(
Some(&wrapper_relative_to_target),
None,
workspace_root,
target_dir,
);
cli_wrapper_relative.extend(["binary", "arg"]);
cfg_if::cfg_if! {
if #[cfg(windows)] {
let wrapper_path = "C:\\foo\\bar\\abc\\def\\my-wrapper";
} else {
let wrapper_path = "/foo/bar/abc/def/my-wrapper";
}
}
assert_eq!(
cli_wrapper_relative.env,
Some(&ScriptCommandEnvMap::default())
);
assert_eq!(
cli_wrapper_relative.to_owned_cli(),
vec![wrapper_path, "--verbose", "binary", "arg"],
);
}
}
#[test]
fn test_parse_list_lines() {
let binary_id = RustBinaryId::new("test-package::test-binary");
let input = indoc! {"
simple_test: test
module::nested_test: test
deeply::nested::module::test_name: test
"};
let results: Vec<_> = parse_list_lines(&binary_id, input)
.collect::<Result<_, _>>()
.expect("parsed valid test output");
insta::assert_debug_snapshot!("valid_tests", results);
let input = indoc! {"
simple_bench: benchmark
benches::module::my_benchmark: benchmark
"};
let results: Vec<_> = parse_list_lines(&binary_id, input)
.collect::<Result<_, _>>()
.expect("parsed valid benchmark output");
insta::assert_debug_snapshot!("valid_benchmarks", results);
let input = indoc! {"
test_one: test
bench_one: benchmark
test_two: test
bench_two: benchmark
"};
let results: Vec<_> = parse_list_lines(&binary_id, input)
.collect::<Result<_, _>>()
.expect("parsed mixed output");
insta::assert_debug_snapshot!("mixed_tests_and_benchmarks", results);
let input = indoc! {r#"
test_with_underscore_123: test
test::with::colons: test
test_with_numbers_42: test
"#};
let results: Vec<_> = parse_list_lines(&binary_id, input)
.collect::<Result<_, _>>()
.expect("parsed tests with special characters");
insta::assert_debug_snapshot!("special_characters", results);
let input = "";
let results: Vec<_> = parse_list_lines(&binary_id, input)
.collect::<Result<_, _>>()
.expect("parsed empty output");
insta::assert_debug_snapshot!("empty_input", results);
let input = "invalid_test: wrong_suffix";
let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
assert!(result.is_err());
insta::assert_snapshot!("invalid_suffix_error", result.unwrap_err());
let input = "test_without_suffix";
let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
assert!(result.is_err());
insta::assert_snapshot!("missing_suffix_error", result.unwrap_err());
let input = indoc! {"
valid_test: test
invalid_line
another_valid: benchmark
"};
let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
assert!(result.is_err());
insta::assert_snapshot!("partial_valid_error", result.unwrap_err());
let input = indoc! {"
valid_test: test
\rinvalid_line
another_valid: benchmark
"};
let result = parse_list_lines(&binary_id, input).collect::<Result<Vec<_>, _>>();
assert!(result.is_err());
insta::assert_snapshot!("control_character_error", result.unwrap_err());
}
#[proptest]
fn test_instance_id_key_borrow_consistency(
owned1: OwnedTestInstanceId,
owned2: OwnedTestInstanceId,
) {
let borrowed1: &dyn TestInstanceIdKey = &owned1;
let borrowed2: &dyn TestInstanceIdKey = &owned2;
assert_eq!(
owned1 == owned2,
borrowed1 == borrowed2,
"Eq must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
);
assert_eq!(
owned1.partial_cmp(&owned2),
borrowed1.partial_cmp(borrowed2),
"PartialOrd must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
);
assert_eq!(
owned1.cmp(&owned2),
borrowed1.cmp(borrowed2),
"Ord must be consistent between OwnedTestInstanceId and dyn TestInstanceIdKey"
);
fn hash_value(x: &impl Hash) -> u64 {
let mut hasher = DefaultHasher::new();
x.hash(&mut hasher);
hasher.finish()
}
assert_eq!(
hash_value(&owned1),
hash_value(&borrowed1),
"Hash must be consistent for owned1 and its borrowed form"
);
assert_eq!(
hash_value(&owned2),
hash_value(&borrowed2),
"Hash must be consistent for owned2 and its borrowed form"
);
}
#[derive(Debug)]
struct MockGroupLookup {
group_name: String,
}
impl GroupLookup for MockGroupLookup {
fn is_member_test(
&self,
_test: &nextest_filtering::TestQuery<'_>,
matcher: &nextest_filtering::NameMatcher,
) -> bool {
matcher.is_match(&self.group_name)
}
}
#[test]
fn test_build_suites_with_group_filter() {
let cx = ParseContext::new(&PACKAGE_GRAPH_FIXTURE);
let test_filter = TestFilter::new(
NextestRunMode::Test,
RunIgnored::Default,
TestFilterPatterns::default(),
vec![
Filterset::parse(
"group(serial)".to_owned(),
&cx,
FiltersetKind::Test,
&KnownGroups::Known {
custom_groups: HashSet::from(["serial".to_owned()]),
},
)
.unwrap(),
],
)
.unwrap();
assert!(
test_filter.has_group_predicates(),
"filter with group() must report has_group_predicates"
);
let fake_binary_id = RustBinaryId::new("fake-package::fake-binary");
let make_parsed = || {
vec![ParsedTestBinary::Listed {
artifact: RustTestArtifact {
binary_path: "/fake/binary".into(),
cwd: "/fake/cwd".into(),
package: package_metadata(),
binary_name: "fake-binary".to_owned(),
binary_id: fake_binary_id.clone(),
kind: RustTestBinaryKind::LIB,
non_test_binaries: BTreeSet::new(),
build_platform: BuildPlatform::Target,
},
test_cases: vec![
ParsedTestCase {
name: TestCaseName::new("serial_test"),
kind: RustTestKind::TEST,
ignored: false,
},
ParsedTestCase {
name: TestCaseName::new("parallel_test"),
kind: RustTestKind::TEST,
ignored: false,
},
],
}]
};
let ecx = EvalContext {
default_filter: &CompiledExpr::ALL,
};
let lookup = MockGroupLookup {
group_name: "serial".to_owned(),
};
let suites = TestList::build_suites(
make_parsed(),
&test_filter,
&ecx,
FilterBound::All,
Some(&lookup),
);
let suite = suites.get(&fake_binary_id).expect("suite exists");
for case in suite.status.test_cases() {
assert_eq!(
case.test_info.filter_match,
FilterMatch::Matches,
"{} should match with serial group lookup",
case.name,
);
}
let lookup_other = MockGroupLookup {
group_name: "batch".to_owned(),
};
let suites = TestList::build_suites(
make_parsed(),
&test_filter,
&ecx,
FilterBound::All,
Some(&lookup_other),
);
let suite = suites.get(&fake_binary_id).expect("suite exists");
for case in suite.status.test_cases() {
assert_eq!(
case.test_info.filter_match,
FilterMatch::Mismatch {
reason: MismatchReason::Expression,
},
"{} should not match with batch group lookup",
case.name,
);
}
}
}