|
1 | 1 | extern crate proc_macro;
|
2 | 2 |
|
3 | 3 | use proc_macro::*;
|
| 4 | +use std::env; |
| 5 | +use std::process::Command; |
| 6 | +use std::sync::Once; |
4 | 7 |
|
5 | 8 | #[proc_macro_attribute]
|
6 | 9 | pub fn cargo_test(attr: TokenStream, item: TokenStream) -> TokenStream {
|
| 10 | + let mut build_std = false; |
| 11 | + let mut ignore = false; |
| 12 | + let mut requires_reason = false; |
| 13 | + let mut found_reason = false; |
| 14 | + for rule in split_rules(attr) { |
| 15 | + match rule.as_str() { |
| 16 | + "build_std" => build_std = true, |
| 17 | + "nightly" => { |
| 18 | + requires_reason = true; |
| 19 | + ignore |= !version().1; |
| 20 | + } |
| 21 | + "disable_git_cli" => { |
| 22 | + ignore |= disable_git_cli(); |
| 23 | + } |
| 24 | + s if s.starts_with("requires_") => { |
| 25 | + let command = &s[9..]; |
| 26 | + ignore |= !has_command(command); |
| 27 | + } |
| 28 | + s if s.starts_with(">=1.") => { |
| 29 | + requires_reason = true; |
| 30 | + let min_minor = s[4..].parse().unwrap(); |
| 31 | + ignore |= version().0 < min_minor; |
| 32 | + } |
| 33 | + s if s.starts_with("reason=") => { |
| 34 | + found_reason = true; |
| 35 | + } |
| 36 | + _ => panic!("unknown rule {:?}", rule), |
| 37 | + } |
| 38 | + } |
| 39 | + if requires_reason && !found_reason { |
| 40 | + panic!( |
| 41 | + "#[cargo_test] with a rule also requires a reason, \ |
| 42 | + such as #[cargo_test(nightly, reason = \"needs -Z unstable-thing\")]" |
| 43 | + ); |
| 44 | + } |
| 45 | + |
7 | 46 | let span = Span::call_site();
|
8 | 47 | 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"); |
| 48 | + let add_attr = |ret: &mut TokenStream, attr_name| { |
| 49 | + ret.extend(Some(TokenTree::from(Punct::new('#', Spacing::Alone)))); |
| 50 | + let attr = TokenTree::from(Ident::new(attr_name, span)); |
| 51 | + ret.extend(Some(TokenTree::from(Group::new( |
| 52 | + Delimiter::Bracket, |
| 53 | + attr.into(), |
| 54 | + )))); |
| 55 | + }; |
| 56 | + add_attr(&mut ret, "test"); |
| 57 | + if ignore { |
| 58 | + add_attr(&mut ret, "ignore"); |
| 59 | + } |
17 | 60 |
|
18 | 61 | for token in item {
|
19 | 62 | let group = match token {
|
@@ -59,13 +102,80 @@ pub fn cargo_test(attr: TokenStream, item: TokenStream) -> TokenStream {
|
59 | 102 | ret
|
60 | 103 | }
|
61 | 104 |
|
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, |
| 105 | +fn split_rules(t: TokenStream) -> Vec<String> { |
| 106 | + let tts: Vec<_> = t.into_iter().collect(); |
| 107 | + tts.split(|tt| match tt { |
| 108 | + TokenTree::Punct(p) => p.as_char() == ',', |
65 | 109 | _ => false,
|
66 | 110 | })
|
| 111 | + .filter(|parts| !parts.is_empty()) |
| 112 | + .map(|parts| { |
| 113 | + parts |
| 114 | + .into_iter() |
| 115 | + .map(|part| part.to_string()) |
| 116 | + .collect::<String>() |
| 117 | + }) |
| 118 | + .collect() |
67 | 119 | }
|
68 | 120 |
|
69 | 121 | fn to_token_stream(code: &str) -> TokenStream {
|
70 | 122 | code.parse().unwrap()
|
71 | 123 | }
|
| 124 | + |
| 125 | +static mut VERSION: (u32, bool) = (0, false); |
| 126 | + |
| 127 | +fn version() -> &'static (u32, bool) { |
| 128 | + static INIT: Once = Once::new(); |
| 129 | + INIT.call_once(|| { |
| 130 | + let output = Command::new("rustc") |
| 131 | + .arg("-V") |
| 132 | + .output() |
| 133 | + .expect("rustc should run"); |
| 134 | + let stdout = std::str::from_utf8(&output.stdout).expect("utf8"); |
| 135 | + let vers = stdout.split_whitespace().skip(1).next().unwrap(); |
| 136 | + let is_nightly = env::var("CARGO_TEST_DISABLE_NIGHTLY").is_err() |
| 137 | + && (vers.contains("-nightly") || vers.contains("-dev")); |
| 138 | + let minor = vers.split('.').skip(1).next().unwrap().parse().unwrap(); |
| 139 | + unsafe { VERSION = (minor, is_nightly) } |
| 140 | + }); |
| 141 | + unsafe { &VERSION } |
| 142 | +} |
| 143 | + |
| 144 | +fn disable_git_cli() -> bool { |
| 145 | + // mingw git on Windows does not support Windows-style file URIs. |
| 146 | + // Appveyor in the rust repo has that git up front in the PATH instead |
| 147 | + // of Git-for-Windows, which causes this to fail. |
| 148 | + env::var("CARGO_TEST_DISABLE_GIT_CLI") == Ok("1".to_string()) |
| 149 | +} |
| 150 | + |
| 151 | +fn has_command(command: &str) -> bool { |
| 152 | + let output = match Command::new(command).arg("--version").output() { |
| 153 | + Ok(output) => output, |
| 154 | + Err(e) => { |
| 155 | + if is_ci() { |
| 156 | + panic!( |
| 157 | + "expected command `{}` to be somewhere in PATH: {}", |
| 158 | + command, e |
| 159 | + ); |
| 160 | + } |
| 161 | + return false; |
| 162 | + } |
| 163 | + }; |
| 164 | + if !output.status.success() { |
| 165 | + panic!( |
| 166 | + "expected command `{}` to be runnable, got error {}:\n\ |
| 167 | + stderr:{}\n\ |
| 168 | + stdout:{}\n", |
| 169 | + command, |
| 170 | + output.status, |
| 171 | + String::from_utf8_lossy(&output.stderr), |
| 172 | + String::from_utf8_lossy(&output.stdout) |
| 173 | + ); |
| 174 | + } |
| 175 | + true |
| 176 | +} |
| 177 | + |
| 178 | +/// Whether or not this running in a Continuous Integration environment. |
| 179 | +fn is_ci() -> bool { |
| 180 | + std::env::var("CI").is_ok() || std::env::var("TF_BUILD").is_ok() |
| 181 | +} |
0 commit comments