Skip to content

Commit 2003914

Browse files
authored
Rollup merge of rust-lang#146119 - Zalathar:capture, r=jieyouxu
compiletest: Implement an experimental `--new-output-capture` mode Thanks to the efforts on rust-lang#140192, compiletest no longer has an unstable dependency on libtest, but it still has an unstable dependency on `#![feature(internal_output_capture)]`. That makes building compiletest more complicated than for most other bootstrap tools. This PR therefore adds opt-in support for an experimental compiletest mode that avoids the use of `internal_output_capture` APIs, and instead uses more mundane means to capture the output of individual test runners. Each `TestCx` now contains `&dyn ConsoleOut` references for stdout and stderr. All print statements in `compiletests::runtest` have been replaced with `write!` or `writeln!` calls that explicitly write to one of those trait objects. The underlying implementation then forwards to `print!` or `eprint!` (for `--no-capture` or old-output-capture mode), or writes to a separate buffer (in new-output-capture mode). --- Currently, new-output-capture is disabled by default. It can be explicitly enabled in one of two ways: - When running `x test`, pass `--new-output-capture=on` as a *compiletest* argument (after `--`). - E.g. `x test ui -- --new-output-capture=on`. - The short form is `-Non` or `-Ny`. - Set environment variable `COMPILETEST_NEW_OUTPUT_CAPTURE=on`. After some amount of opt-in testing, new-output-capture will become the default (with a temporary opt-out). Eventually, old-output-capture and `#![feature(internal_output_capture)]` will be completely removed from compiletest. r? jieyouxu
2 parents e3da4c8 + c2ae2e0 commit 2003914

File tree

13 files changed

+275
-75
lines changed

13 files changed

+275
-75
lines changed

src/tools/compiletest/src/common.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,10 @@ pub struct Config {
667667
/// to avoid `!nocapture` double-negatives.
668668
pub nocapture: bool,
669669

670+
/// True if the experimental new output-capture implementation should be
671+
/// used, avoiding the need for `#![feature(internal_output_capture)]`.
672+
pub new_output_capture: bool,
673+
670674
/// Needed both to construct [`build_helper::git::GitConfig`].
671675
pub nightly_branch: String,
672676
pub git_merge_commit_email: String,
@@ -784,6 +788,7 @@ impl Config {
784788
builtin_cfg_names: Default::default(),
785789
supported_crate_types: Default::default(),
786790
nocapture: Default::default(),
791+
new_output_capture: Default::default(),
787792
nightly_branch: Default::default(),
788793
git_merge_commit_email: Default::default(),
789794
profiler_runtime: Default::default(),

src/tools/compiletest/src/executor.rs

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::sync::{Arc, Mutex, mpsc};
1313
use std::{env, hint, io, mem, panic, thread};
1414

1515
use crate::common::{Config, TestPaths};
16+
use crate::output_capture::{self, ConsoleOut};
1617
use crate::panic_hook;
1718

1819
mod deadline;
@@ -120,28 +121,28 @@ fn run_test_inner(
120121
runnable_test: RunnableTest,
121122
completion_sender: mpsc::Sender<TestCompletion>,
122123
) {
123-
let is_capture = !runnable_test.config.nocapture;
124+
let capture = CaptureKind::for_config(&runnable_test.config);
124125

125126
// Install a panic-capture buffer for use by the custom panic hook.
126-
if is_capture {
127+
if capture.should_set_panic_hook() {
127128
panic_hook::set_capture_buf(Default::default());
128129
}
129-
let capture_buf = is_capture.then(|| Arc::new(Mutex::new(vec![])));
130130

131-
if let Some(capture_buf) = &capture_buf {
132-
io::set_output_capture(Some(Arc::clone(capture_buf)));
131+
if let CaptureKind::Old { ref buf } = capture {
132+
io::set_output_capture(Some(Arc::clone(buf)));
133133
}
134134

135-
let panic_payload = panic::catch_unwind(move || runnable_test.run()).err();
135+
let stdout = capture.stdout();
136+
let stderr = capture.stderr();
137+
138+
let panic_payload = panic::catch_unwind(move || runnable_test.run(stdout, stderr)).err();
136139

137140
if let Some(panic_buf) = panic_hook::take_capture_buf() {
138141
let panic_buf = panic_buf.lock().unwrap_or_else(|e| e.into_inner());
139-
// For now, forward any captured panic message to (captured) stderr.
140-
// FIXME(Zalathar): Once we have our own output-capture buffer for
141-
// non-panic output, append the panic message to that buffer instead.
142-
eprint!("{panic_buf}");
142+
// Forward any captured panic message to (captured) stderr.
143+
write!(stderr, "{panic_buf}");
143144
}
144-
if is_capture {
145+
if matches!(capture, CaptureKind::Old { .. }) {
145146
io::set_output_capture(None);
146147
}
147148

@@ -152,11 +153,70 @@ fn run_test_inner(
152153
TestOutcome::Failed { message: Some("test did not panic as expected") }
153154
}
154155
};
155-
let stdout = capture_buf.map(|mutex| mutex.lock().unwrap_or_else(|e| e.into_inner()).to_vec());
156156

157+
let stdout = capture.into_inner();
157158
completion_sender.send(TestCompletion { id, outcome, stdout }).unwrap();
158159
}
159160

161+
enum CaptureKind {
162+
/// Do not capture test-runner output, for `--no-capture`.
163+
///
164+
/// (This does not affect `rustc` and other subprocesses spawned by test
165+
/// runners, whose output is always captured.)
166+
None,
167+
168+
/// Use the old output-capture implementation, which relies on the unstable
169+
/// library feature `#![feature(internal_output_capture)]`.
170+
Old { buf: Arc<Mutex<Vec<u8>>> },
171+
172+
/// Use the new output-capture implementation, which only uses stable Rust.
173+
New { buf: output_capture::CaptureBuf },
174+
}
175+
176+
impl CaptureKind {
177+
fn for_config(config: &Config) -> Self {
178+
if config.nocapture {
179+
Self::None
180+
} else if config.new_output_capture {
181+
Self::New { buf: output_capture::CaptureBuf::new() }
182+
} else {
183+
// Create a capure buffer for `io::set_output_capture`.
184+
Self::Old { buf: Default::default() }
185+
}
186+
}
187+
188+
fn should_set_panic_hook(&self) -> bool {
189+
match self {
190+
Self::None => false,
191+
Self::Old { .. } => true,
192+
Self::New { .. } => true,
193+
}
194+
}
195+
196+
fn stdout(&self) -> &dyn ConsoleOut {
197+
self.capture_buf_or(&output_capture::Stdout)
198+
}
199+
200+
fn stderr(&self) -> &dyn ConsoleOut {
201+
self.capture_buf_or(&output_capture::Stderr)
202+
}
203+
204+
fn capture_buf_or<'a>(&'a self, fallback: &'a dyn ConsoleOut) -> &'a dyn ConsoleOut {
205+
match self {
206+
Self::None | Self::Old { .. } => fallback,
207+
Self::New { buf } => buf,
208+
}
209+
}
210+
211+
fn into_inner(self) -> Option<Vec<u8>> {
212+
match self {
213+
Self::None => None,
214+
Self::Old { buf } => Some(buf.lock().unwrap_or_else(|e| e.into_inner()).to_vec()),
215+
Self::New { buf } => Some(buf.into_inner().into()),
216+
}
217+
}
218+
}
219+
160220
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
161221
struct TestId(usize);
162222

@@ -174,10 +234,12 @@ impl RunnableTest {
174234
Self { config, testpaths, revision }
175235
}
176236

177-
fn run(&self) {
237+
fn run(&self, stdout: &dyn ConsoleOut, stderr: &dyn ConsoleOut) {
178238
__rust_begin_short_backtrace(|| {
179239
crate::runtest::run(
180240
Arc::clone(&self.config),
241+
stdout,
242+
stderr,
181243
&self.testpaths,
182244
self.revision.as_deref(),
183245
);

src/tools/compiletest/src/lib.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod directives;
1515
pub mod errors;
1616
mod executor;
1717
mod json;
18+
mod output_capture;
1819
mod panic_hook;
1920
mod raise_fd_limit;
2021
mod read2;
@@ -177,6 +178,12 @@ pub fn parse_config(args: Vec<String>) -> Config {
177178
// FIXME: Temporarily retained so we can point users to `--no-capture`
178179
.optflag("", "nocapture", "")
179180
.optflag("", "no-capture", "don't capture stdout/stderr of tests")
181+
.optopt(
182+
"N",
183+
"new-output-capture",
184+
"enables or disables the new output-capture implementation",
185+
"off|on",
186+
)
180187
.optflag("", "profiler-runtime", "is the profiler runtime enabled for this target")
181188
.optflag("h", "help", "show this message")
182189
.reqopt("", "channel", "current Rust channel", "CHANNEL")
@@ -461,6 +468,14 @@ pub fn parse_config(args: Vec<String>) -> Config {
461468
supported_crate_types: OnceLock::new(),
462469

463470
nocapture: matches.opt_present("no-capture"),
471+
new_output_capture: {
472+
let value = matches
473+
.opt_str("new-output-capture")
474+
.or_else(|| env::var("COMPILETEST_NEW_OUTPUT_CAPTURE").ok())
475+
.unwrap_or_else(|| "off".to_owned());
476+
parse_bool_option(&value)
477+
.unwrap_or_else(|| panic!("unknown `--new-output-capture` value `{value}` given"))
478+
},
464479

465480
nightly_branch: matches.opt_str("nightly-branch").unwrap(),
466481
git_merge_commit_email: matches.opt_str("git-merge-commit-email").unwrap(),
@@ -476,6 +491,19 @@ pub fn parse_config(args: Vec<String>) -> Config {
476491
}
477492
}
478493

494+
/// Parses the same set of boolean values accepted by rustc command-line arguments.
495+
///
496+
/// Accepting all of these values is more complicated than just picking one
497+
/// pair, but has the advantage that contributors who are used to rustc
498+
/// shouldn't have to think about which values are legal.
499+
fn parse_bool_option(value: &str) -> Option<bool> {
500+
match value {
501+
"off" | "no" | "n" | "false" => Some(false),
502+
"on" | "yes" | "y" | "true" => Some(true),
503+
_ => None,
504+
}
505+
}
506+
479507
pub fn opt_str(maybestr: &Option<String>) -> &str {
480508
match *maybestr {
481509
None => "(none)",
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use std::fmt;
2+
use std::panic::RefUnwindSafe;
3+
use std::sync::Mutex;
4+
5+
pub trait ConsoleOut: fmt::Debug + RefUnwindSafe {
6+
fn write_fmt(&self, args: fmt::Arguments<'_>);
7+
}
8+
9+
#[derive(Debug)]
10+
pub(crate) struct Stdout;
11+
12+
impl ConsoleOut for Stdout {
13+
fn write_fmt(&self, args: fmt::Arguments<'_>) {
14+
print!("{args}");
15+
}
16+
}
17+
18+
#[derive(Debug)]
19+
pub(crate) struct Stderr;
20+
21+
impl ConsoleOut for Stderr {
22+
fn write_fmt(&self, args: fmt::Arguments<'_>) {
23+
eprint!("{args}");
24+
}
25+
}
26+
27+
pub(crate) struct CaptureBuf {
28+
inner: Mutex<String>,
29+
}
30+
31+
impl CaptureBuf {
32+
pub(crate) fn new() -> Self {
33+
Self { inner: Mutex::new(String::new()) }
34+
}
35+
36+
pub(crate) fn into_inner(self) -> String {
37+
self.inner.into_inner().unwrap_or_else(|e| e.into_inner())
38+
}
39+
}
40+
41+
impl fmt::Debug for CaptureBuf {
42+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43+
f.debug_struct("CaptureBuf").finish_non_exhaustive()
44+
}
45+
}
46+
47+
impl ConsoleOut for CaptureBuf {
48+
fn write_fmt(&self, args: fmt::Arguments<'_>) {
49+
let mut s = self.inner.lock().unwrap_or_else(|e| e.into_inner());
50+
<String as fmt::Write>::write_fmt(&mut s, args).unwrap();
51+
}
52+
}

0 commit comments

Comments
 (0)