feat: implement basic suggest-tests tool

This commit is contained in:
Ezra Shaw 2023-03-01 22:17:08 +13:00
parent 7cd6f55323
commit 1e95cddc74
No known key found for this signature in database
GPG key ID: 67ABF16FB0ECD870
15 changed files with 377 additions and 25 deletions

View file

@ -3459,9 +3459,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.16.0"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "opener"
@ -6097,6 +6097,15 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "suggest-tests"
version = "0.1.0"
dependencies = [
"build_helper",
"glob",
"once_cell",
]
[[package]]
name = "syn"
version = "1.0.102"

View file

@ -44,6 +44,7 @@ members = [
"src/tools/lld-wrapper",
"src/tools/collect-license-metadata",
"src/tools/generate-copyright",
"src/tools/suggest-tests",
]
exclude = [

View file

@ -591,6 +591,7 @@ pub enum Kind {
Install,
Run,
Setup,
Suggest,
}
impl Kind {
@ -610,6 +611,7 @@ pub fn parse(string: &str) -> Option<Kind> {
"install" => Kind::Install,
"run" | "r" => Kind::Run,
"setup" => Kind::Setup,
"suggest" => Kind::Suggest,
_ => return None,
})
}
@ -629,6 +631,7 @@ pub fn as_str(&self) -> &'static str {
Kind::Install => "install",
Kind::Run => "run",
Kind::Setup => "setup",
Kind::Suggest => "suggest",
}
}
}
@ -709,6 +712,7 @@ macro_rules! describe {
test::CrateRustdoc,
test::CrateRustdocJsonTypes,
test::CrateJsonDocLint,
test::SuggestTestsCrate,
test::Linkcheck,
test::TierCheck,
test::ReplacePlaceholderTest,
@ -827,7 +831,7 @@ macro_rules! describe {
Kind::Setup => describe!(setup::Profile, setup::Hook, setup::Link, setup::Vscode),
Kind::Clean => describe!(clean::CleanAll, clean::Rustc, clean::Std),
// special-cased in Build::build()
Kind::Format => vec![],
Kind::Format | Kind::Suggest => vec![],
}
}
@ -891,6 +895,7 @@ pub fn new(build: &Build) -> Builder<'_> {
Subcommand::Run { ref paths, .. } => (Kind::Run, &paths[..]),
Subcommand::Clean { ref paths, .. } => (Kind::Clean, &paths[..]),
Subcommand::Format { .. } => (Kind::Format, &[][..]),
Subcommand::Suggest { .. } => (Kind::Suggest, &[][..]),
Subcommand::Setup { profile: ref path } => (
Kind::Setup,
path.as_ref().map_or([].as_slice(), |path| std::slice::from_ref(path)),
@ -900,6 +905,21 @@ pub fn new(build: &Build) -> Builder<'_> {
Self::new_internal(build, kind, paths.to_owned())
}
/// Creates a new standalone builder for use outside of the normal process
pub fn new_standalone(
build: &mut Build,
kind: Kind,
paths: Vec<PathBuf>,
stage: Option<u32>,
) -> Builder<'_> {
// FIXME: don't mutate `build`
if let Some(stage) = stage {
build.config.stage = stage;
}
Self::new_internal(build, kind, paths.to_owned())
}
pub fn execute_cli(&self) {
self.run_step_descriptions(&Builder::get_step_descriptions(self.kind), &self.paths);
}

View file

@ -56,8 +56,7 @@ pub enum DryRun {
/// filled out from the decoded forms of the structs below. For documentation
/// each field, see the corresponding fields in
/// `config.example.toml`.
#[derive(Default)]
#[cfg_attr(test, derive(Clone))]
#[derive(Default, Clone)]
pub struct Config {
pub changelog_seen: Option<usize>,
pub ccache: Option<String>,
@ -240,23 +239,20 @@ pub struct Config {
pub initial_rustfmt: RefCell<RustfmtState>,
}
#[derive(Default, Deserialize)]
#[cfg_attr(test, derive(Clone))]
#[derive(Default, Deserialize, Clone)]
pub struct Stage0Metadata {
pub compiler: CompilerMetadata,
pub config: Stage0Config,
pub checksums_sha256: HashMap<String, String>,
pub rustfmt: Option<RustfmtMetadata>,
}
#[derive(Default, Deserialize)]
#[cfg_attr(test, derive(Clone))]
#[derive(Default, Deserialize, Clone)]
pub struct CompilerMetadata {
pub date: String,
pub version: String,
}
#[derive(Default, Deserialize)]
#[cfg_attr(test, derive(Clone))]
#[derive(Default, Deserialize, Clone)]
pub struct Stage0Config {
pub dist_server: String,
pub artifacts_server: String,
@ -264,8 +260,7 @@ pub struct Stage0Config {
pub git_merge_commit_email: String,
pub nightly_branch: String,
}
#[derive(Default, Deserialize)]
#[cfg_attr(test, derive(Clone))]
#[derive(Default, Deserialize, Clone)]
pub struct RustfmtMetadata {
pub date: String,
pub version: String,
@ -443,8 +438,7 @@ fn eq(&self, other: &&str) -> bool {
}
/// Per-target configuration stored in the global configuration structure.
#[derive(Default)]
#[cfg_attr(test, derive(Clone))]
#[derive(Default, Clone)]
pub struct Target {
/// Some(path to llvm-config) if using an external LLVM.
pub llvm_config: Option<PathBuf>,
@ -1396,7 +1390,8 @@ fn parse_inner<'a>(args: &[String], get_toml: impl 'a + Fn(&Path) -> TomlConfig)
| Subcommand::Fix { .. }
| Subcommand::Run { .. }
| Subcommand::Setup { .. }
| Subcommand::Format { .. } => flags.stage.unwrap_or(0),
| Subcommand::Format { .. }
| Subcommand::Suggest { .. } => flags.stage.unwrap_or(0),
};
// CI should always run stage 2 builds, unless it specifically states otherwise
@ -1421,7 +1416,8 @@ fn parse_inner<'a>(args: &[String], get_toml: impl 'a + Fn(&Path) -> TomlConfig)
| Subcommand::Fix { .. }
| Subcommand::Run { .. }
| Subcommand::Setup { .. }
| Subcommand::Format { .. } => {}
| Subcommand::Format { .. }
| Subcommand::Suggest { .. } => {}
}
}

View file

@ -84,8 +84,7 @@ pub struct Flags {
pub free_args: Option<Vec<String>>,
}
#[derive(Debug)]
#[cfg_attr(test, derive(Clone))]
#[derive(Debug, Clone)]
pub enum Subcommand {
Build {
paths: Vec<PathBuf>,
@ -149,6 +148,9 @@ pub enum Subcommand {
Setup {
profile: Option<PathBuf>,
},
Suggest {
run: bool,
},
}
impl Default for Subcommand {
@ -183,6 +185,7 @@ pub fn parse(args: &[String]) -> Flags {
install Install distribution artifacts
run, r Run tools contained in this repository
setup Create a config.toml (making it easier to use `x.py` itself)
suggest Suggest a subset of tests to run, based on modified files
To learn more about a subcommand, run `./x.py <subcommand> -h`",
);
@ -349,6 +352,9 @@ pub fn parse(args: &[String]) -> Flags {
Kind::Run => {
opts.optmulti("", "args", "arguments for the tool", "ARGS");
}
Kind::Suggest => {
opts.optflag("", "run", "run suggested tests");
}
_ => {}
};
@ -565,7 +571,7 @@ pub fn parse(args: &[String]) -> Flags {
Profile::all_for_help(" ").trim_end()
));
}
Kind::Bench | Kind::Clean | Kind::Dist | Kind::Install => {}
Kind::Bench | Kind::Clean | Kind::Dist | Kind::Install | Kind::Suggest => {}
};
// Get any optional paths which occur after the subcommand
let mut paths = matches.free[1..].iter().map(|p| p.into()).collect::<Vec<PathBuf>>();
@ -626,6 +632,7 @@ pub fn parse(args: &[String]) -> Flags {
Kind::Format => Subcommand::Format { check: matches.opt_present("check"), paths },
Kind::Dist => Subcommand::Dist { paths },
Kind::Install => Subcommand::Install { paths },
Kind::Suggest => Subcommand::Suggest { run: matches.opt_present("run") },
Kind::Run => {
if paths.is_empty() {
println!("\nrun requires at least a path!\n");
@ -734,6 +741,7 @@ pub fn kind(&self) -> Kind {
Subcommand::Install { .. } => Kind::Install,
Subcommand::Run { .. } => Kind::Run,
Subcommand::Setup { .. } => Kind::Setup,
Subcommand::Suggest { .. } => Kind::Suggest,
}
}

View file

@ -58,6 +58,7 @@
mod run;
mod sanity;
mod setup;
mod suggest;
mod tarball;
mod test;
mod tool;
@ -189,6 +190,7 @@ pub enum GitRepo {
/// although most functions are implemented as free functions rather than
/// methods specifically on this structure itself (to make it easier to
/// organize).
#[derive(Clone)]
pub struct Build {
/// User-specified configuration from `config.toml`.
config: Config,
@ -242,7 +244,7 @@ pub struct Build {
metrics: metrics::BuildMetrics,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
struct Crate {
name: Interned<String>,
deps: HashSet<Interned<String>>,
@ -656,13 +658,20 @@ pub fn build(&mut self) {
job::setup(self);
}
if let Subcommand::Format { check, paths } = &self.config.cmd {
return format::format(&builder::Builder::new(&self), *check, &paths);
}
// Download rustfmt early so that it can be used in rust-analyzer configs.
let _ = &builder::Builder::new(&self).initial_rustfmt();
// hardcoded subcommands
match &self.config.cmd {
Subcommand::Format { check, paths } => {
return format::format(&builder::Builder::new(&self), *check, &paths);
}
Subcommand::Suggest { run } => {
return suggest::suggest(&builder::Builder::new(&self), *run);
}
_ => (),
}
{
let builder = builder::Builder::new(&self);
if let Some(path) = builder.paths.get(0) {

72
src/bootstrap/suggest.rs Normal file
View file

@ -0,0 +1,72 @@
use std::str::FromStr;
use std::path::PathBuf;
use crate::{
builder::{Builder, Kind},
tool::Tool,
};
/// Suggests a list of possible `x.py` commands to run based on modified files in branch.
pub fn suggest(builder: &Builder<'_>, run: bool) {
let suggestions =
builder.tool_cmd(Tool::SuggestTests).output().expect("failed to run `suggest-tests` tool");
if !suggestions.status.success() {
println!("failed to run `suggest-tests` tool ({})", suggestions.status);
println!(
"`suggest_tests` stdout:\n{}`suggest_tests` stderr:\n{}",
String::from_utf8(suggestions.stdout).unwrap(),
String::from_utf8(suggestions.stderr).unwrap()
);
panic!("failed to run `suggest-tests`");
}
let suggestions = String::from_utf8(suggestions.stdout).unwrap();
let suggestions = suggestions
.lines()
.map(|line| {
let mut sections = line.split_ascii_whitespace();
// this code expects one suggestion per line in the following format:
// <x_subcommand> {some number of flags} [optional stage number]
let cmd = sections.next().unwrap();
let stage = sections.next_back().map(|s| str::parse(s).ok()).flatten();
let paths: Vec<PathBuf> = sections.map(|p| PathBuf::from_str(p).unwrap()).collect();
(cmd, stage, paths)
})
.collect::<Vec<_>>();
if !suggestions.is_empty() {
println!("==== SUGGESTIONS ====");
for sug in &suggestions {
print!("x {} ", sug.0);
if let Some(stage) = sug.1 {
print!("--stage {stage} ");
}
for path in &sug.2 {
print!("{} ", path.display());
}
println!();
}
println!("=====================");
} else {
println!("No suggestions found!");
return;
}
if run {
for sug in suggestions {
let mut build = builder.build.clone();
let builder =
Builder::new_standalone(&mut build, Kind::parse(&sug.0).unwrap(), sug.2, sug.1);
builder.execute_cli()
}
} else {
println!("help: consider using the `--run` flag to automatically run suggested tests");
}
}

View file

@ -128,6 +128,42 @@ fn run(self, builder: &Builder<'_>) {
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct SuggestTestsCrate {
host: TargetSelection,
}
impl Step for SuggestTestsCrate {
type Output = ();
const ONLY_HOSTS: bool = true;
const DEFAULT: bool = true;
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
run.path("src/tools/suggest-tests")
}
fn make_run(run: RunConfig<'_>) {
run.builder.ensure(SuggestTestsCrate { host: run.target });
}
fn run(self, builder: &Builder<'_>) {
let bootstrap_host = builder.config.build;
let compiler = builder.compiler(0, bootstrap_host);
let suggest_tests = tool::prepare_tool_cargo(
builder,
compiler,
Mode::ToolBootstrap,
bootstrap_host,
"test",
"src/tools/suggest-tests",
SourceType::InTree,
&[],
);
add_flags_and_try_run_tests(builder, &mut suggest_tests.into());
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct Linkcheck {
host: TargetSelection,

View file

@ -433,6 +433,7 @@ fn run(self, builder: &Builder<'_>) -> PathBuf {
ReplaceVersionPlaceholder, "src/tools/replace-version-placeholder", "replace-version-placeholder";
CollectLicenseMetadata, "src/tools/collect-license-metadata", "collect-license-metadata";
GenerateCopyright, "src/tools/generate-copyright", "generate-copyright";
SuggestTests, "src/tools/suggest-tests", "suggest-tests";
);
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]

View file

@ -0,0 +1,9 @@
[package]
name = "suggest-tests"
version = "0.1.0"
edition = "2021"
[dependencies]
glob = "0.3.0"
build_helper = { version = "0.1.0", path = "../build_helper" }
once_cell = "1.17.1"

View file

@ -0,0 +1,23 @@
use std::path::Path;
use crate::Suggestion;
type DynamicSuggestion = fn(&Path) -> Vec<Suggestion>;
pub(crate) const DYNAMIC_SUGGESTIONS: &[DynamicSuggestion] = &[|path: &Path| -> Vec<Suggestion> {
if path.starts_with("compiler/") || path.starts_with("library/") {
let path = path.components().take(2).collect::<Vec<_>>();
vec![Suggestion::with_single_path(
"test",
None,
&format!(
"{}/{}",
path[0].as_os_str().to_str().unwrap(),
path[1].as_os_str().to_str().unwrap()
),
)]
} else {
Vec::new()
}
}];

View file

@ -0,0 +1,96 @@
use std::{
fmt::{self, Display},
path::Path,
};
use dynamic_suggestions::DYNAMIC_SUGGESTIONS;
use glob::Pattern;
use static_suggestions::STATIC_SUGGESTIONS;
mod dynamic_suggestions;
mod static_suggestions;
#[cfg(test)]
mod tests;
macro_rules! sug {
($cmd:expr) => {
Suggestion::new($cmd, None, &[])
};
($cmd:expr, $paths:expr) => {
Suggestion::new($cmd, None, $paths.as_slice())
};
($cmd:expr, $stage:expr, $paths:expr) => {
Suggestion::new($cmd, Some($stage), $paths.as_slice())
};
}
pub(crate) use sug;
pub fn get_suggestions<T: AsRef<str>>(modified_files: &[T]) -> Vec<Suggestion> {
let mut suggestions = Vec::new();
// static suggestions
for sug in STATIC_SUGGESTIONS.iter() {
let glob = Pattern::new(&sug.0).expect("Found invalid glob pattern!");
for file in modified_files {
if glob.matches(file.as_ref()) {
suggestions.extend_from_slice(&sug.1);
}
}
}
// dynamic suggestions
for sug in DYNAMIC_SUGGESTIONS {
for file in modified_files {
let sugs = sug(Path::new(file.as_ref()));
suggestions.extend_from_slice(&sugs);
}
}
suggestions.sort();
suggestions.dedup();
suggestions
}
#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Debug)]
pub struct Suggestion {
pub cmd: String,
pub stage: Option<u32>,
pub paths: Vec<String>,
}
impl Suggestion {
pub fn new(cmd: &str, stage: Option<u32>, paths: &[&str]) -> Self {
Self { cmd: cmd.to_owned(), stage, paths: paths.iter().map(|p| p.to_string()).collect() }
}
pub fn with_single_path(cmd: &str, stage: Option<u32>, path: &str) -> Self {
Self::new(cmd, stage, &[path])
}
}
impl Display for Suggestion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "{} ", self.cmd)?;
for path in &self.paths {
write!(f, "{} ", path)?;
}
if let Some(stage) = self.stage {
write!(f, "{}", stage)?;
} else {
// write a sentinel value here (in place of a stage) to be consumed
// by the shim in bootstrap, it will be read and ignored.
write!(f, "N/A")?;
}
Ok(())
}
}

View file

@ -0,0 +1,27 @@
use std::process::ExitCode;
use build_helper::git::get_git_modified_files;
use suggest_tests::get_suggestions;
fn main() -> ExitCode {
let modified_files = get_git_modified_files(None, &Vec::new());
let modified_files = match modified_files {
Ok(Some(files)) => files,
Ok(None) => {
eprintln!("git error");
return ExitCode::FAILURE;
}
Err(err) => {
eprintln!("Could not get modified files from git: \"{err}\"");
return ExitCode::FAILURE;
}
};
let suggestions = get_suggestions(&modified_files);
for sug in &suggestions {
println!("{sug}");
}
ExitCode::SUCCESS
}

View file

@ -0,0 +1,24 @@
use crate::{sug, Suggestion};
// FIXME: perhaps this could use `std::lazy` when it is stablizied
macro_rules! static_suggestions {
($( $glob:expr => [ $( $suggestion:expr ),* ] ),*) => {
pub(crate) const STATIC_SUGGESTIONS: ::once_cell::unsync::Lazy<Vec<(&'static str, Vec<Suggestion>)>>
= ::once_cell::unsync::Lazy::new(|| vec![ $( ($glob, vec![ $($suggestion),* ]) ),*]);
}
}
static_suggestions! {
"*.md" => [
sug!("test", 0, ["linkchecker"])
],
"compiler/*" => [
sug!("check"),
sug!("test", 1, ["src/test/ui", "src/test/run-make"])
],
"src/librustdoc/*" => [
sug!("test", 1, ["rustdoc"])
]
}

View file

@ -0,0 +1,21 @@
macro_rules! sugg_test {
( $( $name:ident: $paths:expr => $suggestions:expr ),* ) => {
$(
#[test]
fn $name() {
let suggestions = crate::get_suggestions(&$paths).into_iter().map(|s| s.to_string()).collect::<Vec<_>>();
assert_eq!(suggestions, $suggestions);
}
)*
};
}
sugg_test! {
test_error_code_docs: ["compiler/rustc_error_codes/src/error_codes/E0000.md"] =>
["check N/A", "test compiler/rustc_error_codes N/A", "test linkchecker 0", "test src/test/ui src/test/run-make 1"],
test_rustdoc: ["src/librustdoc/src/lib.rs"] => ["test rustdoc 1"],
test_rustdoc_and_libstd: ["src/librustdoc/src/lib.rs", "library/std/src/lib.rs"] =>
["test library/std N/A", "test rustdoc 1"]
}