Skip to content

Commit 62d2626

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 dff10b1 commit 62d2626

File tree

6 files changed

+442
-8
lines changed

6 files changed

+442
-8
lines changed

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

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

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

468468
bootstrap_tool!(
469469
// 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: PathBuf,
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

+233-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,244 @@
44
//! This will hopefully make it easier to migrate away from libtest someday.
55
66
use std::borrow::Cow;
7-
use std::sync::Arc;
7+
use std::collections::HashMap;
8+
use std::hash::{BuildHasherDefault, DefaultHasher};
9+
use std::num::NonZero;
10+
use std::sync::{Arc, Mutex, mpsc};
11+
use std::{env, hint, io, mem, panic, thread};
812

913
use crate::common::{Config, TestPaths};
1014

15+
mod deadline;
16+
mod json;
1117
pub(crate) mod libtest;
1218

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