Skip to main content

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