compiletest/
executor.rs

1//! This module contains a reimplementation of the subset of libtest
2//! functionality needed by compiletest.
3//!
4//! FIXME(Zalathar): Much of this code was originally designed to mimic libtest
5//! as closely as possible, for ease of migration. Now that libtest is no longer
6//! used, we can potentially redesign things to be a better fit for compiletest.
7
8use std::borrow::Cow;
9use std::collections::HashMap;
10use std::hash::{BuildHasherDefault, DefaultHasher};
11use std::num::NonZero;
12use std::sync::{Arc, Mutex, mpsc};
13use std::{env, hint, io, mem, panic, thread};
14
15use crate::common::{Config, TestPaths};
16use crate::output_capture::{self, ConsoleOut};
17use crate::panic_hook;
18
19mod deadline;
20mod json;
21
22pub(crate) fn run_tests(config: &Config, tests: Vec<CollectedTest>) -> bool {
23    let tests_len = tests.len();
24    let filtered = filter_tests(config, tests);
25    // Iterator yielding tests that haven't been started yet.
26    let mut fresh_tests = (0..).map(TestId).zip(&filtered);
27
28    let concurrency = get_concurrency();
29    assert!(concurrency > 0);
30    let concurrent_capacity = concurrency.min(filtered.len());
31
32    let mut listener = json::Listener::new();
33    let mut running_tests = HashMap::with_capacity_and_hasher(
34        concurrent_capacity,
35        BuildHasherDefault::<DefaultHasher>::new(),
36    );
37    let mut deadline_queue = deadline::DeadlineQueue::with_capacity(concurrent_capacity);
38
39    let num_filtered_out = tests_len - filtered.len();
40    listener.suite_started(filtered.len(), num_filtered_out);
41
42    // Channel used by test threads to report the test outcome when done.
43    let (completion_tx, completion_rx) = mpsc::channel::<TestCompletion>();
44
45    // Unlike libtest, we don't have a separate code path for concurrency=1.
46    // In that case, the tests will effectively be run serially anyway.
47    loop {
48        // Spawn new test threads, up to the concurrency limit.
49        while running_tests.len() < concurrency
50            && let Some((id, test)) = fresh_tests.next()
51        {
52            listener.test_started(test);
53            deadline_queue.push(id, test);
54            let join_handle = spawn_test_thread(id, test, completion_tx.clone());
55            running_tests.insert(id, RunningTest { test, join_handle });
56        }
57
58        // If all running tests have finished, and there weren't any unstarted
59        // tests to spawn, then we're done.
60        if running_tests.is_empty() {
61            break;
62        }
63
64        let completion = deadline_queue
65            .read_channel_while_checking_deadlines(
66                &completion_rx,
67                |id| running_tests.contains_key(&id),
68                |_id, test| listener.test_timed_out(test),
69            )
70            .expect("receive channel should never be closed early");
71
72        let RunningTest { test, join_handle } = running_tests.remove(&completion.id).unwrap();
73        if let Some(join_handle) = join_handle {
74            join_handle.join().unwrap_or_else(|_| {
75                panic!("thread for `{}` panicked after reporting completion", test.desc.name)
76            });
77        }
78
79        listener.test_finished(test, &completion);
80
81        if completion.outcome.is_failed() && config.fail_fast {
82            // Prevent any other in-flight threads from panicking when they
83            // write to the completion channel.
84            mem::forget(completion_rx);
85            break;
86        }
87    }
88
89    let suite_passed = listener.suite_finished();
90    suite_passed
91}
92
93/// Spawns a thread to run a single test, and returns the thread's join handle.
94///
95/// Returns `None` if the test was ignored, so no thread was spawned.
96fn spawn_test_thread(
97    id: TestId,
98    test: &CollectedTest,
99    completion_tx: mpsc::Sender<TestCompletion>,
100) -> Option<thread::JoinHandle<()>> {
101    if test.desc.ignore && !test.config.run_ignored {
102        completion_tx
103            .send(TestCompletion { id, outcome: TestOutcome::Ignored, stdout: None })
104            .unwrap();
105        return None;
106    }
107
108    let runnable_test = RunnableTest::new(test);
109    let should_panic = test.desc.should_panic;
110    let run_test = move || run_test_inner(id, should_panic, runnable_test, completion_tx);
111
112    let thread_builder = thread::Builder::new().name(test.desc.name.clone());
113    let join_handle = thread_builder.spawn(run_test).unwrap();
114    Some(join_handle)
115}
116
117/// Runs a single test, within the dedicated thread spawned by the caller.
118fn run_test_inner(
119    id: TestId,
120    should_panic: ShouldPanic,
121    runnable_test: RunnableTest,
122    completion_sender: mpsc::Sender<TestCompletion>,
123) {
124    let capture = CaptureKind::for_config(&runnable_test.config);
125
126    // Install a panic-capture buffer for use by the custom panic hook.
127    if capture.should_set_panic_hook() {
128        panic_hook::set_capture_buf(Default::default());
129    }
130
131    if let CaptureKind::Old { ref buf } = capture {
132        io::set_output_capture(Some(Arc::clone(buf)));
133    }
134
135    let stdout = capture.stdout();
136    let stderr = capture.stderr();
137
138    let panic_payload = panic::catch_unwind(move || runnable_test.run(stdout, stderr)).err();
139
140    if let Some(panic_buf) = panic_hook::take_capture_buf() {
141        let panic_buf = panic_buf.lock().unwrap_or_else(|e| e.into_inner());
142        // Forward any captured panic message to (captured) stderr.
143        write!(stderr, "{panic_buf}");
144    }
145    if matches!(capture, CaptureKind::Old { .. }) {
146        io::set_output_capture(None);
147    }
148
149    let outcome = match (should_panic, panic_payload) {
150        (ShouldPanic::No, None) | (ShouldPanic::Yes, Some(_)) => TestOutcome::Succeeded,
151        (ShouldPanic::No, Some(_)) => TestOutcome::Failed { message: None },
152        (ShouldPanic::Yes, None) => {
153            TestOutcome::Failed { message: Some("test did not panic as expected") }
154        }
155    };
156
157    let stdout = capture.into_inner();
158    completion_sender.send(TestCompletion { id, outcome, stdout }).unwrap();
159}
160
161enum CaptureKind {
162    /// Do not capture test-runner output, for `--no-capture`.
163    ///
164    /// (This does not affect `rustc` and other subprocesses spawned by test
165    /// runners, whose output is always captured.)
166    None,
167
168    /// Use the old output-capture implementation, which relies on the unstable
169    /// library feature `#![feature(internal_output_capture)]`.
170    Old { buf: Arc<Mutex<Vec<u8>>> },
171
172    /// Use the new output-capture implementation, which only uses stable Rust.
173    New { buf: output_capture::CaptureBuf },
174}
175
176impl CaptureKind {
177    fn for_config(config: &Config) -> Self {
178        if config.nocapture {
179            Self::None
180        } else if config.new_output_capture {
181            Self::New { buf: output_capture::CaptureBuf::new() }
182        } else {
183            // Create a capure buffer for `io::set_output_capture`.
184            Self::Old { buf: Default::default() }
185        }
186    }
187
188    fn should_set_panic_hook(&self) -> bool {
189        match self {
190            Self::None => false,
191            Self::Old { .. } => true,
192            Self::New { .. } => true,
193        }
194    }
195
196    fn stdout(&self) -> &dyn ConsoleOut {
197        self.capture_buf_or(&output_capture::Stdout)
198    }
199
200    fn stderr(&self) -> &dyn ConsoleOut {
201        self.capture_buf_or(&output_capture::Stderr)
202    }
203
204    fn capture_buf_or<'a>(&'a self, fallback: &'a dyn ConsoleOut) -> &'a dyn ConsoleOut {
205        match self {
206            Self::None | Self::Old { .. } => fallback,
207            Self::New { buf } => buf,
208        }
209    }
210
211    fn into_inner(self) -> Option<Vec<u8>> {
212        match self {
213            Self::None => None,
214            Self::Old { buf } => Some(buf.lock().unwrap_or_else(|e| e.into_inner()).to_vec()),
215            Self::New { buf } => Some(buf.into_inner().into()),
216        }
217    }
218}
219
220#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
221struct TestId(usize);
222
223struct RunnableTest {
224    config: Arc<Config>,
225    testpaths: TestPaths,
226    revision: Option<String>,
227}
228
229impl RunnableTest {
230    fn new(test: &CollectedTest) -> Self {
231        let config = Arc::clone(&test.config);
232        let testpaths = test.testpaths.clone();
233        let revision = test.revision.clone();
234        Self { config, testpaths, revision }
235    }
236
237    fn run(&self, stdout: &dyn ConsoleOut, stderr: &dyn ConsoleOut) {
238        __rust_begin_short_backtrace(|| {
239            crate::runtest::run(
240                Arc::clone(&self.config),
241                stdout,
242                stderr,
243                &self.testpaths,
244                self.revision.as_deref(),
245            );
246        });
247    }
248}
249
250/// Fixed frame used to clean the backtrace with `RUST_BACKTRACE=1`.
251#[inline(never)]
252fn __rust_begin_short_backtrace<T, F: FnOnce() -> T>(f: F) -> T {
253    let result = f();
254
255    // prevent this frame from being tail-call optimised away
256    hint::black_box(result)
257}
258
259struct RunningTest<'a> {
260    test: &'a CollectedTest,
261    join_handle: Option<thread::JoinHandle<()>>,
262}
263
264/// Test completion message sent by individual test threads when their test
265/// finishes (successfully or unsuccessfully).
266struct TestCompletion {
267    id: TestId,
268    outcome: TestOutcome,
269    stdout: Option<Vec<u8>>,
270}
271
272#[derive(Clone, Debug, PartialEq, Eq)]
273enum TestOutcome {
274    Succeeded,
275    Failed { message: Option<&'static str> },
276    Ignored,
277}
278
279impl TestOutcome {
280    fn is_failed(&self) -> bool {
281        matches!(self, Self::Failed { .. })
282    }
283}
284
285/// Applies command-line arguments for filtering/skipping tests by name.
286///
287/// Adapted from `filter_tests` in libtest.
288///
289/// FIXME(#139660): Now that libtest has been removed, redesign the whole filtering system to
290/// do a better job of understanding and filtering _paths_, instead of being tied to libtest's
291/// substring/exact matching behaviour.
292fn filter_tests(opts: &Config, tests: Vec<CollectedTest>) -> Vec<CollectedTest> {
293    let mut filtered = tests;
294
295    let matches_filter = |test: &CollectedTest, filter_str: &str| {
296        let test_name = &test.desc.name;
297        if opts.filter_exact { test_name == filter_str } else { test_name.contains(filter_str) }
298    };
299
300    // Remove tests that don't match the test filter
301    if !opts.filters.is_empty() {
302        filtered.retain(|test| opts.filters.iter().any(|filter| matches_filter(test, filter)));
303    }
304
305    // Skip tests that match any of the skip filters
306    if !opts.skip.is_empty() {
307        filtered.retain(|test| !opts.skip.iter().any(|sf| matches_filter(test, sf)));
308    }
309
310    filtered
311}
312
313/// Determines the number of tests to run concurrently.
314///
315/// Copied from `get_concurrency` in libtest.
316///
317/// FIXME(#139660): After the libtest dependency is removed, consider making bootstrap specify the
318/// number of threads on the command-line, instead of propagating the `RUST_TEST_THREADS`
319/// environment variable.
320fn get_concurrency() -> usize {
321    if let Ok(value) = env::var("RUST_TEST_THREADS") {
322        match value.parse::<NonZero<usize>>().ok() {
323            Some(n) => n.get(),
324            _ => panic!("RUST_TEST_THREADS is `{value}`, should be a positive integer."),
325        }
326    } else {
327        thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
328    }
329}
330
331/// Information that was historically needed to create a libtest `TestDescAndFn`.
332pub(crate) struct CollectedTest {
333    pub(crate) desc: CollectedTestDesc,
334    pub(crate) config: Arc<Config>,
335    pub(crate) testpaths: TestPaths,
336    pub(crate) revision: Option<String>,
337}
338
339/// Information that was historically needed to create a libtest `TestDesc`.
340pub(crate) struct CollectedTestDesc {
341    pub(crate) name: String,
342    pub(crate) ignore: bool,
343    pub(crate) ignore_message: Option<Cow<'static, str>>,
344    pub(crate) should_panic: ShouldPanic,
345}
346
347/// Whether console output should be colored or not.
348#[derive(Copy, Clone, Default, Debug)]
349pub enum ColorConfig {
350    #[default]
351    AutoColor,
352    AlwaysColor,
353    NeverColor,
354}
355
356/// Whether test is expected to panic or not.
357#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
358pub(crate) enum ShouldPanic {
359    No,
360    Yes,
361}