| //! This module contains a reimplementation of the subset of libtest | |
| //! functionality needed by compiletest. | |
| //! | |
| //! FIXME(Zalathar): Much of this code was originally designed to mimic libtest | |
| //! as closely as possible, for ease of migration. Now that libtest is no longer | |
| //! used, we can potentially redesign things to be a better fit for compiletest. | |
| use std::borrow::Cow; | |
| use std::collections::HashMap; | |
| use std::hash::{BuildHasherDefault, DefaultHasher}; | |
| use std::num::NonZero; | |
| use std::sync::{Arc, mpsc}; | |
| use std::{env, hint, mem, panic, thread}; | |
| use camino::Utf8PathBuf; | |
| use crate::common::{Config, Debugger, TestPaths}; | |
| use crate::output_capture::{self, ConsoleOut}; | |
| use crate::panic_hook; | |
| mod deadline; | |
| mod json; | |
| pub(crate) fn run_tests(config: &Config, tests: Vec<CollectedTest>) -> bool { | |
| let tests_len = tests.len(); | |
| let filtered = filter_tests(config, tests); | |
| // Iterator yielding tests that haven't been started yet. | |
| let mut fresh_tests = (0..).map(TestId).zip(&filtered); | |
| let concurrency = get_concurrency(); | |
| assert!(concurrency > 0); | |
| let concurrent_capacity = concurrency.min(filtered.len()); | |
| let mut listener = json::Listener::new(); | |
| let mut running_tests = HashMap::with_capacity_and_hasher( | |
| concurrent_capacity, | |
| BuildHasherDefault::<DefaultHasher>::new(), | |
| ); | |
| let mut deadline_queue = deadline::DeadlineQueue::with_capacity(concurrent_capacity); | |
| let num_filtered_out = tests_len - filtered.len(); | |
| listener.suite_started(filtered.len(), num_filtered_out); | |
| // Channel used by test threads to report the test outcome when done. | |
| let (completion_tx, completion_rx) = mpsc::channel::<TestCompletion>(); | |
| // Unlike libtest, we don't have a separate code path for concurrency=1. | |
| // In that case, the tests will effectively be run serially anyway. | |
| loop { | |
| // Spawn new test threads, up to the concurrency limit. | |
| while running_tests.len() < concurrency | |
| && let Some((id, test)) = fresh_tests.next() | |
| { | |
| listener.test_started(test); | |
| deadline_queue.push(id, test); | |
| let join_handle = spawn_test_thread(id, test, completion_tx.clone()); | |
| running_tests.insert(id, RunningTest { test, join_handle }); | |
| } | |
| // If all running tests have finished, and there weren't any unstarted | |
| // tests to spawn, then we're done. | |
| if running_tests.is_empty() { | |
| break; | |
| } | |
| let completion = deadline_queue | |
| .read_channel_while_checking_deadlines( | |
| &completion_rx, | |
| |id| running_tests.contains_key(&id), | |
| |_id, test| listener.test_timed_out(test), | |
| ) | |
| .expect("receive channel should never be closed early"); | |
| let RunningTest { test, join_handle } = running_tests.remove(&completion.id).unwrap(); | |
| if let Some(join_handle) = join_handle { | |
| join_handle.join().unwrap_or_else(|_| { | |
| panic!("thread for `{}` panicked after reporting completion", test.desc.name) | |
| }); | |
| } | |
| listener.test_finished(test, &completion); | |
| if completion.outcome.is_failed() && config.fail_fast { | |
| // Prevent any other in-flight threads from panicking when they | |
| // write to the completion channel. | |
| mem::forget(completion_rx); | |
| break; | |
| } | |
| } | |
| let suite_passed = listener.suite_finished(); | |
| suite_passed | |
| } | |
| /// Spawns a thread to run a single test, and returns the thread's join handle. | |
| /// | |
| /// Returns `None` if the test was ignored, so no thread was spawned. | |
| fn spawn_test_thread( | |
| id: TestId, | |
| test: &CollectedTest, | |
| completion_sender: mpsc::Sender<TestCompletion>, | |
| ) -> Option<thread::JoinHandle<()>> { | |
| if test.desc.is_ignored() && !test.config.run_ignored { | |
| completion_sender | |
| .send(TestCompletion { id, outcome: TestOutcome::Ignored, stdout: None }) | |
| .unwrap(); | |
| return None; | |
| } | |
| let args = TestThreadArgs { | |
| id, | |
| config: Arc::clone(&test.config), | |
| testpaths: test.testpaths.clone(), | |
| variant: test.variant.clone(), | |
| should_fail: test.desc.should_fail, | |
| completion_sender, | |
| }; | |
| let thread_builder = thread::Builder::new().name(test.desc.name.clone()); | |
| let join_handle = thread_builder.spawn(move || test_thread_main(args)).unwrap(); | |
| Some(join_handle) | |
| } | |
| /// All of the owned data needed by `test_thread_main`. | |
| struct TestThreadArgs { | |
| id: TestId, | |
| config: Arc<Config>, | |
| testpaths: TestPaths, | |
| variant: TestVariant, | |
| should_fail: ShouldFail, | |
| completion_sender: mpsc::Sender<TestCompletion>, | |
| } | |
| /// Runs a single test, within the dedicated thread spawned by the caller. | |
| fn test_thread_main(args: TestThreadArgs) { | |
| let capture = CaptureKind::for_config(&args.config); | |
| // Install a panic-capture buffer for use by the custom panic hook. | |
| if capture.should_set_panic_hook() { | |
| panic_hook::set_capture_buf(Default::default()); | |
| } | |
| let stdout = capture.stdout(); | |
| let stderr = capture.stderr(); | |
| // Run the test, catching any panics so that we can gracefully report | |
| // failure (or success). | |
| // | |
| // FIXME(Zalathar): Ideally we would report test failures with `Result`, | |
| // and use panics only for bugs within compiletest itself, but that would | |
| // require a major overhaul of error handling in the test runners. | |
| let panic_payload = panic::catch_unwind(|| { | |
| __rust_begin_short_backtrace(|| { | |
| crate::runtest::run(&args.config, stdout, stderr, &args.testpaths, &args.variant); | |
| }); | |
| }) | |
| .err(); | |
| if let Some(panic_buf) = panic_hook::take_capture_buf() { | |
| let panic_buf = panic_buf.lock().unwrap_or_else(|e| e.into_inner()); | |
| // Forward any captured panic message to (captured) stderr. | |
| write!(stderr, "{panic_buf}"); | |
| } | |
| // Interpret the presence/absence of a panic as test failure/success. | |
| let outcome = match (args.should_fail, panic_payload) { | |
| (ShouldFail::No, None) | (ShouldFail::Yes, Some(_)) => TestOutcome::Succeeded, | |
| (ShouldFail::No, Some(_)) => TestOutcome::Failed { message: None }, | |
| (ShouldFail::Yes, None) => { | |
| TestOutcome::Failed { message: Some("`//@ should-fail` test did not fail as expected") } | |
| } | |
| }; | |
| let stdout = capture.into_inner(); | |
| args.completion_sender.send(TestCompletion { id: args.id, outcome, stdout }).unwrap(); | |
| } | |
| enum CaptureKind { | |
| /// Do not capture test-runner output, for `--no-capture`. | |
| /// | |
| /// (This does not affect `rustc` and other subprocesses spawned by test | |
| /// runners, whose output is always captured.) | |
| None, | |
| /// Capture all console output that would be printed by test runners via | |
| /// their `stdout` and `stderr` trait objects, or via the custom panic hook. | |
| Capture { buf: output_capture::CaptureBuf }, | |
| } | |
| impl CaptureKind { | |
| fn for_config(config: &Config) -> Self { | |
| if config.capture { | |
| Self::Capture { buf: output_capture::CaptureBuf::new() } | |
| } else { | |
| Self::None | |
| } | |
| } | |
| fn should_set_panic_hook(&self) -> bool { | |
| match self { | |
| Self::None => false, | |
| Self::Capture { .. } => true, | |
| } | |
| } | |
| fn stdout(&self) -> &dyn ConsoleOut { | |
| self.capture_buf_or(&output_capture::Stdout) | |
| } | |
| fn stderr(&self) -> &dyn ConsoleOut { | |
| self.capture_buf_or(&output_capture::Stderr) | |
| } | |
| fn capture_buf_or<'a>(&'a self, fallback: &'a dyn ConsoleOut) -> &'a dyn ConsoleOut { | |
| match self { | |
| Self::None => fallback, | |
| Self::Capture { buf } => buf, | |
| } | |
| } | |
| fn into_inner(self) -> Option<Vec<u8>> { | |
| match self { | |
| Self::None => None, | |
| Self::Capture { buf } => Some(buf.into_inner().into()), | |
| } | |
| } | |
| } | |
| #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] | |
| struct TestId(usize); | |
| /// Fixed frame used to clean the backtrace with `RUST_BACKTRACE=1`. | |
| #[inline(never)] | |
| fn __rust_begin_short_backtrace<T, F: FnOnce() -> T>(f: F) -> T { | |
| let result = f(); | |
| // prevent this frame from being tail-call optimised away | |
| hint::black_box(result) | |
| } | |
| struct RunningTest<'a> { | |
| test: &'a CollectedTest, | |
| join_handle: Option<thread::JoinHandle<()>>, | |
| } | |
| /// Test completion message sent by individual test threads when their test | |
| /// finishes (successfully or unsuccessfully). | |
| struct TestCompletion { | |
| id: TestId, | |
| outcome: TestOutcome, | |
| stdout: Option<Vec<u8>>, | |
| } | |
| #[derive(Clone, Debug, PartialEq, Eq)] | |
| enum TestOutcome { | |
| Succeeded, | |
| Failed { message: Option<&'static str> }, | |
| Ignored, | |
| } | |
| impl TestOutcome { | |
| fn is_failed(&self) -> bool { | |
| matches!(self, Self::Failed { .. }) | |
| } | |
| } | |
| /// Applies command-line arguments for filtering/skipping tests by name. | |
| /// | |
| /// Adapted from `filter_tests` in libtest. | |
| /// | |
| /// FIXME(#139660): Now that libtest has been removed, redesign the whole filtering system to | |
| /// do a better job of understanding and filtering _paths_, instead of being tied to libtest's | |
| /// substring/exact matching behaviour. | |
| fn filter_tests(opts: &Config, tests: Vec<CollectedTest>) -> Vec<CollectedTest> { | |
| let mut filtered = tests; | |
| let matches_filter = |test: &CollectedTest, filter_str: &str| { | |
| if opts.filter_exact { | |
| // When `--exact` is used we must use `filterable_path` to get | |
| // reasonable filtering behavior. | |
| test.desc.filterable_path.as_str() == filter_str | |
| } else { | |
| // For compatibility we use the name (which includes the full path) | |
| // if `--exact` is not used. | |
| test.desc.name.contains(filter_str) | |
| } | |
| }; | |
| // 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))); | |
| } | |
| // Skip tests that match any of the skip filters | |
| if !opts.skip.is_empty() { | |
| filtered.retain(|test| !opts.skip.iter().any(|sf| matches_filter(test, sf))); | |
| } | |
| filtered | |
| } | |
| /// Determines the number of tests to run concurrently. | |
| /// | |
| /// Copied from `get_concurrency` in libtest. | |
| /// | |
| /// FIXME(#139660): After the libtest dependency is removed, consider making bootstrap specify the | |
| /// number of threads on the command-line, instead of propagating the `RUST_TEST_THREADS` | |
| /// environment variable. | |
| fn get_concurrency() -> usize { | |
| if let Ok(value) = env::var("RUST_TEST_THREADS") { | |
| match value.parse::<NonZero<usize>>().ok() { | |
| Some(n) => n.get(), | |
| _ => panic!("RUST_TEST_THREADS is `{value}`, should be a positive integer."), | |
| } | |
| } else { | |
| thread::available_parallelism().map(|n| n.get()).unwrap_or(1) | |
| } | |
| } | |
| /// Data related to a specific variant of a test. | |
| #[derive(Clone, Debug)] | |
| pub(crate) struct TestVariant { | |
| pub(crate) revision: Option<String>, | |
| pub(crate) debugger: Option<Debugger>, | |
| } | |
| impl TestVariant { | |
| pub(crate) fn revision(&self) -> Option<&str> { | |
| self.revision.as_deref() | |
| } | |
| } | |
| /// Information that was historically needed to create a libtest `TestDescAndFn`. | |
| pub(crate) struct CollectedTest { | |
| pub(crate) desc: CollectedTestDesc, | |
| pub(crate) config: Arc<Config>, | |
| pub(crate) testpaths: TestPaths, | |
| pub(crate) variant: TestVariant, | |
| } | |
| /// Information that was historically needed to create a libtest `TestDesc`. | |
| pub(crate) struct CollectedTestDesc { | |
| pub(crate) name: String, | |
| pub(crate) filterable_path: Utf8PathBuf, | |
| pub(crate) ignore_message: Option<Cow<'static, str>>, | |
| pub(crate) should_fail: ShouldFail, | |
| } | |
| impl CollectedTestDesc { | |
| pub(crate) fn is_ignored(&self) -> bool { | |
| self.ignore_message.is_some() | |
| } | |
| } | |
| /// Tests with `//@ should-fail` are tests of compiletest itself, and should | |
| /// be reported as successful if and only if they would have _failed_. | |
| #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] | |
| pub(crate) enum ShouldFail { | |
| No, | |
| Yes, | |
| } |