Skip to content

Commit

Permalink
Add requirements to cargo_test.
Browse files Browse the repository at this point in the history
  • Loading branch information
ehuss committed Jul 30, 2022
1 parent 281989f commit 4d26ed8
Show file tree
Hide file tree
Showing 27 changed files with 328 additions and 747 deletions.
132 changes: 121 additions & 11 deletions crates/cargo-test-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,62 @@
extern crate proc_macro;

use proc_macro::*;
use std::env;
use std::process::Command;
use std::sync::Once;

#[proc_macro_attribute]
pub fn cargo_test(attr: TokenStream, item: TokenStream) -> TokenStream {
let mut build_std = false;
let mut ignore = false;
let mut requires_reason = false;
let mut found_reason = false;
for rule in split_rules(attr) {
match rule.as_str() {
"build_std" => build_std = true,
"nightly" => {
requires_reason = true;
ignore |= !version().1;
}
"disable_git_cli" => {
ignore |= disable_git_cli();
}
s if s.starts_with("requires_") => {
let command = &s[9..];
ignore |= !has_command(command);
}
s if s.starts_with(">=1.") => {
requires_reason = true;
let min_minor = s[4..].parse().unwrap();
ignore |= version().0 < min_minor;
}
s if s.starts_with("reason=") => {
found_reason = true;
}
_ => panic!("unknown rule {:?}", rule),
}
}
if requires_reason && !found_reason {
panic!(
"#[cargo_test] with a rule also requires a reason, \
such as #[cargo_test(nightly, reason = \"needs -Z unstable-thing\")]"
);
}

let span = Span::call_site();
let mut ret = TokenStream::new();
ret.extend(Some(TokenTree::from(Punct::new('#', Spacing::Alone))));
let test = TokenTree::from(Ident::new("test", span));
ret.extend(Some(TokenTree::from(Group::new(
Delimiter::Bracket,
test.into(),
))));

let build_std = contains_ident(&attr, "build_std");
let add_attr = |ret: &mut TokenStream, attr_name| {
ret.extend(Some(TokenTree::from(Punct::new('#', Spacing::Alone))));
let attr = TokenTree::from(Ident::new(attr_name, span));
ret.extend(Some(TokenTree::from(Group::new(
Delimiter::Bracket,
attr.into(),
))));
};
add_attr(&mut ret, "test");
if ignore {
add_attr(&mut ret, "ignore");
}

for token in item {
let group = match token {
Expand Down Expand Up @@ -59,13 +102,80 @@ pub fn cargo_test(attr: TokenStream, item: TokenStream) -> TokenStream {
ret
}

fn contains_ident(t: &TokenStream, ident: &str) -> bool {
t.clone().into_iter().any(|t| match t {
TokenTree::Ident(i) => i.to_string() == ident,
fn split_rules(t: TokenStream) -> Vec<String> {
let tts: Vec<_> = t.into_iter().collect();
tts.split(|tt| match tt {
TokenTree::Punct(p) => p.as_char() == ',',
_ => false,
})
.filter(|parts| !parts.is_empty())
.map(|parts| {
parts
.into_iter()
.map(|part| part.to_string())
.collect::<String>()
})
.collect()
}

fn to_token_stream(code: &str) -> TokenStream {
code.parse().unwrap()
}

static mut VERSION: (u32, bool) = (0, false);

fn version() -> &'static (u32, bool) {
static INIT: Once = Once::new();
INIT.call_once(|| {
let output = Command::new("rustc")
.arg("-V")
.output()
.expect("rustc should run");
let stdout = std::str::from_utf8(&output.stdout).expect("utf8");
let vers = stdout.split_whitespace().skip(1).next().unwrap();
let is_nightly = env::var("CARGO_TEST_DISABLE_NIGHTLY").is_err()
&& (vers.contains("-nightly") || vers.contains("-dev"));
let minor = vers.split('.').skip(1).next().unwrap().parse().unwrap();
unsafe { VERSION = (minor, is_nightly) }
});
unsafe { &VERSION }
}

fn disable_git_cli() -> bool {
// mingw git on Windows does not support Windows-style file URIs.
// Appveyor in the rust repo has that git up front in the PATH instead
// of Git-for-Windows, which causes this to fail.
env::var("CARGO_TEST_DISABLE_GIT_CLI") == Ok("1".to_string())
}

fn has_command(command: &str) -> bool {
let output = match Command::new(command).arg("--version").output() {
Ok(output) => output,
Err(e) => {
if is_ci() {
panic!(
"expected command `{}` to be somewhere in PATH: {}",
command, e
);
}
return false;
}
};
if !output.status.success() {
panic!(
"expected command `{}` to be runnable, got error {}:\n\
stderr:{}\n\
stdout:{}\n",
command,
output.status,
String::from_utf8_lossy(&output.stderr),
String::from_utf8_lossy(&output.stdout)
);
}
true
}

/// Whether or not this running in a Continuous Integration environment.
fn is_ci() -> bool {
std::env::var("CI").is_ok() || std::env::var("TF_BUILD").is_ok()
}
14 changes: 4 additions & 10 deletions crates/cargo-test-support/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,10 @@ pub fn rustc_host_env() -> String {

pub fn is_nightly() -> bool {
let vv = &RUSTC_INFO.verbose_version;
// CARGO_TEST_DISABLE_NIGHTLY is set in rust-lang/rust's CI so that all
// nightly-only tests are disabled there. Otherwise, it could make it
// difficult to land changes which would need to be made simultaneously in
// rust-lang/cargo and rust-lan/rust, which isn't possible.
env::var("CARGO_TEST_DISABLE_NIGHTLY").is_err()
&& (vv.contains("-nightly") || vv.contains("-dev"))
}
Expand Down Expand Up @@ -1350,16 +1354,6 @@ pub fn slow_cpu_multiplier(main: u64) -> Duration {
Duration::from_secs(*SLOW_CPU_MULTIPLIER * main)
}

pub fn command_is_available(cmd: &str) -> bool {
if let Err(e) = process(cmd).arg("-V").exec_with_output() {
eprintln!("{} not available, skipping tests", cmd);
eprintln!("{:?}", e);
false
} else {
true
}
}

#[cfg(windows)]
pub fn symlink_supported() -> bool {
if is_ci() {
Expand Down
61 changes: 42 additions & 19 deletions src/doc/contrib/src/tests/writing.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,7 @@ fn <description>() {
}
```

`#[cargo_test]`:
- This is used in place of `#[test]`
- This attribute injects code which does some setup before starting the
test, creating a filesystem "sandbox" under the "cargo integration test"
directory for each test such as
`/path/to/cargo/target/cit/t123/`
- The sandbox will contain a `home` directory that will be used instead of your normal home directory
The [`#[cargo_test]` attribute](#cargo_test-attribute) is used in place of `#[test]` to inject some setup code.

[`ProjectBuilder`] via `project()`:
- Each project is in a separate directory in the sandbox
Expand All @@ -68,6 +62,37 @@ fn <description>() {
- See [`support::compare`] for an explanation of the string pattern matching.
Patterns are used to make it easier to match against the expected output.

#### `#[cargo_test]` attribute

The `#[cargo_test]` attribute injects code which does some setup before starting the test.
It will create a filesystem "sandbox" under the "cargo integration test" directory for each test, such as `/path/to/cargo/target/tmp/cit/t123/`.
The sandbox will contain a `home` directory that will be used instead of your normal home directory.

The `#[cargo_test`] attribute takes several options that will affect how the test is generated.
They are listed in parentheses separated with commas, such as:

```rust,ignore
#[cargo_test(nightly, reason = "-Zfoo is unstable")]
```

The options it supports are:

* `nightly` — This will cause the test to be ignored if not running on the nightly toolchain.
This is useful for tests that use unstable options in `rustc` or `rustdoc`.
These tests are run in Cargo's CI, but are disabled in rust-lang/rust's CI due to the difficulty of updating both repos simultaneously.
A `reason` field is required to explain why it is nightly-only.
* `build_std` — This is a `-Zbuild-std` test.
This only runs on nightly, and only if the environment variable `CARGO_RUN_BUILD_STD_TESTS` is set.
* `requires_` — This indicates a command that is required to be installed to be run.
For example, `requires_rustmft` means the test will only run if the executable `rustfmt` is installed.
These tests are *always* run on CI.
This is mainly used to avoid requiring contributors from having every dependency installed.
* `>=1.64` — This indicates that the test will only run with the given version of `rustc` or newer.
This can be used when a new `rustc` feature has been stabilized that the test depends on.
If this is specified, a `reason` is required to explain why it is being checked.
* `disable_git_cli` — This is needed for `git-fetch-with-cli` tests.
This disables the test in rust-lang/rust's CI due to a compatibility issue.

#### Testing Nightly Features

If you are testing a Cargo feature that only works on "nightly" Cargo, then
Expand All @@ -79,16 +104,15 @@ p.cargo("build").masquerade_as_nightly_cargo(&["print-im-a-teapot"])
```

If you are testing a feature that only works on *nightly rustc* (such as
benchmarks), then you should exit the test if it is not running with nightly
rust, like this:
benchmarks), then you should use the `nightly` option of the `cargo_test`
attribute, like this:

```rust,ignore
if !is_nightly() {
// Add a comment here explaining why this is necessary.
return;
}
#[cargo_test(nightly, reason = "-Zfoo is unstable")]
```

This will cause the test to be ignored if not running on the nightly toolchain.

#### Specifying Dependencies

You should not write any tests that use the network such as contacting
Expand Down Expand Up @@ -201,16 +225,15 @@ the name of the feature as the reason, like this:
```

If you are testing a feature that only works on *nightly rustc* (such as
benchmarks), then you should exit the test if it is not running with nightly
rust, like this:
benchmarks), then you should use the `nightly` option of the `cargo_test`
attribute, like this:

```rust,ignore
if !is_nightly() {
// Add a comment here explaining why this is necessary.
return;
}
#[cargo_test(nightly, reason = "-Zfoo is unstable")]
```

This will cause the test to be ignored if not running on the nightly toolchain.

### Platform-specific Notes

When checking output, use `/` for paths even on Windows: the actual output
Expand Down
Loading

0 comments on commit 4d26ed8

Please sign in to comment.