-
-
Notifications
You must be signed in to change notification settings - Fork 14.9k
libtest: use binary search for --exact test filtering #154865
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,7 +49,7 @@ pub mod test { | |
| pub use crate::time::{TestExecTime, TestTimeOptions}; | ||
| pub use crate::types::{ | ||
| DynTestFn, DynTestName, StaticBenchFn, StaticTestFn, StaticTestName, TestDesc, | ||
| TestDescAndFn, TestId, TestName, TestType, | ||
| TestDescAndFn, TestId, TestList, TestListOrder, TestName, TestType, | ||
| }; | ||
| pub use crate::{assert_test_result, filter_tests, run_test, test_main, test_main_static}; | ||
| } | ||
|
|
@@ -106,6 +106,16 @@ pub fn test_main_with_exit_callback<F: FnOnce()>( | |
| tests: Vec<TestDescAndFn>, | ||
| options: Option<Options>, | ||
| exit_callback: F, | ||
| ) { | ||
| let tests = TestList::new(tests, TestListOrder::Unsorted); | ||
| test_main_inner(args, tests, options, exit_callback) | ||
| } | ||
|
|
||
| fn test_main_inner<F: FnOnce()>( | ||
| args: &[String], | ||
| tests: TestList, | ||
| options: Option<Options>, | ||
| exit_callback: F, | ||
| ) { | ||
| let mut opts = match cli::parse_opts(args) { | ||
| Some(Ok(o)) => o, | ||
|
|
@@ -180,7 +190,9 @@ pub fn test_main_with_exit_callback<F: FnOnce()>( | |
| pub fn test_main_static(tests: &[&TestDescAndFn]) { | ||
| let args = env::args().collect::<Vec<_>>(); | ||
| let owned_tests: Vec<_> = tests.iter().map(make_owned_test).collect(); | ||
| test_main(&args, owned_tests, None) | ||
| // Tests are sorted by name at compile time by mk_tests_slice. | ||
| let tests = TestList::new(owned_tests, TestListOrder::Sorted); | ||
| test_main_inner(&args, tests, None, || {}) | ||
| } | ||
|
|
||
| /// A variant optimized for invocation with a static test vector. | ||
|
|
@@ -229,7 +241,9 @@ pub fn test_main_static_abort(tests: &[&TestDescAndFn]) { | |
|
|
||
| let args = env::args().collect::<Vec<_>>(); | ||
| let owned_tests: Vec<_> = tests.iter().map(make_owned_test).collect(); | ||
| test_main(&args, owned_tests, Some(Options::new().panic_abort(true))) | ||
| // Tests are sorted by name at compile time by mk_tests_slice. | ||
| let tests = TestList::new(owned_tests, TestListOrder::Sorted); | ||
| test_main_inner(&args, tests, Some(Options::new().panic_abort(true)), || {}) | ||
| } | ||
|
|
||
| /// Clones static values for putting into a dynamic vector, which test_main() | ||
|
|
@@ -298,7 +312,7 @@ impl FilteredTests { | |
|
|
||
| pub fn run_tests<F>( | ||
| opts: &TestOpts, | ||
| tests: Vec<TestDescAndFn>, | ||
| tests: TestList, | ||
| mut notify_about_test_event: F, | ||
| ) -> io::Result<()> | ||
| where | ||
|
|
@@ -334,7 +348,7 @@ where | |
| timeout: Instant, | ||
| } | ||
|
|
||
| let tests_len = tests.len(); | ||
| let tests_len = tests.tests.len(); | ||
|
|
||
| let mut filtered = FilteredTests { tests: Vec::new(), benches: Vec::new(), next_id: 0 }; | ||
|
|
||
|
|
@@ -512,25 +526,48 @@ where | |
| Ok(()) | ||
| } | ||
|
|
||
| pub fn filter_tests(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> Vec<TestDescAndFn> { | ||
| pub fn filter_tests(opts: &TestOpts, tests: TestList) -> Vec<TestDescAndFn> { | ||
| let TestList { tests, order } = tests; | ||
| let mut filtered = tests; | ||
| let matches_filter = |test: &TestDescAndFn, filter: &str| { | ||
| let test_name = test.desc.name.as_slice(); | ||
|
|
||
| match opts.filter_exact { | ||
| true => test_name == filter, | ||
| false => test_name.contains(filter), | ||
| } | ||
| }; | ||
|
|
||
| // Remove tests that don't match the test filter | ||
| // Remove tests that don't match the test filter. | ||
| if !opts.filters.is_empty() { | ||
| filtered.retain(|test| opts.filters.iter().any(|filter| matches_filter(test, filter))); | ||
| if opts.filter_exact && order == TestListOrder::Sorted { | ||
| // Let's say that `f` is the number of filters and `n` is the number | ||
| // of tests. | ||
| // | ||
| // The test array is sorted by name (guaranteed by the caller via | ||
| // TestListOrder::Sorted), so use binary search for O(f log n) | ||
| // exact-match lookups instead of an O(n) linear scan. | ||
| // | ||
| // This is important for Miri, where the interpreted execution makes | ||
| // the linear scan very expensive. | ||
| filtered = filter_exact_match(filtered, &opts.filters); | ||
| } else { | ||
| filtered.retain(|test| { | ||
| let test_name = test.desc.name.as_slice(); | ||
| opts.filters.iter().any(|filter| { | ||
| if opts.filter_exact { | ||
| test_name == filter.as_str() | ||
| } else { | ||
| test_name.contains(filter.as_str()) | ||
| } | ||
| }) | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // Skip tests that match any of the skip filters | ||
| // | ||
| // After exact positive filtering above, the filtered set is small, so a | ||
| // linear scan is acceptable even under Miri. | ||
| if !opts.skip.is_empty() { | ||
| filtered.retain(|test| !opts.skip.iter().any(|sf| matches_filter(test, sf))); | ||
| filtered.retain(|test| { | ||
| let name = test.desc.name.as_slice(); | ||
| !opts.skip.iter().any(|sf| { | ||
| if opts.filter_exact { name == sf.as_str() } else { name.contains(sf.as_str()) } | ||
| }) | ||
| }); | ||
| } | ||
|
|
||
| // Excludes #[should_panic] tests | ||
|
|
@@ -553,6 +590,30 @@ pub fn filter_tests(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> Vec<TestDescA | |
| filtered | ||
| } | ||
|
|
||
| /// Extract tests whose names exactly match one of the given `filters`, using | ||
| /// binary search on the (assumed sorted) test list. | ||
| fn filter_exact_match(mut tests: Vec<TestDescAndFn>, filters: &[String]) -> Vec<TestDescAndFn> { | ||
| // Binary search for each filter in the sorted test list. | ||
| let mut indexes: Vec<usize> = filters | ||
| .iter() | ||
| .filter_map(|f| tests.binary_search_by(|t| t.desc.name.as_slice().cmp(f.as_str())).ok()) | ||
| .collect(); | ||
| indexes.sort_unstable(); | ||
| indexes.dedup(); | ||
|
|
||
| // Extract matching tests. Process indexes in descending order so that | ||
| // swap_remove (which replaces the removed element with the last) does not | ||
| // invalidate indexes we haven't visited yet. | ||
| let mut result = Vec::with_capacity(indexes.len()); | ||
| for &idx in indexes.iter().rev() { | ||
| result.push(tests.swap_remove(idx)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have no feedback here, but I just wanted to say this is the first time I've seen And it's really cool how you avoid an allocation/making a copy of the I'll keep a note of this in mind if I need to do something like this in the future. |
||
| } | ||
| // Reverse to restore the original sorted order, since we extracted the | ||
| // matching tests in descending index order. | ||
| result.reverse(); | ||
| result | ||
| } | ||
|
|
||
| pub fn convert_benchmarks_to_tests(tests: Vec<TestDescAndFn>) -> Vec<TestDescAndFn> { | ||
| // convert benchmarks to tests, if we're not benchmarking them | ||
| tests | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can keep the
test_main(&args, owned_tests, None)invocation here, and just construct theTestListinsidetest_main(), wheretest_main()would calltest_main_inner(). This would reduce code duplication a bit since bothtest_main_static()andtest_main_static_abort()seem to be the only functions that invoketest_main()and they both have aTestListOrder::Sorted. I might be incorrect though since I notice thattest_main()is calling ontest_main_with_exit_callback(), which denotes the test list order as unsorted, but I'm not sure why it needs to be marked as unsorted fortest_main()when thetest_main_static_*functions are seemingly only one using it.Ideally, I would have prefer
test_main()to keep with calling ontest_main_with_exit_callback()and not need to introduce atest_main_inner()through, making it take aTestListinstead ofVec<TestDescAndFn>. However, I can see thatlibrustdoc/doctest.rsdoes utilizetest_main_with_exit_callback(), though I don't see why aTestListobject can not be constructed there.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I definitely thought of that, as well as the related approach of making TestList be a newtype wrapper which always ensures sortedness. Happy to hear opinions before I go about making these bigger changes.
At the moment, I believe that within this repo, merged doctests pass in a potentially-unsorted list. But I wasn't sure how much these methods are callable outside the repo.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at
librustdoc/doctest.rs, which is the only file (as far as my Go to references are telling me and a quick text search on codebase) outside this file that callstest_main_with_exit_callback, it looks like it sorts the test list as well?But also since these are public methods accessible in the test crate, I'm not sure if people would need to use these methods and expect the passed in test array to handle both sorted/unsorted (which I think making
TestListbe a newtype wrapper that ensures sortedness is a good idea). The test module docs:feels like it implies that people won't really use the
test_main_*methods. Are these methods used in nextest?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Never mind, I do see it in
librust/doctest/runner.rshereThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, nextest doesn't insert itself into the build process in any way.
How do you think I should proceed with this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was reading through,
librustdoc/lib.rs,librustdoc/doctest/markdown, andlibrustdoc/doctest.rs,librustdoc/doctest/runner, and I think it does sort the merged doctests already.In
librustdoc/doctest.rs, that's what I'm assuming the iterative loop forrun_tests(...)(lines 352-371) does for us (correct me if I'm wrong though). If that's true, then it seems like all usage oftest_main*functions pass in a sorted test list.To answer this question, I prefer what you mentioned earlier here:
We wouldn't need the
TestListOrderenum with this approach, and can construct aTestListtype in each of thetest_main*functions.test_main_static*would invoketest_mainunderneath the hood like before. The test list being sorted internally would have to be documented for these functions as well.Moreover, I feel like the
TestListnewtype should not be a publicly accessible struct, and should remain as an internal helper struct within the rust repo (pub(crate)visibility?); I can't see in what situations people would create aTestListstruct and use it when all the test crate function usesTestDescAndFn(and we can't change the signature oftest_main*since it would cause a breaking change -- though it is marked as experimental so not sure what policy is on that -- + means that more librustdoc code needs to be modified).The consequence to this change is that some changes in
librustdocmight need to be made (probably separately) to prevent redundant sorting, or there should be a note somewhere that there's some redundant sorting occurring inlibrustdoc.Those are my thoughts and how I would probably proceed with this.