Skip to content

Commit bb6f90e

Browse files
Rollup merge of #154586 - jdonszelmann:record-rerun, r=clubby789,jieyouxu
Record failed tests with `--record`, and rerun them with `--rerun` This adds two parameters to `x test`: ## `--record` Writes a file, by default `build/failed-tests`, but this can be overwritten with ```toml [build] record_failed_tests_path = "somepath" ``` with a list of all tests that fail that run. ## `--rerun` Looks for the failed-tests file, parse it, and attempt to rerun only those tests. No cli-arguments are necessary, i.e. ``` x test tests/ui --record x test --rerun ``` Will run all failed uitests. No need to pass tests/ui to the rerun invocation. The last commit is a little awkward, but I think it's the best way to make it so that we *first* run all tests that have to be rerun, and *then* rerun tests passed through the cli. This makes it so: ``` x test tests/ui --rerun ``` will *first* rerun failed tests, some of which may be uitests, if any fail it quits and reports failed tests, but if all pass it will run all normally passed tests. In other words, only if all previously-failed tests pass on the rerun, we then also run uitests. Without the last commit, this would instead just run all uitests, since the failed tests form a subset of all uitests. I think that's less useful.
2 parents 0cf9681 + ab1aa4b commit bb6f90e

15 files changed

Lines changed: 344 additions & 31 deletions

File tree

src/bootstrap/src/core/build_steps/test.rs

Lines changed: 109 additions & 21 deletions
Large diffs are not rendered by default.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
use std::collections::BTreeSet;
2+
use std::fs::{self, File};
3+
use std::io::{BufRead, BufReader, ErrorKind};
4+
use std::path::{Path, PathBuf};
5+
6+
use crate::core::builder::{Builder, ShouldRun, Step};
7+
use crate::t;
8+
9+
#[derive(Clone)]
10+
pub struct RecordFailedTests {
11+
failed_tests_path: Option<PathBuf>,
12+
}
13+
14+
impl RecordFailedTests {
15+
pub fn path(&self) -> Option<&Path> {
16+
self.failed_tests_path.as_deref()
17+
}
18+
}
19+
20+
/// This step is run as a dependency of most testing steps.
21+
/// Upon running, a file is created for failed tests to be recorded in if `--record` is passed on
22+
/// the command line.
23+
///
24+
/// This step is the only way to get access to a token type called [`RecordFailedTests`].
25+
/// Having this token type signifies the fact that a file was created to store failed tests in,
26+
/// and is required to create a `Renderer`, the type that renders the outputs of tests.
27+
///
28+
/// If `--rerun` isn't passed, or we're in dry-run mode, running this step is a no-op,
29+
/// and the `RecordFailedTest` type doesn't (need to) signify anything.
30+
#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
31+
pub struct SetupFailedTestsFile;
32+
impl Step for SetupFailedTestsFile {
33+
type Output = RecordFailedTests;
34+
35+
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
36+
run.never()
37+
}
38+
39+
fn run(self, builder: &Builder<'_>) -> Self::Output {
40+
if !builder.config.cmd.record() || builder.config.dry_run() {
41+
return RecordFailedTests { failed_tests_path: None };
42+
}
43+
44+
let failed_tests_path = builder.config.record_failed_tests_path.clone();
45+
println!(
46+
"setting up tracking of failed tests in {} (`--record` was passed)",
47+
failed_tests_path.display()
48+
);
49+
if failed_tests_path.exists() {
50+
println!("deleting previously recorded failed tests");
51+
t!(fs::remove_file(&failed_tests_path));
52+
}
53+
RecordFailedTests { failed_tests_path: Some(failed_tests_path) }
54+
}
55+
}
56+
57+
pub fn collect_previously_failed_tests(failed_tests_file_path: &PathBuf) -> Vec<PathBuf> {
58+
let mut paths = BTreeSet::new();
59+
60+
println!(
61+
"`--rerun` passed so looking for failed tests in {}",
62+
failed_tests_file_path.display()
63+
);
64+
65+
let lines: Vec<String> = match File::open(failed_tests_file_path) {
66+
Ok(f) => t!(BufReader::new(f).lines().collect()),
67+
Err(e) if e.kind() == ErrorKind::NotFound => {
68+
println!(
69+
"WARNING: failed tests file doesn't exist: `--rerun` only makes sense after a previous test run with `--record`"
70+
);
71+
return Vec::new();
72+
}
73+
Err(e) => t!(Err(e)),
74+
};
75+
76+
const MAX_RERUN_PRINTS: usize = 10;
77+
78+
for line in lines {
79+
let trimmed = line.as_str().trim();
80+
let without_revision =
81+
trimmed.rsplit_once("#").map(|(before, _)| before).unwrap_or(trimmed);
82+
let without_suite_prefix = without_revision
83+
.strip_prefix("[")
84+
.and_then(|rest| rest.split_once("]"))
85+
.map(|(_, after)| after.trim())
86+
.unwrap_or(without_revision);
87+
88+
let failed_test_path = PathBuf::from(without_suite_prefix.to_string());
89+
if paths.insert(failed_test_path.clone()) {
90+
if paths.len() == 1 {
91+
println!("rerunning previously failed tests:");
92+
}
93+
if paths.len() <= MAX_RERUN_PRINTS {
94+
println!(" {}", failed_test_path.display());
95+
}
96+
}
97+
}
98+
99+
if paths.len() > MAX_RERUN_PRINTS {
100+
println!(" and {} more...", paths.len() - MAX_RERUN_PRINTS)
101+
}
102+
103+
if paths.is_empty() {
104+
println!(
105+
"WARNING: failed tests file doesn't contain any failed tests: `--rerun` only makes sense after a previous test run with `--record`"
106+
);
107+
}
108+
109+
paths.into_iter().collect()
110+
}

src/bootstrap/src/core/config/config.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use tracing::{instrument, span};
3030

3131
use crate::core::build_steps::llvm;
3232
use crate::core::build_steps::llvm::LLVM_INVALIDATION_PATHS;
33+
use crate::core::build_steps::test::failed_tests::collect_previously_failed_tests;
3334
pub use crate::core::config::flags::Subcommand;
3435
use crate::core::config::flags::{Color, Flags, Warnings};
3536
use crate::core::config::target_selection::TargetSelectionList;
@@ -125,6 +126,7 @@ pub struct Config {
125126
pub stage0_metadata: build_helper::stage0_parser::Stage0,
126127
pub android_ndk: Option<PathBuf>,
127128
pub optimized_compiler_builtins: CompilerBuiltins,
129+
pub record_failed_tests_path: PathBuf,
128130

129131
pub stdout_is_tty: bool,
130132
pub stderr_is_tty: bool,
@@ -507,6 +509,7 @@ impl Config {
507509
dist_stage: build_dist_stage,
508510
bench_stage: build_bench_stage,
509511
patch_binaries_for_nix: build_patch_binaries_for_nix,
512+
record_failed_tests_path: build_record_failed_tests_path,
510513
// This field is only used by bootstrap.py
511514
metrics: _,
512515
android_ndk: build_android_ndk,
@@ -1305,6 +1308,19 @@ impl Config {
13051308
);
13061309
let verbose_tests = rust_verbose_tests.unwrap_or(exec_ctx.is_verbose());
13071310

1311+
let record_failed_tests_path =
1312+
out.join(build_record_failed_tests_path.unwrap_or_else(|| "failed-tests".to_string()));
1313+
1314+
let paths = {
1315+
let mut paths = Vec::new();
1316+
if flags_cmd.rerun() {
1317+
paths = collect_previously_failed_tests(&record_failed_tests_path);
1318+
} else {
1319+
paths.extend(flags_paths);
1320+
}
1321+
paths
1322+
};
1323+
13081324
Config {
13091325
// tidy-alphabetical-start
13101326
android_ndk: build_android_ndk,
@@ -1435,13 +1451,14 @@ impl Config {
14351451
out,
14361452
patch_binaries_for_nix: build_patch_binaries_for_nix,
14371453
path_modification_cache,
1438-
paths: flags_paths,
1454+
paths,
14391455
prefix: install_prefix.map(PathBuf::from),
14401456
print_step_rusage: build_print_step_rusage.unwrap_or(false),
14411457
print_step_timings: build_print_step_timings.unwrap_or(false),
14421458
profiler: build_profiler.unwrap_or(false),
14431459
python: build_python.map(PathBuf::from),
14441460
quiet: flags_quiet,
1461+
record_failed_tests_path,
14451462
reproducible_artifacts: flags_reproducible_artifact,
14461463
reuse: build_reuse.map(PathBuf::from),
14471464
rust_analyzer_info,

src/bootstrap/src/core/config/flags.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,15 @@ pub enum Subcommand {
440440
#[arg(long)]
441441
#[doc(hidden)]
442442
no_doc: bool,
443+
444+
/// Record all the failed tests in a file in the build directory.
445+
///
446+
/// On subsequent invocations, this set of tests can be rerun by passing `--rerun`
447+
#[arg(long)]
448+
record: bool,
449+
/// Rerun tests that previously failed, and stored with `--record`.
450+
#[arg(long)]
451+
rerun: bool,
443452
},
444453
/// Build and run some test suites *in Miri*
445454
Miri {
@@ -723,6 +732,20 @@ impl Subcommand {
723732
_ => false,
724733
}
725734
}
735+
736+
pub fn record(&self) -> bool {
737+
match self {
738+
Subcommand::Test { record, .. } => *record,
739+
_ => false,
740+
}
741+
}
742+
743+
pub fn rerun(&self) -> bool {
744+
match self {
745+
Subcommand::Test { rerun, .. } => *rerun,
746+
_ => false,
747+
}
748+
}
726749
}
727750

728751
/// Returns the shell completion for a given shell, if the result differs from the current

src/bootstrap/src/core/config/toml/build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ define_config! {
7878
tidy_extra_checks: Option<String> = "tidy-extra-checks",
7979
ccache: Option<StringOrBool> = "ccache",
8080
exclude: Option<Vec<PathBuf>> = "exclude",
81+
record_failed_tests_path: Option<String> = "record_failed_tests_path",
8182
}
8283
}
8384

src/bootstrap/src/utils/change_tracker.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,11 @@ pub const CONFIG_CHANGE_HISTORY: &[ChangeInfo] = &[
621621
severity: ChangeSeverity::Info,
622622
summary: "`x.py` stopped accepting partial argument names. Use full names to avoid errors.",
623623
},
624+
ChangeInfo {
625+
change_id: 154586,
626+
severity: ChangeSeverity::Info,
627+
summary: "New option `build.record_failed_tests_path` to store failed tests when passing `--record`. These can be rerun with `--rerun`.",
628+
},
624629
ChangeInfo {
625630
change_id: 154587,
626631
severity: ChangeSeverity::Info,

src/bootstrap/src/utils/render_tests.rs

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
//! and rustc) libtest doesn't include the rendered human-readable output as a JSON field. We had
77
//! to reimplement all the rendering logic in this module because of that.
88
9+
use std::fs::File;
910
use std::io::{BufRead, BufReader, Read, Write};
1011
use std::process::ChildStdout;
1112
use std::time::Duration;
1213

1314
use termcolor::{Color, ColorSpec, WriteColor};
1415

16+
use crate::core::build_steps::test::failed_tests::RecordFailedTests;
1517
use crate::core::builder::Builder;
1618
use crate::utils::exec::BootstrapCommand;
1719

@@ -20,21 +22,23 @@ const TERSE_TESTS_PER_LINE: usize = 88;
2022
pub(crate) fn add_flags_and_try_run_tests(
2123
builder: &Builder<'_>,
2224
cmd: &mut BootstrapCommand,
25+
record_failed_tests: RecordFailedTests,
2326
) -> bool {
2427
if !cmd.get_args().any(|arg| arg == "--") {
2528
cmd.arg("--");
2629
}
2730
cmd.args(["-Z", "unstable-options", "--format", "json"]);
2831

29-
try_run_tests(builder, cmd, false)
32+
try_run_tests(builder, cmd, false, record_failed_tests)
3033
}
3134

3235
pub(crate) fn try_run_tests(
3336
builder: &Builder<'_>,
3437
cmd: &mut BootstrapCommand,
3538
stream: bool,
39+
record_failed_tests: RecordFailedTests,
3640
) -> bool {
37-
if run_tests(builder, cmd, stream) {
41+
if run_tests(builder, cmd, stream, record_failed_tests) {
3842
return true;
3943
}
4044

@@ -47,7 +51,12 @@ pub(crate) fn try_run_tests(
4751
false
4852
}
4953

50-
fn run_tests(builder: &Builder<'_>, cmd: &mut BootstrapCommand, stream: bool) -> bool {
54+
fn run_tests(
55+
builder: &Builder<'_>,
56+
cmd: &mut BootstrapCommand,
57+
stream: bool,
58+
record_failed_tests: RecordFailedTests,
59+
) -> bool {
5160
builder.do_if_verbose(|| println!("running: {cmd:?}"));
5261

5362
let Some(mut streaming_command) = cmd.stream_capture_stdout(&builder.config.exec_ctx) else {
@@ -56,7 +65,8 @@ fn run_tests(builder: &Builder<'_>, cmd: &mut BootstrapCommand, stream: bool) ->
5665

5766
// This runs until the stdout of the child is closed, which means the child exited. We don't
5867
// run this on another thread since the builder is not Sync.
59-
let renderer = Renderer::new(streaming_command.stdout.take().unwrap(), builder);
68+
let renderer =
69+
Renderer::new(streaming_command.stdout.take().unwrap(), builder, record_failed_tests);
6070
if stream {
6171
renderer.stream_all();
6272
} else {
@@ -87,10 +97,30 @@ struct Renderer<'a> {
8797
ignored_tests: usize,
8898
terse_tests_in_line: usize,
8999
ci_latest_logged_percentage: f64,
100+
101+
failed_tests: Option<File>,
90102
}
91103

92104
impl<'a> Renderer<'a> {
93-
fn new(stdout: ChildStdout, builder: &'a Builder<'a>) -> Self {
105+
fn new(
106+
stdout: ChildStdout,
107+
builder: &'a Builder<'a>,
108+
record_failed_tests: RecordFailedTests,
109+
) -> Self {
110+
let failed_tests = record_failed_tests.path().and_then(|path| {
111+
// create the file (overwriting any previous) to get ready to record new failed tests
112+
match File::options().create(true).append(true).truncate(false).open(path) {
113+
Ok(f) => Some(f),
114+
Err(e) => {
115+
println!(
116+
"Couldn't open file {} to write test failures to: {e}. (attempted because `--record` was passed). Test failures will not be recorded.",
117+
path.display()
118+
);
119+
None
120+
}
121+
}
122+
});
123+
94124
Self {
95125
stdout: BufReader::new(stdout),
96126
benches: Vec::new(),
@@ -102,6 +132,7 @@ impl<'a> Renderer<'a> {
102132
ignored_tests: 0,
103133
terse_tests_in_line: 0,
104134
ci_latest_logged_percentage: 0.0,
135+
failed_tests,
105136
}
106137
}
107138

@@ -268,6 +299,13 @@ impl<'a> Renderer<'a> {
268299
for failure in &self.failures {
269300
println!(" {}", failure.name);
270301
}
302+
303+
if self.failed_tests.is_some() {
304+
println!(
305+
"This list of test failures was recorded.\nUse `x test --rerun` to retry just these {} failed tests.",
306+
self.failures.len(),
307+
)
308+
}
271309
}
272310

273311
if !self.benches.is_empty() {
@@ -360,6 +398,13 @@ impl<'a> Renderer<'a> {
360398
}
361399
Message::Test(TestMessage::Failed(outcome)) => {
362400
self.render_test_outcome(Outcome::Failed, &outcome);
401+
if let Some(failed_tests) = &mut self.failed_tests
402+
&& let Err(e) = writeln!(failed_tests, "{}", outcome.name)
403+
{
404+
eprintln!(
405+
"failed to write test failure to file: {e} (attempted because `--record` was passed)"
406+
);
407+
}
363408
self.failures.push(outcome);
364409
}
365410
Message::Test(TestMessage::Timeout { name }) => {

src/etc/completions/x.fish

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,8 @@ complete -c x -n "__fish_x_using_subcommand test" -l rustfix-coverage -d 'enable
465465
complete -c x -n "__fish_x_using_subcommand test" -l no-capture -d 'don\'t capture stdout/stderr of tests'
466466
complete -c x -n "__fish_x_using_subcommand test" -l bypass-ignore-backends -d 'Ignore `//@ ignore-backends` directives'
467467
complete -c x -n "__fish_x_using_subcommand test" -l no-doc -d 'Deprecated. Use `--all-targets` or `--tests` instead'
468+
complete -c x -n "__fish_x_using_subcommand test" -l record -d 'Record all the failed tests in a file in the build directory'
469+
complete -c x -n "__fish_x_using_subcommand test" -l rerun -d 'Rerun tests that previously failed, and stored with `--record`'
468470
complete -c x -n "__fish_x_using_subcommand test" -s v -l verbose -d 'use verbose output (-vv for very verbose)'
469471
complete -c x -n "__fish_x_using_subcommand test" -s q -l quiet -d 'use quiet output'
470472
complete -c x -n "__fish_x_using_subcommand test" -s i -l incremental -d 'use incremental compilation'
@@ -520,6 +522,8 @@ complete -c x -n "__fish_x_using_subcommand t" -l rustfix-coverage -d 'enable th
520522
complete -c x -n "__fish_x_using_subcommand t" -l no-capture -d 'don\'t capture stdout/stderr of tests'
521523
complete -c x -n "__fish_x_using_subcommand t" -l bypass-ignore-backends -d 'Ignore `//@ ignore-backends` directives'
522524
complete -c x -n "__fish_x_using_subcommand t" -l no-doc -d 'Deprecated. Use `--all-targets` or `--tests` instead'
525+
complete -c x -n "__fish_x_using_subcommand t" -l record -d 'Record all the failed tests in a file in the build directory'
526+
complete -c x -n "__fish_x_using_subcommand t" -l rerun -d 'Rerun tests that previously failed, and stored with `--record`'
523527
complete -c x -n "__fish_x_using_subcommand t" -s v -l verbose -d 'use verbose output (-vv for very verbose)'
524528
complete -c x -n "__fish_x_using_subcommand t" -s q -l quiet -d 'use quiet output'
525529
complete -c x -n "__fish_x_using_subcommand t" -s i -l incremental -d 'use incremental compilation'

src/etc/completions/x.ps1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,8 @@ Register-ArgumentCompleter -Native -CommandName 'x' -ScriptBlock {
543543
[CompletionResult]::new('--no-capture', '--no-capture', [CompletionResultType]::ParameterName, 'don''t capture stdout/stderr of tests')
544544
[CompletionResult]::new('--bypass-ignore-backends', '--bypass-ignore-backends', [CompletionResultType]::ParameterName, 'Ignore `//@ ignore-backends` directives')
545545
[CompletionResult]::new('--no-doc', '--no-doc', [CompletionResultType]::ParameterName, 'Deprecated. Use `--all-targets` or `--tests` instead')
546+
[CompletionResult]::new('--record', '--record', [CompletionResultType]::ParameterName, 'Record all the failed tests in a file in the build directory')
547+
[CompletionResult]::new('--rerun', '--rerun', [CompletionResultType]::ParameterName, 'Rerun tests that previously failed, and stored with `--record`')
546548
[CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'use verbose output (-vv for very verbose)')
547549
[CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'use verbose output (-vv for very verbose)')
548550
[CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'use quiet output')
@@ -606,6 +608,8 @@ Register-ArgumentCompleter -Native -CommandName 'x' -ScriptBlock {
606608
[CompletionResult]::new('--no-capture', '--no-capture', [CompletionResultType]::ParameterName, 'don''t capture stdout/stderr of tests')
607609
[CompletionResult]::new('--bypass-ignore-backends', '--bypass-ignore-backends', [CompletionResultType]::ParameterName, 'Ignore `//@ ignore-backends` directives')
608610
[CompletionResult]::new('--no-doc', '--no-doc', [CompletionResultType]::ParameterName, 'Deprecated. Use `--all-targets` or `--tests` instead')
611+
[CompletionResult]::new('--record', '--record', [CompletionResultType]::ParameterName, 'Record all the failed tests in a file in the build directory')
612+
[CompletionResult]::new('--rerun', '--rerun', [CompletionResultType]::ParameterName, 'Rerun tests that previously failed, and stored with `--record`')
609613
[CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'use verbose output (-vv for very verbose)')
610614
[CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'use verbose output (-vv for very verbose)')
611615
[CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'use quiet output')

0 commit comments

Comments
 (0)