Skip to content

Commit e3d6813

Browse files
committed
compiletest: Add an experimental new executor to replace libtest
The new executor can be enabled by passing `--new-executor` or `-n` to compiletest. For example: `./x test ui -- -n`
1 parent 6fda3e5 commit e3d6813

File tree

6 files changed

+452
-12
lines changed

6 files changed

+452
-12
lines changed

src/bootstrap/src/core/build_steps/tool.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ macro_rules! bootstrap_tool {
462462
}
463463
}
464464

465-
pub(crate) const COMPILETEST_ALLOW_FEATURES: &str = "test";
465+
pub(crate) const COMPILETEST_ALLOW_FEATURES: &str = "test,internal_output_capture";
466466

467467
bootstrap_tool!(
468468
// This is marked as an external tool because it includes dependencies

src/tools/compiletest/src/common.rs

+5
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,11 @@ pub struct Config {
414414
/// cross-compilation scenarios that do not otherwise want/need to `-Zbuild-std`. Used in e.g.
415415
/// ABI tests.
416416
pub minicore_path: Utf8PathBuf,
417+
418+
/// If true, run tests with the "new" executor that was written to replace
419+
/// compiletest's dependency on libtest. Eventually this will become the
420+
/// default, and the libtest dependency will be removed.
421+
pub new_executor: bool,
417422
}
418423

419424
impl Config {

src/tools/compiletest/src/executor.rs

+243-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,253 @@
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.
53
64
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};
810

911
use crate::common::{Config, TestPaths};
1012

13+
mod deadline;
14+
mod json;
1115
pub(crate) mod libtest;
1216

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+
13251
/// Information needed to create a `test::TestDescAndFn`.
14252
pub(crate) struct CollectedTest {
15253
pub(crate) desc: CollectedTestDesc,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use std::collections::VecDeque;
2+
use std::sync::mpsc::{self, RecvError, RecvTimeoutError};
3+
use std::time::{Duration, Instant};
4+
5+
use crate::executor::{CollectedTest, TestId};
6+
7+
const TEST_WARN_TIMEOUT_S: u64 = 60;
8+
9+
struct DeadlineEntry<'a> {
10+
id: TestId,
11+
test: &'a CollectedTest,
12+
deadline: Instant,
13+
}
14+
15+
pub(crate) struct DeadlineQueue<'a> {
16+
queue: VecDeque<DeadlineEntry<'a>>,
17+
}
18+
19+
impl<'a> DeadlineQueue<'a> {
20+
pub(crate) fn with_capacity(capacity: usize) -> Self {
21+
Self { queue: VecDeque::with_capacity(capacity) }
22+
}
23+
24+
pub(crate) fn push(&mut self, id: TestId, test: &'a CollectedTest) {
25+
let deadline = Instant::now() + Duration::from_secs(TEST_WARN_TIMEOUT_S);
26+
self.queue.push_back(DeadlineEntry { id, test, deadline });
27+
}
28+
29+
/// Equivalent to `rx.read()`, except that if any test exceeds its deadline
30+
/// during the wait, the given callback will also be called for that test.
31+
pub(crate) fn read_channel_while_checking_deadlines<T>(
32+
&mut self,
33+
rx: &mpsc::Receiver<T>,
34+
mut on_deadline_passed: impl FnMut(TestId, &CollectedTest),
35+
) -> Result<T, RecvError> {
36+
loop {
37+
let Some(next_deadline) = self.next_deadline() else {
38+
// All currently-running tests have already exceeded their
39+
// deadline, so do a normal receive.
40+
return rx.recv();
41+
};
42+
let wait_duration = next_deadline.saturating_duration_since(Instant::now());
43+
44+
let recv_result = rx.recv_timeout(wait_duration);
45+
match recv_result {
46+
Ok(value) => return Ok(value),
47+
Err(RecvTimeoutError::Timeout) => {
48+
// Notify the callback of tests that have exceeded their
49+
// deadline, then loop and do annother channel read.
50+
for DeadlineEntry { id, test, .. } in self.remove_tests_past_deadline() {
51+
on_deadline_passed(id, test);
52+
}
53+
}
54+
Err(RecvTimeoutError::Disconnected) => return Err(RecvError),
55+
}
56+
}
57+
}
58+
59+
fn next_deadline(&self) -> Option<Instant> {
60+
Some(self.queue.front()?.deadline)
61+
}
62+
63+
fn remove_tests_past_deadline(&mut self) -> Vec<DeadlineEntry<'a>> {
64+
let now = Instant::now();
65+
let mut timed_out = vec![];
66+
while let Some(deadline_entry) = pop_front_if(&mut self.queue, |entry| now < entry.deadline)
67+
{
68+
timed_out.push(deadline_entry);
69+
}
70+
timed_out
71+
}
72+
}
73+
74+
/// FIXME(vec_deque_pop_if): Use `VecDeque::pop_front_if` when it is stable in bootstrap.
75+
fn pop_front_if<T>(queue: &mut VecDeque<T>, predicate: impl FnOnce(&T) -> bool) -> Option<T> {
76+
let first = queue.front()?;
77+
if predicate(first) { queue.pop_front() } else { None }
78+
}

0 commit comments

Comments
 (0)