1use proc_macro::*;
11use std::path::Path;
12use std::process::Command;
13use std::sync::LazyLock;
14
15#[proc_macro_attribute]
62pub fn cargo_test(attr: TokenStream, item: TokenStream) -> TokenStream {
63 let mut ignore = false;
76 let mut requires_reason = false;
77 let mut explicit_reason = None;
78 let mut implicit_reasons = Vec::new();
79 macro_rules! set_ignore {
80 ($predicate:expr, $($arg:tt)*) => {
81 let p = $predicate;
82 ignore |= p;
83 if p {
84 implicit_reasons.push(std::fmt::format(format_args!($($arg)*)));
85 }
86 };
87 }
88 let is_not_nightly = !version().1;
89 for rule in split_rules(attr) {
90 match rule.as_str() {
91 "build_std_real" => {
92 set_ignore!(is_not_nightly, "requires nightly");
97 set_ignore!(
98 option_env!("CARGO_RUN_BUILD_STD_TESTS").is_none(),
99 "CARGO_RUN_BUILD_STD_TESTS must be set"
100 );
101 }
102 "build_std_mock" => {
103 set_ignore!(is_not_nightly, "requires nightly");
107 set_ignore!(
108 cfg!(all(target_os = "windows", target_env = "gnu")),
109 "does not work on windows-gnu"
110 );
111 }
112 "container_test" => {
113 set_ignore!(
115 option_env!("CARGO_CONTAINER_TESTS").is_none(),
116 "CARGO_CONTAINER_TESTS must be set"
117 );
118 }
119 "public_network_test" => {
120 set_ignore!(
125 option_env!("CARGO_PUBLIC_NETWORK_TESTS").is_none(),
126 "CARGO_PUBLIC_NETWORK_TESTS must be set"
127 );
128 }
129 "nightly" => {
130 requires_reason = true;
131 set_ignore!(is_not_nightly, "requires nightly");
132 }
133 "requires_rustup_stable" => {
134 set_ignore!(
135 !has_rustup_stable(),
136 "rustup or stable toolchain not installed"
137 );
138 }
139 s if s.starts_with("requires=") => {
140 let command = &s[9..];
141 let Ok(literal) = command.parse::<Literal>() else {
142 panic!("expect a string literal, found: {command}");
143 };
144 let literal = literal.to_string();
145 let Some(command) = literal
146 .strip_prefix('"')
147 .and_then(|lit| lit.strip_suffix('"'))
148 else {
149 panic!("expect a quoted string literal, found: {literal}");
150 };
151 set_ignore!(!has_command(command), "{command} not installed");
152 }
153 s if s.starts_with("requires_host_split_debuginfo=") => {
154 let split_debuginfo = &s[30..];
155 let Ok(literal) = split_debuginfo.parse::<Literal>() else {
156 panic!("expect a string literal, found: {split_debuginfo}");
157 };
158 let literal = literal.to_string();
159 let Some(split_debuginfo) = literal
160 .strip_prefix('"')
161 .and_then(|lit| lit.strip_suffix('"'))
162 else {
163 panic!("expect a quoted string literal, found: {literal}");
164 };
165 set_ignore!(
166 !host_supports_split_debuginfo(split_debuginfo),
167 "host rustc does not support -Csplit-debuginfo={split_debuginfo}"
168 );
169 }
170 s if s.starts_with(">=1.") => {
171 requires_reason = true;
172 let min_minor = s[4..].parse().unwrap();
173 let minor = version().0;
174 set_ignore!(minor < min_minor, "requires rustc 1.{minor} or newer");
175 }
176 s if s.starts_with("reason=") => {
177 explicit_reason = Some(s[7..].parse().unwrap());
178 }
179 s if s.starts_with("ignore_windows=") => {
180 set_ignore!(cfg!(windows), "{}", &s[16..s.len() - 1]);
181 }
182 _ => panic!("unknown rule {:?}", rule),
183 }
184 }
185 if requires_reason && explicit_reason.is_none() {
186 panic!(
187 "#[cargo_test] with a rule also requires a reason, \
188 such as #[cargo_test(nightly, reason = \"needs -Z unstable-thing\")]"
189 );
190 }
191
192 let span = Span::call_site();
194 let mut ret = TokenStream::new();
195 let add_attr = |ret: &mut TokenStream, attr_name, attr_input| {
196 ret.extend(Some(TokenTree::from(Punct::new('#', Spacing::Alone))));
197 let attr = TokenTree::from(Ident::new(attr_name, span));
198 let mut attr_stream: TokenStream = attr.into();
199 if let Some(input) = attr_input {
200 attr_stream.extend(input);
201 }
202 ret.extend(Some(TokenTree::from(Group::new(
203 Delimiter::Bracket,
204 attr_stream,
205 ))));
206 };
207 add_attr(&mut ret, "test", None);
208 if ignore {
209 let reason = explicit_reason
210 .or_else(|| {
211 (!implicit_reasons.is_empty())
212 .then(|| TokenTree::from(Literal::string(&implicit_reasons.join(", "))).into())
213 })
214 .map(|reason: TokenStream| {
215 let mut stream = TokenStream::new();
216 stream.extend(Some(TokenTree::from(Punct::new('=', Spacing::Alone))));
217 stream.extend(Some(reason));
218 stream
219 });
220 add_attr(&mut ret, "ignore", reason);
221 }
222
223 let mut test_name = None;
224 let mut num = 0;
225
226 for token in item {
228 let group = match token {
229 TokenTree::Group(g) => {
230 if g.delimiter() == Delimiter::Brace {
231 g
232 } else {
233 ret.extend(Some(TokenTree::Group(g)));
234 continue;
235 }
236 }
237 TokenTree::Ident(i) => {
238 if test_name.is_none() && num == 1 {
241 test_name = Some(i.to_string())
242 } else {
243 num += 1;
244 }
245 ret.extend(Some(TokenTree::Ident(i)));
246 continue;
247 }
248 other => {
249 ret.extend(Some(other));
250 continue;
251 }
252 };
253
254 let name = &test_name
255 .clone()
256 .map(|n| n.split("::").next().unwrap().to_string())
257 .unwrap();
258
259 let mut new_body = to_token_stream(&format!(
260 r#"let _test_guard = {{
261 let tmp_dir = env!("CARGO_TARGET_TMPDIR");
262 let test_dir = cargo_test_support::paths::test_dir(std::file!(), "{name}");
263 cargo_test_support::paths::init_root(tmp_dir, test_dir)
264 }};"#
265 ));
266
267 new_body.extend(group.stream());
268 ret.extend(Some(TokenTree::from(Group::new(
269 group.delimiter(),
270 new_body,
271 ))));
272 }
273
274 ret
275}
276
277fn split_rules(t: TokenStream) -> Vec<String> {
278 let tts: Vec<_> = t.into_iter().collect();
279 tts.split(|tt| match tt {
280 TokenTree::Punct(p) => p.as_char() == ',',
281 _ => false,
282 })
283 .filter(|parts| !parts.is_empty())
284 .map(|parts| {
285 parts
286 .into_iter()
287 .map(|part| part.to_string())
288 .collect::<String>()
289 })
290 .collect()
291}
292
293fn to_token_stream(code: &str) -> TokenStream {
294 code.parse().unwrap()
295}
296
297static VERSION: std::sync::LazyLock<(u32, bool)> = LazyLock::new(|| {
298 let output = Command::new("rustc")
299 .arg("-V")
300 .output()
301 .expect("rustc should run");
302
303 let exit_code = output.status.code();
304
305 if exit_code != Some(0) {
306 let stdout = std::str::from_utf8(&output.stdout).unwrap_or("<invalid utf8>");
307 let stderr = std::str::from_utf8(&output.stderr).unwrap_or("<invalid utf8>");
308 let extra = if exit_code.map(|c| c as u32) == Some(0xC0000017) {
313 "This likely indicates that PATH is too large for Windows.\n"
314 } else {
315 ""
316 };
317 panic!(
318 "'rustc -V' exited with non-zero exit code: {exit_code:?}\n{extra}stdout: {stdout}\nstderr: {stderr}",
319 );
320 }
321
322 let stdout = std::str::from_utf8(&output.stdout).expect("utf8");
323 let vers = stdout
324 .split_whitespace()
325 .skip(1)
326 .next()
327 .expect("version should have contain at least one space");
328 let is_nightly = option_env!("CARGO_TEST_DISABLE_NIGHTLY").is_none()
329 && (vers.contains("-nightly") || vers.contains("-dev"));
330 let minor = vers
331 .split('.')
332 .skip(1)
333 .next()
334 .expect("malformed rustc version (not semver)")
335 .parse()
336 .expect("malformed rustc version (minor version was not u32)");
337 (minor, is_nightly)
338});
339
340fn version() -> (u32, bool) {
341 LazyLock::force(&VERSION).clone()
342}
343
344fn host_supports_split_debuginfo(split_debuginfo: &str) -> bool {
345 static SPLIT_DEBUGINFO: LazyLock<Vec<String>> = LazyLock::new(|| {
346 let output = Command::new("rustc")
347 .arg("--print=split-debuginfo")
348 .output()
349 .expect("rustc should run");
350
351 let exit_code = output.status.code();
352 if exit_code != Some(0) {
353 let stdout = std::str::from_utf8(&output.stdout).unwrap_or("<invalid utf8>");
354 let stderr = std::str::from_utf8(&output.stderr).unwrap_or("<invalid utf8>");
355 panic!(
356 "'rustc --print=split-debuginfo' exited with non-zero exit code: {exit_code:?}\n\
357 stdout: {stdout}\n\
358 stderr: {stderr}",
359 );
360 }
361
362 let stdout = std::str::from_utf8(&output.stdout).expect("utf8");
363 stdout.lines().map(str::to_owned).collect()
364 });
365
366 SPLIT_DEBUGINFO
367 .iter()
368 .any(|supported| supported == split_debuginfo)
369}
370
371fn check_command(command_path: &Path, args: &[&str]) -> bool {
372 let mut command = Command::new(command_path);
373 let command_name = command.get_program().to_str().unwrap().to_owned();
374 command.args(args);
375 let output = match command.output() {
376 Ok(output) => output,
377 Err(e) => {
378 if is_ci() {
379 panic!("expected command `{command_name}` to be somewhere in PATH: {e}",);
380 }
381 return false;
382 }
383 };
384 if !output.status.success() {
385 panic!(
386 "expected command `{command_name}` to be runnable, got error {}:\n\
387 stderr:{}\n\
388 stdout:{}\n",
389 output.status,
390 String::from_utf8_lossy(&output.stderr),
391 String::from_utf8_lossy(&output.stdout)
392 );
393 }
394 true
395}
396
397fn has_command(command: &str) -> bool {
398 use std::env::consts::EXE_EXTENSION;
399 #[allow(clippy::disallowed_methods)]
401 let Some(paths) = std::env::var_os("PATH") else {
402 return false;
403 };
404 let found = std::env::split_paths(&paths)
405 .flat_map(|path| {
406 let candidate = path.join(&command);
407 let with_exe = if EXE_EXTENSION.is_empty() {
408 None
409 } else {
410 Some(candidate.with_extension(EXE_EXTENSION))
411 };
412 std::iter::once(candidate).chain(with_exe)
413 })
414 .find(|p| is_executable(p))
415 .is_some();
416 if !found && requires_external_tools() && command != "hg" {
420 panic!("expected command `{command}` to be somewhere in PATH");
421 }
422 found
423}
424
425#[cfg(unix)]
426fn is_executable<P: AsRef<Path>>(path: P) -> bool {
427 use std::os::unix::prelude::*;
428 std::fs::metadata(path)
429 .map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
430 .unwrap_or(false)
431}
432
433#[cfg(windows)]
434fn is_executable<P: AsRef<Path>>(path: P) -> bool {
435 path.as_ref().is_file()
436}
437
438fn has_rustup_stable() -> bool {
439 if option_env!("CARGO_TEST_DISABLE_NIGHTLY").is_some() {
440 return false;
442 }
443 let home = match option_env!("CARGO_HOME") {
447 Some(home) => home,
448 None if is_ci() => panic!("expected to run under rustup"),
449 None => return false,
450 };
451 let cargo = Path::new(home).join("bin/cargo");
452 check_command(&cargo, &["+stable", "--version"])
453}
454
455fn is_ci() -> bool {
457 option_env!("CI").is_some() || option_env!("TF_BUILD").is_some()
461}
462
463fn requires_external_tools() -> bool {
465 option_env!("CARGO_TEST_REQUIRE_EXTERNAL_TOOLS").is_some()
466}