Skip to content

Commit

Permalink
[testutil] Initial commit
Browse files Browse the repository at this point in the history
The `testutil` crate currently supports extracting pinned toolchain
versions and comparing these to the toolchain version used to run the
test. This is used to replace `#[rustversion::*]` attributes. While it
adds more code, it also means that, in order to roll pinned toolchain
versions, only the root `Cargo.toml` needs to be updated (previously, it
was also necessary to update `tests/trybuild.rs` and
`zerocopy-derive/tests/trybuild.rs`). This paves the way to automate the
rolling of pinned toolchain versions (see #348).
  • Loading branch information
joshlf committed Sep 21, 2023
1 parent cc7a0eb commit 7add34f
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 44 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ itertools = "0.11"
rand = { version = "0.8.5", features = ["small_rng"] }
rustversion = "1.0"
static_assertions = "1.1"
testutil = { path = "testutil" }
# Pinned to a specific version so that the version used for local development
# and the version used in CI are guaranteed to be the same. Future versions
# sometimes change the output format slightly, so a version mismatch can cause
Expand Down
34 changes: 13 additions & 21 deletions tests/trybuild.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,18 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// UI tests depend on the exact error messages emitted by rustc, but those error
// messages are not stable, and sometimes change between Rust versions. Thus, we
// maintain one set of UI tests for each Rust version that we test in CI, and we
// pin to specific versions in CI (a specific stable version, a specific date of
// the nightly compiler, and a specific MSRV). Updating those pinned versions
// may also require updating these tests.
// - `tests/ui-nightly` - Contains the source of truth for our UI test source
// files (`.rs`), and contains `.err` and `.out` files for nightly
// - `tests/ui-stable` - Contains symlinks to the `.rs` files in
// `tests/ui-nightly`, and contains `.err` and `.out` files for stable
// - `tests/ui-msrv` - Contains symlinks to the `.rs` files in
// `tests/ui-nightly`, and contains `.err` and `.out` files for MSRV

#[rustversion::nightly]
const SOURCE_FILES_DIR: &str = "tests/ui-nightly";
#[rustversion::stable(1.69.0)]
const SOURCE_FILES_DIR: &str = "tests/ui-stable";
#[rustversion::stable(1.61.0)]
const SOURCE_FILES_DIR: &str = "tests/ui-msrv";
use testutil::ToolchainVersion;

#[test]
#[cfg_attr(miri, ignore)]
fn ui() {
let version = ToolchainVersion::extract_from_pwd().unwrap();
// See the doc comment on this method for an explanation of what this does
// and why we store source files in different directories.
let source_files_dirname = version.get_ui_source_files_dirname_and_maybe_print_warning();

let t = trybuild::TestCases::new();
t.compile_fail(format!("{SOURCE_FILES_DIR}/*.rs"));
t.compile_fail(format!("tests/{source_files_dirname}/*.rs"));
}

// The file `invalid-impls.rs` directly includes `src/macros.rs` in order to
Expand All @@ -40,6 +27,11 @@ fn ui() {
#[test]
#[cfg_attr(miri, ignore)]
fn ui_invalid_impls() {
let version = ToolchainVersion::extract_from_pwd().unwrap();
// See the doc comment on this method for an explanation of what this does
// and why we store source files in different directories.
let source_files_dirname = version.get_ui_source_files_dirname_and_maybe_print_warning();

let t = trybuild::TestCases::new();
t.compile_fail(format!("{SOURCE_FILES_DIR}/invalid-impls/*.rs"));
t.compile_fail(format!("tests/{source_files_dirname}/invalid-impls/*.rs"));
}
15 changes: 15 additions & 0 deletions testutil/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2023 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

[package]
name = "testutil"
version = "0.0.0"
edition = "2021"

[dependencies]
cargo_metadata = "0.18.0"
rustc_version = "0.4.0"
# Pin to 0.3.0 because more recent versions require a Rust version more recent
# than our MSRV.
time = { version = "=0.3.0", default-features = false, features = ["formatting", "macros", "parsing"] }
155 changes: 155 additions & 0 deletions testutil/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use cargo_metadata::MetadataCommand;
use rustc_version::{Channel, Version};
use std::error::Error;

struct PinnedVersions {
msrv: String,
stable: String,
nightly: String,
}

impl PinnedVersions {
/// Attempts to extract pinned toolchain versions based on the current
/// working directory.
///
/// `extract_from_pwd` expects to be called from a directory which is a
/// child of a Cargo workspace. It extracts the pinned versions from the
/// metadata of the root package.
fn extract_from_pwd() -> Result<PinnedVersions, Box<dyn Error>> {
let meta = MetadataCommand::new().exec()?;
// NOTE(joshlf): In theory `meta.root_package()` should work instead of
// this manual search, but for some reason it breaks when called from
// zerocopy-derive's tests. This works as a workaround, and it's just
// test code, so I didn't bother investigating.
let pkg = meta
.workspace_packages()
.into_iter()
.find(|pkg| pkg.name == "zerocopy")
.ok_or("no `zerocopy` package found; are we in a workspace?")?;
let msrv = pkg
.rust_version
.as_ref()
.ok_or("failed to find msrv: no `rust-version` key present")?
.to_string();
let extract = |version_name, key| -> Result<String, String> {
let value = pkg.metadata.pointer(&format!("/ci/{key}")).ok_or_else(|| {
format!("failed to find {version_name}: no `metadata.ci.{key}` key present")
})?;
value.as_str().map(str::to_string).ok_or_else(|| format!("failed to find {version_name}: key `metadata.ci.{key}` (contents: {value:?}) failed to parse as JSON string"))
};
let stable = extract("stable", "pinned-stable")?;
let nightly = extract("nightly", "pinned-nightly")?;
Ok(PinnedVersions { msrv, stable, nightly })
}
}

#[derive(Debug)]
pub enum ToolchainVersion {
/// The version listed as our MSRV (ie, the `package.rust-version` key in
/// `Cargo.toml`).
PinnedMsrv,
/// The stable version pinned in CI.
PinnedStable,
/// The nightly version pinned in CI
PinnedNightly,
/// A stable version other than the one pinned in CI.
OtherStable,
/// A nightly version other than the one pinned in CI.
OtherNightly,
}

impl ToolchainVersion {
/// Attempts to determine whether the current toolchain version matches one
/// of the versions pinned in CI and if so, which one.
pub fn extract_from_pwd() -> Result<ToolchainVersion, Box<dyn Error>> {
let pinned_versions = PinnedVersions::extract_from_pwd()?;
let current = rustc_version::version_meta()?;

let s = match current.channel {
Channel::Dev | Channel::Beta => {
return Err(format!("unsupported channel: {:?}", current.channel).into())
}
Channel::Nightly => {
format!(
"nightly-{}",
current.commit_date.as_ref().ok_or("nightly channel missing commit date")?
)
}
Channel::Stable => {
let Version { major, minor, patch, .. } = current.semver;
format!("{major}.{minor}.{patch}")
}
};

// Due to a quirk of how Rust nightly versions are encoded and published
// [1], the version as understood by rustup uses a date one day ahead of
// the version as encoded in the `rustc` binary itself.
// `pinned_versions` encodes the former notion of the date (as it is
// meant to be passed as the `+<toolchain>` selector syntax understood
// by rustup), while `current` encodes the latter notion of the date (as
// it is extracted from `rustc`). Without this adjustment, toolchain
// versions that should be considered equal would not be.
//
// [1] https://github.com/rust-lang/rust/issues/51533
let pinned_nightly_adjusted = {
let desc = time::macros::format_description!("nightly-[year]-[month]-[day]");
let date = time::Date::parse(&pinned_versions.nightly, &desc).map_err(|_| {
format!("failed to parse nightly version: {}", pinned_versions.nightly)
})?;
let adjusted = date - time::Duration::DAY;
adjusted.format(&desc).unwrap()
};

Ok(match s {
s if s == pinned_versions.msrv => ToolchainVersion::PinnedMsrv,
s if s == pinned_versions.stable => ToolchainVersion::PinnedStable,
s if s == pinned_nightly_adjusted => ToolchainVersion::PinnedNightly,
_ if current.channel == Channel::Stable => ToolchainVersion::OtherStable,
_ if current.channel == Channel::Nightly => ToolchainVersion::OtherNightly,
_ => {
return Err(format!(
"current toolchain ({current:?}) doesn't match any known version"
)
.into())
}
})
}

/// Gets the name of the directory in which to store source files and
/// expected output for UI tests for this toolchain version.
///
/// For toolchain versions which are not pinned in CI, prints a warning to
/// `stderr` which will be captured by the test harness and only printed on
/// test failure.
///
/// UI tests depend on the exact error messages emitted by rustc, but those
/// error messages are not stable, and sometimes change between Rust
/// versions. Thus, we maintain one set of UI tests for each Rust version
/// that we test in CI, and we pin to specific versions in CI (a specific
/// MSRV, a specific stable version, and a specific date of the nightly
/// compiler). Updating those pinned versions may also require updating
/// these tests.
/// - `tests/ui-nightly` - Contains the source of truth for our UI test
/// source files (`.rs`), and contains `.err` and `.out` files for nightly
/// - `tests/ui-stable` - Contains symlinks to the `.rs` files in
/// `tests/ui-nightly`, and contains `.err` and `.out` files for stable
/// - `tests/ui-msrv` - Contains symlinks to the `.rs` files in
/// `tests/ui-nightly`, and contains `.err` and `.out` files for MSRV
pub fn get_ui_source_files_dirname_and_maybe_print_warning(&self) -> &'static str {
if matches!(self, ToolchainVersion::OtherStable | ToolchainVersion::OtherNightly) {
// This will be eaten by the test harness and only displayed on
// failure.
eprintln!("warning: current toolchain does not match any toolchain pinned in CI; this may cause spurious test failure");
}

match self {
ToolchainVersion::PinnedMsrv => "ui-msrv",
ToolchainVersion::PinnedStable | ToolchainVersion::OtherStable => "ui-stable",
ToolchainVersion::PinnedNightly | ToolchainVersion::OtherNightly => "ui-nightly",
}
}
}
4 changes: 2 additions & 2 deletions zerocopy-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ quote = "1.0.10"
syn = "2.0.31"

[dev-dependencies]
rustversion = "1.0"
static_assertions = "1.1"
testutil = { path = "../testutil" }
# Pinned to a specific version so that the version used for local development
# and the version used in CI are guaranteed to be the same. Future versions
# sometimes change the output format slightly, so a version mismatch can cause
# CI test failures.
trybuild = "=1.0.80"
zerocopy = { path = "../", features = ["default", "derive"] }
zerocopy = { path = "../", features = ["default", "derive"] }
27 changes: 6 additions & 21 deletions zerocopy-derive/tests/trybuild.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// UI tests depend on the exact error messages emitted by rustc, but those error
// messages are not stable, and sometimes change between Rust versions. Thus, we
// maintain one set of UI tests for each Rust version that we test in CI, and we
// pin to specific versions in CI (a specific stable version, a specific date of
// the nightly compiler, and a specific MSRV). Updating those pinned versions
// may also require updating these tests.
// - `tests/ui-nightly` - Contains the source of truth for our UI test source
// files (`.rs`), and contains `.err` and `.out` files for nightly
// - `tests/ui-stable` - Contains symlinks to the `.rs` files in
// `tests/ui-nightly`, and contains `.err` and `.out` files for stable
// - `tests/ui-msrv` - Contains symlinks to the `.rs` files in
// `tests/ui-nightly`, and contains `.err` and `.out` files for MSRV

#[rustversion::nightly]
const SOURCE_FILES_GLOB: &str = "tests/ui-nightly/*.rs";
#[rustversion::stable(1.69.0)]
const SOURCE_FILES_GLOB: &str = "tests/ui-stable/*.rs";
#[rustversion::stable(1.61.0)]
const SOURCE_FILES_GLOB: &str = "tests/ui-msrv/*.rs";

#[test]
#[cfg_attr(miri, ignore)]
fn ui() {
let version = testutil::ToolchainVersion::extract_from_pwd().unwrap();
// See the doc comment on this method for an explanation of what this does
// and why we store source files in different directories.
let source_files_dirname = version.get_ui_source_files_dirname_and_maybe_print_warning();

let t = trybuild::TestCases::new();
t.compile_fail(SOURCE_FILES_GLOB);
t.compile_fail(format!("tests/{source_files_dirname}/*.rs"));
}

0 comments on commit 7add34f

Please sign in to comment.