use super::{
test_helpers::{collect_matching_tests, make_test_artifact, simple_build_meta, simple_ecx},
*,
};
use crate::{
cargo_config::EnvironmentMap,
partition::PartitionerBuilder,
record::{ComputedRerunInfo, RerunTestSuiteInfo},
run_mode::NextestRunMode,
test_filter::{FilterBound, RunIgnored, TestFilter, TestFilterPatterns},
};
use camino::Utf8PathBuf;
use nextest_metadata::{FilterMatch, MismatchReason, RustBinaryId, TestCaseName};
use proptest::prelude::*;
use std::collections::BTreeSet;
use test_strategy::proptest;
#[derive(Debug)]
struct PartitionTestInput {
binary_specs: Vec<(usize, usize)>,
total_shards: u64,
shard: u64,
}
impl Arbitrary for PartitionTestInput {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
(
prop::collection::vec((0..=8usize, 0..=4usize), 1..=5),
1..=6u64,
any::<proptest::sample::Index>(),
)
.prop_map(|(binary_specs, total_shards, shard_idx)| {
let shard = shard_idx.index(total_shards as usize) as u64 + 1;
PartitionTestInput {
binary_specs,
total_shards,
shard,
}
})
.boxed()
}
}
impl PartitionTestInput {
fn non_ignored_only_specs(&self) -> Vec<(usize, usize)> {
self.binary_specs.iter().map(|&(n, _)| (n, 0)).collect()
}
fn total_non_ignored(&self) -> usize {
self.binary_specs.iter().map(|(n, _)| n).sum()
}
}
const FOUR_TEST_OUTPUT: &str = indoc::indoc! {"
alpha: test
beta: test
gamma: test
delta: test
"};
fn default_filter() -> TestFilter {
TestFilter::new(
NextestRunMode::Test,
RunIgnored::Default,
TestFilterPatterns::default(),
Vec::new(),
)
.unwrap()
}
fn count_1_of_2() -> PartitionerBuilder {
PartitionerBuilder::Count {
shard: 1,
total_shards: 2,
}
}
fn build_test_list(
outputs: impl IntoIterator<Item = (&'static str, &'static str, &'static str)>,
filter: &TestFilter,
partitioner: &PartitionerBuilder,
) -> TestList<'static> {
let ecx = simple_ecx();
let artifacts: Vec<_> = outputs
.into_iter()
.map(|(id, non_ign, ign)| (make_test_artifact(id), non_ign, ign))
.collect();
TestList::new_with_outputs(
artifacts,
Utf8PathBuf::from("/fake/path"),
simple_build_meta(),
filter,
Some(partitioner),
EnvironmentMap::empty(),
&ecx,
FilterBound::All,
)
.expect("valid output")
}
#[test]
fn test_apply_per_binary_partitioning_count() {
let binary2_output = indoc::indoc! {"
one: test
two: test
three: test
"};
let filter = default_filter();
let partitioner = count_1_of_2();
let test_list = build_test_list(
[
("pkg::binary1", FOUR_TEST_OUTPUT, ""),
("pkg::binary2", binary2_output, ""),
],
&filter,
&partitioner,
);
let binary1_suite = test_list
.get_suite(&RustBinaryId::new("pkg::binary1"))
.expect("binary1 should exist");
assert_eq!(
collect_matching_tests(binary1_suite),
vec!["alpha", "delta"]
);
let binary2_suite = test_list
.get_suite(&RustBinaryId::new("pkg::binary2"))
.expect("binary2 should exist");
assert_eq!(collect_matching_tests(binary2_suite), vec!["one", "two"]);
}
#[test]
fn test_partition_ignored_independent() {
let ignored_output = indoc::indoc! {"
ig_one: test
ig_two: test
ig_three: test
"};
let filter = TestFilter::new(
NextestRunMode::Test,
RunIgnored::All,
TestFilterPatterns::default(),
Vec::new(),
)
.unwrap();
let partitioner = count_1_of_2();
let test_list = build_test_list(
[("pkg::binary", FOUR_TEST_OUTPUT, ignored_output)],
&filter,
&partitioner,
);
let suite = test_list
.get_suite(&RustBinaryId::new("pkg::binary"))
.expect("binary should exist");
let non_ignored_matches: Vec<_> = suite
.status
.test_cases()
.filter(|tc| !tc.test_info.ignored && tc.test_info.filter_match.is_match())
.map(|tc| tc.name.as_str())
.collect();
assert_eq!(non_ignored_matches, vec!["alpha", "delta"]);
let ignored_matches: Vec<_> = suite
.status
.test_cases()
.filter(|tc| tc.test_info.ignored && tc.test_info.filter_match.is_match())
.map(|tc| tc.name.as_str())
.collect();
assert_eq!(ignored_matches, vec!["ig_one", "ig_two"]);
}
#[test]
fn test_partition_rerun_already_passed() {
let binary_id = RustBinaryId::new("pkg::binary");
let partitioner = count_1_of_2();
let rerun_suite = RerunTestSuiteInfo {
binary_id: binary_id.clone(),
passing: BTreeSet::from([TestCaseName::new("beta")]),
outstanding: BTreeSet::from([
TestCaseName::new("alpha"),
TestCaseName::new("delta"),
TestCaseName::new("gamma"),
]),
};
let mut rerun_filter = default_filter();
rerun_filter.set_outstanding_tests(ComputedRerunInfo {
test_suites: iddqd::id_ord_map! { rerun_suite },
});
let rerun_list = build_test_list(
[("pkg::binary", FOUR_TEST_OUTPUT, "")],
&rerun_filter,
&partitioner,
);
let suite = rerun_list
.get_suite(&binary_id)
.expect("binary should exist");
let rerun_results: Vec<_> = suite
.status
.test_cases()
.map(|tc| (tc.name.as_str(), tc.test_info.filter_match))
.collect();
assert_eq!(
rerun_results,
vec![
("alpha", FilterMatch::Matches),
(
"beta",
FilterMatch::Mismatch {
reason: MismatchReason::RerunAlreadyPassed,
}
),
("delta", FilterMatch::Matches),
(
"gamma",
FilterMatch::Mismatch {
reason: MismatchReason::Partition,
}
),
]
);
let no_rerun_filter = default_filter();
let no_rerun_list = build_test_list(
[("pkg::binary", FOUR_TEST_OUTPUT, "")],
&no_rerun_filter,
&partitioner,
);
let suite = no_rerun_list
.get_suite(&binary_id)
.expect("binary should exist");
let no_rerun_results: Vec<_> = suite
.status
.test_cases()
.map(|tc| (tc.name.as_str(), tc.test_info.filter_match))
.collect();
assert_eq!(
no_rerun_results,
vec![
("alpha", FilterMatch::Matches),
(
"beta",
FilterMatch::Mismatch {
reason: MismatchReason::Partition,
}
),
("delta", FilterMatch::Matches),
(
"gamma",
FilterMatch::Mismatch {
reason: MismatchReason::Partition,
}
),
]
);
let rerun_running: Vec<_> = rerun_results
.iter()
.filter(|(_, fm)| fm.is_match())
.map(|(name, _)| *name)
.collect();
let no_rerun_running: Vec<_> = no_rerun_results
.iter()
.filter(|(_, fm)| fm.is_match())
.map(|(name, _)| *name)
.collect();
assert_eq!(rerun_running, no_rerun_running);
}
#[test]
fn test_partition_prefiltered_excluded_from_counting() {
let five_test_output = indoc::indoc! {"
alpha: test
beta: test
gamma: test
delta: test
epsilon: test
"};
let name_filter = TestFilter::new(
NextestRunMode::Test,
RunIgnored::Default,
TestFilterPatterns::new(vec!["a".to_string()]),
Vec::new(),
)
.unwrap();
let partitioner = count_1_of_2();
let filtered_list = build_test_list(
[("pkg::binary", five_test_output, "")],
&name_filter,
&partitioner,
);
let suite = filtered_list
.get_suite(&RustBinaryId::new("pkg::binary"))
.expect("binary should exist");
assert_eq!(collect_matching_tests(suite), vec!["alpha", "delta"]);
let epsilon = suite
.status
.get(&TestCaseName::new("epsilon"))
.expect("epsilon should exist");
assert_eq!(
epsilon.test_info.filter_match,
FilterMatch::Mismatch {
reason: MismatchReason::String,
}
);
let no_name_filter = default_filter();
let unfiltered_list = build_test_list(
[("pkg::binary", five_test_output, "")],
&no_name_filter,
&partitioner,
);
let suite = unfiltered_list
.get_suite(&RustBinaryId::new("pkg::binary"))
.expect("binary should exist");
assert_eq!(
collect_matching_tests(suite),
vec!["alpha", "delta", "gamma"]
);
}
#[test]
fn test_partition_hash() {
let filter = default_filter();
let partitioner_shard1 = PartitionerBuilder::Hash {
shard: 1,
total_shards: 2,
};
let partitioner_shard2 = PartitionerBuilder::Hash {
shard: 2,
total_shards: 2,
};
let list_shard1 = build_test_list(
[("pkg::binary", FOUR_TEST_OUTPUT, "")],
&filter,
&partitioner_shard1,
);
let list_shard2 = build_test_list(
[("pkg::binary", FOUR_TEST_OUTPUT, "")],
&filter,
&partitioner_shard2,
);
let suite1 = list_shard1
.get_suite(&RustBinaryId::new("pkg::binary"))
.expect("binary should exist");
let suite2 = list_shard2
.get_suite(&RustBinaryId::new("pkg::binary"))
.expect("binary should exist");
let shard1_matches = collect_matching_tests(suite1);
let shard2_matches = collect_matching_tests(suite2);
let mut all_tests: Vec<&str> = shard1_matches
.iter()
.chain(shard2_matches.iter())
.copied()
.collect();
all_tests.sort();
assert_eq!(all_tests, vec!["alpha", "beta", "delta", "gamma"]);
assert!(
!shard1_matches.is_empty() && !shard2_matches.is_empty(),
"both shards should have tests: shard1={shard1_matches:?}, shard2={shard2_matches:?}"
);
for test in &shard1_matches {
assert!(
!shard2_matches.contains(test),
"test {test} should not appear in both shards"
);
}
}
#[test]
fn test_partition_test_count_and_run_count() {
let filter = default_filter();
let partitioner = count_1_of_2();
let test_list = build_test_list(
[("pkg::binary", FOUR_TEST_OUTPUT, "")],
&filter,
&partitioner,
);
assert_eq!(test_list.test_count(), 4);
assert_eq!(test_list.run_count(), 2);
assert_eq!(
test_list.run_count() + test_list.skip_counts().skipped_tests,
test_list.test_count()
);
}
const THREE_TEST_OUTPUT: &str = indoc::indoc! {"
delta: test
epsilon: test
zeta: test
"};
fn slice_partitioner(shard: u64, total_shards: u64) -> PartitionerBuilder {
PartitionerBuilder::Slice {
shard,
total_shards,
}
}
fn collect_all_matching_tests<'a>(test_list: &'a TestList<'a>) -> Vec<(&'a str, &'a str)> {
test_list
.iter()
.flat_map(|suite| {
let binary_id = suite.binary_id.as_str();
collect_matching_tests(suite)
.into_iter()
.map(move |name| (binary_id, name))
})
.collect()
}
fn collect_non_ignored_matching_tests<'a>(test_list: &'a TestList<'a>) -> Vec<(&'a str, &'a str)> {
test_list
.iter()
.flat_map(|suite| {
let binary_id = suite.binary_id.as_str();
suite
.status
.test_cases()
.filter(|tc| !tc.test_info.ignored && tc.test_info.filter_match.is_match())
.map(move |tc| (binary_id, tc.name.as_str()))
})
.collect()
}
#[test]
fn test_slice_cross_binary_distribution() {
let filter = default_filter();
let partitioner = slice_partitioner(1, 2);
let test_list = build_test_list(
[
("pkg::binary1", THREE_TEST_OUTPUT, ""),
("pkg::binary2", FOUR_TEST_OUTPUT, ""),
],
&filter,
&partitioner,
);
let binary1_suite = test_list
.get_suite(&RustBinaryId::new("pkg::binary1"))
.expect("binary1 should exist");
assert_eq!(collect_matching_tests(binary1_suite), vec!["delta", "zeta"],);
let binary2_suite = test_list
.get_suite(&RustBinaryId::new("pkg::binary2"))
.expect("binary2 should exist");
assert_eq!(collect_matching_tests(binary2_suite), vec!["beta", "gamma"],);
let count_list = build_test_list(
[
("pkg::binary1", THREE_TEST_OUTPUT, ""),
("pkg::binary2", FOUR_TEST_OUTPUT, ""),
],
&filter,
&count_1_of_2(),
);
let count_binary1 = count_list
.get_suite(&RustBinaryId::new("pkg::binary1"))
.expect("binary1 should exist");
let count_binary2 = count_list
.get_suite(&RustBinaryId::new("pkg::binary2"))
.expect("binary2 should exist");
assert_eq!(collect_matching_tests(count_binary1), vec!["delta", "zeta"],);
assert_eq!(
collect_matching_tests(count_binary2),
vec!["alpha", "delta"],
);
let count_shard2_list = build_test_list(
[
("pkg::binary1", THREE_TEST_OUTPUT, ""),
("pkg::binary2", FOUR_TEST_OUTPUT, ""),
],
&filter,
&PartitionerBuilder::Count {
shard: 2,
total_shards: 2,
},
);
let slice_shard2_list = build_test_list(
[
("pkg::binary1", THREE_TEST_OUTPUT, ""),
("pkg::binary2", FOUR_TEST_OUTPUT, ""),
],
&filter,
&slice_partitioner(2, 2),
);
let count_shard2_all = collect_all_matching_tests(&count_shard2_list);
let slice_shard2_all = collect_all_matching_tests(&slice_shard2_list);
assert_eq!(
count_shard2_all,
vec![
("pkg::binary1", "epsilon"),
("pkg::binary2", "beta"),
("pkg::binary2", "gamma"),
],
);
assert_eq!(
slice_shard2_all,
vec![
("pkg::binary1", "epsilon"),
("pkg::binary2", "alpha"),
("pkg::binary2", "delta"),
],
);
}
#[test]
fn test_slice_rerun_already_passed_cross_binary() {
let binary1_id = RustBinaryId::new("pkg::binary1");
let binary2_id = RustBinaryId::new("pkg::binary2");
let partitioner = slice_partitioner(1, 2);
let rerun_suite = RerunTestSuiteInfo {
binary_id: binary1_id.clone(),
passing: BTreeSet::from([TestCaseName::new("epsilon")]),
outstanding: BTreeSet::from([TestCaseName::new("delta"), TestCaseName::new("zeta")]),
};
let mut rerun_filter = default_filter();
rerun_filter.set_outstanding_tests(ComputedRerunInfo {
test_suites: iddqd::id_ord_map! { rerun_suite },
});
let rerun_list = build_test_list(
[
("pkg::binary1", THREE_TEST_OUTPUT, ""),
("pkg::binary2", FOUR_TEST_OUTPUT, ""),
],
&rerun_filter,
&partitioner,
);
let binary1_suite = rerun_list
.get_suite(&binary1_id)
.expect("binary1 should exist");
let binary1_results: Vec<_> = binary1_suite
.status
.test_cases()
.map(|tc| (tc.name.as_str(), tc.test_info.filter_match))
.collect();
assert_eq!(
binary1_results,
vec![
("delta", FilterMatch::Matches),
(
"epsilon",
FilterMatch::Mismatch {
reason: MismatchReason::RerunAlreadyPassed,
}
),
("zeta", FilterMatch::Matches),
]
);
let binary2_suite = rerun_list
.get_suite(&binary2_id)
.expect("binary2 should exist");
let binary2_results: Vec<_> = binary2_suite
.status
.test_cases()
.map(|tc| (tc.name.as_str(), tc.test_info.filter_match))
.collect();
assert_eq!(
binary2_results,
vec![
(
"alpha",
FilterMatch::Mismatch {
reason: MismatchReason::Partition,
}
),
("beta", FilterMatch::Matches),
(
"delta",
FilterMatch::Mismatch {
reason: MismatchReason::Partition,
}
),
("gamma", FilterMatch::Matches),
]
);
let no_rerun_list = build_test_list(
[
("pkg::binary1", THREE_TEST_OUTPUT, ""),
("pkg::binary2", FOUR_TEST_OUTPUT, ""),
],
&default_filter(),
&partitioner,
);
let rerun_running = collect_all_matching_tests(&rerun_list);
let no_rerun_running = collect_all_matching_tests(&no_rerun_list);
assert_eq!(
rerun_running, no_rerun_running,
"RerunAlreadyPassed test must participate in cross-binary counting",
);
}
#[test]
fn test_slice_prefiltered_excluded_from_cross_binary_counting() {
let name_filter = TestFilter::new(
NextestRunMode::Test,
RunIgnored::Default,
TestFilterPatterns::new(vec!["a".to_string()]),
Vec::new(),
)
.unwrap();
let partitioner = slice_partitioner(1, 2);
let filtered_list = build_test_list(
[
("pkg::binary1", FOUR_TEST_OUTPUT, ""),
("pkg::binary2", THREE_TEST_OUTPUT, ""),
],
&name_filter,
&partitioner,
);
let binary2_suite = filtered_list
.get_suite(&RustBinaryId::new("pkg::binary2"))
.expect("binary2 should exist");
let epsilon = binary2_suite
.status
.get(&TestCaseName::new("epsilon"))
.expect("epsilon should exist");
assert_eq!(
epsilon.test_info.filter_match,
FilterMatch::Mismatch {
reason: MismatchReason::String,
}
);
let filtered_running = collect_all_matching_tests(&filtered_list);
assert_eq!(
filtered_running,
vec![
("pkg::binary1", "alpha"),
("pkg::binary1", "delta"),
("pkg::binary2", "delta"),
],
);
let no_filter_list = build_test_list(
[
("pkg::binary1", FOUR_TEST_OUTPUT, ""),
("pkg::binary2", THREE_TEST_OUTPUT, ""),
],
&default_filter(),
&partitioner,
);
let unfiltered_running = collect_all_matching_tests(&no_filter_list);
assert_eq!(
unfiltered_running,
vec![
("pkg::binary1", "alpha"),
("pkg::binary1", "delta"),
("pkg::binary2", "delta"),
("pkg::binary2", "zeta"),
],
);
}
fn make_test_output(count: usize, offset: usize) -> String {
(0..count)
.map(|i| format!("t_{:03}: test", offset + i))
.collect::<Vec<_>>()
.join("\n")
}
fn build_test_list_from_specs(
specs: &[(usize, usize)],
filter: &TestFilter,
partitioner: &PartitionerBuilder,
) -> TestList<'static> {
let ecx = simple_ecx();
let artifacts: Vec<_> = specs
.iter()
.enumerate()
.map(|(i, &(non_ign_count, ign_count))| {
let binary_id = format!("pkg::bin_{i:02}");
let non_ign_output = make_test_output(non_ign_count, 0);
let ign_output = make_test_output(ign_count, non_ign_count);
(make_test_artifact(&binary_id), non_ign_output, ign_output)
})
.collect();
TestList::new_with_outputs(
artifacts,
Utf8PathBuf::from("/fake/path"),
simple_build_meta(),
filter,
Some(partitioner),
EnvironmentMap::empty(),
&ecx,
FilterBound::All,
)
.expect("valid output")
}
#[proptest(cases = 64)]
fn proptest_slice_shards_complete_and_disjoint(input: PartitionTestInput) {
let filter = default_filter();
let specs = input.non_ignored_only_specs();
let total_tests = input.total_non_ignored();
let shard_lists: Vec<_> = (1..=input.total_shards)
.map(|shard| {
build_test_list_from_specs(
&specs,
&filter,
&slice_partitioner(shard, input.total_shards),
)
})
.collect();
let mut all_tests: Vec<(String, String)> = Vec::new();
for test_list in &shard_lists {
for suite in test_list.iter() {
let binary_id = suite.binary_id.as_str();
for tc in suite.status.test_cases() {
if tc.test_info.filter_match.is_match() {
all_tests.push((binary_id.to_owned(), tc.name.as_str().to_owned()));
}
}
}
}
prop_assert_eq!(
all_tests.len(),
total_tests,
"union of all shards should cover all {} tests",
total_tests,
);
all_tests.sort();
let len_before_dedup = all_tests.len();
all_tests.dedup();
prop_assert_eq!(
all_tests.len(),
len_before_dedup,
"shards must be disjoint (no duplicate tests)",
);
}
#[proptest(cases = 64)]
fn proptest_slice_even_distribution(input: PartitionTestInput) {
let filter = default_filter();
let specs = input.non_ignored_only_specs();
let total_tests = input.total_non_ignored();
let test_list = build_test_list_from_specs(
&specs,
&filter,
&slice_partitioner(input.shard, input.total_shards),
);
prop_assert_eq!(test_list.test_count(), total_tests);
prop_assert_eq!(
test_list.run_count() + test_list.skip_counts().skipped_tests,
test_list.test_count(),
"run_count + skip_count must equal test_count",
);
let n = input.total_shards as usize;
let min_per_shard = total_tests / n;
let max_per_shard = min_per_shard + usize::from(total_tests % n != 0);
prop_assert!(
test_list.run_count() >= min_per_shard && test_list.run_count() <= max_per_shard,
"shard {}/{} with {} total tests: run_count={} not in [{}, {}]",
input.shard,
input.total_shards,
total_tests,
test_list.run_count(),
min_per_shard,
max_per_shard,
);
}
#[proptest(cases = 64)]
fn proptest_slice_ignored_independence(input: PartitionTestInput) {
let partitioner = slice_partitioner(input.shard, input.total_shards);
let filter_all = TestFilter::new(
NextestRunMode::Test,
RunIgnored::All,
TestFilterPatterns::default(),
Vec::new(),
)
.unwrap();
let list_with_ign = build_test_list_from_specs(&input.binary_specs, &filter_all, &partitioner);
let filter_default = default_filter();
let specs_no_ign = input.non_ignored_only_specs();
let list_no_ign = build_test_list_from_specs(&specs_no_ign, &filter_default, &partitioner);
prop_assert_eq!(
collect_non_ignored_matching_tests(&list_with_ign),
collect_non_ignored_matching_tests(&list_no_ign),
"non-ignored test selection must be independent of ignored tests",
);
}