Skip to content

Commit aebea40

Browse files
committed
Add requirements to cargo_test.
1 parent 281989f commit aebea40

File tree

29 files changed

+427
-878
lines changed

29 files changed

+427
-878
lines changed

crates/cargo-test-macro/src/lib.rs

Lines changed: 152 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,87 @@
11
extern crate proc_macro;
22

33
use proc_macro::*;
4+
use std::process::Command;
5+
use std::sync::Once;
46

57
#[proc_macro_attribute]
68
pub fn cargo_test(attr: TokenStream, item: TokenStream) -> TokenStream {
9+
// Ideally these options would be embedded in the test itself. However, I
10+
// find it very helpful to have the test clearly state whether or not it
11+
// is ignored. It would be nice to have some kind of runtime ignore
12+
// support (such as
13+
// https://internals.rust-lang.org/t/pre-rfc-skippable-tests/14611).
14+
//
15+
// Unfortunately a big drawback here is that if the environment changes
16+
// (such as the existence of the `git` CLI), this will not trigger a
17+
// rebuild and the test will still be ignored. In theory, something like
18+
// `tracked_env` or `tracked_path`
19+
// (https://github.com/rust-lang/rust/issues/99515) could help with this,
20+
// but they don't really handle the absence of files well.
21+
let mut ignore = false;
22+
let mut requires_reason = false;
23+
let mut found_reason = false;
24+
let is_not_nightly = || !version().1;
25+
for rule in split_rules(attr) {
26+
match rule.as_str() {
27+
"build_std_real" => {
28+
// Only run the "real" build-std tests on nightly and with an
29+
// explicit opt-in (these generally only work on linux, and
30+
// have some extra requirements, and are slow, and can pollute
31+
// the environment since it downloads dependencies).
32+
ignore |= is_not_nightly();
33+
ignore |= option_env!("CARGO_RUN_BUILD_STD_TESTS").is_none();
34+
}
35+
"build_std_mock" => {
36+
// Only run the "mock" build-std tests on nightly and disable
37+
// for windows-gnu which is missing object files (see
38+
// https://github.com/rust-lang/wg-cargo-std-aware/issues/46).
39+
ignore |= is_not_nightly();
40+
ignore |= cfg!(all(target_os = "windows", target_env = "gnu"));
41+
}
42+
"nightly" => {
43+
requires_reason = true;
44+
ignore |= is_not_nightly();
45+
}
46+
"disable_git_cli" => {
47+
ignore |= disable_git_cli();
48+
}
49+
s if s.starts_with("requires_") => {
50+
let command = &s[9..];
51+
ignore |= !has_command(command);
52+
}
53+
s if s.starts_with(">=1.") => {
54+
requires_reason = true;
55+
let min_minor = s[4..].parse().unwrap();
56+
ignore |= version().0 < min_minor;
57+
}
58+
s if s.starts_with("reason=") => {
59+
found_reason = true;
60+
}
61+
_ => panic!("unknown rule {:?}", rule),
62+
}
63+
}
64+
if requires_reason && !found_reason {
65+
panic!(
66+
"#[cargo_test] with a rule also requires a reason, \
67+
such as #[cargo_test(nightly, reason = \"needs -Z unstable-thing\")]"
68+
);
69+
}
70+
771
let span = Span::call_site();
872
let mut ret = TokenStream::new();
9-
ret.extend(Some(TokenTree::from(Punct::new('#', Spacing::Alone))));
10-
let test = TokenTree::from(Ident::new("test", span));
11-
ret.extend(Some(TokenTree::from(Group::new(
12-
Delimiter::Bracket,
13-
test.into(),
14-
))));
15-
16-
let build_std = contains_ident(&attr, "build_std");
73+
let add_attr = |ret: &mut TokenStream, attr_name| {
74+
ret.extend(Some(TokenTree::from(Punct::new('#', Spacing::Alone))));
75+
let attr = TokenTree::from(Ident::new(attr_name, span));
76+
ret.extend(Some(TokenTree::from(Group::new(
77+
Delimiter::Bracket,
78+
attr.into(),
79+
))));
80+
};
81+
add_attr(&mut ret, "test");
82+
if ignore {
83+
add_attr(&mut ret, "ignore");
84+
}
1785

1886
for token in item {
1987
let group = match token {
@@ -38,17 +106,6 @@ pub fn cargo_test(attr: TokenStream, item: TokenStream) -> TokenStream {
38106
};"#,
39107
);
40108

41-
// If this is a `build_std` test (aka `tests/build-std/*.rs`) then they
42-
// only run on nightly and they only run when specifically instructed to
43-
// on CI.
44-
if build_std {
45-
let ts = to_token_stream("if !cargo_test_support::is_nightly() { return }");
46-
new_body.extend(ts);
47-
let ts = to_token_stream(
48-
"if std::env::var(\"CARGO_RUN_BUILD_STD_TESTS\").is_err() { return }",
49-
);
50-
new_body.extend(ts);
51-
}
52109
new_body.extend(group.stream());
53110
ret.extend(Some(TokenTree::from(Group::new(
54111
group.delimiter(),
@@ -59,13 +116,86 @@ pub fn cargo_test(attr: TokenStream, item: TokenStream) -> TokenStream {
59116
ret
60117
}
61118

62-
fn contains_ident(t: &TokenStream, ident: &str) -> bool {
63-
t.clone().into_iter().any(|t| match t {
64-
TokenTree::Ident(i) => i.to_string() == ident,
119+
fn split_rules(t: TokenStream) -> Vec<String> {
120+
let tts: Vec<_> = t.into_iter().collect();
121+
tts.split(|tt| match tt {
122+
TokenTree::Punct(p) => p.as_char() == ',',
65123
_ => false,
66124
})
125+
.filter(|parts| !parts.is_empty())
126+
.map(|parts| {
127+
parts
128+
.into_iter()
129+
.map(|part| part.to_string())
130+
.collect::<String>()
131+
})
132+
.collect()
67133
}
68134

69135
fn to_token_stream(code: &str) -> TokenStream {
70136
code.parse().unwrap()
71137
}
138+
139+
static mut VERSION: (u32, bool) = (0, false);
140+
141+
fn version() -> &'static (u32, bool) {
142+
static INIT: Once = Once::new();
143+
INIT.call_once(|| {
144+
let output = Command::new("rustc")
145+
.arg("-V")
146+
.output()
147+
.expect("rustc should run");
148+
let stdout = std::str::from_utf8(&output.stdout).expect("utf8");
149+
let vers = stdout.split_whitespace().skip(1).next().unwrap();
150+
let is_nightly = option_env!("CARGO_TEST_DISABLE_NIGHTLY").is_none()
151+
&& (vers.contains("-nightly") || vers.contains("-dev"));
152+
let minor = vers.split('.').skip(1).next().unwrap().parse().unwrap();
153+
unsafe { VERSION = (minor, is_nightly) }
154+
});
155+
unsafe { &VERSION }
156+
}
157+
158+
fn disable_git_cli() -> bool {
159+
// mingw git on Windows does not support Windows-style file URIs.
160+
// Appveyor in the rust repo has that git up front in the PATH instead
161+
// of Git-for-Windows, which causes this to fail.
162+
matches!(option_env!("CARGO_TEST_DISABLE_GIT_CLI"), Some("1"))
163+
}
164+
165+
fn has_command(command: &str) -> bool {
166+
let output = match Command::new(command).arg("--version").output() {
167+
Ok(output) => output,
168+
Err(e) => {
169+
// hg is not installed on GitHub macos.
170+
// Consider installing it if Cargo gains more hg support, but
171+
// otherwise it isn't critical.
172+
if is_ci() && !(cfg!(target_os = "macos") && command == "hg") {
173+
panic!(
174+
"expected command `{}` to be somewhere in PATH: {}",
175+
command, e
176+
);
177+
}
178+
return false;
179+
}
180+
};
181+
if !output.status.success() {
182+
panic!(
183+
"expected command `{}` to be runnable, got error {}:\n\
184+
stderr:{}\n\
185+
stdout:{}\n",
186+
command,
187+
output.status,
188+
String::from_utf8_lossy(&output.stderr),
189+
String::from_utf8_lossy(&output.stdout)
190+
);
191+
}
192+
true
193+
}
194+
195+
/// Whether or not this running in a Continuous Integration environment.
196+
fn is_ci() -> bool {
197+
// Consider using `tracked_env` instead of option_env! when it is stabilized.
198+
// `tracked_env` will handle changes, but not require rebuilding the macro
199+
// itself like option_env does.
200+
option_env!("CI").is_some() || option_env!("TF_BUILD").is_some()
201+
}

crates/cargo-test-support/src/lib.rs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,10 @@ pub fn rustc_host_env() -> String {
11281128

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

1353-
pub fn command_is_available(cmd: &str) -> bool {
1354-
if let Err(e) = process(cmd).arg("-V").exec_with_output() {
1355-
eprintln!("{} not available, skipping tests", cmd);
1356-
eprintln!("{:?}", e);
1357-
false
1358-
} else {
1359-
true
1360-
}
1361-
}
1362-
13631357
#[cfg(windows)]
13641358
pub fn symlink_supported() -> bool {
13651359
if is_ci() {

src/doc/contrib/src/tests/writing.md

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,7 @@ fn <description>() {
5050
}
5151
```
5252

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

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

65+
#### `#[cargo_test]` attribute
66+
67+
The `#[cargo_test]` attribute injects code which does some setup before starting the test.
68+
It will create a filesystem "sandbox" under the "cargo integration test" directory for each test, such as `/path/to/cargo/target/tmp/cit/t123/`.
69+
The sandbox will contain a `home` directory that will be used instead of your normal home directory.
70+
71+
The `#[cargo_test`] attribute takes several options that will affect how the test is generated.
72+
They are listed in parentheses separated with commas, such as:
73+
74+
```rust,ignore
75+
#[cargo_test(nightly, reason = "-Zfoo is unstable")]
76+
```
77+
78+
The options it supports are:
79+
80+
* `nightly` — This will cause the test to be ignored if not running on the nightly toolchain.
81+
This is useful for tests that use unstable options in `rustc` or `rustdoc`.
82+
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.
83+
A `reason` field is required to explain why it is nightly-only.
84+
* `build_std_real` — This is a "real" `-Zbuild-std` test (in the `build_std` integration test).
85+
This only runs on nightly, and only if the environment variable `CARGO_RUN_BUILD_STD_TESTS` is set (these tests on run on Linux).
86+
* `build_std_mock` — This is a "mock" `-Zbuild-std` test (which uses a mock standard library).
87+
This only runs on nightly, and is disabled for windows-gnu.
88+
* `requires_` — This indicates a command that is required to be installed to be run.
89+
For example, `requires_rustmft` means the test will only run if the executable `rustfmt` is installed.
90+
These tests are *always* run on CI.
91+
This is mainly used to avoid requiring contributors from having every dependency installed.
92+
* `>=1.64` — This indicates that the test will only run with the given version of `rustc` or newer.
93+
This can be used when a new `rustc` feature has been stabilized that the test depends on.
94+
If this is specified, a `reason` is required to explain why it is being checked.
95+
* `disable_git_cli` — This is needed for `git-fetch-with-cli` tests.
96+
This disables the test in rust-lang/rust's CI due to a compatibility issue.
97+
7198
#### Testing Nightly Features
7299

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

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

85112
```rust,ignore
86-
if !is_nightly() {
87-
// Add a comment here explaining why this is necessary.
88-
return;
89-
}
113+
#[cargo_test(nightly, reason = "-Zfoo is unstable")]
90114
```
91115

116+
This will cause the test to be ignored if not running on the nightly toolchain.
117+
92118
#### Specifying Dependencies
93119

94120
You should not write any tests that use the network such as contacting
@@ -201,16 +227,15 @@ the name of the feature as the reason, like this:
201227
```
202228

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

207233
```rust,ignore
208-
if !is_nightly() {
209-
// Add a comment here explaining why this is necessary.
210-
return;
211-
}
234+
#[cargo_test(nightly, reason = "-Zfoo is unstable")]
212235
```
213236

237+
This will cause the test to be ignored if not running on the nightly toolchain.
238+
214239
### Platform-specific Notes
215240

216241
When checking output, use `/` for paths even on Windows: the actual output

tests/build-std/main.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
//! not catching any regressions that `tests/testsuite/standard_lib.rs` isn't
1414
//! already catching.
1515
//!
16-
//! All tests here should use `#[cargo_test(build_std)]` to indicate that
16+
//! All tests here should use `#[cargo_test(build_std_real)]` to indicate that
1717
//! boilerplate should be generated to require the nightly toolchain and the
1818
//! `CARGO_RUN_BUILD_STD_TESTS` env var to be set to actually run these tests.
1919
//! Otherwise the tests are skipped.
@@ -59,7 +59,7 @@ impl BuildStd for Execs {
5959
}
6060
}
6161

62-
#[cargo_test(build_std)]
62+
#[cargo_test(build_std_real)]
6363
fn basic() {
6464
let p = project()
6565
.file(
@@ -127,7 +127,7 @@ fn basic() {
127127
assert_eq!(p.glob(deps_dir.join("*.dylib")).count(), 0);
128128
}
129129

130-
#[cargo_test(build_std)]
130+
#[cargo_test(build_std_real)]
131131
fn cross_custom() {
132132
let p = project()
133133
.file(
@@ -170,7 +170,7 @@ fn cross_custom() {
170170
.run();
171171
}
172172

173-
#[cargo_test(build_std)]
173+
#[cargo_test(build_std_real)]
174174
fn custom_test_framework() {
175175
let p = project()
176176
.file(

0 commit comments

Comments
 (0)