mirror of https://github.com/rust-lang/cargo
243 lines
8.9 KiB
Rust
243 lines
8.9 KiB
Rust
extern crate proc_macro;
|
|
|
|
use proc_macro::*;
|
|
use std::process::Command;
|
|
use std::sync::Once;
|
|
|
|
#[proc_macro_attribute]
|
|
pub fn cargo_test(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
// Ideally these options would be embedded in the test itself. However, I
|
|
// find it very helpful to have the test clearly state whether or not it
|
|
// is ignored. It would be nice to have some kind of runtime ignore
|
|
// support (such as
|
|
// https://internals.rust-lang.org/t/pre-rfc-skippable-tests/14611).
|
|
//
|
|
// Unfortunately a big drawback here is that if the environment changes
|
|
// (such as the existence of the `git` CLI), this will not trigger a
|
|
// rebuild and the test will still be ignored. In theory, something like
|
|
// `tracked_env` or `tracked_path`
|
|
// (https://github.com/rust-lang/rust/issues/99515) could help with this,
|
|
// but they don't really handle the absence of files well.
|
|
let mut ignore = false;
|
|
let mut requires_reason = false;
|
|
let mut explicit_reason = None;
|
|
let mut implicit_reasons = Vec::new();
|
|
macro_rules! set_ignore {
|
|
($predicate:expr, $($arg:tt)*) => {
|
|
let p = $predicate;
|
|
ignore |= p;
|
|
if p {
|
|
implicit_reasons.push(std::fmt::format(format_args!($($arg)*)));
|
|
}
|
|
};
|
|
}
|
|
let is_not_nightly = !version().1;
|
|
for rule in split_rules(attr) {
|
|
match rule.as_str() {
|
|
"build_std_real" => {
|
|
// Only run the "real" build-std tests on nightly and with an
|
|
// explicit opt-in (these generally only work on linux, and
|
|
// have some extra requirements, and are slow, and can pollute
|
|
// the environment since it downloads dependencies).
|
|
set_ignore!(is_not_nightly, "requires nightly");
|
|
set_ignore!(
|
|
option_env!("CARGO_RUN_BUILD_STD_TESTS").is_none(),
|
|
"CARGO_RUN_BUILD_STD_TESTS must be set"
|
|
);
|
|
}
|
|
"build_std_mock" => {
|
|
// Only run the "mock" build-std tests on nightly and disable
|
|
// for windows-gnu which is missing object files (see
|
|
// https://github.com/rust-lang/wg-cargo-std-aware/issues/46).
|
|
set_ignore!(is_not_nightly, "requires nightly");
|
|
set_ignore!(
|
|
cfg!(all(target_os = "windows", target_env = "gnu")),
|
|
"does not work on windows-gnu"
|
|
);
|
|
}
|
|
"container_test" => {
|
|
// These tests must be opt-in because they require docker.
|
|
set_ignore!(
|
|
option_env!("CARGO_CONTAINER_TESTS").is_none(),
|
|
"CARGO_CONTAINER_TESTS must be set"
|
|
);
|
|
}
|
|
"public_network_test" => {
|
|
// These tests must be opt-in because they touch the public
|
|
// network. The use of these should be **EXTREMELY RARE**, and
|
|
// should only touch things which would nearly certainly work
|
|
// in CI (like github.com).
|
|
set_ignore!(
|
|
option_env!("CARGO_PUBLIC_NETWORK_TESTS").is_none(),
|
|
"CARGO_PUBLIC_NETWORK_TESTS must be set"
|
|
);
|
|
}
|
|
"nightly" => {
|
|
requires_reason = true;
|
|
set_ignore!(is_not_nightly, "requires nightly");
|
|
}
|
|
s if s.starts_with("requires_") => {
|
|
let command = &s[9..];
|
|
set_ignore!(!has_command(command), "{command} not installed");
|
|
}
|
|
s if s.starts_with(">=1.") => {
|
|
requires_reason = true;
|
|
let min_minor = s[4..].parse().unwrap();
|
|
let minor = version().0;
|
|
set_ignore!(minor < min_minor, "requires rustc 1.{minor} or newer");
|
|
}
|
|
s if s.starts_with("reason=") => {
|
|
explicit_reason = Some(s[7..].parse().unwrap());
|
|
}
|
|
_ => panic!("unknown rule {:?}", rule),
|
|
}
|
|
}
|
|
if requires_reason && explicit_reason.is_none() {
|
|
panic!(
|
|
"#[cargo_test] with a rule also requires a reason, \
|
|
such as #[cargo_test(nightly, reason = \"needs -Z unstable-thing\")]"
|
|
);
|
|
}
|
|
|
|
// Construct the appropriate attributes.
|
|
let span = Span::call_site();
|
|
let mut ret = TokenStream::new();
|
|
let add_attr = |ret: &mut TokenStream, attr_name, attr_input| {
|
|
ret.extend(Some(TokenTree::from(Punct::new('#', Spacing::Alone))));
|
|
let attr = TokenTree::from(Ident::new(attr_name, span));
|
|
let mut attr_stream: TokenStream = attr.into();
|
|
if let Some(input) = attr_input {
|
|
attr_stream.extend(input);
|
|
}
|
|
ret.extend(Some(TokenTree::from(Group::new(
|
|
Delimiter::Bracket,
|
|
attr_stream,
|
|
))));
|
|
};
|
|
add_attr(&mut ret, "test", None);
|
|
if ignore {
|
|
let reason = explicit_reason
|
|
.or_else(|| {
|
|
(!implicit_reasons.is_empty())
|
|
.then(|| TokenTree::from(Literal::string(&implicit_reasons.join(", "))).into())
|
|
})
|
|
.map(|reason: TokenStream| {
|
|
let mut stream = TokenStream::new();
|
|
stream.extend(Some(TokenTree::from(Punct::new('=', Spacing::Alone))));
|
|
stream.extend(Some(reason));
|
|
stream
|
|
});
|
|
add_attr(&mut ret, "ignore", reason);
|
|
}
|
|
|
|
// Find where the function body starts, and add the boilerplate at the start.
|
|
for token in item {
|
|
let group = match token {
|
|
TokenTree::Group(g) => {
|
|
if g.delimiter() == Delimiter::Brace {
|
|
g
|
|
} else {
|
|
ret.extend(Some(TokenTree::Group(g)));
|
|
continue;
|
|
}
|
|
}
|
|
other => {
|
|
ret.extend(Some(other));
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let mut new_body = to_token_stream(
|
|
r#"let _test_guard = {
|
|
let tmp_dir = option_env!("CARGO_TARGET_TMPDIR");
|
|
cargo_test_support::paths::init_root(tmp_dir)
|
|
};"#,
|
|
);
|
|
|
|
new_body.extend(group.stream());
|
|
ret.extend(Some(TokenTree::from(Group::new(
|
|
group.delimiter(),
|
|
new_body,
|
|
))));
|
|
}
|
|
|
|
ret
|
|
}
|
|
|
|
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 = option_env!("CARGO_TEST_DISABLE_NIGHTLY").is_none()
|
|
&& (vers.contains("-nightly") || vers.contains("-dev"));
|
|
let minor = vers.split('.').skip(1).next().unwrap().parse().unwrap();
|
|
unsafe { VERSION = (minor, is_nightly) }
|
|
});
|
|
unsafe { &VERSION }
|
|
}
|
|
|
|
fn has_command(command: &str) -> bool {
|
|
let output = match Command::new(command).arg("--version").output() {
|
|
Ok(output) => output,
|
|
Err(e) => {
|
|
// hg is not installed on GitHub macOS or certain constrained
|
|
// environments like Docker. Consider installing it if Cargo gains
|
|
// more hg support, but otherwise it isn't critical.
|
|
if is_ci() && command != "hg" {
|
|
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 {
|
|
// Consider using `tracked_env` instead of option_env! when it is stabilized.
|
|
// `tracked_env` will handle changes, but not require rebuilding the macro
|
|
// itself like option_env does.
|
|
option_env!("CI").is_some() || option_env!("TF_BUILD").is_some()
|
|
}
|