Rollup merge of #139660 - Zalathar:new-executor, r=jieyouxu

compiletest: Add an experimental new executor to replace libtest

This PR adds a new "executor" to compiletest for running the list of collected tests, to eventually replace the current dependency on unstable libtest internals.

The new executor is currently inactive by default. It must be activated explicitly by passing `-n` or `--new-executor` to compiletest, e.g. `./x test ui -- -n`.

(After some amount of wider manual testing, the new executor will hopefully be made the default, and the libtest dependency can be removed. Contributors should not notice any change.)

The new executor is a stripped-down rewrite of the subset of libtest needed by compiletest.

# Supported functionality
- Specifying the number of concurrent tests with `RUST_TEST_THREADS`
- Filtering and skipping tests by name (substring or exact-match)
- Forcibly running ignored tests with `--ignored`
- Optional fail-fast with `--fail-fast`
- JSON output, compatible with bootstrap's parser for libtest output
- Running each test in its own thread
- Short backtraces that ignore the executor itself
- Slow test detection, with a hard-coded timeout of 60 seconds
- Capturing stdout/stderr, via `#![feature(internal_output_capture)]`
- Suppressing output capture with `--no-capture`

# Unsupported functionality
- Non-JSON output, as this is handled by bootstrap instead
- Separate code path for concurrency=1, as the concurrent path should handle this case naturally
- Fallback to running tests synchronously if new threads can't be spawned
- Special handling of hosts that don't support basic functionality like threads or timers
  - Our ability to test *targets* should be unaffected
- Graceful handling of some edge cases that could occur in arbitrary user-written unit tests, but would represent bugs in compiletest
- Due to the current need for output capture, the new executor is still not entirely written in stable Rust

---
r? jieyouxu
This commit is contained in:
Stuart Cook 2025-04-15 15:47:26 +10:00 committed by GitHub
commit efcf4f9d2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 575 additions and 118 deletions

View file

@ -3,7 +3,7 @@
use crate::core::build_steps::compile::{
add_to_sysroot, run_cargo, rustc_cargo, rustc_cargo_env, std_cargo, std_crates_for_run_make,
};
use crate::core::build_steps::tool::{SourceType, prepare_tool_cargo};
use crate::core::build_steps::tool::{COMPILETEST_ALLOW_FEATURES, SourceType, prepare_tool_cargo};
use crate::core::builder::{
self, Alias, Builder, Kind, RunConfig, ShouldRun, Step, crate_description,
};
@ -416,7 +416,7 @@ impl Step for Compiletest {
&[],
);
cargo.allow_features("test");
cargo.allow_features(COMPILETEST_ALLOW_FEATURES);
// For ./x.py clippy, don't run with --all-targets because
// linting tests and benchmarks can produce very noisy results

View file

@ -15,7 +15,7 @@ use crate::core::build_steps::doc::DocumentationFormat;
use crate::core::build_steps::gcc::{Gcc, add_cg_gcc_cargo_flags};
use crate::core::build_steps::llvm::get_llvm_version;
use crate::core::build_steps::synthetic_targets::MirOptPanicAbortSyntheticTarget;
use crate::core::build_steps::tool::{self, SourceType, Tool};
use crate::core::build_steps::tool::{self, COMPILETEST_ALLOW_FEATURES, SourceType, Tool};
use crate::core::build_steps::toolstate::ToolState;
use crate::core::build_steps::{compile, dist, llvm};
use crate::core::builder::{
@ -721,7 +721,7 @@ impl Step for CompiletestTest {
SourceType::InTree,
&[],
);
cargo.allow_features("test");
cargo.allow_features(COMPILETEST_ALLOW_FEATURES);
run_cargo_test(cargo, &[], &[], "compiletest self test", host, builder);
}
}

View file

@ -444,7 +444,11 @@ macro_rules! bootstrap_tool {
SourceType::InTree
},
extra_features: vec![],
allow_features: concat!($($allow_features)*),
allow_features: {
let mut _value = "";
$( _value = $allow_features; )?
_value
},
cargo_args: vec![],
artifact_kind: if false $(|| $artifact_kind == ToolArtifactKind::Library)* {
ToolArtifactKind::Library
@ -458,6 +462,8 @@ macro_rules! bootstrap_tool {
}
}
pub(crate) const COMPILETEST_ALLOW_FEATURES: &str = "test,internal_output_capture";
bootstrap_tool!(
// This is marked as an external tool because it includes dependencies
// from submodules. Trying to keep the lints in sync between all the repos
@ -468,7 +474,7 @@ bootstrap_tool!(
Tidy, "src/tools/tidy", "tidy";
Linkchecker, "src/tools/linkchecker", "linkchecker";
CargoTest, "src/tools/cargotest", "cargotest";
Compiletest, "src/tools/compiletest", "compiletest", is_unstable_tool = true, allow_features = "test";
Compiletest, "src/tools/compiletest", "compiletest", is_unstable_tool = true, allow_features = COMPILETEST_ALLOW_FEATURES;
BuildManifest, "src/tools/build-manifest", "build-manifest";
RemoteTestClient, "src/tools/remote-test-client", "remote-test-client";
RustInstaller, "src/tools/rust-installer", "rust-installer";
@ -483,7 +489,8 @@ bootstrap_tool!(
GenerateCopyright, "src/tools/generate-copyright", "generate-copyright";
SuggestTests, "src/tools/suggest-tests", "suggest-tests";
GenerateWindowsSys, "src/tools/generate-windows-sys", "generate-windows-sys";
RustdocGUITest, "src/tools/rustdoc-gui-test", "rustdoc-gui-test", is_unstable_tool = true, allow_features = "test";
// rustdoc-gui-test has a crate dependency on compiletest, so it needs the same unstable features.
RustdocGUITest, "src/tools/rustdoc-gui-test", "rustdoc-gui-test", is_unstable_tool = true, allow_features = COMPILETEST_ALLOW_FEATURES;
CoverageDump, "src/tools/coverage-dump", "coverage-dump";
WasmComponentLd, "src/tools/wasm-component-ld", "wasm-component-ld", is_unstable_tool = true, allow_features = "min_specialization";
UnicodeTableGenerator, "src/tools/unicode-table-generator", "unicode-table-generator";

View file

@ -414,6 +414,11 @@ pub struct Config {
/// cross-compilation scenarios that do not otherwise want/need to `-Zbuild-std`. Used in e.g.
/// ABI tests.
pub minicore_path: Utf8PathBuf,
/// If true, run tests with the "new" executor that was written to replace
/// compiletest's dependency on libtest. Eventually this will become the
/// default, and the libtest dependency will be removed.
pub new_executor: bool,
}
impl Config {

View file

@ -1,22 +1,251 @@
//! This module encapsulates all of the code that interacts directly with
//! libtest, to execute the collected tests.
//!
//! This will hopefully make it easier to migrate away from libtest someday.
//! This module contains a reimplementation of the subset of libtest
//! functionality needed by compiletest.
use std::borrow::Cow;
use std::io;
use std::sync::Arc;
use std::collections::HashMap;
use std::hash::{BuildHasherDefault, DefaultHasher};
use std::num::NonZero;
use std::sync::{Arc, Mutex, mpsc};
use std::{env, hint, io, mem, panic, thread};
use crate::common::{Config, TestPaths};
/// Delegates to libtest to run the list of collected tests.
///
/// Returns `Ok(true)` if all tests passed, or `Ok(false)` if one or more tests failed.
pub(crate) fn execute_tests(config: &Config, tests: Vec<CollectedTest>) -> io::Result<bool> {
let opts = test_opts(config);
let tests = tests.into_iter().map(|t| t.into_libtest()).collect::<Vec<_>>();
mod deadline;
mod json;
pub(crate) mod libtest;
test::run_tests_console(&opts, tests)
pub(crate) fn run_tests(config: &Config, tests: Vec<CollectedTest>) -> bool {
let tests_len = tests.len();
let filtered = filter_tests(config, tests);
// Iterator yielding tests that haven't been started yet.
let mut fresh_tests = (0..).map(TestId).zip(&filtered);
let concurrency = get_concurrency();
assert!(concurrency > 0);
let concurrent_capacity = concurrency.min(filtered.len());
let mut listener = json::Listener::new();
let mut running_tests = HashMap::with_capacity_and_hasher(
concurrent_capacity,
BuildHasherDefault::<DefaultHasher>::new(),
);
let mut deadline_queue = deadline::DeadlineQueue::with_capacity(concurrent_capacity);
let num_filtered_out = tests_len - filtered.len();
listener.suite_started(filtered.len(), num_filtered_out);
// Channel used by test threads to report the test outcome when done.
let (completion_tx, completion_rx) = mpsc::channel::<TestCompletion>();
// Unlike libtest, we don't have a separate code path for concurrency=1.
// In that case, the tests will effectively be run serially anyway.
loop {
// Spawn new test threads, up to the concurrency limit.
// FIXME(let_chains): Use a let-chain here when stable in bootstrap.
'spawn: while running_tests.len() < concurrency {
let Some((id, test)) = fresh_tests.next() else { break 'spawn };
listener.test_started(test);
deadline_queue.push(id, test);
let join_handle = spawn_test_thread(id, test, completion_tx.clone());
running_tests.insert(id, RunningTest { test, join_handle });
}
// If all running tests have finished, and there weren't any unstarted
// tests to spawn, then we're done.
if running_tests.is_empty() {
break;
}
let completion = deadline_queue
.read_channel_while_checking_deadlines(&completion_rx, |_id, test| {
listener.test_timed_out(test);
})
.expect("receive channel should never be closed early");
let RunningTest { test, join_handle } = running_tests.remove(&completion.id).unwrap();
if let Some(join_handle) = join_handle {
join_handle.join().unwrap_or_else(|_| {
panic!("thread for `{}` panicked after reporting completion", test.desc.name)
});
}
listener.test_finished(test, &completion);
if completion.outcome.is_failed() && config.fail_fast {
// Prevent any other in-flight threads from panicking when they
// write to the completion channel.
mem::forget(completion_rx);
break;
}
}
let suite_passed = listener.suite_finished();
suite_passed
}
/// Spawns a thread to run a single test, and returns the thread's join handle.
///
/// Returns `None` if the test was ignored, so no thread was spawned.
fn spawn_test_thread(
id: TestId,
test: &CollectedTest,
completion_tx: mpsc::Sender<TestCompletion>,
) -> Option<thread::JoinHandle<()>> {
if test.desc.ignore && !test.config.run_ignored {
completion_tx
.send(TestCompletion { id, outcome: TestOutcome::Ignored, stdout: None })
.unwrap();
return None;
}
let runnable_test = RunnableTest::new(test);
let should_panic = test.desc.should_panic;
let run_test = move || run_test_inner(id, should_panic, runnable_test, completion_tx);
let thread_builder = thread::Builder::new().name(test.desc.name.clone());
let join_handle = thread_builder.spawn(run_test).unwrap();
Some(join_handle)
}
/// Runs a single test, within the dedicated thread spawned by the caller.
fn run_test_inner(
id: TestId,
should_panic: ShouldPanic,
runnable_test: RunnableTest,
completion_sender: mpsc::Sender<TestCompletion>,
) {
let is_capture = !runnable_test.config.nocapture;
let capture_buf = is_capture.then(|| Arc::new(Mutex::new(vec![])));
if let Some(capture_buf) = &capture_buf {
io::set_output_capture(Some(Arc::clone(capture_buf)));
}
let panic_payload = panic::catch_unwind(move || runnable_test.run()).err();
if is_capture {
io::set_output_capture(None);
}
let outcome = match (should_panic, panic_payload) {
(ShouldPanic::No, None) | (ShouldPanic::Yes, Some(_)) => TestOutcome::Succeeded,
(ShouldPanic::No, Some(_)) => TestOutcome::Failed { message: None },
(ShouldPanic::Yes, None) => {
TestOutcome::Failed { message: Some("test did not panic as expected") }
}
};
let stdout = capture_buf.map(|mutex| mutex.lock().unwrap_or_else(|e| e.into_inner()).to_vec());
completion_sender.send(TestCompletion { id, outcome, stdout }).unwrap();
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct TestId(usize);
struct RunnableTest {
config: Arc<Config>,
testpaths: TestPaths,
revision: Option<String>,
}
impl RunnableTest {
fn new(test: &CollectedTest) -> Self {
let config = Arc::clone(&test.config);
let testpaths = test.testpaths.clone();
let revision = test.revision.clone();
Self { config, testpaths, revision }
}
fn run(&self) {
__rust_begin_short_backtrace(|| {
crate::runtest::run(
Arc::clone(&self.config),
&self.testpaths,
self.revision.as_deref(),
);
});
}
}
/// Fixed frame used to clean the backtrace with `RUST_BACKTRACE=1`.
#[inline(never)]
fn __rust_begin_short_backtrace<T, F: FnOnce() -> T>(f: F) -> T {
let result = f();
// prevent this frame from being tail-call optimised away
hint::black_box(result)
}
struct RunningTest<'a> {
test: &'a CollectedTest,
join_handle: Option<thread::JoinHandle<()>>,
}
/// Test completion message sent by individual test threads when their test
/// finishes (successfully or unsuccessfully).
struct TestCompletion {
id: TestId,
outcome: TestOutcome,
stdout: Option<Vec<u8>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum TestOutcome {
Succeeded,
Failed { message: Option<&'static str> },
Ignored,
}
impl TestOutcome {
fn is_failed(&self) -> bool {
matches!(self, Self::Failed { .. })
}
}
/// Applies command-line arguments for filtering/skipping tests by name.
///
/// Adapted from `filter_tests` in libtest.
///
/// FIXME(#139660): After the libtest dependency is removed, redesign the whole
/// filtering system to do a better job of understanding and filtering _paths_,
/// instead of being tied to libtest's substring/exact matching behaviour.
fn filter_tests(opts: &Config, tests: Vec<CollectedTest>) -> Vec<CollectedTest> {
let mut filtered = tests;
let matches_filter = |test: &CollectedTest, filter_str: &str| {
let test_name = &test.desc.name;
if opts.filter_exact { test_name == filter_str } else { test_name.contains(filter_str) }
};
// Remove tests that don't match the test filter
if !opts.filters.is_empty() {
filtered.retain(|test| opts.filters.iter().any(|filter| matches_filter(test, filter)));
}
// Skip tests that match any of the skip filters
if !opts.skip.is_empty() {
filtered.retain(|test| !opts.skip.iter().any(|sf| matches_filter(test, sf)));
}
filtered
}
/// Determines the number of tests to run concurrently.
///
/// Copied from `get_concurrency` in libtest.
///
/// FIXME(#139660): After the libtest dependency is removed, consider making
/// bootstrap specify the number of threads on the command-line, instead of
/// propagating the `RUST_TEST_THREADS` environment variable.
fn get_concurrency() -> usize {
if let Ok(value) = env::var("RUST_TEST_THREADS") {
match value.parse::<NonZero<usize>>().ok() {
Some(n) => n.get(),
_ => panic!("RUST_TEST_THREADS is `{value}`, should be a positive integer."),
}
} else {
thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
}
}
/// Information needed to create a `test::TestDescAndFn`.
@ -35,45 +264,6 @@ pub(crate) struct CollectedTestDesc {
pub(crate) should_panic: ShouldPanic,
}
impl CollectedTest {
fn into_libtest(self) -> test::TestDescAndFn {
let Self { desc, config, testpaths, revision } = self;
let CollectedTestDesc { name, ignore, ignore_message, should_panic } = desc;
// Libtest requires the ignore message to be a &'static str, so we might
// have to leak memory to create it. This is fine, as we only do so once
// per test, so the leak won't grow indefinitely.
let ignore_message = ignore_message.map(|msg| match msg {
Cow::Borrowed(s) => s,
Cow::Owned(s) => &*String::leak(s),
});
let desc = test::TestDesc {
name: test::DynTestName(name),
ignore,
ignore_message,
source_file: "",
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 0,
should_panic: should_panic.to_libtest(),
compile_fail: false,
no_run: false,
test_type: test::TestType::Unknown,
};
// This closure is invoked when libtest returns control to compiletest
// to execute the test.
let testfn = test::DynTestFn(Box::new(move || {
crate::runtest::run(config, &testpaths, revision.as_deref());
Ok(())
}));
test::TestDescAndFn { desc, testfn }
}
}
/// Whether console output should be colored or not.
#[derive(Copy, Clone, Default, Debug)]
pub enum ColorConfig {
@ -83,16 +273,6 @@ pub enum ColorConfig {
NeverColor,
}
impl ColorConfig {
fn to_libtest(self) -> test::ColorConfig {
match self {
Self::AutoColor => test::ColorConfig::AutoColor,
Self::AlwaysColor => test::ColorConfig::AlwaysColor,
Self::NeverColor => test::ColorConfig::NeverColor,
}
}
}
/// Format of the test results output.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum OutputFormat {
@ -105,52 +285,9 @@ pub enum OutputFormat {
Json,
}
impl OutputFormat {
fn to_libtest(self) -> test::OutputFormat {
match self {
Self::Pretty => test::OutputFormat::Pretty,
Self::Terse => test::OutputFormat::Terse,
Self::Json => test::OutputFormat::Json,
}
}
}
/// Whether test is expected to panic or not.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) enum ShouldPanic {
No,
Yes,
}
impl ShouldPanic {
fn to_libtest(self) -> test::ShouldPanic {
match self {
Self::No => test::ShouldPanic::No,
Self::Yes => test::ShouldPanic::Yes,
}
}
}
fn test_opts(config: &Config) -> test::TestOpts {
test::TestOpts {
exclude_should_panic: false,
filters: config.filters.clone(),
filter_exact: config.filter_exact,
run_ignored: if config.run_ignored { test::RunIgnored::Yes } else { test::RunIgnored::No },
format: config.format.to_libtest(),
logfile: None,
run_tests: true,
bench_benchmarks: true,
nocapture: config.nocapture,
color: config.color.to_libtest(),
shuffle: false,
shuffle_seed: None,
test_threads: None,
skip: config.skip.clone(),
list: false,
options: test::Options::new(),
time_options: None,
force_run_in_process: false,
fail_fast: config.fail_fast,
}
}

View file

@ -0,0 +1,78 @@
use std::collections::VecDeque;
use std::sync::mpsc::{self, RecvError, RecvTimeoutError};
use std::time::{Duration, Instant};
use crate::executor::{CollectedTest, TestId};
const TEST_WARN_TIMEOUT_S: u64 = 60;
struct DeadlineEntry<'a> {
id: TestId,
test: &'a CollectedTest,
deadline: Instant,
}
pub(crate) struct DeadlineQueue<'a> {
queue: VecDeque<DeadlineEntry<'a>>,
}
impl<'a> DeadlineQueue<'a> {
pub(crate) fn with_capacity(capacity: usize) -> Self {
Self { queue: VecDeque::with_capacity(capacity) }
}
pub(crate) fn push(&mut self, id: TestId, test: &'a CollectedTest) {
let deadline = Instant::now() + Duration::from_secs(TEST_WARN_TIMEOUT_S);
self.queue.push_back(DeadlineEntry { id, test, deadline });
}
/// Equivalent to `rx.read()`, except that if any test exceeds its deadline
/// during the wait, the given callback will also be called for that test.
pub(crate) fn read_channel_while_checking_deadlines<T>(
&mut self,
rx: &mpsc::Receiver<T>,
mut on_deadline_passed: impl FnMut(TestId, &CollectedTest),
) -> Result<T, RecvError> {
loop {
let Some(next_deadline) = self.next_deadline() else {
// All currently-running tests have already exceeded their
// deadline, so do a normal receive.
return rx.recv();
};
let wait_duration = next_deadline.saturating_duration_since(Instant::now());
let recv_result = rx.recv_timeout(wait_duration);
match recv_result {
Ok(value) => return Ok(value),
Err(RecvTimeoutError::Timeout) => {
// Notify the callback of tests that have exceeded their
// deadline, then loop and do annother channel read.
for DeadlineEntry { id, test, .. } in self.remove_tests_past_deadline() {
on_deadline_passed(id, test);
}
}
Err(RecvTimeoutError::Disconnected) => return Err(RecvError),
}
}
}
fn next_deadline(&self) -> Option<Instant> {
Some(self.queue.front()?.deadline)
}
fn remove_tests_past_deadline(&mut self) -> Vec<DeadlineEntry<'a>> {
let now = Instant::now();
let mut timed_out = vec![];
while let Some(deadline_entry) = pop_front_if(&mut self.queue, |entry| now < entry.deadline)
{
timed_out.push(deadline_entry);
}
timed_out
}
}
/// FIXME(vec_deque_pop_if): Use `VecDeque::pop_front_if` when it is stable in bootstrap.
fn pop_front_if<T>(queue: &mut VecDeque<T>, predicate: impl FnOnce(&T) -> bool) -> Option<T> {
let first = queue.front()?;
if predicate(first) { queue.pop_front() } else { None }
}

View file

@ -0,0 +1,111 @@
//! Collects statistics and emits suite/test events as JSON messages, using
//! the same JSON format as libtest's JSON formatter.
//!
//! These messages are then parsed by bootstrap, which replaces them with
//! user-friendly terminal output.
use std::time::Instant;
use serde_json::json;
use crate::executor::{CollectedTest, TestCompletion, TestOutcome};
pub(crate) struct Listener {
suite_start: Option<Instant>,
passed: usize,
failed: usize,
ignored: usize,
filtered_out: usize,
}
impl Listener {
pub(crate) fn new() -> Self {
Self { suite_start: None, passed: 0, failed: 0, ignored: 0, filtered_out: 0 }
}
fn print_message(&self, message: &serde_json::Value) {
println!("{message}");
}
fn now(&self) -> Instant {
Instant::now()
}
pub(crate) fn suite_started(&mut self, test_count: usize, filtered_out: usize) {
self.suite_start = Some(self.now());
self.filtered_out = filtered_out;
let message = json!({ "type": "suite", "event": "started", "test_count": test_count });
self.print_message(&message);
}
pub(crate) fn test_started(&mut self, test: &CollectedTest) {
let name = test.desc.name.as_str();
let message = json!({ "type": "test", "event": "started", "name": name });
self.print_message(&message);
}
pub(crate) fn test_timed_out(&mut self, test: &CollectedTest) {
let name = test.desc.name.as_str();
let message = json!({ "type": "test", "event": "timeout", "name": name });
self.print_message(&message);
}
pub(crate) fn test_finished(&mut self, test: &CollectedTest, completion: &TestCompletion) {
let event;
let name = test.desc.name.as_str();
let mut maybe_message = None;
let maybe_stdout = completion.stdout.as_deref().map(String::from_utf8_lossy);
match completion.outcome {
TestOutcome::Succeeded => {
self.passed += 1;
event = "ok";
}
TestOutcome::Failed { message } => {
self.failed += 1;
maybe_message = message;
event = "failed";
}
TestOutcome::Ignored => {
self.ignored += 1;
maybe_message = test.desc.ignore_message.as_deref();
event = "ignored";
}
};
// This emits optional fields as `null`, instead of omitting them
// completely as libtest does, but bootstrap can parse the result
// either way.
let json = json!({
"type": "test",
"event": event,
"name": name,
"message": maybe_message,
"stdout": maybe_stdout,
});
self.print_message(&json);
}
pub(crate) fn suite_finished(&mut self) -> bool {
let exec_time = self.suite_start.map(|start| (self.now() - start).as_secs_f64());
let suite_passed = self.failed == 0;
let event = if suite_passed { "ok" } else { "failed" };
let message = json!({
"type": "suite",
"event": event,
"passed": self.passed,
"failed": self.failed,
"ignored": self.ignored,
// Compiletest doesn't run any benchmarks, but we still need to set this
// field to 0 so that bootstrap's JSON parser can read our message.
"measured": 0,
"filtered_out": self.filtered_out,
"exec_time": exec_time,
});
self.print_message(&message);
suite_passed
}
}

View file

@ -0,0 +1,111 @@
//! This submodule encapsulates all of the code that actually interacts with
//! libtest, so that it can be easily removed after the new executor becomes
//! the default.
use std::borrow::Cow;
use std::io;
use crate::common::Config;
use crate::executor::{CollectedTest, CollectedTestDesc, ColorConfig, OutputFormat, ShouldPanic};
/// Delegates to libtest to run the list of collected tests.
///
/// Returns `Ok(true)` if all tests passed, or `Ok(false)` if one or more tests failed.
pub(crate) fn execute_tests(config: &Config, tests: Vec<CollectedTest>) -> io::Result<bool> {
let opts = test_opts(config);
let tests = tests.into_iter().map(|t| t.into_libtest()).collect::<Vec<_>>();
test::run_tests_console(&opts, tests)
}
impl CollectedTest {
fn into_libtest(self) -> test::TestDescAndFn {
let Self { desc, config, testpaths, revision } = self;
let CollectedTestDesc { name, ignore, ignore_message, should_panic } = desc;
// Libtest requires the ignore message to be a &'static str, so we might
// have to leak memory to create it. This is fine, as we only do so once
// per test, so the leak won't grow indefinitely.
let ignore_message = ignore_message.map(|msg| match msg {
Cow::Borrowed(s) => s,
Cow::Owned(s) => &*String::leak(s),
});
let desc = test::TestDesc {
name: test::DynTestName(name),
ignore,
ignore_message,
source_file: "",
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 0,
should_panic: should_panic.to_libtest(),
compile_fail: false,
no_run: false,
test_type: test::TestType::Unknown,
};
// This closure is invoked when libtest returns control to compiletest
// to execute the test.
let testfn = test::DynTestFn(Box::new(move || {
crate::runtest::run(config, &testpaths, revision.as_deref());
Ok(())
}));
test::TestDescAndFn { desc, testfn }
}
}
impl ColorConfig {
fn to_libtest(self) -> test::ColorConfig {
match self {
Self::AutoColor => test::ColorConfig::AutoColor,
Self::AlwaysColor => test::ColorConfig::AlwaysColor,
Self::NeverColor => test::ColorConfig::NeverColor,
}
}
}
impl OutputFormat {
fn to_libtest(self) -> test::OutputFormat {
match self {
Self::Pretty => test::OutputFormat::Pretty,
Self::Terse => test::OutputFormat::Terse,
Self::Json => test::OutputFormat::Json,
}
}
}
impl ShouldPanic {
fn to_libtest(self) -> test::ShouldPanic {
match self {
Self::No => test::ShouldPanic::No,
Self::Yes => test::ShouldPanic::Yes,
}
}
}
fn test_opts(config: &Config) -> test::TestOpts {
test::TestOpts {
exclude_should_panic: false,
filters: config.filters.clone(),
filter_exact: config.filter_exact,
run_ignored: if config.run_ignored { test::RunIgnored::Yes } else { test::RunIgnored::No },
format: config.format.to_libtest(),
logfile: None,
run_tests: true,
bench_benchmarks: true,
nocapture: config.nocapture,
color: config.color.to_libtest(),
shuffle: false,
shuffle_seed: None,
test_threads: None,
skip: config.skip.clone(),
list: false,
options: test::Options::new(),
time_options: None,
force_run_in_process: false,
fail_fast: config.fail_fast,
}
}

View file

@ -1,7 +1,8 @@
#![crate_name = "compiletest"]
// The `test` crate is the only unstable feature
// allowed here, just to share similar code.
// Needed by the libtest-based test executor.
#![feature(test)]
// Needed by the "new" test executor that does not depend on libtest.
#![feature(internal_output_capture)]
extern crate test;
@ -202,6 +203,7 @@ pub fn parse_config(args: Vec<String>) -> Config {
"COMMAND",
)
.reqopt("", "minicore-path", "path to minicore aux library", "PATH")
.optflag("n", "new-executor", "enables the new test executor instead of using libtest")
.optopt(
"",
"debugger",
@ -447,6 +449,8 @@ pub fn parse_config(args: Vec<String>) -> Config {
diff_command: matches.opt_str("compiletest-diff-tool"),
minicore_path: opt_path(matches, "minicore-path"),
new_executor: matches.opt_present("new-executor"),
}
}
@ -570,10 +574,14 @@ pub fn run_tests(config: Arc<Config>) {
tests.sort_by(|a, b| Ord::cmp(&a.desc.name, &b.desc.name));
// Delegate to libtest to filter and run the big list of structures created
// during test discovery. When libtest decides to run a test, it will
// return control to compiletest by invoking a closure.
let res = crate::executor::execute_tests(&config, tests);
// Delegate to the executor to filter and run the big list of test structures
// created during test discovery. When the executor decides to run a test,
// it will return control to the rest of compiletest by calling `runtest::run`.
let res = if config.new_executor {
Ok(executor::run_tests(&config, tests))
} else {
crate::executor::libtest::execute_tests(&config, tests)
};
// Check the outcome reported by libtest.
match res {