blob: a55c2dcf83ddc8ed557078737f59e6858b3eb7d5 [file]
use std::collections::{HashMap, HashSet};
use std::ffi::OsStr;
use std::fs::{File, read_to_string, remove_dir_all};
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use boml::Toml;
use crate::build;
use crate::config::{Channel, ConfigInfo};
use crate::utils::{
create_dir, get_sysroot_dir, get_toolchain, git_clone, git_clone_root_dir, remove_file,
run_command, run_command_with_env, run_command_with_output_and_env,
run_command_with_output_and_env_no_err, rustc_version_info, split_args, walk_dir,
};
/// Exit code of `y.sh test` when the tests ran and reported failures, as opposed to the build
/// system failing to run them at all. CI relies on the distinction: the suites of known-failing
/// tests are expected to report failures, but a broken build system must never pass silently.
pub const TESTS_FAILED_EXIT_CODE: i32 = 2;
/// The error returned for that case. `main` compares against it to pick the exit code, so no other
/// error may use this message.
pub const TESTS_FAILED_ERROR: &str = "the test suite reported failures";
type Env = HashMap<String, String>;
type Runner = fn(&Env, &TestArg) -> Result<(), String>;
type Runners = HashMap<&'static str, (&'static str, Runner)>;
fn get_runners() -> Runners {
let mut runners = HashMap::new();
runners.insert("--test-rustc", ("Run all rustc tests", test_rustc as Runner));
runners
.insert("--test-successful-rustc", ("Run successful rustc tests", test_successful_rustc));
runners.insert(
"--test-failing-ui-pattern-tests",
("Run failing ui pattern tests", test_failing_ui_pattern_tests),
);
runners.insert("--test-failing-rustc", ("Run failing rustc tests", test_failing_rustc));
runners.insert("--run-ui-tests", ("Run specified rustc UI tests", run_ui_tests));
runners.insert("--projects", ("Run the tests of popular crates", test_projects));
runners.insert("--test-libcore", ("Run libcore tests", test_libcore));
runners.insert("--test-release-libcore", ("Run libcore tests", test_release_libcore));
runners.insert("--test-libcore-doctests", ("Run libcore doc-tests", test_libcore_doctests));
runners.insert("--alloc-tests", ("Run alloc tests", test_alloc));
runners.insert("--clean", ("Empty cargo target directory", clean));
runners.insert("--build-sysroot", ("Build sysroot", build_sysroot));
runners.insert("--std-tests", ("Run std tests", std_tests));
runners.insert("--asm-tests", ("Run asm tests", asm_tests));
runners.insert("--extended-tests", ("Run extended sysroot tests", extended_sysroot_tests));
runners.insert("--extended-rand-tests", ("Run extended rand tests", extended_rand_tests));
runners.insert(
"--extended-regex-example-tests",
("Run extended regex example tests", extended_regex_example_tests),
);
runners.insert("--extended-regex-tests", ("Run extended regex tests", extended_regex_tests));
runners.insert("--mini-tests", ("Run mini tests", mini_tests));
runners.insert("--gcc-asm-tests", ("Run cg_gcc asm tests", test_asm));
runners.insert("--cargo-tests", ("Run cargo tests", cargo_tests));
runners.insert("--no-builtins-tests", ("Test #![no_builtins] attribute", no_builtins_tests));
runners.insert("--stdarch-tests", ("Run stdarch tests", test_stdarch as Runner));
runners
}
fn get_number_after_arg(
args: &mut impl Iterator<Item = String>,
option: &str,
) -> Result<usize, String> {
match args.next() {
Some(nb) if !nb.is_empty() => match usize::from_str(&nb) {
Ok(nb) => Ok(nb),
Err(_) => Err(format!("Expected a number after `{option}`, found `{nb}`")),
},
_ => Err(format!("Expected a number after `{option}`, found nothing")),
}
}
fn show_usage() {
println!(
r#"
`test` command help:
--features [arg] : Add a new feature [arg]
--use-system-gcc : Use system installed libgccjit
--build-only : Only build rustc_codegen_gcc then exits
--nb-parts : Used to split rustc_tests (for CI needs)
--current-part : Used with `--nb-parts`, allows you to specify which parts to test"#
);
ConfigInfo::show_usage();
for (option, (doc, _)) in get_runners() {
// FIXME: Instead of using the hard-coded `23` value, better to compute it instead.
let needed_spaces = 23_usize.saturating_sub(option.len());
let spaces: String = std::iter::repeat_n(' ', needed_spaces).collect();
println!(" {option}{spaces}: {doc}");
}
println!(" --help : Show this help");
}
#[derive(Default, Debug)]
struct TestArg {
build_only: bool,
use_system_gcc: bool,
runners: Vec<String>,
flags: Vec<String>,
/// Additional arguments, to be passed to commands like `cargo test`.
test_args: Vec<String>,
nb_parts: Option<usize>,
current_part: Option<usize>,
config_info: ConfigInfo,
sysroot_features: Vec<String>,
keep_lto_tests: bool,
}
impl TestArg {
fn new() -> Result<Option<Self>, String> {
let mut test_arg = Self::default();
// We skip binary name and the `test` command.
let mut args = std::env::args().skip(2);
let runners = get_runners();
while let Some(arg) = args.next() {
match arg.as_str() {
"--features" => match args.next() {
Some(feature) if !feature.is_empty() => {
test_arg.flags.extend_from_slice(&["--features".into(), feature]);
}
_ => {
return Err("Expected an argument after `--features`, found nothing".into());
}
},
"--use-system-gcc" => {
println!("Using system GCC");
test_arg.use_system_gcc = true;
}
"--build-only" => test_arg.build_only = true,
"--nb-parts" => {
test_arg.nb_parts = Some(get_number_after_arg(&mut args, "--nb-parts")?);
}
"--current-part" => {
test_arg.current_part =
Some(get_number_after_arg(&mut args, "--current-part")?);
}
"--keep-lto-tests" => {
test_arg.keep_lto_tests = true;
}
"--sysroot-features" => match args.next() {
Some(feature) if !feature.is_empty() => {
test_arg.sysroot_features.push(feature);
}
_ => {
return Err(format!("Expected an argument after `{arg}`, found nothing"));
}
},
"--help" => {
show_usage();
return Ok(None);
}
"--" => test_arg.test_args.extend(&mut args),
x if runners.contains_key(x)
&& !test_arg.runners.iter().any(|runner| runner == x) =>
{
test_arg.runners.push(x.into());
}
arg => {
if !test_arg.config_info.parse_argument(arg, &mut args)? {
return Err(format!("Unknown option {arg}"));
}
}
}
}
match (test_arg.current_part, test_arg.nb_parts) {
(Some(_), Some(_)) | (None, None) => {}
_ => {
return Err(
"If either `--current-part` or `--nb-parts` is specified, the other one \
needs to be specified as well!"
.to_string(),
);
}
}
if test_arg.config_info.no_default_features {
test_arg.flags.push("--no-default-features".into());
}
Ok(Some(test_arg))
}
pub fn is_using_gcc_master_branch(&self) -> bool {
!self.config_info.no_default_features
}
}
fn build_if_no_backend(env: &Env, args: &TestArg) -> Result<(), String> {
if args.config_info.backend.is_some() {
return Ok(());
}
let mut command: Vec<&dyn AsRef<OsStr>> = vec![&"cargo", &"rustc"];
let mut tmp_env;
let env = if args.config_info.channel == Channel::Release {
tmp_env = env.clone();
tmp_env.insert("CARGO_INCREMENTAL".to_string(), "1".to_string());
command.push(&"--release");
&tmp_env
} else {
env
};
for flag in args.flags.iter() {
command.push(flag);
}
run_command_with_output_and_env(&command, None, Some(env))
}
fn clean(_env: &Env, args: &TestArg) -> Result<(), String> {
let _ = remove_dir_all(&args.config_info.cargo_target_dir);
let path = Path::new(&args.config_info.cargo_target_dir).join("gccjit");
create_dir(&path)
}
fn cargo_tests(test_env: &Env, test_args: &TestArg) -> Result<(), String> {
// First, we call `mini_tests` to build minicore for us. This ensures we are testing with a working `minicore`,
// and that any changes we have made affect `minicore`(since it would get rebuilt).
mini_tests(test_env, test_args)?;
// Then, we copy some of the env vars from `test_env`
// We don't want to pass things like `RUSTFLAGS`, since they contain the -Zcodegen-backend flag.
// That would force `cg_gcc` to *rebuild itself* and only then run tests, which is undesirable.
let mut env = HashMap::new();
env.insert(
"CG_RUSTFLAGS".into(),
test_env.get("CG_RUSTFLAGS").map(|s| s.as_str()).unwrap_or("").to_string(),
);
// Pass all the default args + the user-specified ones.
let mut args: Vec<&dyn AsRef<OsStr>> = vec![&"cargo", &"test"];
args.extend(test_args.test_args.iter().map(|s| s as &dyn AsRef<OsStr>));
run_command_with_output_and_env(&args, None, Some(&env))?;
Ok(())
}
fn mini_tests(env: &Env, args: &TestArg) -> Result<(), String> {
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[BUILD] mini_core");
let crate_types = if args.config_info.host_triple != args.config_info.target_triple {
"lib"
} else {
"lib,dylib"
}
.to_string();
let mut command = args.config_info.rustc_command_vec();
command.extend_from_slice(&[
&"example/mini_core.rs",
&"--crate-name",
&"mini_core",
&"--crate-type",
&crate_types,
&"--target",
&args.config_info.target_triple,
]);
run_command_with_output_and_env(&command, None, Some(env))?;
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[BUILD] example");
let mut command = args.config_info.rustc_command_vec();
command.extend_from_slice(&[
&"example/example.rs",
&"--crate-type",
&"lib",
&"--target",
&args.config_info.target_triple,
]);
run_command_with_output_and_env(&command, None, Some(env))?;
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[AOT] mini_core_hello_world");
let mut command = args.config_info.rustc_command_vec();
command.extend_from_slice(&[
&"example/mini_core_hello_world.rs",
&"--crate-name",
&"mini_core_hello_world",
&"--crate-type",
&"bin",
&"-g",
&"--target",
&args.config_info.target_triple,
]);
run_command_with_output_and_env(&command, None, Some(env))?;
let command: &[&dyn AsRef<OsStr>] = &[
&Path::new(&args.config_info.cargo_target_dir).join("mini_core_hello_world"),
&"abc",
&"bcd",
];
maybe_run_command_in_vm(command, env, args)?;
Ok(())
}
fn build_sysroot(env: &Env, args: &TestArg) -> Result<(), String> {
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[BUILD] sysroot");
let mut config = args.config_info.clone();
config.features.extend(args.sysroot_features.iter().cloned());
build::build_sysroot(env, &config)?;
Ok(())
}
// FIXME(GuillaumeGomez): when rewriting in Rust, refactor with the code in tests/lang_tests_common.rs if possible.
fn maybe_run_command_in_vm(
command: &[&dyn AsRef<OsStr>],
env: &Env,
args: &TestArg,
) -> Result<(), String> {
if !args.config_info.run_in_vm {
run_command_with_output_and_env(command, None, Some(env))?;
return Ok(());
}
let vm_parent_dir = match env.get("CG_GCC_VM_DIR") {
Some(dir) if !dir.is_empty() => PathBuf::from(dir.clone()),
_ => std::env::current_dir().unwrap(),
};
let vm_dir = "vm";
let exe_to_run = command.first().unwrap();
let exe = Path::new(&exe_to_run);
let exe_filename = exe.file_name().unwrap();
let vm_home_dir = vm_parent_dir.join(vm_dir).join("home");
let vm_exe_path = vm_home_dir.join(exe_filename);
let inside_vm_exe_path = Path::new("/home").join(exe_filename);
let sudo_command: &[&dyn AsRef<OsStr>] = &[&"sudo", &"cp", &exe, &vm_exe_path];
run_command_with_env(sudo_command, None, Some(env))?;
let mut vm_command: Vec<&dyn AsRef<OsStr>> =
vec![&"sudo", &"chroot", &vm_dir, &"qemu-m68k-static", &inside_vm_exe_path];
vm_command.extend_from_slice(command);
run_command_with_output_and_env(&vm_command, Some(&vm_parent_dir), Some(env))?;
Ok(())
}
/// Compile a source file to an object file and check if it contains a memset reference.
fn object_has_memset(
env: &Env,
args: &TestArg,
src_file: &str,
obj_file_name: &str,
) -> Result<bool, String> {
let cargo_target_dir = Path::new(&args.config_info.cargo_target_dir);
let obj_file = cargo_target_dir.join(obj_file_name);
let obj_file_str = obj_file.to_str().expect("obj_file to_str");
let mut command = args.config_info.rustc_command_vec();
command.extend_from_slice(&[
&src_file,
&"--emit",
&"obj",
&"-O",
&"--target",
&args.config_info.target_triple,
&"-o",
]);
command.push(&obj_file_str);
run_command_with_env(&command, None, Some(env))?;
let nm_output = run_command_with_env(&[&"nm", &obj_file_str], None, Some(env))?;
let nm_stdout = String::from_utf8_lossy(&nm_output.stdout);
Ok(nm_stdout.contains("memset"))
}
fn no_builtins_tests(env: &Env, args: &TestArg) -> Result<(), String> {
// Test that the #![no_builtins] attribute prevents GCC from replacing
// code patterns (like loops) with calls to builtins (like memset).
// See https://github.com/rust-lang/rustc_codegen_gcc/issues/570
// Test 1: WITH #![no_builtins] - memset should NOT be present
println!("[TEST] no_builtins attribute (with #![no_builtins])");
let has_memset =
object_has_memset(env, args, "tests/no_builtins/no_builtins.rs", "no_builtins_test.o")?;
if has_memset {
return Err("no_builtins test FAILED: Found 'memset' in object file.\n\
The #![no_builtins] attribute should prevent GCC from replacing \n\
code patterns with builtin calls."
.to_string());
}
// Test 2: WITHOUT #![no_builtins] - memset SHOULD be present
println!("[TEST] no_builtins attribute (without #![no_builtins])");
let has_memset =
object_has_memset(env, args, "tests/no_builtins/with_builtins.rs", "with_builtins_test.o")?;
if !has_memset {
return Err("no_builtins test FAILED: 'memset' NOT found in object file.\n\
Without #![no_builtins], GCC should replace the loop with memset."
.to_string());
}
Ok(())
}
fn std_tests(env: &Env, args: &TestArg) -> Result<(), String> {
let cargo_target_dir = Path::new(&args.config_info.cargo_target_dir);
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[AOT] arbitrary_self_types_pointers_and_wrappers");
let mut command = args.config_info.rustc_command_vec();
command.extend_from_slice(&[
&"example/arbitrary_self_types_pointers_and_wrappers.rs",
&"--crate-name",
&"arbitrary_self_types_pointers_and_wrappers",
&"--crate-type",
&"bin",
&"--target",
&args.config_info.target_triple,
]);
run_command_with_env(&command, None, Some(env))?;
maybe_run_command_in_vm(
&[&cargo_target_dir.join("arbitrary_self_types_pointers_and_wrappers")],
env,
args,
)?;
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[AOT] alloc_system");
let mut command = args.config_info.rustc_command_vec();
command.extend_from_slice(&[
&"example/alloc_system.rs",
&"--crate-type",
&"lib",
&"--target",
&args.config_info.target_triple,
]);
if args.is_using_gcc_master_branch() {
command.extend_from_slice(&[&"--cfg", &"feature=\"master\""]);
}
run_command_with_env(&command, None, Some(env))?;
// FIXME: doesn't work on m68k.
if args.config_info.host_triple == args.config_info.target_triple {
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[AOT] alloc_example");
let mut command = args.config_info.rustc_command_vec();
command.extend_from_slice(&[
&"example/alloc_example.rs",
&"--crate-type",
&"bin",
&"--target",
&args.config_info.target_triple,
]);
run_command_with_env(&command, None, Some(env))?;
maybe_run_command_in_vm(&[&cargo_target_dir.join("alloc_example")], env, args)?;
}
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[AOT] dst_field_align");
// FIXME(antoyo): Re-add -Zmir-opt-level=2 once rust-lang/rust#67529 is fixed.
let mut command = args.config_info.rustc_command_vec();
command.extend_from_slice(&[
&"example/dst-field-align.rs",
&"--crate-name",
&"dst_field_align",
&"--crate-type",
&"bin",
&"--target",
&args.config_info.target_triple,
]);
run_command_with_env(&command, None, Some(env))?;
maybe_run_command_in_vm(&[&cargo_target_dir.join("dst_field_align")], env, args)?;
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[AOT] std_example");
let mut command = args.config_info.rustc_command_vec();
command.extend_from_slice(&[
&"example/std_example.rs",
&"--crate-type",
&"bin",
&"--target",
&args.config_info.target_triple,
]);
if args.is_using_gcc_master_branch() {
command.extend_from_slice(&[&"--cfg", &"feature=\"master\""]);
}
run_command_with_env(&command, None, Some(env))?;
maybe_run_command_in_vm(
&[&cargo_target_dir.join("std_example"), &"--target", &args.config_info.target_triple],
env,
args,
)?;
let test_flags = if let Some(test_flags) = env.get("TEST_FLAGS") {
split_args(test_flags)?
} else {
Vec::new()
};
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[AOT] subslice-patterns-const-eval");
let mut command = args.config_info.rustc_command_vec();
command.extend_from_slice(&[
&"example/subslice-patterns-const-eval.rs",
&"--crate-type",
&"bin",
&"--target",
&args.config_info.target_triple,
]);
for test_flag in &test_flags {
command.push(test_flag);
}
run_command_with_env(&command, None, Some(env))?;
maybe_run_command_in_vm(&[&cargo_target_dir.join("subslice-patterns-const-eval")], env, args)?;
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[AOT] track-caller-attribute");
let mut command = args.config_info.rustc_command_vec();
command.extend_from_slice(&[
&"example/track-caller-attribute.rs",
&"--crate-type",
&"bin",
&"--target",
&args.config_info.target_triple,
]);
for test_flag in &test_flags {
command.push(test_flag);
}
run_command_with_env(&command, None, Some(env))?;
maybe_run_command_in_vm(&[&cargo_target_dir.join("track-caller-attribute")], env, args)?;
Ok(())
}
fn get_llvm_filecheck(env: &Env) -> Result<String, String> {
match run_command_with_env(
&[
&"bash",
&"-c",
&"which FileCheck-10 || \
which FileCheck-11 || \
which FileCheck-12 || \
which FileCheck-13 || \
which FileCheck-14 || \
which FileCheck",
],
None,
Some(env),
) {
Ok(cmd) => Ok(String::from_utf8_lossy(&cmd.stdout).trim().to_string()),
Err(_) => Err("Failed to retrieve LLVM FileCheck, ignoring...".to_owned()),
}
}
fn setup_rustc(env: &mut Env, args: &TestArg) -> Result<PathBuf, String> {
let toolchain = format!(
"+{channel}-{host}",
channel = get_toolchain()?, // May also include date
host = args.config_info.host_triple
);
let rust_dir_path = Path::new(crate::BUILD_DIR).join("rust");
// If the repository was already cloned, command will fail, so doesn't matter.
let _ = git_clone("https://github.com/rust-lang/rust.git", Some(&rust_dir_path), false);
let rust_dir: Option<&Path> = Some(&rust_dir_path);
run_command(&[&"git", &"checkout", &"--", &"tests/"], rust_dir)?;
run_command_with_output_and_env(&[&"git", &"fetch"], rust_dir, Some(env))?;
let rustc_commit = match rustc_version_info(env.get("RUSTC").map(|s| s.as_str()))?.commit_hash {
Some(commit_hash) => commit_hash,
None => return Err("Couldn't retrieve rustc commit hash".to_string()),
};
if rustc_commit != "unknown" {
run_command_with_output_and_env(
&[&"git", &"checkout", &rustc_commit],
rust_dir,
Some(env),
)?;
} else {
run_command_with_output_and_env(&[&"git", &"checkout"], rust_dir, Some(env))?;
}
let cargo = String::from_utf8(
run_command_with_env(&[&"rustup", &"which", &"cargo"], rust_dir, Some(env))?.stdout,
)
.map_err(|error| format!("Failed to retrieve cargo path: {error:?}"))
.and_then(|cargo| {
let cargo = cargo.trim().to_owned();
if cargo.is_empty() { Err("`cargo` path is empty".to_string()) } else { Ok(cargo) }
})?;
let rustc = String::from_utf8(
run_command_with_env(&[&"rustup", &toolchain, &"which", &"rustc"], rust_dir, Some(env))?
.stdout,
)
.map_err(|error| format!("Failed to retrieve rustc path: {error:?}"))
.and_then(|rustc| {
let rustc = rustc.trim().to_owned();
if rustc.is_empty() { Err("`rustc` path is empty".to_string()) } else { Ok(rustc) }
})?;
let llvm_filecheck = match get_llvm_filecheck(env) {
Ok(l) => l,
Err(error) => {
eprintln!("{error}");
// FIXME: the test tests/run-make/no-builtins-attribute will fail if we cannot find
// FileCheck.
String::new()
}
};
let file_path = rust_dir_path.join("config.toml");
std::fs::write(
&file_path,
format!(
r#"change-id = 115898
[rust]
codegen-backends = ["gcc"]
deny-warnings = false
verbose-tests = true
[build]
cargo = "{cargo}"
local-rebuild = true
rustc = "{rustc}"
[target.x86_64-unknown-linux-gnu]
llvm-filecheck = "{llvm_filecheck}"
[llvm]
download-ci-llvm = false
"#,
cargo = cargo,
rustc = rustc,
llvm_filecheck = llvm_filecheck.trim(),
),
)
.map_err(|error| format!("Failed to write into `{}`: {:?}", file_path.display(), error))?;
Ok(rust_dir_path)
}
fn asm_tests(env: &Env, args: &TestArg) -> Result<(), String> {
let mut env = env.clone();
let rust_dir = setup_rustc(&mut env, args)?;
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[TEST] rustc asm test suite");
let codegen_backend_path = format!(
"{pwd}/target/{channel}/librustc_codegen_gcc.{dylib_ext}",
pwd = std::env::current_dir()
.map_err(|error| format!("`current_dir` failed: {error:?}"))?
.display(),
channel = args.config_info.channel.as_str(),
dylib_ext = args.config_info.dylib_ext,
);
let extra =
if args.is_using_gcc_master_branch() { "" } else { " -Csymbol-mangling-version=v0" };
let rustc_args = format!(
"-Zpanic-abort-tests -Zcodegen-backend={codegen_backend_path} --sysroot {} -Cpanic=abort{extra}",
args.config_info.sysroot_path
);
run_command_with_env(
&[
&"./x.py",
&"test",
&"--run",
&"always",
&"--stage",
&"0",
&"--set",
&"build.compiletest-allow-stage0=true",
&"tests/assembly-gcc/asm",
&"--compiletest-rustc-args",
&rustc_args,
],
Some(&rust_dir),
Some(&env),
)?;
Ok(())
}
fn run_cargo_command(
command: &[&dyn AsRef<OsStr>],
cwd: Option<&Path>,
env: &Env,
args: &TestArg,
) -> Result<(), String> {
run_cargo_command_with_callback(command, cwd, env, args, |cargo_command, cwd, env| {
run_command_with_output_and_env(cargo_command, cwd, Some(env))?;
Ok(())
})
}
fn run_cargo_command_with_callback<F>(
command: &[&dyn AsRef<OsStr>],
cwd: Option<&Path>,
env: &Env,
args: &TestArg,
callback: F,
) -> Result<(), String>
where
F: Fn(&[&dyn AsRef<OsStr>], Option<&Path>, &Env) -> Result<(), String>,
{
let toolchain = get_toolchain()?;
let toolchain_arg = format!("+{toolchain}");
let rustc_version = String::from_utf8(
run_command_with_env(&[&args.config_info.rustc_command[0], &"-V"], cwd, Some(env))?.stdout,
)
.map_err(|error| format!("Failed to retrieve rustc version: {error:?}"))?;
let rustc_toolchain_version = String::from_utf8(
run_command_with_env(
&[&args.config_info.rustc_command[0], &toolchain_arg, &"-V"],
cwd,
Some(env),
)?
.stdout,
)
.map_err(|error| format!("Failed to retrieve rustc +toolchain version: {error:?}"))?;
if rustc_version != rustc_toolchain_version {
eprintln!(
"rustc_codegen_gcc is built for `{rustc_toolchain_version}` but the default rustc version is `{rustc_version}`.",
);
eprintln!("Using `{rustc_toolchain_version}`.");
}
let mut env = env.clone();
let rustflags = env.get("RUSTFLAGS").cloned().unwrap_or_default();
env.insert("RUSTDOCFLAGS".to_string(), rustflags);
let mut cargo_command: Vec<&dyn AsRef<OsStr>> = vec![&"cargo", &toolchain_arg];
cargo_command.extend_from_slice(command);
callback(&cargo_command, cwd, &env)
}
// FIXME(antoyo): linker gives multiple definitions error on Linux
// echo "[BUILD] sysroot in release mode"
// ./build_sysroot/build_sysroot.sh --release
struct Project {
url: &'static str,
/// Arguments added to both the `cargo build` and the `cargo test` invocations.
cargo_arguments: &'static [&'static str],
/// Arguments forwarded to the test harness by `cargo test`.
test_harness_arguments: &'static [&'static str],
/// Variables added to the environment of both invocations.
environment_variables: &'static [(&'static str, &'static str)],
}
impl Project {
const fn new(url: &'static str) -> Self {
Self { url, cargo_arguments: &[], test_harness_arguments: &[], environment_variables: &[] }
}
const fn cargo_arguments(mut self, arguments: &'static [&'static str]) -> Self {
self.cargo_arguments = arguments;
self
}
const fn test_harness_arguments(mut self, arguments: &'static [&'static str]) -> Self {
self.test_harness_arguments = arguments;
self
}
const fn environment_variables(
mut self,
variables: &'static [(&'static str, &'static str)],
) -> Self {
self.environment_variables = variables;
self
}
}
fn test_projects(env: &Env, args: &TestArg) -> Result<(), String> {
let projects = [
// The reference images assume the exact cairo, pango and freetype that librsvg pins in its
// own CI; this one renders text decorations a pixel off with the versions Ubuntu ships.
Project::new("https://gitlab.gnome.org/GNOME/librsvg")
.test_harness_arguments(&["--skip", "tests::svg1_1_text_text_03_b_svg", "--exact"])
// A debug build of librsvg needs about 5 MB of stack per `cargo test` thread to reach
// its maximum layer nesting depth; librsvg's own CI sets the same value.
.environment_variables(&[("RUST_MIN_STACK", "8388608")]),
Project::new("https://github.com/rust-random/getrandom"),
Project::new("https://github.com/BurntSushi/memchr"),
Project::new("https://github.com/dtolnay/itoa"),
Project::new("https://github.com/rust-lang/cfg-if"),
// The `ui` test compares against the diagnostics of the compiler it was blessed with, so it
// fails on the nightly we use no matter which backend produces the code.
Project::new("https://github.com/rust-lang-nursery/lazy-static.rs")
.test_harness_arguments(&["--skip", "ui", "--exact"]),
Project::new("https://github.com/marshallpierce/rust-base64"),
// The test suite refuses to build unless every feature is enabled; it otherwise spawns a
// nested `cargo test --all-features` which would not use this backend.
Project::new("https://github.com/time-rs/time").cargo_arguments(&["--all-features"]),
Project::new("https://github.com/rust-lang/log"),
Project::new("https://github.com/bitflags/bitflags"),
Project::new("https://github.com/serde-rs/serde"),
Project::new("https://github.com/rayon-rs/rayon"),
// FIXME: too slow to run in the CI: the release build alone takes 46 minutes and the
// `cargo` crate itself needs 5.4 GB of memory in a single rustc process.
//Project::new("https://github.com/rust-lang/cargo"),
];
let mut env = env.clone();
let rustflags =
format!("{} --cap-lints allow", env.get("RUSTFLAGS").cloned().unwrap_or_default());
env.insert("RUSTFLAGS".to_string(), rustflags);
let run_tests =
|projects_path, iter: &mut dyn Iterator<Item = &Project>| -> Result<(), String> {
for project in iter {
let clone_result = git_clone_root_dir(project.url, projects_path, true)?;
let repo_path = Path::new(&clone_result.repo_dir);
let mut project_environment = env.clone();
for (name, value) in project.environment_variables {
project_environment.insert(name.to_string(), value.to_string());
}
let mut build_command: Vec<&dyn AsRef<OsStr>> = vec![&"build", &"--release"];
build_command.extend(
project.cargo_arguments.iter().map(|argument| argument as &dyn AsRef<OsStr>),
);
run_cargo_command(&build_command, Some(repo_path), &project_environment, args)?;
let mut test_command: Vec<&dyn AsRef<OsStr>> = vec![&"test"];
test_command.extend(
project.cargo_arguments.iter().map(|argument| argument as &dyn AsRef<OsStr>),
);
if !project.test_harness_arguments.is_empty() {
test_command.push(&"--");
test_command.extend(
project
.test_harness_arguments
.iter()
.map(|argument| argument as &dyn AsRef<OsStr>),
);
}
run_cargo_command(&test_command, Some(repo_path), &project_environment, args)?;
}
Ok(())
};
let projects_path = Path::new("projects");
create_dir(projects_path)?;
let nb_parts = args.nb_parts.unwrap_or(0);
if let Some(count) = projects.len().checked_div(nb_parts) {
// We increment the number of tests by one because if this is an odd number, we would skip
// one test.
let count = count + 1;
let current_part = args.current_part.unwrap();
let start = current_part * count;
// We remove the projects we don't want to test.
run_tests(projects_path, &mut projects.iter().skip(start).take(count))?;
} else {
run_tests(projects_path, &mut projects.iter())?;
}
Ok(())
}
fn test_libcore(env: &Env, args: &TestArg) -> Result<(), String> {
test_libcore_inner(env, args, false)
}
fn test_release_libcore(env: &Env, args: &TestArg) -> Result<(), String> {
test_libcore_inner(env, args, true)
}
fn test_libcore_inner(env: &Env, args: &TestArg, release: bool) -> Result<(), String> {
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[TEST] libcore");
let path = get_sysroot_dir().join("sysroot_src/library/coretests");
let _ = remove_dir_all(path.join("target"));
let mut command: Vec<&dyn AsRef<OsStr>> = vec![&"test"];
if release {
command.push(&"--release");
}
run_cargo_command(&command, Some(&path), env, args)?;
Ok(())
}
/// Returns the edition declared in the manifest of the given library crate, so that the doctests
/// are run with the same edition as the crate they are extracted from.
fn get_crate_edition(crate_dir: &Path) -> Result<String, String> {
let manifest_path = crate_dir.join("Cargo.toml");
let content = read_to_string(&manifest_path)
.map_err(|error| format!("Failed to read `{}`: {error:?}", manifest_path.display()))?;
let manifest = Toml::parse(&content)
.map_err(|error| format!("Failed to parse `{}`: {error:?}", manifest_path.display()))?;
manifest
.get_table("package")
.and_then(|package| package.get_string("edition"))
.map(|edition| edition.to_string())
.map_err(|error| {
format!("Failed to get `package.edition` from `{}`: {error:?}", manifest_path.display())
})
}
fn test_libcore_doctests(env: &Env, args: &TestArg) -> Result<(), String> {
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[TEST] libcore doctests");
let library_dir = get_sysroot_dir().join("sysroot_src/library");
let edition = get_crate_edition(&library_dir.join("core"))?;
// `rustdoc` is called directly instead of through `cargo test --doc` because `cargo` builds its
// own `core` and passes it with `--extern`, which then conflicts with the `core` of the sysroot
// the doctests are linked against ("duplicate lang item" errors).
let toolchain = get_toolchain()?;
let toolchain_arg = format!("+{toolchain}");
let rustflags = split_args(&env.get("RUSTFLAGS").cloned().unwrap_or_default())?;
// `-Zunstable-options` is needed for `--test-args`.
let mut command: Vec<&dyn AsRef<OsStr>> = vec![
&"rustdoc",
&toolchain_arg,
&"--test",
&"core/src/lib.rs",
&"--crate-name",
&"core",
&"--crate-type",
&"lib",
&"--edition",
&edition,
&"-Zunstable-options",
// FIXME: remove `-Zforce-unstable-if-unmarked` once the doctest of
// `core::io::ErrorKind`'s `Display` impl declares `#![feature(core_io)]` upstream: without
// it, that doctest fails to compile with `E0658` on any backend.
&"-Zforce-unstable-if-unmarked",
// FIXME: one test cannot compile due to an upstream bug in the new trait solver.
&"-Znext-solver=coherence",
];
for flag in &rustflags {
command.push(flag);
}
// Additional arguments are forwarded to the test harness, so that a subset of the doctests can
// be run.
let test_args =
args.test_args.iter().map(|test_arg| format!("--test-args={test_arg}")).collect::<Vec<_>>();
for test_arg in &test_args {
command.push(test_arg);
}
run_command_with_output_and_env(&command, Some(&library_dir), Some(env))?;
Ok(())
}
fn test_stdarch(env: &Env, args: &TestArg) -> Result<(), String> {
println!("[TEST] stdarch");
let manifest_path = get_sysroot_dir().join("sysroot_src/library/stdarch/Cargo.toml");
let mut env = env.clone();
// `config.setup` already baked `CG_RUSTFLAGS` into `RUSTFLAGS`, so append the lint-allow to
// `RUSTFLAGS` directly (which `run_cargo_command` also propagates to `RUSTDOCFLAGS`).
let rustflags = env.get("RUSTFLAGS").cloned().unwrap_or_default();
env.insert(
"RUSTFLAGS".to_string(),
format!("{rustflags} -Ainternal_features").trim().to_owned(),
);
env.insert("TARGET".to_string(), args.config_info.target_triple.clone());
let mut command: Vec<&dyn AsRef<OsStr>> =
vec![&"test", &"--manifest-path", &manifest_path, &"--"];
for test_name in &args.test_args {
command.push(test_name);
}
run_cargo_command(&command, None, &env, args)?;
Ok(())
}
fn test_alloc(env: &Env, args: &TestArg) -> Result<(), String> {
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[TEST] alloc");
let path = get_sysroot_dir().join("sysroot_src/library/alloctests");
let _ = remove_dir_all(path.join("target"));
// FIXME(antoyo): run in release mode when we fix the failures.
run_cargo_command(&[&"test"], Some(&path), env, args)?;
Ok(())
}
fn extended_rand_tests(env: &Env, args: &TestArg) -> Result<(), String> {
if !args.is_using_gcc_master_branch() {
println!("Not using GCC master branch. Skipping `extended_rand_tests`.");
return Ok(());
}
let mut env = env.clone();
// newer aho_corasick versions throw a deprecation warning
let rustflags =
format!("{} --cap-lints warn", env.get("RUSTFLAGS").cloned().unwrap_or_default());
env.insert("RUSTFLAGS".to_string(), rustflags);
let path = Path::new(crate::BUILD_DIR).join("rand");
run_cargo_command(&[&"clean"], Some(&path), &env, args)?;
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[TEST] rust-random/rand");
run_cargo_command(&[&"test", &"--workspace"], Some(&path), &env, args)?;
Ok(())
}
fn extended_regex_example_tests(env: &Env, args: &TestArg) -> Result<(), String> {
if !args.is_using_gcc_master_branch() {
println!("Not using GCC master branch. Skipping `extended_regex_example_tests`.");
return Ok(());
}
let path = Path::new(crate::BUILD_DIR).join("regex");
run_cargo_command(&[&"clean"], Some(&path), env, args)?;
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[TEST] rust-lang/regex example shootout-regex-dna");
let mut env = env.clone();
// newer aho_corasick versions throw a deprecation warning
let rustflags =
format!("{} --cap-lints warn", env.get("RUSTFLAGS").cloned().unwrap_or_default());
env.insert("RUSTFLAGS".to_string(), rustflags);
// Make sure `[codegen mono items] start` doesn't poison the diff
run_cargo_command(&[&"build", &"--example", &"shootout-regex-dna"], Some(&path), &env, args)?;
run_cargo_command_with_callback(
&[&"run", &"--example", &"shootout-regex-dna"],
Some(&path),
&env,
args,
|cargo_command, cwd, env| {
// FIXME: rewrite this with `child.stdin.write_all()` because
// `examples/regexdna-input.txt` is very small.
let mut command: Vec<&dyn AsRef<OsStr>> = vec![&"bash", &"-c"];
let cargo_args =
cargo_command.iter().map(|s| s.as_ref().to_str().unwrap()).collect::<Vec<_>>();
let bash_command = format!(
"cat examples/regexdna-input.txt | {} | grep -v 'Spawned thread' > res.txt",
cargo_args.join(" "),
);
command.push(&bash_command);
run_command_with_output_and_env(&command, cwd, Some(env))?;
run_command_with_output_and_env(
&[&"diff", &"-u", &"res.txt", &"examples/regexdna-output.txt"],
cwd,
Some(env),
)?;
Ok(())
},
)?;
Ok(())
}
fn extended_regex_tests(env: &Env, args: &TestArg) -> Result<(), String> {
if !args.is_using_gcc_master_branch() {
println!("Not using GCC master branch. Skipping `extended_regex_tests`.");
return Ok(());
}
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[TEST] rust-lang/regex tests");
let mut env = env.clone();
// newer aho_corasick versions throw a deprecation warning
let rustflags =
format!("{} --cap-lints warn", env.get("RUSTFLAGS").cloned().unwrap_or_default());
env.insert("RUSTFLAGS".to_string(), rustflags);
let path = Path::new(crate::BUILD_DIR).join("regex");
run_cargo_command(
&[
&"test",
&"--tests",
&"--",
// FIXME: try removing `--exclude-should-panic` argument
&"--exclude-should-panic",
&"--test-threads",
&"1",
&"-Zunstable-options",
&"-q",
],
Some(&path),
&env,
args,
)?;
Ok(())
}
fn extended_sysroot_tests(env: &Env, args: &TestArg) -> Result<(), String> {
// pushd simple-raytracer
// echo "[BENCH COMPILE] ebobby/simple-raytracer"
// hyperfine --runs "${RUN_RUNS:-10}" --warmup 1 --prepare "cargo clean" \
// "RUSTC=rustc RUSTFLAGS='' cargo build" \
// "../y.sh cargo build"
// echo "[BENCH RUN] ebobby/simple-raytracer"
// cp ./target/debug/main ./raytracer_cg_gcc
// hyperfine --runs "${RUN_RUNS:-10}" ./raytracer_cg_llvm ./raytracer_cg_gcc
// popd
extended_rand_tests(env, args)?;
extended_regex_example_tests(env, args)?;
extended_regex_tests(env, args)?;
Ok(())
}
fn valid_ui_error_pattern_test(file: &str) -> bool {
// contains //~ERROR, but shouldn't be removed
[
"issues/auxiliary/issue-3136-a.rs",
"type-alias-impl-trait/auxiliary/cross_crate_ice.rs",
"type-alias-impl-trait/auxiliary/cross_crate_ice2.rs",
"macros/rfc-2011-nicer-assert-messages/auxiliary/common.rs",
"entry-point/auxiliary/bad_main_functions.rs",
]
.iter()
.any(|to_ignore| file.ends_with(to_ignore))
}
fn contains_ui_error_patterns(file_path: &Path, keep_lto_tests: bool) -> Result<bool, String> {
// Tests generating errors.
let file = File::open(file_path)
.map_err(|error| format!("Failed to read `{}`: {:?}", file_path.display(), error))?;
for line in BufReader::new(file).lines().map_while(Result::ok) {
let line = line.trim();
if line.is_empty() {
continue;
}
if [
"//@ error-pattern:",
"//@ build-fail",
"//@ run-fail",
"//@ known-bug",
"-Cllvm-args",
"//~",
]
.iter()
.any(|check| line.contains(check))
{
return Ok(true);
}
if !keep_lto_tests
&& (line.contains("-Clto")
|| line.contains("-C lto")
|| line.contains("compile-flags: -Clinker-plugin-lto"))
&& !line.contains("-Clto=thin")
{
return Ok(true);
}
if line.contains("//[") && line.contains("]~") {
return Ok(true);
}
}
let file_path = file_path.display().to_string();
if file_path.contains("ambiguous-4-extern.rs") {
eprintln!("nothing found for {file_path:?}");
}
// The files in this directory contain errors.
Ok(file_path.contains("/error-emitter/"))
}
// # Parameters
//
// * `env`: An environment variable that provides context for the function.
// * `args`: The arguments passed to the test. This could include things like the flags, config etc.
// * `prepare_files_callback`: A callback function that prepares the files needed for the test. Its used to remove/retain tests giving Error to run various rust test suits.
// * `run_error_pattern_test`: A boolean that determines whether to run only error pattern tests.
// * `test_type`: A string that indicates the type of the test being run.
// * `retained_tests_list_path`: The list of tests that `prepare_files_callback` retained, if any.
// It is checked against the tests remaining after the filtering to report dead lines.
//
fn test_rustc_inner<F>(
env: &Env,
args: &TestArg,
prepare_files_callback: F,
run_error_pattern_test: bool,
test_type: &str,
retained_tests_list_path: Option<&str>,
) -> Result<(), String>
where
F: Fn(&Path) -> Result<bool, String>,
{
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[TEST] rust-lang/rust");
let mut env = env.clone();
let rust_path = setup_rustc(&mut env, args)?;
if !prepare_files_callback(&rust_path)? {
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("Keeping all {test_type} tests");
}
if test_type == "ui" {
// Each mode runs one half of the ui tests and removes the other: `run_error_pattern_test`
// runs the tests expected to error, the other mode runs the rest. Only `.rs` files outside
// `auxiliary` are tests, so the expected output and the auxiliary crates are left alone.
fn dir_handling(
keep_lto_tests: bool,
remove_error_pattern_tests: bool,
) -> impl Fn(&Path) -> Result<(), String> {
move |dir| {
if dir.file_name().map(|name| name == "auxiliary").unwrap_or(true) {
return Ok(());
}
walk_dir(
dir,
&mut dir_handling(keep_lto_tests, remove_error_pattern_tests),
&mut file_handling(keep_lto_tests, remove_error_pattern_tests),
false,
)
}
}
fn file_handling(
keep_lto_tests: bool,
remove_error_pattern_tests: bool,
) -> impl Fn(&Path) -> Result<(), String> {
move |file_path| {
if !file_path.extension().map(|extension| extension == "rs").unwrap_or(false) {
return Ok(());
}
let path_str = file_path.display().to_string().replace("\\", "/");
if valid_ui_error_pattern_test(&path_str) {
return Ok(());
}
if contains_ui_error_patterns(file_path, keep_lto_tests)?
== remove_error_pattern_tests
{
return remove_file(file_path);
}
Ok(())
}
}
let remove_error_pattern_tests = !run_error_pattern_test;
walk_dir(
rust_path.join("tests/ui"),
&mut dir_handling(args.keep_lto_tests, remove_error_pattern_tests),
&mut file_handling(args.keep_lto_tests, remove_error_pattern_tests),
false,
)?;
if let Some(retained_tests_list_path) = retained_tests_list_path {
check_for_dead_listed_tests(&rust_path, retained_tests_list_path)?;
}
let nb_parts = args.nb_parts.unwrap_or(0);
if nb_parts > 0 {
let current_part = args.current_part.unwrap();
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("Splitting ui_test into {nb_parts} parts (and running part {current_part})");
let out = String::from_utf8(
run_command(
&[
&"find",
&"tests/ui",
&"-type",
&"f",
&"-name",
&"*.rs",
&"-not",
&"-path",
&"*/auxiliary/*",
],
Some(&rust_path),
)?
.stdout,
)
.map_err(|error| format!("Failed to retrieve output of find command: {error:?}"))?;
let mut files = out
.split('\n')
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.collect::<Vec<_>>();
// To ensure it'll be always the same sub files, we sort the content.
files.sort();
// We increment the number of tests by one because if this is an odd number, we would skip
// one test.
let count = files.len() / nb_parts + 1;
// We remove the files we don't want to test.
let start = current_part * count;
for path in files.iter().skip(start).take(count) {
remove_file(&rust_path.join(path))?;
}
}
}
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[TEST] rustc {test_type} test suite");
let extra =
if args.is_using_gcc_master_branch() { "" } else { " -Csymbol-mangling-version=v0" };
let rustc_args = format!(
"{test_flags} -Zcodegen-backend={backend} --sysroot {sysroot}{extra}",
test_flags = env.get("TEST_FLAGS").unwrap_or(&String::new()),
backend = args.config_info.cg_backend_path,
sysroot = args.config_info.sysroot_path,
extra = extra,
);
env.get_mut("RUSTFLAGS").unwrap().clear();
let test_dir = format!("tests/{test_type}");
let command: Vec<&dyn AsRef<OsStr>> = vec![
&"./x.py",
&"test",
&"--run",
&"always",
&"--stage",
&"0",
&"--set",
&"build.compiletest-allow-stage0=true",
&test_dir,
&"--compiletest-rustc-args",
&rustc_args,
&"--bypass-ignore-backends",
];
run_test_command(&command, &rust_path, &env)
}
/// Reads the list of tests at `list_path`, checking that each of them still exists in the rust
/// checkout at `rust_path` and that none is listed twice.
///
/// Both problems make a line a no-op: the test it names is neither kept nor removed, so the test
/// suite silently drifts away from what the list claims to describe.
fn read_test_list(rust_path: &Path, list_path: &str) -> Result<Vec<String>, String> {
let content = std::fs::read_to_string(list_path)
.map_err(|error| format!("Failed to read `{list_path}`: {error:?}"))?;
let mut tests = Vec::new();
let mut seen = HashSet::new();
let mut missing = Vec::new();
let mut duplicated = Vec::new();
for line in content.lines().map(|line| line.trim()).filter(|line| !line.is_empty()) {
if !seen.insert(line) {
duplicated.push(line);
continue;
}
if !rust_path.join(line.trim_end_matches('/')).exists() {
missing.push(line);
}
tests.push(line.to_string());
}
if missing.is_empty() && duplicated.is_empty() {
return Ok(tests);
}
let mut error = format!("`{list_path}` is out of date:\n");
if !missing.is_empty() {
error.push_str(&format!(
"\nThese tests no longer exist in `{rust_path}`:\n{missing}\n",
rust_path = rust_path.display(),
missing = missing.join("\n"),
));
}
if !duplicated.is_empty() {
error.push_str(&format!(
"\nThese tests are listed more than once:\n{}\n",
duplicated.join("\n")
));
}
error.push_str(
"\nEvery line must name a test that exists, exactly once, otherwise the line filters \
nothing. Delete the stale lines, or update them to the test's current path.",
);
Err(error)
}
/// Checks that every test listed in `list_path` survived the filtering done by
/// `contains_ui_error_patterns`.
fn check_for_dead_listed_tests(rust_path: &Path, list_path: &str) -> Result<(), String> {
let listed_tests = std::fs::read_to_string(list_path)
.map_err(|error| format!("Failed to read `{list_path}`: {error:?}"))?;
let dead_tests = listed_tests
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty() && !rust_path.join(line).exists())
.collect::<Vec<_>>();
if dead_tests.is_empty() {
return Ok(());
}
Err(format!(
"The following tests listed in `{list_path}` are filtered out before the tests are run, \
so listing them has no effect:\n{}\n\nThis happens when a test contains an error pattern \
(like `//~` or `//@ known-bug`), in which case it should be removed from `{list_path}`, \
or when it uses LTO, in which case it should be moved to `tests/failing-lto-tests.txt`.",
dead_tests.join("\n")
))
}
fn test_rustc(env: &Env, args: &TestArg) -> Result<(), String> {
test_rustc_inner(env, args, |_| Ok(false), false, "run-make", None)?;
test_rustc_inner(env, args, |_| Ok(false), false, "run-make-cargo", None)?;
test_rustc_inner(env, args, |_| Ok(false), false, "ui", None)
}
fn test_failing_rustc(env: &Env, args: &TestArg) -> Result<(), String> {
let run_make_result = test_rustc_inner(
env,
args,
retain_files_callback("tests/failing-run-make-tests.txt", "run-make"),
false,
"run-make",
None,
);
let run_make_cargo_result = test_rustc_inner(
env,
args,
retain_files_callback("tests/failing-run-make-tests.txt", "run-make-cargo"),
false,
"run-make-cargo",
None,
);
let ui_result = test_rustc_inner(
env,
args,
retain_files_callback("tests/failing-ui-tests.txt", "ui"),
false,
"ui",
Some("tests/failing-ui-tests.txt"),
);
combine_test_results([run_make_result, run_make_cargo_result, ui_result])
}
/// Combines the results of several test suites, letting a build system error win over a test
/// failure so that a broken build system is never reported to CI as the failures those suites
/// expect.
fn combine_test_results<const N: usize>(results: [Result<(), String>; N]) -> Result<(), String> {
let mut tests_failed = false;
for result in results {
match result {
Ok(()) => {}
Err(error) if error == TESTS_FAILED_ERROR => tests_failed = true,
Err(error) => return Err(error),
}
}
if tests_failed { Err(TESTS_FAILED_ERROR.to_string()) } else { Ok(()) }
}
fn test_successful_rustc(env: &Env, args: &TestArg) -> Result<(), String> {
test_rustc_inner(
env,
args,
remove_files_callback("tests/failing-ui-tests.txt"),
false,
"ui",
None,
)?;
test_rustc_inner(
env,
args,
remove_files_callback("tests/failing-run-make-tests.txt"),
false,
"run-make",
None,
)?;
test_rustc_inner(
env,
args,
remove_files_callback("tests/failing-run-make-tests.txt"),
false,
"run-make-cargo",
None,
)
}
fn test_failing_ui_pattern_tests(env: &Env, args: &TestArg) -> Result<(), String> {
test_rustc_inner(
env,
args,
remove_files_callback("tests/failing-ice-tests.txt"),
true,
"ui",
None,
)
}
fn run_ui_tests(env: &Env, args: &TestArg) -> Result<(), String> {
let mut env = env.clone();
let rust_path = setup_rustc(&mut env, args)?;
let extra =
if args.is_using_gcc_master_branch() { "" } else { " -Csymbol-mangling-version=v0" };
let rustc_args = format!(
"{test_flags} -Zcodegen-backend={backend} --sysroot {sysroot}{extra}",
test_flags = env.get("TEST_FLAGS").unwrap_or(&String::new()),
backend = args.config_info.cg_backend_path,
sysroot = args.config_info.sysroot_path,
extra = extra,
);
env.get_mut("RUSTFLAGS").unwrap().clear();
let mut command: Vec<&dyn AsRef<OsStr>> = vec![
&"./x.py",
&"test",
&"--run",
&"always",
&"--stage",
&"0",
&"--set",
&"build.compiletest-allow-stage0=true",
&"--compiletest-rustc-args",
&rustc_args,
&"--bypass-ignore-backends",
&"--force-rerun",
];
for test_name in &args.test_args {
command.push(test_name);
}
run_test_command(&command, &rust_path, &env)
}
/// Runs the command that actually runs a test suite, mapping its failure to `TESTS_FAILED_ERROR`.
fn run_test_command(
command: &[&dyn AsRef<OsStr>],
rust_path: &Path,
env: &Env,
) -> Result<(), String> {
if let Err(error) = run_command_with_output_and_env(command, Some(rust_path), Some(env)) {
// The failures themselves were already streamed to the console.
eprintln!("{error}");
return Err(TESTS_FAILED_ERROR.to_string());
}
Ok(())
}
fn retain_files_callback<'a>(
file_path: &'a str,
test_type: &'a str,
) -> impl Fn(&Path) -> Result<bool, String> + 'a {
move |rust_path| {
let tests = read_test_list(rust_path, file_path)?;
let first_file_name = tests.first().map(String::as_str).unwrap_or("");
// If the first line ends with a `/`, we treat all lines in the file as a directory.
if first_file_name.ends_with('/') {
// Treat as directory
// Removing all tests.
run_command(
&[
&"find",
&format!("tests/{test_type}"),
&"-mindepth",
&"1",
&"-type",
&"d",
&"-exec",
&"rm",
&"-rf",
&"{}",
&"+",
],
Some(rust_path),
)?;
} else {
// Treat as file
// Removing all tests.
run_command(
&[
&"find",
&format!("tests/{test_type}"),
&"-type",
&"f",
&"-name",
&"*.rs",
&"-not",
&"-path",
&"*/auxiliary/*",
&"-delete",
],
Some(rust_path),
)?;
}
// Putting back only the failing ones.
for test in &tests {
run_command(&[&"git", &"checkout", &"--", test], Some(rust_path))?;
}
Ok(true)
}
}
fn remove_files_callback(file_path: &str) -> impl Fn(&Path) -> Result<bool, String> + '_ {
move |rust_path| {
let tests = read_test_list(rust_path, file_path)?;
let first_file_name = tests.first().map(String::as_str).unwrap_or("");
// If the first line ends with a `/`, we treat all lines in the file as a directory.
if first_file_name.ends_with('/') {
// Removing the failing tests.
for test in &tests {
let path = rust_path.join(test);
remove_dir_all(&path).map_err(|error| {
format!("Failed to remove directory `{}`: {error}", path.display())
})?;
}
} else {
// Removing the failing tests.
for test in &tests {
remove_file(&rust_path.join(test))?;
}
}
Ok(true)
}
}
fn test_asm(env: &Env, args: &TestArg) -> Result<(), String> {
fn is_path_time_more_recent(ref_time: std::time::SystemTime, path: &str) -> bool {
std::fs::metadata(path)
.and_then(|metadata| metadata.modified())
.is_ok_and(|time| ref_time < time)
}
// FIXME: create a function "display_if_not_quiet" or something along the line.
println!("[TEST] cg_gcc assembly");
let llvm_filecheck = get_llvm_filecheck(env)?;
let target_dir = std::env::current_dir().unwrap().join("build_system/asm-tester/target");
// All this code is because `cargo` keeps recompiling this file, and we can't figure out why.
let binary_file_path = "build_system/asm-tester/target/debug/asm-tester";
let mut need_recompilation = true;
if let Ok(metadata) = std::fs::metadata(binary_file_path)
&& let Ok(ref_time) = metadata.modified()
&& !is_path_time_more_recent(ref_time, "build_system/asm-tester/Cargo.toml")
&& !is_path_time_more_recent(ref_time, "build_system/asm-tester/Cargo.lock")
&& !is_path_time_more_recent(ref_time, "build_system/asm-tester/src/main.rs")
{
need_recompilation = false;
}
if need_recompilation {
let build_asm_args: Vec<&dyn AsRef<OsStr>> = vec![
&"cargo",
&"build",
&"--manifest-path",
&"build_system/asm-tester/Cargo.toml",
&"--target-dir",
&target_dir,
&"--",
];
run_command_with_output_and_env_no_err(&build_asm_args, Some(Path::new(".")), Some(env))?;
}
let mut test_asm_args: Vec<&dyn AsRef<OsStr>> = vec![
&"build_system/asm-tester/target/debug/asm-tester",
&"--llvm-filecheck",
&llvm_filecheck,
];
for test_arg in &args.test_args {
test_asm_args.push(&"--filter");
test_asm_args.push(test_arg);
}
test_asm_args.push(&"--");
for arg in args.config_info.rustc_command_vec().into_iter().skip(1) {
test_asm_args.push(arg);
}
run_command_with_output_and_env_no_err(&test_asm_args, Some(Path::new(".")), Some(env))
}
fn run_all(env: &Env, args: &TestArg) -> Result<(), String> {
clean(env, args)?;
mini_tests(env, args)?;
build_sysroot(env, args)?;
std_tests(env, args)?;
// asm_tests(env, args)?;
test_libcore(env, args)?;
extended_sysroot_tests(env, args)?;
cargo_tests(env, args)?;
no_builtins_tests(env, args)?;
test_rustc(env, args)?;
test_asm(env, args)?;
Ok(())
}
pub fn run() -> Result<(), String> {
let mut args = match TestArg::new()? {
Some(args) => args,
None => return Ok(()),
};
let mut env: HashMap<String, String> = std::env::vars().collect();
if !args.use_system_gcc {
args.config_info.setup_gcc_path()?;
}
build_if_no_backend(&env, &args)?;
if args.build_only {
println!("Since it's build only, exiting...");
return Ok(());
}
args.config_info.setup(&mut env, args.use_system_gcc, true)?;
if args.runners.is_empty() {
run_all(&env, &args)?;
} else {
let runners = get_runners();
for runner in args.runners.iter() {
runners.get(runner.as_str()).unwrap().1(&env, &args)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn write_test_list(directory: &Path, content: &str) -> PathBuf {
let list_path = directory.join("failing-tests.txt");
std::fs::write(&list_path, content).unwrap();
list_path
}
#[test]
fn test_combine_test_results() {
let tests_failed = || Err(TESTS_FAILED_ERROR.to_string());
let build_error = || Err("could not clone rust".to_string());
assert_eq!(combine_test_results([Ok(()), Ok(())]), Ok(()));
assert_eq!(combine_test_results([Ok(()), tests_failed()]), tests_failed());
assert_eq!(combine_test_results([Ok(()), build_error()]), build_error());
// A build system error wins, whichever suite reported it.
assert_eq!(combine_test_results([tests_failed(), build_error()]), build_error());
assert_eq!(combine_test_results([build_error(), tests_failed()]), build_error());
}
#[test]
fn test_read_test_list() {
let rust_path = std::env::temp_dir().join("cg_gcc_read_test_list");
let _ = remove_dir_all(&rust_path);
create_dir(rust_path.join("tests/ui")).unwrap();
std::fs::write(rust_path.join("tests/ui/alive.rs"), "").unwrap();
let list_path = write_test_list(&rust_path, "\ntests/ui/alive.rs\n \n");
let list_path = list_path.display().to_string();
assert_eq!(
read_test_list(&rust_path, &list_path),
Ok(vec!["tests/ui/alive.rs".to_string()])
);
write_test_list(&rust_path, "tests/ui/alive.rs\ntests/ui/gone.rs\n");
let error = read_test_list(&rust_path, &list_path).unwrap_err();
assert!(error.contains("no longer exist"), "{error}");
assert!(error.contains("tests/ui/gone.rs"), "{error}");
write_test_list(&rust_path, "tests/ui/alive.rs\ntests/ui/alive.rs\n");
let error = read_test_list(&rust_path, &list_path).unwrap_err();
assert!(error.contains("listed more than once"), "{error}");
assert!(error.contains("tests/ui/alive.rs"), "{error}");
// Directories are listed with a trailing `/`.
write_test_list(&rust_path, "tests/ui/\n");
assert_eq!(read_test_list(&rust_path, &list_path), Ok(vec!["tests/ui/".to_string()]));
remove_dir_all(&rust_path).unwrap();
}
}