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