Skip to content

Rollup of 7 pull requests #140017

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 35 commits into from
Closed
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3fe0b3d
not lint break with label and unsafe block
mu001999 Feb 23, 2025
1c1febc
add new config option: `include`
onur-ozkan Apr 1, 2025
89e3bef
document config extensions
onur-ozkan Mar 25, 2025
4e80659
implement cyclic inclusion handling
onur-ozkan Apr 1, 2025
78cb453
document `include` in `bootstrap.example.toml`
onur-ozkan Apr 1, 2025
3f70f19
apply nit notes
onur-ozkan Apr 1, 2025
8e6f50b
fix path and the ordering logic
onur-ozkan Apr 15, 2025
7dfb457
add FIXME note in `TomlConfig::merge`
onur-ozkan Apr 15, 2025
6d52b51
add comment in `TomlConfig::merge` about the merge order
onur-ozkan Apr 15, 2025
2024e26
Don't canonicalize crate paths
ChrisDenton Apr 15, 2025
52f35d0
Test for relative paths in crate path diagnostics
ChrisDenton Apr 15, 2025
8270478
resolve config include FIXME
onur-ozkan Apr 16, 2025
5a38550
Deduplicate nix code
RossSmyth Apr 3, 2025
d14df26
Make `parent` in `download_auto_job_metrics` optional
Kobzol Apr 16, 2025
111c15c
Extract function for normalizing path delimiters to `utils`
Kobzol Apr 16, 2025
c8a882b
Add command to `citool` for generating a test dashboard
Kobzol Apr 17, 2025
a326afd
Add buttons for expanding and collapsing all test suites
Kobzol Apr 17, 2025
4b31033
Add a note about how to find tests that haven't been executed anywhere.
Kobzol Apr 17, 2025
1a6e0d5
Render test revisions separately
Kobzol Apr 17, 2025
d2c1763
Create a macro for rendering test results
Kobzol Apr 17, 2025
aa9cb70
Print number of root tests and subdirectories
Kobzol Apr 17, 2025
08cb187
Turn `test_dashboard` into a file
Kobzol Apr 17, 2025
cecf167
Add a note about the test dashboard to the post-merge report
Kobzol Apr 17, 2025
136171c
skip llvm-config in autodiff check builds, when its unavailable
ZuseZ4 Apr 18, 2025
65ce38a
Add a note that explains the counts
Kobzol Apr 18, 2025
b18e373
Reduce duplicated test prefixes in nested subdirectories
Kobzol Apr 18, 2025
1b39302
Disable has_thread_local on i686-win7-windows-msvc
roblabla Apr 18, 2025
ac7d1be
add coverage on config include logic
onur-ozkan Apr 16, 2025
b31aff5
Rollup merge of #137454 - mu001999-contrib:fix-137414, r=wesleywiser
matthiaskrgr Apr 18, 2025
f6a878c
Rollup merge of #138934 - onur-ozkan:extended-config-profiles, r=Kobzol
matthiaskrgr Apr 18, 2025
a1c3f0d
Rollup merge of #139297 - RossSmyth:NixClean, r=WaffleLapkin
matthiaskrgr Apr 18, 2025
d359307
Rollup merge of #139834 - ChrisDenton:spf, r=WaffleLapkin
matthiaskrgr Apr 18, 2025
1de30fc
Rollup merge of #139978 - Kobzol:ci-test-summary, r=jieyouxu
matthiaskrgr Apr 18, 2025
ef0f7da
Rollup merge of #140000 - EnzymeAD:autodiff-check-builds, r=onur-ozkan
matthiaskrgr Apr 18, 2025
b7b3dde
Rollup merge of #140007 - roblabla:fix-win7, r=ChrisDenton
matthiaskrgr Apr 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions bootstrap.example.toml
Original file line number Diff line number Diff line change
@@ -19,6 +19,14 @@
# Note that this has no default value (x.py uses the defaults in `bootstrap.example.toml`).
#profile = <none>

# Inherits configuration values from different configuration files (a.k.a. config extensions).
# Supports absolute paths, and uses the current directory (where the bootstrap was invoked)
# as the base if the given path is not absolute.
#
# The overriding logic follows a right-to-left order. For example, in `include = ["a.toml", "b.toml"]`,
# extension `b.toml` overrides `a.toml`. Also, parent extensions always overrides the inner ones.
#include = []

# Keeps track of major changes made to this configuration.
#
# This value also represents ID of the PR that caused major changes. Meaning,
21 changes: 15 additions & 6 deletions compiler/rustc_metadata/src/locator.rs
Original file line number Diff line number Diff line change
@@ -427,12 +427,21 @@ impl<'a> CrateLocator<'a> {

let (rlibs, rmetas, dylibs) =
candidates.entry(hash.to_string()).or_default();
let path =
try_canonicalize(&spf.path).unwrap_or_else(|_| spf.path.to_path_buf());
if seen_paths.contains(&path) {
continue;
};
seen_paths.insert(path.clone());
{
// As a perforamnce optimisation we canonicalize the path and skip
// ones we've already seeen. This allows us to ignore crates
// we know are exactual equal to ones we've already found.
// Going to the same crate through different symlinks does not change the result.
let path = try_canonicalize(&spf.path)
.unwrap_or_else(|_| spf.path.to_path_buf());
if seen_paths.contains(&path) {
continue;
};
seen_paths.insert(path);
}
// Use the original path (potentially with unresolved symlinks),
// filesystem code should not care, but this is nicer for diagnostics.
let path = spf.path.to_path_buf();
match kind {
CrateFlavor::Rlib => rlibs.insert(path, search_path.kind),
CrateFlavor::Rmeta => rmetas.insert(path, search_path.kind),
14 changes: 8 additions & 6 deletions compiler/rustc_parse/src/parser/expr.rs
Original file line number Diff line number Diff line change
@@ -1884,13 +1884,15 @@ impl<'a> Parser<'a> {
let mut expr = self.parse_expr_opt()?;
if let Some(expr) = &mut expr {
if label.is_some()
&& matches!(
expr.kind,
&& match &expr.kind {
ExprKind::While(_, _, None)
| ExprKind::ForLoop { label: None, .. }
| ExprKind::Loop(_, None, _)
| ExprKind::Block(_, None)
)
| ExprKind::ForLoop { label: None, .. }
| ExprKind::Loop(_, None, _) => true,
ExprKind::Block(block, None) => {
matches!(block.rules, BlockCheckMode::Default)
}
_ => false,
}
{
self.psess.buffer_lint(
BREAK_WITH_LABEL_AND_LOOP,
Original file line number Diff line number Diff line change
@@ -7,6 +7,12 @@ pub(crate) fn target() -> Target {
base.cpu = "pentium4".into();
base.max_atomic_width = Some(64);
base.supported_sanitizers = SanitizerSet::ADDRESS;
// On Windows 7 32-bit, the alignment characteristic of the TLS Directory
// don't appear to be respected by the PE Loader, leading to crashes. As
// a result, let's disable has_thread_local to make sure TLS goes through
// the emulation layer.
// See https://github.com/rust-lang/rust/issues/138903
base.has_thread_local = false;

base.add_pre_link_args(
LinkerFlavor::Msvc(Lld::No),
3 changes: 1 addition & 2 deletions src/bootstrap/src/core/build_steps/compile.rs
Original file line number Diff line number Diff line change
@@ -1194,8 +1194,7 @@ pub fn rustc_cargo(
let enzyme_dir = builder.build.out.join(arch).join("enzyme").join("lib");
cargo.rustflag("-L").rustflag(enzyme_dir.to_str().expect("Invalid path"));

if !builder.config.dry_run() {
let llvm_config = builder.llvm_config(builder.config.build).unwrap();
if let Some(llvm_config) = builder.llvm_config(builder.config.build) {
let llvm_version_major = llvm::get_llvm_version_major(builder, &llvm_config);
cargo.rustflag("-l").rustflag(&format!("Enzyme-{llvm_version_major}"));
}
133 changes: 115 additions & 18 deletions src/bootstrap/src/core/config/config.rs
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
use std::cell::{Cell, RefCell};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::fmt::{self, Display};
use std::hash::Hash;
use std::io::IsTerminal;
use std::path::{Path, PathBuf, absolute};
use std::process::Command;
@@ -701,6 +702,7 @@ pub(crate) struct TomlConfig {
target: Option<HashMap<String, TomlTarget>>,
dist: Option<Dist>,
profile: Option<String>,
include: Option<Vec<PathBuf>>,
}

/// This enum is used for deserializing change IDs from TOML, allowing both numeric values and the string `"ignore"`.
@@ -747,27 +749,35 @@ enum ReplaceOpt {
}

trait Merge {
fn merge(&mut self, other: Self, replace: ReplaceOpt);
fn merge(
&mut self,
parent_config_path: Option<PathBuf>,
included_extensions: &mut HashSet<PathBuf>,
other: Self,
replace: ReplaceOpt,
);
}

impl Merge for TomlConfig {
fn merge(
&mut self,
TomlConfig { build, install, llvm, gcc, rust, dist, target, profile, change_id }: Self,
parent_config_path: Option<PathBuf>,
included_extensions: &mut HashSet<PathBuf>,
TomlConfig { build, install, llvm, gcc, rust, dist, target, profile, change_id, include }: Self,
replace: ReplaceOpt,
) {
fn do_merge<T: Merge>(x: &mut Option<T>, y: Option<T>, replace: ReplaceOpt) {
if let Some(new) = y {
if let Some(original) = x {
original.merge(new, replace);
original.merge(None, &mut Default::default(), new, replace);
} else {
*x = Some(new);
}
}
}

self.change_id.inner.merge(change_id.inner, replace);
self.profile.merge(profile, replace);
self.change_id.inner.merge(None, &mut Default::default(), change_id.inner, replace);
self.profile.merge(None, &mut Default::default(), profile, replace);

do_merge(&mut self.build, build, replace);
do_merge(&mut self.install, install, replace);
@@ -782,13 +792,50 @@ impl Merge for TomlConfig {
(Some(original_target), Some(new_target)) => {
for (triple, new) in new_target {
if let Some(original) = original_target.get_mut(&triple) {
original.merge(new, replace);
original.merge(None, &mut Default::default(), new, replace);
} else {
original_target.insert(triple, new);
}
}
}
}

let parent_dir = parent_config_path
.as_ref()
.and_then(|p| p.parent().map(ToOwned::to_owned))
.unwrap_or_default();

// `include` handled later since we ignore duplicates using `ReplaceOpt::IgnoreDuplicate` to
// keep the upper-level configuration to take precedence.
for include_path in include.clone().unwrap_or_default().iter().rev() {
let include_path = parent_dir.join(include_path);
let include_path = include_path.canonicalize().unwrap_or_else(|e| {
eprintln!("ERROR: Failed to canonicalize '{}' path: {e}", include_path.display());
exit!(2);
});

let included_toml = Config::get_toml_inner(&include_path).unwrap_or_else(|e| {
eprintln!("ERROR: Failed to parse '{}': {e}", include_path.display());
exit!(2);
});

assert!(
included_extensions.insert(include_path.clone()),
"Cyclic inclusion detected: '{}' is being included again before its previous inclusion was fully processed.",
include_path.display()
);

self.merge(
Some(include_path.clone()),
included_extensions,
included_toml,
// Ensures that parent configuration always takes precedence
// over child configurations.
ReplaceOpt::IgnoreDuplicate,
);

included_extensions.remove(&include_path);
}
}
}

@@ -803,7 +850,13 @@ macro_rules! define_config {
}

impl Merge for $name {
fn merge(&mut self, other: Self, replace: ReplaceOpt) {
fn merge(
&mut self,
_parent_config_path: Option<PathBuf>,
_included_extensions: &mut HashSet<PathBuf>,
other: Self,
replace: ReplaceOpt
) {
$(
match replace {
ReplaceOpt::IgnoreDuplicate => {
@@ -903,7 +956,13 @@ macro_rules! define_config {
}

impl<T> Merge for Option<T> {
fn merge(&mut self, other: Self, replace: ReplaceOpt) {
fn merge(
&mut self,
_parent_config_path: Option<PathBuf>,
_included_extensions: &mut HashSet<PathBuf>,
other: Self,
replace: ReplaceOpt,
) {
match replace {
ReplaceOpt::IgnoreDuplicate => {
if self.is_none() {
@@ -1363,13 +1422,15 @@ impl Config {
Self::get_toml(&builder_config_path)
}

#[cfg(test)]
pub(crate) fn get_toml(_: &Path) -> Result<TomlConfig, toml::de::Error> {
Ok(TomlConfig::default())
pub(crate) fn get_toml(file: &Path) -> Result<TomlConfig, toml::de::Error> {
#[cfg(test)]
return Ok(TomlConfig::default());

#[cfg(not(test))]
Self::get_toml_inner(file)
}

#[cfg(not(test))]
pub(crate) fn get_toml(file: &Path) -> Result<TomlConfig, toml::de::Error> {
fn get_toml_inner(file: &Path) -> Result<TomlConfig, toml::de::Error> {
let contents =
t!(fs::read_to_string(file), format!("config file {} not found", file.display()));
// Deserialize to Value and then TomlConfig to prevent the Deserialize impl of
@@ -1548,7 +1609,8 @@ impl Config {
// but not if `bootstrap.toml` hasn't been created.
let mut toml = if !using_default_path || toml_path.exists() {
config.config = Some(if cfg!(not(test)) {
toml_path.canonicalize().unwrap()
toml_path = toml_path.canonicalize().unwrap();
toml_path.clone()
} else {
toml_path.clone()
});
@@ -1576,6 +1638,26 @@ impl Config {
toml.profile = Some("dist".into());
}

// Reverse the list to ensure the last added config extension remains the most dominant.
// For example, given ["a.toml", "b.toml"], "b.toml" should take precedence over "a.toml".
//
// This must be handled before applying the `profile` since `include`s should always take
// precedence over `profile`s.
for include_path in toml.include.clone().unwrap_or_default().iter().rev() {
let include_path = toml_path.parent().unwrap().join(include_path);

let included_toml = get_toml(&include_path).unwrap_or_else(|e| {
eprintln!("ERROR: Failed to parse '{}': {e}", include_path.display());
exit!(2);
});
toml.merge(
Some(include_path),
&mut Default::default(),
included_toml,
ReplaceOpt::IgnoreDuplicate,
);
}

if let Some(include) = &toml.profile {
// Allows creating alias for profile names, allowing
// profiles to be renamed while maintaining back compatibility
@@ -1597,7 +1679,12 @@ impl Config {
);
exit!(2);
});
toml.merge(included_toml, ReplaceOpt::IgnoreDuplicate);
toml.merge(
Some(include_path),
&mut Default::default(),
included_toml,
ReplaceOpt::IgnoreDuplicate,
);
}

let mut override_toml = TomlConfig::default();
@@ -1608,7 +1695,12 @@ impl Config {

let mut err = match get_table(option) {
Ok(v) => {
override_toml.merge(v, ReplaceOpt::ErrorOnDuplicate);
override_toml.merge(
None,
&mut Default::default(),
v,
ReplaceOpt::ErrorOnDuplicate,
);
continue;
}
Err(e) => e,
@@ -1619,7 +1711,12 @@ impl Config {
if !value.contains('"') {
match get_table(&format!(r#"{key}="{value}""#)) {
Ok(v) => {
override_toml.merge(v, ReplaceOpt::ErrorOnDuplicate);
override_toml.merge(
None,
&mut Default::default(),
v,
ReplaceOpt::ErrorOnDuplicate,
);
continue;
}
Err(e) => err = e,
@@ -1629,7 +1726,7 @@ impl Config {
eprintln!("failed to parse override `{option}`: `{err}");
exit!(2)
}
toml.merge(override_toml, ReplaceOpt::Override);
toml.merge(None, &mut Default::default(), override_toml, ReplaceOpt::Override);

config.change_id = toml.change_id.inner;

211 changes: 209 additions & 2 deletions src/bootstrap/src/core/config/tests.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::collections::BTreeSet;
use std::env;
use std::fs::{File, remove_file};
use std::io::Write;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::{env, fs};

use build_helper::ci::CiEnv;
use clap::CommandFactory;
@@ -23,6 +23,27 @@ pub(crate) fn parse(config: &str) -> Config {
)
}

fn get_toml(file: &Path) -> Result<TomlConfig, toml::de::Error> {
let contents = std::fs::read_to_string(file).unwrap();
toml::from_str(&contents).and_then(|table: toml::Value| TomlConfig::deserialize(table))
}

/// Helps with debugging by using consistent test-specific directories instead of
/// random temporary directories.
fn prepare_test_specific_dir() -> PathBuf {
let current = std::thread::current();
// Replace "::" with "_" to make it safe for directory names on Windows systems
let test_path = current.name().unwrap().replace("::", "_");

let testdir = parse("").tempdir().join(test_path);

// clean up any old test files
let _ = fs::remove_dir_all(&testdir);
let _ = fs::create_dir_all(&testdir);

testdir
}

#[test]
fn download_ci_llvm() {
let config = parse("llvm.download-ci-llvm = false");
@@ -539,3 +560,189 @@ fn test_ci_flag() {
let config = Config::parse_inner(Flags::parse(&["check".into()]), |&_| toml::from_str(""));
assert_eq!(config.is_running_on_ci, CiEnv::is_ci());
}

#[test]
fn test_precedence_of_includes() {
let testdir = prepare_test_specific_dir();

let root_config = testdir.join("config.toml");
let root_config_content = br#"
include = ["./extension.toml"]
[llvm]
link-jobs = 2
"#;
File::create(&root_config).unwrap().write_all(root_config_content).unwrap();

let extension = testdir.join("extension.toml");
let extension_content = br#"
change-id=543
include = ["./extension2.toml"]
"#;
File::create(extension).unwrap().write_all(extension_content).unwrap();

let extension = testdir.join("extension2.toml");
let extension_content = br#"
change-id=742
[llvm]
link-jobs = 10
[build]
description = "Some creative description"
"#;
File::create(extension).unwrap().write_all(extension_content).unwrap();

let config = Config::parse_inner(
Flags::parse(&["check".to_owned(), format!("--config={}", root_config.to_str().unwrap())]),
get_toml,
);

assert_eq!(config.change_id.unwrap(), ChangeId::Id(543));
assert_eq!(config.llvm_link_jobs.unwrap(), 2);
assert_eq!(config.description.unwrap(), "Some creative description");
}

#[test]
#[should_panic(expected = "Cyclic inclusion detected")]
fn test_cyclic_include_direct() {
let testdir = prepare_test_specific_dir();

let root_config = testdir.join("config.toml");
let root_config_content = br#"
include = ["./extension.toml"]
"#;
File::create(&root_config).unwrap().write_all(root_config_content).unwrap();

let extension = testdir.join("extension.toml");
let extension_content = br#"
include = ["./config.toml"]
"#;
File::create(extension).unwrap().write_all(extension_content).unwrap();

let config = Config::parse_inner(
Flags::parse(&["check".to_owned(), format!("--config={}", root_config.to_str().unwrap())]),
get_toml,
);
}

#[test]
#[should_panic(expected = "Cyclic inclusion detected")]
fn test_cyclic_include_indirect() {
let testdir = prepare_test_specific_dir();

let root_config = testdir.join("config.toml");
let root_config_content = br#"
include = ["./extension.toml"]
"#;
File::create(&root_config).unwrap().write_all(root_config_content).unwrap();

let extension = testdir.join("extension.toml");
let extension_content = br#"
include = ["./extension2.toml"]
"#;
File::create(extension).unwrap().write_all(extension_content).unwrap();

let extension = testdir.join("extension2.toml");
let extension_content = br#"
include = ["./extension3.toml"]
"#;
File::create(extension).unwrap().write_all(extension_content).unwrap();

let extension = testdir.join("extension3.toml");
let extension_content = br#"
include = ["./extension.toml"]
"#;
File::create(extension).unwrap().write_all(extension_content).unwrap();

let config = Config::parse_inner(
Flags::parse(&["check".to_owned(), format!("--config={}", root_config.to_str().unwrap())]),
get_toml,
);
}

#[test]
fn test_include_absolute_paths() {
let testdir = prepare_test_specific_dir();

let extension = testdir.join("extension.toml");
File::create(&extension).unwrap().write_all(&[]).unwrap();

let root_config = testdir.join("config.toml");
let extension_absolute_path =
extension.canonicalize().unwrap().to_str().unwrap().replace('\\', r"\\");
let root_config_content = format!(r#"include = ["{}"]"#, extension_absolute_path);
File::create(&root_config).unwrap().write_all(root_config_content.as_bytes()).unwrap();

let config = Config::parse_inner(
Flags::parse(&["check".to_owned(), format!("--config={}", root_config.to_str().unwrap())]),
get_toml,
);
}

#[test]
fn test_include_relative_paths() {
let testdir = prepare_test_specific_dir();

let _ = fs::create_dir_all(&testdir.join("subdir/another_subdir"));

let root_config = testdir.join("config.toml");
let root_config_content = br#"
include = ["./subdir/extension.toml"]
"#;
File::create(&root_config).unwrap().write_all(root_config_content).unwrap();

let extension = testdir.join("subdir/extension.toml");
let extension_content = br#"
include = ["../extension2.toml"]
"#;
File::create(extension).unwrap().write_all(extension_content).unwrap();

let extension = testdir.join("extension2.toml");
let extension_content = br#"
include = ["./subdir/another_subdir/extension3.toml"]
"#;
File::create(extension).unwrap().write_all(extension_content).unwrap();

let extension = testdir.join("subdir/another_subdir/extension3.toml");
let extension_content = br#"
include = ["../../extension4.toml"]
"#;
File::create(extension).unwrap().write_all(extension_content).unwrap();

let extension = testdir.join("extension4.toml");
File::create(extension).unwrap().write_all(&[]).unwrap();

let config = Config::parse_inner(
Flags::parse(&["check".to_owned(), format!("--config={}", root_config.to_str().unwrap())]),
get_toml,
);
}

#[test]
fn test_include_precedence_over_profile() {
let testdir = prepare_test_specific_dir();

let root_config = testdir.join("config.toml");
let root_config_content = br#"
profile = "dist"
include = ["./extension.toml"]
"#;
File::create(&root_config).unwrap().write_all(root_config_content).unwrap();

let extension = testdir.join("extension.toml");
let extension_content = br#"
[rust]
channel = "dev"
"#;
File::create(extension).unwrap().write_all(extension_content).unwrap();

let config = Config::parse_inner(
Flags::parse(&["check".to_owned(), format!("--config={}", root_config.to_str().unwrap())]),
get_toml,
);

// "dist" profile would normally set the channel to "auto-detect", but includes should
// override profile settings, so we expect this to be "dev" here.
assert_eq!(config.channel, "dev");
}
5 changes: 5 additions & 0 deletions src/bootstrap/src/utils/change_tracker.rs
Original file line number Diff line number Diff line change
@@ -396,4 +396,9 @@ pub const CONFIG_CHANGE_HISTORY: &[ChangeInfo] = &[
severity: ChangeSeverity::Info,
summary: "Added a new option `build.compiletest-use-stage0-libtest` to force `compiletest` to use the stage 0 libtest.",
},
ChangeInfo {
change_id: 138934,
severity: ChangeSeverity::Info,
summary: "Added new option `include` to create config extensions.",
},
];
67 changes: 67 additions & 0 deletions src/ci/citool/Cargo.lock
1 change: 1 addition & 0 deletions src/ci/citool/Cargo.toml
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ edition = "2021"

[dependencies]
anyhow = "1"
askama = "0.13"
clap = { version = "4.5", features = ["derive"] }
csv = "1"
diff = "0.1"
13 changes: 6 additions & 7 deletions src/ci/citool/src/analysis.rs
Original file line number Diff line number Diff line change
@@ -8,9 +8,9 @@ use build_helper::metrics::{
};

use crate::github::JobInfoResolver;
use crate::metrics;
use crate::metrics::{JobMetrics, JobName, get_test_suites};
use crate::utils::{output_details, pluralize};
use crate::{metrics, utils};

/// Outputs durations of individual bootstrap steps from the gathered bootstrap invocations,
/// and also a table with summarized information about executed tests.
@@ -394,18 +394,17 @@ fn aggregate_tests(metrics: &JsonRoot) -> TestSuiteData {
// Poor man's detection of doctests based on the "(line XYZ)" suffix
let is_doctest = matches!(suite.metadata, TestSuiteMetadata::CargoPackage { .. })
&& test.name.contains("(line");
let test_entry = Test { name: generate_test_name(&test.name), stage, is_doctest };
let test_entry = Test {
name: utils::normalize_path_delimiters(&test.name).to_string(),
stage,
is_doctest,
};
tests.insert(test_entry, test.outcome.clone());
}
}
TestSuiteData { tests }
}

/// Normalizes Windows-style path delimiters to Unix-style paths.
fn generate_test_name(name: &str) -> String {
name.replace('\\', "/")
}

/// Prints test changes in Markdown format to stdout.
fn report_test_diffs(
diff: AggregatedTestDiffs,
34 changes: 31 additions & 3 deletions src/ci/citool/src/main.rs
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ mod datadog;
mod github;
mod jobs;
mod metrics;
mod test_dashboard;
mod utils;

use std::collections::{BTreeMap, HashMap};
@@ -22,7 +23,8 @@ use crate::datadog::upload_datadog_metric;
use crate::github::JobInfoResolver;
use crate::jobs::RunType;
use crate::metrics::{JobMetrics, download_auto_job_metrics, download_job_metrics, load_metrics};
use crate::utils::load_env_var;
use crate::test_dashboard::generate_test_dashboard;
use crate::utils::{load_env_var, output_details};

const CI_DIRECTORY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/..");
const DOCKER_DIRECTORY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../docker");
@@ -180,12 +182,26 @@ fn postprocess_metrics(
}

fn post_merge_report(db: JobDatabase, current: String, parent: String) -> anyhow::Result<()> {
let metrics = download_auto_job_metrics(&db, &parent, &current)?;
let metrics = download_auto_job_metrics(&db, Some(&parent), &current)?;

println!("\nComparing {parent} (parent) -> {current} (this PR)\n");

let mut job_info_resolver = JobInfoResolver::new();
output_test_diffs(&metrics, &mut job_info_resolver);

output_details("Test dashboard", || {
println!(
r#"\nRun
```bash
cargo run --manifest-path src/ci/citool/Cargo.toml -- \
test-dashboard {current} --output-dir test-dashboard
```
And then open `test-dashboard/index.html` in your browser to see an overview of all executed tests.
"#
);
});

output_largest_duration_changes(&metrics, &mut job_info_resolver);

Ok(())
@@ -234,6 +250,14 @@ enum Args {
/// Current commit that will be compared to `parent`.
current: String,
},
/// Generate a directory containing a HTML dashboard of test results from a CI run.
TestDashboard {
/// Commit SHA that was tested on CI to analyze.
current: String,
/// Output path for the HTML directory.
#[clap(long)]
output_dir: PathBuf,
},
}

#[derive(clap::ValueEnum, Clone)]
@@ -275,7 +299,11 @@ fn main() -> anyhow::Result<()> {
postprocess_metrics(metrics_path, parent, job_name)?;
}
Args::PostMergeReport { current, parent } => {
post_merge_report(load_db(default_jobs_file)?, current, parent)?;
post_merge_report(load_db(&default_jobs_file)?, current, parent)?;
}
Args::TestDashboard { current, output_dir } => {
let db = load_db(&default_jobs_file)?;
generate_test_dashboard(db, &current, &output_dir)?;
}
}

23 changes: 12 additions & 11 deletions src/ci/citool/src/metrics.rs
Original file line number Diff line number Diff line change
@@ -46,24 +46,25 @@ pub struct JobMetrics {
/// `parent` and `current` should be commit SHAs.
pub fn download_auto_job_metrics(
job_db: &JobDatabase,
parent: &str,
parent: Option<&str>,
current: &str,
) -> anyhow::Result<HashMap<JobName, JobMetrics>> {
let mut jobs = HashMap::default();

for job in &job_db.auto_jobs {
eprintln!("Downloading metrics of job {}", job.name);
let metrics_parent = match download_job_metrics(&job.name, parent) {
Ok(metrics) => Some(metrics),
Err(error) => {
eprintln!(
r#"Did not find metrics for job `{}` at `{parent}`: {error:?}.
let metrics_parent =
parent.and_then(|parent| match download_job_metrics(&job.name, parent) {
Ok(metrics) => Some(metrics),
Err(error) => {
eprintln!(
r#"Did not find metrics for job `{}` at `{parent}`: {error:?}.
Maybe it was newly added?"#,
job.name
);
None
}
};
job.name
);
None
}
});
let metrics_current = download_job_metrics(&job.name, current)?;
jobs.insert(
job.name.clone(),
216 changes: 216 additions & 0 deletions src/ci/citool/src/test_dashboard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
use std::collections::{BTreeMap, HashMap};
use std::fs::File;
use std::io::BufWriter;
use std::path::{Path, PathBuf};

use askama::Template;
use build_helper::metrics::{TestOutcome, TestSuiteMetadata};

use crate::jobs::JobDatabase;
use crate::metrics::{JobMetrics, JobName, download_auto_job_metrics, get_test_suites};
use crate::utils::normalize_path_delimiters;

/// Generate a set of HTML files into a directory that contain a dashboard of test results.
pub fn generate_test_dashboard(
db: JobDatabase,
current: &str,
output_dir: &Path,
) -> anyhow::Result<()> {
let metrics = download_auto_job_metrics(&db, None, current)?;
let suites = gather_test_suites(&metrics);

std::fs::create_dir_all(output_dir)?;

let test_count = suites.test_count();
write_page(output_dir, "index.html", &TestSuitesPage { suites, test_count })?;

Ok(())
}

fn write_page<T: Template>(dir: &Path, name: &str, template: &T) -> anyhow::Result<()> {
let mut file = BufWriter::new(File::create(dir.join(name))?);
Template::write_into(template, &mut file)?;
Ok(())
}

fn gather_test_suites(job_metrics: &HashMap<JobName, JobMetrics>) -> TestSuites {
struct CoarseTestSuite<'a> {
tests: BTreeMap<String, Test<'a>>,
}

let mut suites: HashMap<String, CoarseTestSuite> = HashMap::new();

// First, gather tests from all jobs, stages and targets, and aggregate them per suite
// Only work with compiletest suites.
for (job, metrics) in job_metrics {
let test_suites = get_test_suites(&metrics.current);
for suite in test_suites {
let (suite_name, stage, target) = match &suite.metadata {
TestSuiteMetadata::CargoPackage { .. } => {
continue;
}
TestSuiteMetadata::Compiletest { suite, stage, target, .. } => {
(suite.clone(), *stage, target)
}
};
let suite_entry = suites
.entry(suite_name.clone())
.or_insert_with(|| CoarseTestSuite { tests: Default::default() });
let test_metadata = TestMetadata { job, stage, target };

for test in &suite.tests {
let test_name = normalize_test_name(&test.name, &suite_name);
let (test_name, variant_name) = match test_name.rsplit_once('#') {
Some((name, variant)) => (name.to_string(), variant.to_string()),
None => (test_name, "".to_string()),
};
let test_entry = suite_entry
.tests
.entry(test_name.clone())
.or_insert_with(|| Test { revisions: Default::default() });
let variant_entry = test_entry
.revisions
.entry(variant_name)
.or_insert_with(|| TestResults { passed: vec![], ignored: vec![] });

match test.outcome {
TestOutcome::Passed => {
variant_entry.passed.push(test_metadata);
}
TestOutcome::Ignored { ignore_reason: _ } => {
variant_entry.ignored.push(test_metadata);
}
TestOutcome::Failed => {
eprintln!("Warning: failed test {test_name}");
}
}
}
}
}

// Then, split the suites per directory
let mut suites = suites.into_iter().collect::<Vec<_>>();
suites.sort_by(|a, b| a.0.cmp(&b.0));

let suites = suites
.into_iter()
.map(|(suite_name, suite)| TestSuite { group: build_test_group(&suite_name, suite.tests) })
.collect();

TestSuites { suites }
}

/// Recursively expand a test group based on filesystem hierarchy.
fn build_test_group<'a>(name: &str, tests: BTreeMap<String, Test<'a>>) -> TestGroup<'a> {
let mut root_tests = vec![];
let mut subdirs: BTreeMap<String, BTreeMap<String, Test<'a>>> = Default::default();

// Split tests into root tests and tests located in subdirectories
for (name, test) in tests {
let mut components = Path::new(&name).components().peekable();
let subdir = components.next().unwrap();

if components.peek().is_none() {
// This is a root test
root_tests.push((name, test));
} else {
// This is a test in a nested directory
let subdir_tests =
subdirs.entry(subdir.as_os_str().to_str().unwrap().to_string()).or_default();
let test_name =
components.into_iter().collect::<PathBuf>().to_str().unwrap().to_string();
subdir_tests.insert(test_name, test);
}
}
let dirs = subdirs
.into_iter()
.map(|(name, tests)| {
let group = build_test_group(&name, tests);
(name, group)
})
.collect();

TestGroup { name: name.to_string(), root_tests, groups: dirs }
}

/// Compiletest tests start with `[suite] tests/[suite]/a/b/c...`.
/// Remove the `[suite] tests/[suite]/` prefix so that we can find the filesystem path.
/// Also normalizes path delimiters.
fn normalize_test_name(name: &str, suite_name: &str) -> String {
let name = normalize_path_delimiters(name);
let name = name.as_ref();
let name = name.strip_prefix(&format!("[{suite_name}]")).unwrap_or(name).trim();
let name = name.strip_prefix("tests/").unwrap_or(name);
let name = name.strip_prefix(suite_name).unwrap_or(name);
name.trim_start_matches("/").to_string()
}

struct TestSuites<'a> {
suites: Vec<TestSuite<'a>>,
}

impl<'a> TestSuites<'a> {
fn test_count(&self) -> u64 {
self.suites.iter().map(|suite| suite.group.test_count()).sum::<u64>()
}
}

struct TestSuite<'a> {
group: TestGroup<'a>,
}

struct TestResults<'a> {
passed: Vec<TestMetadata<'a>>,
ignored: Vec<TestMetadata<'a>>,
}

struct Test<'a> {
revisions: BTreeMap<String, TestResults<'a>>,
}

impl<'a> Test<'a> {
/// If this is a test without revisions, it will have a single entry in `revisions` with
/// an empty string as the revision name.
fn single_test(&self) -> Option<&TestResults<'a>> {
if self.revisions.len() == 1 {
self.revisions.iter().next().take_if(|e| e.0.is_empty()).map(|e| e.1)
} else {
None
}
}
}

#[derive(Clone, Copy)]
#[allow(dead_code)]
struct TestMetadata<'a> {
job: &'a str,
stage: u32,
target: &'a str,
}

// We have to use a template for the TestGroup instead of a macro, because
// macros cannot be recursive in askama at the moment.
#[derive(Template)]
#[template(path = "test_group.askama")]
/// Represents a group of tests
struct TestGroup<'a> {
name: String,
/// Tests located directly in this directory
root_tests: Vec<(String, Test<'a>)>,
/// Nested directories with additional tests
groups: Vec<(String, TestGroup<'a>)>,
}

impl<'a> TestGroup<'a> {
fn test_count(&self) -> u64 {
let root = self.root_tests.len() as u64;
self.groups.iter().map(|(_, group)| group.test_count()).sum::<u64>() + root
}
}

#[derive(Template)]
#[template(path = "test_suites.askama")]
struct TestSuitesPage<'a> {
suites: TestSuites<'a>,
test_count: u64,
}
6 changes: 6 additions & 0 deletions src/ci/citool/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::path::Path;

use anyhow::Context;
@@ -28,3 +29,8 @@ where
func();
println!("</details>\n");
}

/// Normalizes Windows-style path delimiters to Unix-style paths.
pub fn normalize_path_delimiters(name: &str) -> Cow<str> {
if name.contains("\\") { name.replace('\\', "/").into() } else { name.into() }
}
22 changes: 22 additions & 0 deletions src/ci/citool/templates/layout.askama
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<html>
<head>
<meta charset="UTF-8">
<title>Rust CI Test Dashboard</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
max-width: 1500px;
margin: 0 auto;
padding: 20px;
background: #F5F5F5;
}
{% block styles %}{% endblock %}
</style>
</head>

<body>
{% block content %}{% endblock %}
{% block scripts %}{% endblock %}
</body>
</html>
42 changes: 42 additions & 0 deletions src/ci/citool/templates/test_group.askama
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{% macro test_result(r) -%}
passed: {{ r.passed.len() }}, ignored: {{ r.ignored.len() }}
{%- endmacro %}

<li>
<details>
<summary>{{ name }} ({{ test_count() }} test{{ test_count() | pluralize }}{% if !root_tests.is_empty() && root_tests.len() as u64 != test_count() -%}
, {{ root_tests.len() }} root test{{ root_tests.len() | pluralize }}
{%- endif %}{% if !groups.is_empty() -%}
, {{ groups.len() }} subdir{{ groups.len() | pluralize }}
{%- endif %})
</summary>

{% if !groups.is_empty() %}
<ul>
{% for (dir_name, subgroup) in groups %}
{{ subgroup|safe }}
{% endfor %}
</ul>
{% endif %}

{% if !root_tests.is_empty() %}
<ul>
{% for (name, test) in root_tests %}
<li>
{% if let Some(result) = test.single_test() %}
<b>{{ name }}</b> ({% call test_result(result) %})
{% else %}
<b>{{ name }}</b> ({{ test.revisions.len() }} revision{{ test.revisions.len() | pluralize }})
<ul>
{% for (revision, result) in test.revisions %}
<li>#<i>{{ revision }}</i> ({% call test_result(result) %})</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}

</details>
</li>
108 changes: 108 additions & 0 deletions src/ci/citool/templates/test_suites.askama
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
{% extends "layout.askama" %}

{% block content %}
<h1>Rust CI test dashboard</h1>
<div>
Here's how to interpret the "passed" and "ignored" counts:
the count includes all combinations of "stage" x "target" x "CI job where the test was executed or ignored".
</div>
<div class="test-suites">
<div class="summary">
<div>
<div class="test-count">Total tests: {{ test_count }}</div>
<div>
To find tests that haven't been executed anywhere, click on "Open all" and search for "passed: 0".
</div>
</div>
<div>
<button onclick="openAll()">Open all</button>
<button onclick="closeAll()">Close all</button>
</div>
</div>

<ul>
{% for suite in suites.suites %}
{{ suite.group|safe }}
{% endfor %}
</ul>
</div>
{% endblock %}

{% block styles %}
h1 {
text-align: center;
color: #333333;
margin-bottom: 30px;
}

.summary {
display: flex;
justify-content: space-between;
}

.test-count {
font-size: 1.2em;
}

.test-suites {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 20px;
}

ul {
padding-left: 0;
}

li {
list-style: none;
padding-left: 20px;
}
summary {
margin-bottom: 5px;
padding: 6px;
background-color: #F4F4F4;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
summary:hover {
background-color: #CFCFCF;
}

/* Style the disclosure triangles */
details > summary {
list-style: none;
position: relative;
}

details > summary::before {
content: "▶";
position: absolute;
left: -15px;
transform: rotate(0);
transition: transform 0.2s;
}

details[open] > summary::before {
transform: rotate(90deg);
}
{% endblock %}

{% block scripts %}
<script type="text/javascript">
function openAll() {
const details = document.getElementsByTagName("details");
for (const elem of details) {
elem.open = true;
}
}
function closeAll() {
const details = document.getElementsByTagName("details");
for (const elem of details) {
elem.open = false;
}
}
</script>
{% endblock %}
37 changes: 37 additions & 0 deletions src/doc/rustc-dev-guide/src/building/suggested.md
Original file line number Diff line number Diff line change
@@ -20,6 +20,43 @@ your `.git/hooks` folder as `pre-push` (without the `.sh` extension!).

You can also install the hook as a step of running `./x setup`!

## Config extensions

When working on different tasks, you might need to switch between different bootstrap configurations.
Sometimes you may want to keep an old configuration for future use. But saving raw config values in
random files and manually copying and pasting them can quickly become messy, especially if you have a
long history of different configurations.

To simplify managing multiple configurations, you can create config extensions.

For example, you can create a simple config file named `cross.toml`:

```toml
[build]
build = "x86_64-unknown-linux-gnu"
host = ["i686-unknown-linux-gnu"]
target = ["i686-unknown-linux-gnu"]


[llvm]
download-ci-llvm = false

[target.x86_64-unknown-linux-gnu]
llvm-config = "/path/to/llvm-19/bin/llvm-config"
```

Then, include this in your `bootstrap.toml`:

```toml
include = ["cross.toml"]
```

You can also include extensions within extensions recursively.

**Note:** In the `include` field, the overriding logic follows a right-to-left order. For example,
in `include = ["a.toml", "b.toml"]`, extension `b.toml` overrides `a.toml`. Also, parent extensions
always overrides the inner ones.

## Configuring `rust-analyzer` for `rustc`

### Project-local rust-analyzer setup
46 changes: 19 additions & 27 deletions src/tools/nix-dev-shell/flake.nix
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
{
description = "rustc dev shell";

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

outputs = { self, nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
x = import ./x { inherit pkgs; };
in
{
devShells.default = with pkgs; mkShell {
name = "rustc-dev-shell";
nativeBuildInputs = with pkgs; [
binutils cmake ninja pkg-config python3 git curl cacert patchelf nix
];
buildInputs = with pkgs; [
openssl glibc.out glibc.static x
];
# Avoid creating text files for ICEs.
RUSTC_ICE = "0";
# Provide `libstdc++.so.6` for the self-contained lld.
# Provide `libz.so.1`.
LD_LIBRARY_PATH = "${with pkgs; lib.makeLibraryPath [stdenv.cc.cc.lib zlib]}";
};
}
);
outputs =
{
self,
nixpkgs,
}:
let
inherit (nixpkgs) lib;
forEachSystem = lib.genAttrs lib.systems.flakeExposed;
in
{
devShells = forEachSystem (system: {
default = nixpkgs.legacyPackages.${system}.callPackage ./shell.nix { };
});

packages = forEachSystem (system: {
default = nixpkgs.legacyPackages.${system}.callPackage ./x { };
});
};
}
38 changes: 23 additions & 15 deletions src/tools/nix-dev-shell/shell.nix
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
{ pkgs ? import <nixpkgs> {} }:
let
x = import ./x { inherit pkgs; };
{
pkgs ? import <nixpkgs> { },
}:
let
inherit (pkgs.lib) lists attrsets;

x = pkgs.callPackage ./x { };
inherit (x.passthru) cacert env;
in
pkgs.mkShell {
name = "rustc";
nativeBuildInputs = with pkgs; [
binutils cmake ninja pkg-config python3 git curl cacert patchelf nix
];
buildInputs = with pkgs; [
openssl glibc.out glibc.static x
];
# Avoid creating text files for ICEs.
RUSTC_ICE = "0";
# Provide `libstdc++.so.6` for the self-contained lld.
# Provide `libz.so.1`
LD_LIBRARY_PATH = "${with pkgs; lib.makeLibraryPath [stdenv.cc.cc.lib zlib]}";
name = "rustc-shell";

inputsFrom = [ x ];
packages = [
pkgs.git
pkgs.nix
x
# Get the runtime deps of the x wrapper
] ++ lists.flatten (attrsets.attrValues env);

env = {
# Avoid creating text files for ICEs.
RUSTC_ICE = 0;
SSL_CERT_FILE = cacert;
};
}
77 changes: 69 additions & 8 deletions src/tools/nix-dev-shell/x/default.nix
Original file line number Diff line number Diff line change
@@ -1,22 +1,83 @@
{
pkgs ? import <nixpkgs> { },
pkgs,
lib,
stdenv,
rustc,
python3,
makeBinaryWrapper,
# Bootstrap
curl,
pkg-config,
libiconv,
openssl,
patchelf,
cacert,
zlib,
# LLVM Deps
ninja,
cmake,
glibc,
}:
pkgs.stdenv.mkDerivation {
name = "x";
stdenv.mkDerivation (self: {
strictDeps = true;
name = "x-none";

outputs = [
"out"
"unwrapped"
];

src = ./x.rs;
dontUnpack = true;

nativeBuildInputs = with pkgs; [ rustc ];
nativeBuildInputs = [
rustc
makeBinaryWrapper
];

env.PYTHON = python3.interpreter;
buildPhase = ''
PYTHON=${pkgs.lib.getExe pkgs.python3} rustc -Copt-level=3 --crate-name x $src --out-dir $out/bin
rustc -Copt-level=3 --crate-name x $src --out-dir $unwrapped/bin
'';

meta = with pkgs.lib; {
installPhase =
let
inherit (self.passthru) cacert env;
in
''
makeWrapper $unwrapped/bin/x $out/bin/x \
--set-default SSL_CERT_FILE ${cacert} \
--prefix CPATH ";" "${lib.makeSearchPath "include" env.cpath}" \
--prefix PATH : ${lib.makeBinPath env.path} \
--prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath env.ldLib}
'';

# For accessing them in the devshell
passthru = {
env = {
cpath = [ libiconv ];
path = [
python3
patchelf
curl
pkg-config
cmake
ninja
stdenv.cc
];
ldLib = [
openssl
zlib
stdenv.cc.cc.lib
];
};
cacert = "${cacert}/etc/ssl/certs/ca-bundle.crt";
};

meta = {
description = "Helper for rust-lang/rust x.py";
homepage = "https://github.com/rust-lang/rust/blob/master/src/tools/x";
license = licenses.mit;
license = lib.licenses.mit;
mainProgram = "x";
};
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#![crate_name = "crateresolve1"]
#![crate_type = "lib"]

pub fn f() -> isize {
10
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#![crate_name = "crateresolve1"]
#![crate_type = "lib"]

pub fn f() -> isize {
20
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
extern crate crateresolve1;

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
error[E0464]: multiple candidates for `rlib` dependency `crateresolve1` found
--> multiple-candidates.rs:1:1
|
LL | extern crate crateresolve1;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: candidate #1: ./mylibs/libcrateresolve1-1.rlib
= note: candidate #2: ./mylibs/libcrateresolve1-2.rlib

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0464`.
34 changes: 34 additions & 0 deletions tests/run-make/crate-loading-multiple-candidates/rmake.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//@ needs-symlink
//@ ignore-cross-compile

// Tests that the multiple candidate dependencies diagnostic prints relative
// paths if a relative library path was passed in.

use run_make_support::{bare_rustc, diff, rfs, rustc};

fn main() {
// Check that relative paths are preserved in the diagnostic
rfs::create_dir("mylibs");
rustc().input("crateresolve1-1.rs").out_dir("mylibs").extra_filename("-1").run();
rustc().input("crateresolve1-2.rs").out_dir("mylibs").extra_filename("-2").run();
check("./mylibs");

// Check that symlinks aren't followed when printing the diagnostic
rfs::rename("mylibs", "original");
rfs::symlink_dir("original", "mylibs");
check("./mylibs");
}

fn check(library_path: &str) {
let out = rustc()
.input("multiple-candidates.rs")
.library_search_path(library_path)
.ui_testing()
.run_fail()
.stderr_utf8();
diff()
.expected_file("multiple-candidates.stderr")
.normalize(r"\\", "/")
.actual_text("(rustc)", &out)
.run();
}
11 changes: 11 additions & 0 deletions tests/ui/lint/break-with-label-and-unsafe-block.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//@ check-pass

#![deny(break_with_label_and_loop)]

unsafe fn foo() -> i32 { 42 }

fn main () {
'label: loop {
break 'label unsafe { foo() }
};
}