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).

TODO:
- More comments in `testutil`?
- Make `testutil::PinnedVersions` private?
  • Loading branch information
joshlf committed Sep 21, 2023
1 parent cc7a0eb commit 49d087f
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 17 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
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
29 changes: 21 additions & 8 deletions tests/trybuild.rs
Expand Up @@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use testutil::ToolchainVersion;

// 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
Expand All @@ -14,19 +16,27 @@
// `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
fn get_source_files_dir(version: &ToolchainVersion) -> &'static str {
if matches!(version, 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");
}

#[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";
match version {
ToolchainVersion::PinnedMsrv => "tests/ui-msrv",
ToolchainVersion::PinnedStable | ToolchainVersion::OtherStable => "tests/ui-stable",
ToolchainVersion::PinnedNightly | ToolchainVersion::OtherNightly => "tests/ui-nightly",
}
}

#[test]
#[cfg_attr(miri, ignore)]
fn ui() {
let version = testutil::ToolchainVersion::extract_from_pwd().unwrap();
let source_files_dir = get_source_files_dir(&version);

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

// The file `invalid-impls.rs` directly includes `src/macros.rs` in order to
Expand All @@ -40,6 +50,9 @@ fn ui() {
#[test]
#[cfg_attr(miri, ignore)]
fn ui_invalid_impls() {
let version = testutil::ToolchainVersion::extract_from_pwd().unwrap();
let source_files_dir = get_source_files_dir(&version);

let t = trybuild::TestCases::new();
t.compile_fail(format!("{SOURCE_FILES_DIR}/invalid-impls/*.rs"));
t.compile_fail(format!("{source_files_dir}/invalid-impls/*.rs"));
}
15 changes: 15 additions & 0 deletions testutil/Cargo.toml
@@ -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"] }
126 changes: 126 additions & 0 deletions testutil/src/lib.rs
@@ -0,0 +1,126 @@
// 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};

#[derive(Debug)]
pub struct PinnedVersions {
pub msrv: String,
pub stable: String,
pub 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.
pub fn extract_from_pwd() -> Result<PinnedVersions, Box<dyn std::error::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 {
pub fn extract_from_pwd() -> Result<ToolchainVersion, Box<dyn std::error::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())
}
})
}
}

// rust-version = "1.61.0"

// [package.metadata.ci]
// # The versions of the stable and nightly compiler toolchains to use in CI.
// pinned-stable = "1.69.0"
// pinned-nightly = "nightly-2023-05-25"
4 changes: 2 additions & 2 deletions zerocopy-derive/Cargo.toml
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"] }
24 changes: 17 additions & 7 deletions zerocopy-derive/tests/trybuild.rs
Expand Up @@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use testutil::ToolchainVersion;

// 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
Expand All @@ -14,17 +16,25 @@
// `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
fn get_source_files_dir(version: &ToolchainVersion) -> &'static str {
if matches!(version, 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");
}

#[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";
match version {
ToolchainVersion::PinnedMsrv => "tests/ui-msrv",
ToolchainVersion::PinnedStable | ToolchainVersion::OtherStable => "tests/ui-stable",
ToolchainVersion::PinnedNightly | ToolchainVersion::OtherNightly => "tests/ui-nightly",
}
}

#[test]
#[cfg_attr(miri, ignore)]
fn ui() {
let version = testutil::ToolchainVersion::extract_from_pwd().unwrap();
let source_files_dir = get_source_files_dir(&version);

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

0 comments on commit 49d087f

Please sign in to comment.