compiletest/
executor.rs

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