compiletest: Add an experimental new executor to replace libtest · rust-lang/rust@e3d6813 (original) (raw)
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.
`
5
3
``
6
4
`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};
`
8
10
``
9
11
`use crate::common::{Config, TestPaths};
`
10
12
``
``
13
`+
mod deadline;
`
``
14
`+
mod json;
`
11
15
`pub(crate) mod libtest;
`
12
16
``
``
17
`+
pub(crate) fn run_tests(config: &Config, tests: Vec) -> 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::::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::();
`
``
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,
`
``
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,
`
``
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,
`
``
147
`+
testpaths: TestPaths,
`
``
148
`+
revision: Option,
`
``
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>,
`
``
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) -> Vec {
`
``
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>().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
+
13
251
`` /// Information needed to create a test::TestDescAndFn
.
``
14
252
`pub(crate) struct CollectedTest {
`
15
253
`pub(crate) desc: CollectedTestDesc,
`