1
Fork 0

Auto merge of #115706 - RalfJung:compiletest-truncation, r=wesleywiser

Make compiletest output truncation less disruptive

When the test output becomes too large, compiletest stops recording all of it. However:
- this can lead to invalid JSON, which then causes compiletest itself to throw further errors
- the note that output was truncated is in the middle of the output, with >100kb of text on each side; that makes it almost impossible to actually see that note in the terminal

So assuming that we do need to keep the output truncation, I propose that we only ever do a cut at the end, so that it is very clear by looking at the end of the log that truncation happened. I added a message at the beginning of the output as well. Also I added some logic to make it less likely that we'll cut things off in the middle of a JSON record. (I tested that successfully by reducing the output limit to something very low and running a few ui tests.) Furthermore I increased the max buffer size to 512KB; that's really not a lot of memory compared to how much RAM it takes to build rustc (it's ~25% more than the previous maximum HEAD+TAIL length). And finally, the information that things got truncated is now propagated to the higher levels, so that we can fail the test instead of comparing the truncated output with the reference.

Fixes https://github.com/rust-lang/rust/issues/115675
Fixes https://github.com/rust-lang/rust/issues/96229
Fixes https://github.com/rust-lang/rust/issues/94322
Fixes https://github.com/rust-lang/rust/issues/92211
This commit is contained in:
bors 2023-09-13 01:54:47 +00:00
commit 8e455dba67
3 changed files with 75 additions and 71 deletions

View file

@ -9,7 +9,16 @@ use std::io::{self, Write};
use std::mem::replace; use std::mem::replace;
use std::process::{Child, Output}; use std::process::{Child, Output};
pub fn read2_abbreviated(mut child: Child, filter_paths_from_len: &[String]) -> io::Result<Output> { #[derive(Copy, Clone, Debug)]
pub enum Truncated {
Yes,
No,
}
pub fn read2_abbreviated(
mut child: Child,
filter_paths_from_len: &[String],
) -> io::Result<(Output, Truncated)> {
let mut stdout = ProcOutput::new(); let mut stdout = ProcOutput::new();
let mut stderr = ProcOutput::new(); let mut stderr = ProcOutput::new();
@ -24,11 +33,12 @@ pub fn read2_abbreviated(mut child: Child, filter_paths_from_len: &[String]) ->
)?; )?;
let status = child.wait()?; let status = child.wait()?;
Ok(Output { status, stdout: stdout.into_bytes(), stderr: stderr.into_bytes() }) let truncated =
if stdout.truncated() || stderr.truncated() { Truncated::Yes } else { Truncated::No };
Ok((Output { status, stdout: stdout.into_bytes(), stderr: stderr.into_bytes() }, truncated))
} }
const HEAD_LEN: usize = 160 * 1024; const MAX_OUT_LEN: usize = 512 * 1024;
const TAIL_LEN: usize = 256 * 1024;
// Whenever a path is filtered when counting the length of the output, we need to add some // Whenever a path is filtered when counting the length of the output, we need to add some
// placeholder length to ensure a compiler emitting only filtered paths doesn't cause a OOM. // placeholder length to ensure a compiler emitting only filtered paths doesn't cause a OOM.
@ -39,7 +49,7 @@ const FILTERED_PATHS_PLACEHOLDER_LEN: usize = 32;
enum ProcOutput { enum ProcOutput {
Full { bytes: Vec<u8>, filtered_len: usize }, Full { bytes: Vec<u8>, filtered_len: usize },
Abbreviated { head: Vec<u8>, skipped: usize, tail: Box<[u8]> }, Abbreviated { head: Vec<u8>, skipped: usize },
} }
impl ProcOutput { impl ProcOutput {
@ -47,6 +57,10 @@ impl ProcOutput {
ProcOutput::Full { bytes: Vec::new(), filtered_len: 0 } ProcOutput::Full { bytes: Vec::new(), filtered_len: 0 }
} }
fn truncated(&self) -> bool {
matches!(self, Self::Abbreviated { .. })
}
fn extend(&mut self, data: &[u8], filter_paths_from_len: &[String]) { fn extend(&mut self, data: &[u8], filter_paths_from_len: &[String]) {
let new_self = match *self { let new_self = match *self {
ProcOutput::Full { ref mut bytes, ref mut filtered_len } => { ProcOutput::Full { ref mut bytes, ref mut filtered_len } => {
@ -83,24 +97,21 @@ impl ProcOutput {
} }
let new_len = bytes.len(); let new_len = bytes.len();
if (*filtered_len).min(new_len) <= HEAD_LEN + TAIL_LEN { if (*filtered_len).min(new_len) <= MAX_OUT_LEN {
return; return;
} }
let mut head = replace(bytes, Vec::new()); let mut head = replace(bytes, Vec::new());
let mut middle = head.split_off(HEAD_LEN); // Don't truncate if this as a whole line.
let tail = middle.split_off(middle.len() - TAIL_LEN).into_boxed_slice(); // That should make it less likely that we cut a JSON line in half.
let skipped = new_len - HEAD_LEN - TAIL_LEN; if head.last() != Some(&('\n' as u8)) {
ProcOutput::Abbreviated { head, skipped, tail } head.truncate(MAX_OUT_LEN);
}
ProcOutput::Abbreviated { ref mut skipped, ref mut tail, .. } => {
*skipped += data.len();
if data.len() <= TAIL_LEN {
tail[..data.len()].copy_from_slice(data);
tail.rotate_left(data.len());
} else {
tail.copy_from_slice(&data[(data.len() - TAIL_LEN)..]);
} }
let skipped = new_len - head.len();
ProcOutput::Abbreviated { head, skipped }
}
ProcOutput::Abbreviated { ref mut skipped, .. } => {
*skipped += data.len();
return; return;
} }
}; };
@ -110,18 +121,12 @@ impl ProcOutput {
fn into_bytes(self) -> Vec<u8> { fn into_bytes(self) -> Vec<u8> {
match self { match self {
ProcOutput::Full { bytes, .. } => bytes, ProcOutput::Full { bytes, .. } => bytes,
ProcOutput::Abbreviated { mut head, mut skipped, tail } => { ProcOutput::Abbreviated { mut head, skipped } => {
let mut tail = &*tail; let head_note =
format!("<<<<<< TRUNCATED, SHOWING THE FIRST {} BYTES >>>>>>\n\n", head.len());
// Skip over '{' at the start of the tail, so we don't later wrongfully consider this as json. head.splice(0..0, head_note.into_bytes());
// See <https://rust-lang.zulipchat.com/#narrow/stream/182449-t-compiler.2Fhelp/topic/Weird.20CI.20failure/near/321797811> write!(&mut head, "\n\n<<<<<< TRUNCATED, DROPPED {} BYTES >>>>>>", skipped)
while tail.get(0) == Some(&b'{') { .unwrap();
tail = &tail[1..];
skipped += 1;
}
write!(&mut head, "\n\n<<<<<< SKIPPED {} BYTES >>>>>>\n\n", skipped).unwrap();
head.extend_from_slice(tail);
head head
} }
} }

View file

@ -1,4 +1,6 @@
use crate::read2::{ProcOutput, FILTERED_PATHS_PLACEHOLDER_LEN, HEAD_LEN, TAIL_LEN}; use std::io::Write;
use crate::read2::{ProcOutput, FILTERED_PATHS_PLACEHOLDER_LEN, MAX_OUT_LEN};
#[test] #[test]
fn test_abbreviate_short_string() { fn test_abbreviate_short_string() {
@ -21,35 +23,13 @@ fn test_abbreviate_short_string_multiple_steps() {
fn test_abbreviate_long_string() { fn test_abbreviate_long_string() {
let mut out = ProcOutput::new(); let mut out = ProcOutput::new();
let data = vec![b'.'; HEAD_LEN + TAIL_LEN + 16]; let data = vec![b'.'; MAX_OUT_LEN + 16];
out.extend(&data, &[]); out.extend(&data, &[]);
let mut expected = vec![b'.'; HEAD_LEN]; let mut expected = Vec::new();
expected.extend_from_slice(b"\n\n<<<<<< SKIPPED 16 BYTES >>>>>>\n\n"); write!(expected, "<<<<<< TRUNCATED, SHOWING THE FIRST {MAX_OUT_LEN} BYTES >>>>>>\n\n").unwrap();
expected.extend_from_slice(&vec![b'.'; TAIL_LEN]); expected.extend_from_slice(&[b'.'; MAX_OUT_LEN]);
expected.extend_from_slice(b"\n\n<<<<<< TRUNCATED, DROPPED 16 BYTES >>>>>>");
// We first check the length to avoid endless terminal output if the length differs, since
// `out` is hundreds of KBs in size.
let out = out.into_bytes();
assert_eq!(expected.len(), out.len());
assert_eq!(expected, out);
}
#[test]
fn test_abbreviate_long_string_multiple_steps() {
let mut out = ProcOutput::new();
out.extend(&vec![b'.'; HEAD_LEN], &[]);
out.extend(&vec![b'.'; TAIL_LEN], &[]);
// Also test whether the rotation works
out.extend(&vec![b'!'; 16], &[]);
out.extend(&vec![b'?'; 16], &[]);
let mut expected = vec![b'.'; HEAD_LEN];
expected.extend_from_slice(b"\n\n<<<<<< SKIPPED 32 BYTES >>>>>>\n\n");
expected.extend_from_slice(&vec![b'.'; TAIL_LEN - 32]);
expected.extend_from_slice(&vec![b'!'; 16]);
expected.extend_from_slice(&vec![b'?'; 16]);
// We first check the length to avoid endless terminal output if the length differs, since // We first check the length to avoid endless terminal output if the length differs, since
// `out` is hundreds of KBs in size. // `out` is hundreds of KBs in size.
@ -86,9 +66,8 @@ fn test_abbreviate_filters_avoid_abbreviations() {
let mut out = ProcOutput::new(); let mut out = ProcOutput::new();
let filters = &[std::iter::repeat('a').take(64).collect::<String>()]; let filters = &[std::iter::repeat('a').take(64).collect::<String>()];
let mut expected = vec![b'.'; HEAD_LEN - FILTERED_PATHS_PLACEHOLDER_LEN as usize]; let mut expected = vec![b'.'; MAX_OUT_LEN - FILTERED_PATHS_PLACEHOLDER_LEN as usize];
expected.extend_from_slice(filters[0].as_bytes()); expected.extend_from_slice(filters[0].as_bytes());
expected.extend_from_slice(&vec![b'.'; TAIL_LEN]);
out.extend(&expected, filters); out.extend(&expected, filters);
@ -104,14 +83,13 @@ fn test_abbreviate_filters_can_still_cause_abbreviations() {
let mut out = ProcOutput::new(); let mut out = ProcOutput::new();
let filters = &[std::iter::repeat('a').take(64).collect::<String>()]; let filters = &[std::iter::repeat('a').take(64).collect::<String>()];
let mut input = vec![b'.'; HEAD_LEN]; let mut input = vec![b'.'; MAX_OUT_LEN];
input.extend_from_slice(&vec![b'.'; TAIL_LEN]);
input.extend_from_slice(filters[0].as_bytes()); input.extend_from_slice(filters[0].as_bytes());
let mut expected = vec![b'.'; HEAD_LEN]; let mut expected = Vec::new();
expected.extend_from_slice(b"\n\n<<<<<< SKIPPED 64 BYTES >>>>>>\n\n"); write!(expected, "<<<<<< TRUNCATED, SHOWING THE FIRST {MAX_OUT_LEN} BYTES >>>>>>\n\n").unwrap();
expected.extend_from_slice(&vec![b'.'; TAIL_LEN - 64]); expected.extend_from_slice(&[b'.'; MAX_OUT_LEN]);
expected.extend_from_slice(&vec![b'a'; 64]); expected.extend_from_slice(b"\n\n<<<<<< TRUNCATED, DROPPED 64 BYTES >>>>>>");
out.extend(&input, filters); out.extend(&input, filters);

View file

@ -12,7 +12,7 @@ use crate::compute_diff::{write_diff, write_filtered_diff};
use crate::errors::{self, Error, ErrorKind}; use crate::errors::{self, Error, ErrorKind};
use crate::header::TestProps; use crate::header::TestProps;
use crate::json; use crate::json;
use crate::read2::read2_abbreviated; use crate::read2::{read2_abbreviated, Truncated};
use crate::util::{add_dylib_path, dylib_env_var, logv, PathBufExt}; use crate::util::{add_dylib_path, dylib_env_var, logv, PathBufExt};
use crate::ColorConfig; use crate::ColorConfig;
use regex::{Captures, Regex}; use regex::{Captures, Regex};
@ -701,6 +701,7 @@ impl<'test> TestCx<'test> {
status: output.status, status: output.status,
stdout: String::from_utf8(output.stdout).unwrap(), stdout: String::from_utf8(output.stdout).unwrap(),
stderr: String::from_utf8(output.stderr).unwrap(), stderr: String::from_utf8(output.stderr).unwrap(),
truncated: Truncated::No,
cmdline: format!("{cmd:?}"), cmdline: format!("{cmd:?}"),
}; };
self.dump_output(&proc_res.stdout, &proc_res.stderr); self.dump_output(&proc_res.stdout, &proc_res.stderr);
@ -1275,6 +1276,7 @@ impl<'test> TestCx<'test> {
status, status,
stdout: String::from_utf8(stdout).unwrap(), stdout: String::from_utf8(stdout).unwrap(),
stderr: String::from_utf8(stderr).unwrap(), stderr: String::from_utf8(stderr).unwrap(),
truncated: Truncated::No,
cmdline, cmdline,
}; };
if adb.kill().is_err() { if adb.kill().is_err() {
@ -1566,7 +1568,13 @@ impl<'test> TestCx<'test> {
}; };
self.dump_output(&out, &err); self.dump_output(&out, &err);
ProcRes { status, stdout: out, stderr: err, cmdline: format!("{:?}", cmd) } ProcRes {
status,
stdout: out,
stderr: err,
truncated: Truncated::No,
cmdline: format!("{:?}", cmd),
}
} }
fn cleanup_debug_info_options(&self, options: &Vec<String>) -> Vec<String> { fn cleanup_debug_info_options(&self, options: &Vec<String>) -> Vec<String> {
@ -2227,7 +2235,7 @@ impl<'test> TestCx<'test> {
dylib dylib
} }
fn read2_abbreviated(&self, child: Child) -> Output { fn read2_abbreviated(&self, child: Child) -> (Output, Truncated) {
let mut filter_paths_from_len = Vec::new(); let mut filter_paths_from_len = Vec::new();
let mut add_path = |path: &Path| { let mut add_path = |path: &Path| {
let path = path.display().to_string(); let path = path.display().to_string();
@ -2274,12 +2282,13 @@ impl<'test> TestCx<'test> {
child.stdin.as_mut().unwrap().write_all(input.as_bytes()).unwrap(); child.stdin.as_mut().unwrap().write_all(input.as_bytes()).unwrap();
} }
let Output { status, stdout, stderr } = self.read2_abbreviated(child); let (Output { status, stdout, stderr }, truncated) = self.read2_abbreviated(child);
let result = ProcRes { let result = ProcRes {
status, status,
stdout: String::from_utf8_lossy(&stdout).into_owned(), stdout: String::from_utf8_lossy(&stdout).into_owned(),
stderr: String::from_utf8_lossy(&stderr).into_owned(), stderr: String::from_utf8_lossy(&stderr).into_owned(),
truncated,
cmdline, cmdline,
}; };
@ -3610,12 +3619,14 @@ impl<'test> TestCx<'test> {
} }
} }
let output = self.read2_abbreviated(cmd.spawn().expect("failed to spawn `make`")); let (output, truncated) =
self.read2_abbreviated(cmd.spawn().expect("failed to spawn `make`"));
if !output.status.success() { if !output.status.success() {
let res = ProcRes { let res = ProcRes {
status: output.status, status: output.status,
stdout: String::from_utf8_lossy(&output.stdout).into_owned(), stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(), stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
truncated,
cmdline: format!("{:?}", cmd), cmdline: format!("{:?}", cmd),
}; };
self.fatal_proc_rec("make failed", &res); self.fatal_proc_rec("make failed", &res);
@ -3777,6 +3788,15 @@ impl<'test> TestCx<'test> {
let emit_metadata = self.should_emit_metadata(pm); let emit_metadata = self.should_emit_metadata(pm);
let proc_res = self.compile_test(should_run, emit_metadata); let proc_res = self.compile_test(should_run, emit_metadata);
self.check_if_test_should_compile(&proc_res, pm); self.check_if_test_should_compile(&proc_res, pm);
if matches!(proc_res.truncated, Truncated::Yes)
&& !self.props.dont_check_compiler_stdout
&& !self.props.dont_check_compiler_stderr
{
self.fatal_proc_rec(
&format!("compiler output got truncated, cannot compare with reference file"),
&proc_res,
);
}
// if the user specified a format in the ui test // if the user specified a format in the ui test
// print the output to the stderr file, otherwise extract // print the output to the stderr file, otherwise extract
@ -4468,6 +4488,7 @@ pub struct ProcRes {
status: ExitStatus, status: ExitStatus,
stdout: String, stdout: String,
stderr: String, stderr: String,
truncated: Truncated,
cmdline: String, cmdline: String,
} }