|
1 |
| -//! This module encapsulates all of the code that interacts directly with |
2 |
| -//! libtest, to execute the collected tests. |
3 |
| -//! |
4 |
| -//! This will hopefully make it easier to migrate away from libtest someday. |
| 1 | +//! This module contains a reimplementation of the subset of libtest |
| 2 | +//! functionality needed by compiletest. |
5 | 3 |
|
6 | 4 | use std::borrow::Cow;
|
7 |
| -use std::sync::Arc; |
| 5 | +use std::collections::HashMap; |
| 6 | +use std::hash::{BuildHasherDefault, DefaultHasher}; |
| 7 | +use std::num::NonZero; |
| 8 | +use std::sync::{Arc, Mutex, mpsc}; |
| 9 | +use std::{env, hint, io, mem, panic, thread}; |
8 | 10 |
|
9 | 11 | use crate::common::{Config, TestPaths};
|
10 | 12 |
|
| 13 | +mod deadline; |
| 14 | +mod json; |
11 | 15 | pub(crate) mod libtest;
|
12 | 16 |
|
| 17 | +pub(crate) fn run_tests(config: &Config, tests: Vec<CollectedTest>) -> bool { |
| 18 | + let tests_len = tests.len(); |
| 19 | + let filtered = filter_tests(config, tests); |
| 20 | + // Iterator yielding tests that haven't been started yet. |
| 21 | + let mut fresh_tests = (0..).map(TestId).zip(&filtered); |
| 22 | + |
| 23 | + let concurrency = get_concurrency(); |
| 24 | + assert!(concurrency > 0); |
| 25 | + let concurrent_capacity = concurrency.min(filtered.len()); |
| 26 | + |
| 27 | + let mut listener = json::Listener::new(); |
| 28 | + let mut running_tests = HashMap::with_capacity_and_hasher( |
| 29 | + concurrent_capacity, |
| 30 | + BuildHasherDefault::<DefaultHasher>::new(), |
| 31 | + ); |
| 32 | + let mut deadline_queue = deadline::DeadlineQueue::with_capacity(concurrent_capacity); |
| 33 | + |
| 34 | + let num_filtered_out = tests_len - filtered.len(); |
| 35 | + listener.suite_started(filtered.len(), num_filtered_out); |
| 36 | + |
| 37 | + // Channel used by test threads to report the test outcome when done. |
| 38 | + let (completion_tx, completion_rx) = mpsc::channel::<TestCompletion>(); |
| 39 | + |
| 40 | + // Unlike libtest, we don't have a separate code path for concurrency=1. |
| 41 | + // In that case, the tests will effectively be run serially anyway. |
| 42 | + loop { |
| 43 | + // Spawn new test threads, up to the concurrency limit. |
| 44 | + // FIXME(let_chains): Use a let-chain here when stable in bootstrap. |
| 45 | + 'spawn: while running_tests.len() < concurrency { |
| 46 | + let Some((id, test)) = fresh_tests.next() else { break 'spawn }; |
| 47 | + listener.test_started(test); |
| 48 | + deadline_queue.push(id, test); |
| 49 | + let join_handle = spawn_test_thread(id, test, completion_tx.clone()); |
| 50 | + running_tests.insert(id, RunningTest { test, join_handle }); |
| 51 | + } |
| 52 | + |
| 53 | + // If all running tests have finished, and there weren't any unstarted |
| 54 | + // tests to spawn, then we're done. |
| 55 | + if running_tests.is_empty() { |
| 56 | + break; |
| 57 | + } |
| 58 | + |
| 59 | + let completion = deadline_queue |
| 60 | + .read_channel_while_checking_deadlines(&completion_rx, |_id, test| { |
| 61 | + listener.test_timed_out(test); |
| 62 | + }) |
| 63 | + .expect("receive channel should never be closed early"); |
| 64 | + |
| 65 | + let RunningTest { test, join_handle } = running_tests.remove(&completion.id).unwrap(); |
| 66 | + if let Some(join_handle) = join_handle { |
| 67 | + join_handle.join().unwrap_or_else(|_| { |
| 68 | + panic!("thread for `{}` panicked after reporting completion", test.desc.name) |
| 69 | + }); |
| 70 | + } |
| 71 | + |
| 72 | + listener.test_finished(test, &completion); |
| 73 | + |
| 74 | + if completion.outcome.is_failed() && config.fail_fast { |
| 75 | + // Prevent any other in-flight threads from panicking when they |
| 76 | + // write to the completion channel. |
| 77 | + mem::forget(completion_rx); |
| 78 | + break; |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + let suite_passed = listener.suite_finished(); |
| 83 | + suite_passed |
| 84 | +} |
| 85 | + |
| 86 | +/// Spawns a thread to run a single test, and returns the thread's join handle. |
| 87 | +/// |
| 88 | +/// Returns `None` if the test was ignored, so no thread was spawned. |
| 89 | +fn spawn_test_thread( |
| 90 | + id: TestId, |
| 91 | + test: &CollectedTest, |
| 92 | + completion_tx: mpsc::Sender<TestCompletion>, |
| 93 | +) -> Option<thread::JoinHandle<()>> { |
| 94 | + if test.desc.ignore && !test.config.run_ignored { |
| 95 | + completion_tx |
| 96 | + .send(TestCompletion { id, outcome: TestOutcome::Ignored, stdout: None }) |
| 97 | + .unwrap(); |
| 98 | + return None; |
| 99 | + } |
| 100 | + |
| 101 | + let runnable_test = RunnableTest::new(test); |
| 102 | + let should_panic = test.desc.should_panic; |
| 103 | + let run_test = move || run_test_inner(id, should_panic, runnable_test, completion_tx); |
| 104 | + |
| 105 | + let thread_builder = thread::Builder::new().name(test.desc.name.clone()); |
| 106 | + let join_handle = thread_builder.spawn(run_test).unwrap(); |
| 107 | + Some(join_handle) |
| 108 | +} |
| 109 | + |
| 110 | +/// Runs a single test, within the dedicated thread spawned by the caller. |
| 111 | +fn run_test_inner( |
| 112 | + id: TestId, |
| 113 | + should_panic: ShouldPanic, |
| 114 | + runnable_test: RunnableTest, |
| 115 | + completion_sender: mpsc::Sender<TestCompletion>, |
| 116 | +) { |
| 117 | + let is_capture = !runnable_test.config.nocapture; |
| 118 | + let capture_buf = is_capture.then(|| Arc::new(Mutex::new(vec![]))); |
| 119 | + |
| 120 | + if let Some(capture_buf) = &capture_buf { |
| 121 | + io::set_output_capture(Some(Arc::clone(capture_buf))); |
| 122 | + } |
| 123 | + |
| 124 | + let panic_payload = panic::catch_unwind(move || runnable_test.run()).err(); |
| 125 | + |
| 126 | + if is_capture { |
| 127 | + io::set_output_capture(None); |
| 128 | + } |
| 129 | + |
| 130 | + let outcome = match (should_panic, panic_payload) { |
| 131 | + (ShouldPanic::No, None) | (ShouldPanic::Yes, Some(_)) => TestOutcome::Succeeded, |
| 132 | + (ShouldPanic::No, Some(_)) => TestOutcome::Failed { message: None }, |
| 133 | + (ShouldPanic::Yes, None) => { |
| 134 | + TestOutcome::Failed { message: Some("test did not panic as expected") } |
| 135 | + } |
| 136 | + }; |
| 137 | + let stdout = capture_buf.map(|mutex| mutex.lock().unwrap_or_else(|e| e.into_inner()).to_vec()); |
| 138 | + |
| 139 | + completion_sender.send(TestCompletion { id, outcome, stdout }).unwrap(); |
| 140 | +} |
| 141 | + |
| 142 | +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] |
| 143 | +struct TestId(usize); |
| 144 | + |
| 145 | +struct RunnableTest { |
| 146 | + config: Arc<Config>, |
| 147 | + testpaths: TestPaths, |
| 148 | + revision: Option<String>, |
| 149 | +} |
| 150 | + |
| 151 | +impl RunnableTest { |
| 152 | + fn new(test: &CollectedTest) -> Self { |
| 153 | + let config = Arc::clone(&test.config); |
| 154 | + let testpaths = test.testpaths.clone(); |
| 155 | + let revision = test.revision.clone(); |
| 156 | + Self { config, testpaths, revision } |
| 157 | + } |
| 158 | + |
| 159 | + fn run(&self) { |
| 160 | + __rust_begin_short_backtrace(|| { |
| 161 | + crate::runtest::run( |
| 162 | + Arc::clone(&self.config), |
| 163 | + &self.testpaths, |
| 164 | + self.revision.as_deref(), |
| 165 | + ); |
| 166 | + }); |
| 167 | + } |
| 168 | +} |
| 169 | + |
| 170 | +/// Fixed frame used to clean the backtrace with `RUST_BACKTRACE=1`. |
| 171 | +#[inline(never)] |
| 172 | +fn __rust_begin_short_backtrace<T, F: FnOnce() -> T>(f: F) -> T { |
| 173 | + let result = f(); |
| 174 | + |
| 175 | + // prevent this frame from being tail-call optimised away |
| 176 | + hint::black_box(result) |
| 177 | +} |
| 178 | + |
| 179 | +struct RunningTest<'a> { |
| 180 | + test: &'a CollectedTest, |
| 181 | + join_handle: Option<thread::JoinHandle<()>>, |
| 182 | +} |
| 183 | + |
| 184 | +/// Test completion message sent by individual test threads when their test |
| 185 | +/// finishes (successfully or unsuccessfully). |
| 186 | +struct TestCompletion { |
| 187 | + id: TestId, |
| 188 | + outcome: TestOutcome, |
| 189 | + stdout: Option<Vec<u8>>, |
| 190 | +} |
| 191 | + |
| 192 | +#[derive(Clone, Debug, PartialEq, Eq)] |
| 193 | +enum TestOutcome { |
| 194 | + Succeeded, |
| 195 | + Failed { message: Option<&'static str> }, |
| 196 | + Ignored, |
| 197 | +} |
| 198 | + |
| 199 | +impl TestOutcome { |
| 200 | + fn is_failed(&self) -> bool { |
| 201 | + matches!(self, Self::Failed { .. }) |
| 202 | + } |
| 203 | +} |
| 204 | + |
| 205 | +/// Applies command-line arguments for filtering/skipping tests by name. |
| 206 | +/// |
| 207 | +/// Adapted from `filter_tests` in libtest. |
| 208 | +/// |
| 209 | +/// FIXME(#139660): After the libtest dependency is removed, redesign the whole |
| 210 | +/// filtering system to do a better job of understanding and filtering _paths_, |
| 211 | +/// instead of being tied to libtest's substring/exact matching behaviour. |
| 212 | +fn filter_tests(opts: &Config, tests: Vec<CollectedTest>) -> Vec<CollectedTest> { |
| 213 | + let mut filtered = tests; |
| 214 | + |
| 215 | + let matches_filter = |test: &CollectedTest, filter_str: &str| { |
| 216 | + let test_name = &test.desc.name; |
| 217 | + if opts.filter_exact { test_name == filter_str } else { test_name.contains(filter_str) } |
| 218 | + }; |
| 219 | + |
| 220 | + // Remove tests that don't match the test filter |
| 221 | + if !opts.filters.is_empty() { |
| 222 | + filtered.retain(|test| opts.filters.iter().any(|filter| matches_filter(test, filter))); |
| 223 | + } |
| 224 | + |
| 225 | + // Skip tests that match any of the skip filters |
| 226 | + if !opts.skip.is_empty() { |
| 227 | + filtered.retain(|test| !opts.skip.iter().any(|sf| matches_filter(test, sf))); |
| 228 | + } |
| 229 | + |
| 230 | + filtered |
| 231 | +} |
| 232 | + |
| 233 | +/// Determines the number of tests to run concurrently. |
| 234 | +/// |
| 235 | +/// Copied from `get_concurrency` in libtest. |
| 236 | +/// |
| 237 | +/// FIXME(#139660): After the libtest dependency is removed, consider making |
| 238 | +/// bootstrap specify the number of threads on the command-line, instead of |
| 239 | +/// propagating the `RUST_TEST_THREADS` environment variable. |
| 240 | +fn get_concurrency() -> usize { |
| 241 | + if let Ok(value) = env::var("RUST_TEST_THREADS") { |
| 242 | + match value.parse::<NonZero<usize>>().ok() { |
| 243 | + Some(n) => n.get(), |
| 244 | + _ => panic!("RUST_TEST_THREADS is `{value}`, should be a positive integer."), |
| 245 | + } |
| 246 | + } else { |
| 247 | + thread::available_parallelism().map(|n| n.get()).unwrap_or(1) |
| 248 | + } |
| 249 | +} |
| 250 | + |
13 | 251 | /// Information needed to create a `test::TestDescAndFn`.
|
14 | 252 | pub(crate) struct CollectedTest {
|
15 | 253 | pub(crate) desc: CollectedTestDesc,
|
|
0 commit comments