Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions src/doc/rustc-dev-guide/src/tests/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,20 @@ Consider writing the test as a proper incremental test instead.

</div>

#### The edition directive

The `//@ edition` directive can take an exact edition, a bounded half-open range of editions or a left-bounded half-open range of editions, this affects which edition is used by `./x test` to run the test. For example:

- A test with the `//@ edition: 2018` directive will only run under the 2018 edition.
- A test with the `//@ edition: 2015..2021` directive can be run under both the 2015 and 2018 editions. However, CI will only run the test with the lowest edition possible (2015 in this case).
- A test with the `//@ edition: 2018..` directive will run under any edition greater or equal than 2018. However, CI will only run the test with the lowest edition possible (2018 in this case).

You can also force `./x test` to use a specific edition by passing the `-- --edition=` argument. However, tests with the `//@ edition` directive will clamp the value passed to the argument. For example, if we run `./x test -- --edition=2015`:

- A test with the `//@ edition: 2018` will run with the 2018 edition.
- A test with the `//@ edition: 2015..2021` will be run with the 2015 edition.
- A test with the `//@ edition: 2018..` will run with the 2018 edition.

### Rustdoc

| Directive | Explanation | Supported test suites | Possible values |
Expand Down
6 changes: 2 additions & 4 deletions src/tools/compiletest/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use build_helper::git::GitConfig;
use camino::{Utf8Path, Utf8PathBuf};
use semver::Version;

use crate::edition::Edition;
use crate::executor::ColorConfig;
use crate::fatal;
use crate::util::{Utf8PathBufExt, add_dylib_path, string_enum};
Expand Down Expand Up @@ -612,10 +613,7 @@ pub struct Config {
pub git_hash: bool,

/// The default Rust edition.
///
/// FIXME: perform stronger validation for this. There are editions that *definitely* exists,
/// but there might also be "future" edition.
pub edition: Option<String>,
pub edition: Option<Edition>,

// Configuration for various run-make tests frobbing things like C compilers or querying about
// various LLVM component information.
Expand Down
97 changes: 90 additions & 7 deletions src/tools/compiletest/src/directives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ use crate::directives::directive_names::{
KNOWN_DIRECTIVE_NAMES, KNOWN_HTMLDOCCK_DIRECTIVE_NAMES, KNOWN_JSONDOCCK_DIRECTIVE_NAMES,
};
use crate::directives::needs::CachedNeedsConditions;
use crate::edition::{Edition, parse_edition};
use crate::errors::ErrorKind;
use crate::executor::{CollectedTestDesc, ShouldPanic};
use crate::help;
use crate::util::static_regex;
use crate::{fatal, help};

pub(crate) mod auxiliary;
mod cfg;
Expand Down Expand Up @@ -437,10 +438,13 @@ impl TestProps {
panic!("`compiler-flags` directive should be spelled `compile-flags`");
}

if let Some(edition) = config.parse_edition(ln, testfile) {
if let Some(range) = parse_edition_range(config, ln, testfile) {
// The edition is added at the start, since flags from //@compile-flags must
// be passed to rustc last.
self.compile_flags.insert(0, format!("--edition={}", edition.trim()));
self.compile_flags.insert(
0,
format!("--edition={}", range.edition_to_test(config.edition)),
);
has_edition = true;
}

Expand Down Expand Up @@ -1124,10 +1128,6 @@ impl Config {
}
}

fn parse_edition(&self, line: &DirectiveLine<'_>, testfile: &Utf8Path) -> Option<String> {
self.parse_name_value_directive(line, "edition", testfile)
}

fn set_name_directive(&self, line: &DirectiveLine<'_>, directive: &str, value: &mut bool) {
// If the flag is already true, don't bother looking at the directive.
*value = *value || self.parse_name_directive(line, directive);
Expand Down Expand Up @@ -1769,3 +1769,86 @@ enum IgnoreDecision {
Continue,
Error { message: String },
}

fn parse_edition_range(
config: &Config,
line: &DirectiveLine<'_>,
testfile: &Utf8Path,
) -> Option<EditionRange> {
let raw = config.parse_name_value_directive(line, "edition", testfile)?;
let line_number = line.line_number;

// Edition range is half-open: `[lower_bound, upper_bound)`
if let Some((lower_bound, upper_bound)) = raw.split_once("..") {
Some(match (maybe_parse_edition(lower_bound), maybe_parse_edition(upper_bound)) {
(Some(lower_bound), Some(upper_bound)) if upper_bound <= lower_bound => {
fatal!(
"{testfile}:{line_number}: the left side of `//@ edition` cannot be greater than or equal to the right side"
);
}
(Some(lower_bound), Some(upper_bound)) => {
EditionRange::Range { lower_bound, upper_bound }
}
(Some(lower_bound), None) => EditionRange::RangeFrom(lower_bound),
(None, Some(_)) => {
fatal!(
"{testfile}:{line_number}: `..edition` is not a supported range in `//@ edition`"
);
}
(None, None) => {
fatal!("{testfile}:{line_number}: `..` is not a supported range in `//@ edition`");
}
})
} else {
match maybe_parse_edition(&raw) {
Some(edition) => Some(EditionRange::Exact(edition)),
None => {
fatal!("{testfile}:{line_number}: empty value for `//@ edition`");
}
}
}
}

fn maybe_parse_edition(mut input: &str) -> Option<Edition> {
input = input.trim();
if input.is_empty() {
return None;
}
Some(parse_edition(input))
}

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum EditionRange {
Exact(Edition),
RangeFrom(Edition),
/// Half-open range: `[lower_bound, upper_bound)`
Range {
lower_bound: Edition,
upper_bound: Edition,
},
Comment on lines +1823 to +1828
Copy link
Member

@fmease fmease Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These could trivially be collapsed into Range { lower: Edition, upper: Option<Edition> } + using e.g., .map_or(true, inside edition_to_test but if you both find the current version more legible, I'm okay with it and won't block it.

}

impl EditionRange {
fn edition_to_test(&self, requested: impl Into<Option<Edition>>) -> Edition {
let min_edition = Edition::Year(2015);
let requested = requested.into().unwrap_or(min_edition);

match *self {
EditionRange::Exact(exact) => exact,
EditionRange::RangeFrom(lower_bound) => {
if requested >= lower_bound {
requested
} else {
lower_bound
}
}
EditionRange::Range { lower_bound, upper_bound } => {
if requested >= lower_bound && requested < upper_bound {
requested
} else {
lower_bound
}
}
}
}
}
142 changes: 140 additions & 2 deletions src/tools/compiletest/src/directives/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ use camino::Utf8Path;
use semver::Version;

use super::{
DirectivesCache, EarlyProps, extract_llvm_version, extract_version_range, iter_directives,
parse_normalize_rule,
DirectivesCache, EarlyProps, Edition, EditionRange, extract_llvm_version,
extract_version_range, iter_directives, parse_normalize_rule,
};
use crate::common::{Config, Debugger, TestMode};
use crate::directives::parse_edition;
use crate::executor::{CollectedTestDesc, ShouldPanic};

fn make_test_description<R: Read>(
Expand Down Expand Up @@ -73,6 +74,7 @@ fn test_parse_normalize_rule() {
struct ConfigBuilder {
mode: Option<String>,
channel: Option<String>,
edition: Option<Edition>,
host: Option<String>,
target: Option<String>,
stage: Option<u32>,
Expand All @@ -96,6 +98,11 @@ impl ConfigBuilder {
self
}

fn edition(&mut self, e: Edition) -> &mut Self {
self.edition = Some(e);
self
}

fn host(&mut self, s: &str) -> &mut Self {
self.host = Some(s.to_owned());
self
Expand Down Expand Up @@ -183,6 +190,10 @@ impl ConfigBuilder {
];
let mut args: Vec<String> = args.iter().map(ToString::to_string).collect();

if let Some(edition) = &self.edition {
args.push(format!("--edition={edition}"));
}

if let Some(ref llvm_version) = self.llvm_version {
args.push("--llvm-version".to_owned());
args.push(llvm_version.clone());
Expand Down Expand Up @@ -941,3 +952,130 @@ fn test_needs_target_std() {
let config = cfg().target("x86_64-unknown-linux-gnu").build();
assert!(!check_ignore(&config, "//@ needs-target-std"));
}

fn parse_edition_range(line: &str) -> Option<EditionRange> {
let config = cfg().build();
let line = super::DirectiveLine { line_number: 0, revision: None, raw_directive: line };

super::parse_edition_range(&config, &line, "tmp.rs".into())
}

#[test]
fn test_parse_edition_range() {
assert_eq!(None, parse_edition_range("hello-world"));
assert_eq!(None, parse_edition_range("edition"));

assert_eq!(Some(EditionRange::Exact(2018.into())), parse_edition_range("edition: 2018"));
assert_eq!(Some(EditionRange::Exact(2021.into())), parse_edition_range("edition:2021"));
assert_eq!(Some(EditionRange::Exact(2024.into())), parse_edition_range("edition: 2024 "));
assert_eq!(Some(EditionRange::Exact(Edition::Future)), parse_edition_range("edition: future"));

assert_eq!(Some(EditionRange::RangeFrom(2018.into())), parse_edition_range("edition: 2018.."));
assert_eq!(Some(EditionRange::RangeFrom(2021.into())), parse_edition_range("edition:2021 .."));
assert_eq!(
Some(EditionRange::RangeFrom(2024.into())),
parse_edition_range("edition: 2024 .. ")
);
assert_eq!(
Some(EditionRange::RangeFrom(Edition::Future)),
parse_edition_range("edition: future.. ")
);

assert_eq!(
Some(EditionRange::Range { lower_bound: 2018.into(), upper_bound: 2024.into() }),
parse_edition_range("edition: 2018..2024")
);
assert_eq!(
Some(EditionRange::Range { lower_bound: 2015.into(), upper_bound: 2021.into() }),
parse_edition_range("edition:2015 .. 2021 ")
);
assert_eq!(
Some(EditionRange::Range { lower_bound: 2021.into(), upper_bound: 2027.into() }),
parse_edition_range("edition: 2021 .. 2027 ")
);
assert_eq!(
Some(EditionRange::Range { lower_bound: 2021.into(), upper_bound: Edition::Future }),
parse_edition_range("edition: 2021..future")
);
}

#[test]
#[should_panic]
fn test_parse_edition_range_empty() {
parse_edition_range("edition:");
}

#[test]
#[should_panic]
fn test_parse_edition_range_invalid_edition() {
parse_edition_range("edition: hello");
}

#[test]
#[should_panic]
fn test_parse_edition_range_double_dots() {
parse_edition_range("edition: ..");
}

#[test]
#[should_panic]
fn test_parse_edition_range_inverted_range() {
parse_edition_range("edition: 2021..2015");
}

#[test]
#[should_panic]
fn test_parse_edition_range_inverted_range_future() {
parse_edition_range("edition: future..2015");
}

#[test]
#[should_panic]
fn test_parse_edition_range_empty_range() {
parse_edition_range("edition: 2021..2021");
}

#[track_caller]
fn assert_edition_to_test(
expected: impl Into<Edition>,
range: EditionRange,
default: Option<Edition>,
) {
let mut cfg = cfg();
if let Some(default) = default {
cfg.edition(default);
}
assert_eq!(expected.into(), range.edition_to_test(cfg.build().edition));
}

#[test]
fn test_edition_range_edition_to_test() {
let e2015 = parse_edition("2015");
let e2018 = parse_edition("2018");
let e2021 = parse_edition("2021");
let e2024 = parse_edition("2024");
let efuture = parse_edition("future");

let exact = EditionRange::Exact(2021.into());
assert_edition_to_test(2021, exact, None);
assert_edition_to_test(2021, exact, Some(e2018));
assert_edition_to_test(2021, exact, Some(efuture));

assert_edition_to_test(Edition::Future, EditionRange::Exact(Edition::Future), None);

let greater_equal_than = EditionRange::RangeFrom(2021.into());
assert_edition_to_test(2021, greater_equal_than, None);
assert_edition_to_test(2021, greater_equal_than, Some(e2015));
assert_edition_to_test(2021, greater_equal_than, Some(e2018));
assert_edition_to_test(2021, greater_equal_than, Some(e2021));
assert_edition_to_test(2024, greater_equal_than, Some(e2024));
assert_edition_to_test(Edition::Future, greater_equal_than, Some(efuture));

let range = EditionRange::Range { lower_bound: 2018.into(), upper_bound: 2024.into() };
assert_edition_to_test(2018, range, None);
assert_edition_to_test(2018, range, Some(e2015));
assert_edition_to_test(2018, range, Some(e2018));
assert_edition_to_test(2021, range, Some(e2021));
assert_edition_to_test(2018, range, Some(e2024));
assert_edition_to_test(2018, range, Some(efuture));
}
35 changes: 35 additions & 0 deletions src/tools/compiletest/src/edition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use crate::fatal;

#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Edition {
// Note that the ordering here is load-bearing, as we want the future edition to be greater than
// any year-based edition.
Year(u32),
Future,
}

impl std::fmt::Display for Edition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Edition::Year(year) => write!(f, "{year}"),
Edition::Future => f.write_str("future"),
}
}
}

impl From<u32> for Edition {
fn from(value: u32) -> Self {
Edition::Year(value)
}
}

pub fn parse_edition(mut input: &str) -> Edition {
input = input.trim();
if input == "future" {
Edition::Future
} else {
Edition::Year(input.parse().unwrap_or_else(|_| {
fatal!("`{input}` doesn't look like an edition");
}))
}
}
Loading
Loading