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.strip_circumfix('"', '"') else {
146 panic!("expect a quoted string literal, found: {literal}");
147 };
148 set_ignore!(!has_command(command), "{command} not installed");
149 }
150 s if s.starts_with("requires_host_split_debuginfo=") => {
151 let split_debuginfo = &s[30..];
152 let Ok(literal) = split_debuginfo.parse::<Literal>() else {
153 panic!("expect a string literal, found: {split_debuginfo}");
154 };
155 let literal = literal.to_string();
156 let Some(split_debuginfo) = literal.strip_circumfix('"', '"') else {
157 panic!("expect a quoted string literal, found: {literal}");
158 };
159 set_ignore!(
160 !host_supports_split_debuginfo(split_debuginfo),
161 "host rustc does not support -Csplit-debuginfo={split_debuginfo}"
162 );
163 }
164 s if s.starts_with(">=1.") => {
165 requires_reason = true;
166 let min_minor = s[4..].parse().unwrap();
167 let minor = version().0;
168 set_ignore!(minor < min_minor, "requires rustc 1.{minor} or newer");
169 }
170 s if s.starts_with("reason=") => {
171 explicit_reason = Some(s[7..].parse().unwrap());
172 }
173 s if s.starts_with("ignore_windows=") => {
174 set_ignore!(cfg!(windows), "{}", &s[16..s.len() - 1]);
175 }
176 _ => panic!("unknown rule {:?}", rule),
177 }
178 }
179 if requires_reason && explicit_reason.is_none() {
180 panic!(
181 "#[cargo_test] with a rule also requires a reason, \
182 such as #[cargo_test(nightly, reason = \"needs -Z unstable-thing\")]"
183 );
184 }
185
186 let span = Span::call_site();
188 let mut ret = TokenStream::new();
189 let add_attr = |ret: &mut TokenStream, attr_name, attr_input| {
190 ret.extend(Some(TokenTree::from(Punct::new('#', Spacing::Alone))));
191 let attr = TokenTree::from(Ident::new(attr_name, span));
192 let mut attr_stream: TokenStream = attr.into();
193 if let Some(input) = attr_input {
194 attr_stream.extend(input);
195 }
196 ret.extend(Some(TokenTree::from(Group::new(
197 Delimiter::Bracket,
198 attr_stream,
199 ))));
200 };
201 add_attr(&mut ret, "test", None);
202 if ignore {
203 let reason = explicit_reason
204 .or_else(|| {
205 (!implicit_reasons.is_empty())
206 .then(|| TokenTree::from(Literal::string(&implicit_reasons.join(", "))).into())
207 })
208 .map(|reason: TokenStream| {
209 let mut stream = TokenStream::new();
210 stream.extend(Some(TokenTree::from(Punct::new('=', Spacing::Alone))));
211 stream.extend(Some(reason));
212 stream
213 });
214 add_attr(&mut ret, "ignore", reason);
215 }
216
217 let mut test_name = None;
218 let mut num = 0;
219
220 for token in item {
222 let group = match token {
223 TokenTree::Group(g) => {
224 if g.delimiter() == Delimiter::Brace {
225 g
226 } else {
227 ret.extend(Some(TokenTree::Group(g)));
228 continue;
229 }
230 }
231 TokenTree::Ident(i) => {
232 if test_name.is_none() && num == 1 {
235 test_name = Some(i.to_string())
236 } else {
237 num += 1;
238 }
239 ret.extend(Some(TokenTree::Ident(i)));
240 continue;
241 }
242 other => {
243 ret.extend(Some(other));
244 continue;
245 }
246 };
247
248 let name = &test_name
249 .clone()
250 .map(|n| n.split("::").next().unwrap().to_string())
251 .unwrap();
252
253 let mut new_body = to_token_stream(&format!(
254 r#"let _test_guard = {{
255 let tmp_dir = env!("CARGO_TARGET_TMPDIR");
256 let test_dir = cargo_test_support::paths::test_dir(std::file!(), "{name}");
257 cargo_test_support::paths::init_root(tmp_dir, test_dir)
258 }};"#
259 ));
260
261 new_body.extend(group.stream());
262 ret.extend(Some(TokenTree::from(Group::new(
263 group.delimiter(),
264 new_body,
265 ))));
266 }
267
268 ret
269}
270
271fn split_rules(t: TokenStream) -> Vec<String> {
272 let tts: Vec<_> = t.into_iter().collect();
273 tts.split(|tt| match tt {
274 TokenTree::Punct(p) => p.as_char() == ',',
275 _ => false,
276 })
277 .filter(|parts| !parts.is_empty())
278 .map(|parts| {
279 parts
280 .into_iter()
281 .map(|part| part.to_string())
282 .collect::<String>()
283 })
284 .collect()
285}
286
287fn to_token_stream(code: &str) -> TokenStream {
288 code.parse().unwrap()
289}
290
291static VERSION: std::sync::LazyLock<(u32, bool)> = LazyLock::new(|| {
292 let output = Command::new("rustc")
293 .arg("-V")
294 .output()
295 .expect("rustc should run");
296
297 let exit_code = output.status.code();
298
299 if exit_code != Some(0) {
300 let stdout = std::str::from_utf8(&output.stdout).unwrap_or("<invalid utf8>");
301 let stderr = std::str::from_utf8(&output.stderr).unwrap_or("<invalid utf8>");
302 let extra = if exit_code.map(|c| c as u32) == Some(0xC0000017) {
307 "This likely indicates that PATH is too large for Windows.\n"
308 } else {
309 ""
310 };
311 panic!(
312 "'rustc -V' exited with non-zero exit code: {exit_code:?}\n{extra}stdout: {stdout}\nstderr: {stderr}",
313 );
314 }
315
316 let stdout = std::str::from_utf8(&output.stdout).expect("utf8");
317 let vers = stdout
318 .split_whitespace()
319 .skip(1)
320 .next()
321 .expect("version should have contain at least one space");
322 let is_nightly = option_env!("CARGO_TEST_DISABLE_NIGHTLY").is_none()
323 && (vers.contains("-nightly") || vers.contains("-dev"));
324 let minor = vers
325 .split('.')
326 .skip(1)
327 .next()
328 .expect("malformed rustc version (not semver)")
329 .parse()
330 .expect("malformed rustc version (minor version was not u32)");
331 (minor, is_nightly)
332});
333
334fn version() -> (u32, bool) {
335 LazyLock::force(&VERSION).clone()
336}
337
338fn host_supports_split_debuginfo(split_debuginfo: &str) -> bool {
339 static SPLIT_DEBUGINFO: LazyLock<Vec<String>> = LazyLock::new(|| {
340 let output = Command::new("rustc")
341 .arg("--print=split-debuginfo")
342 .output()
343 .expect("rustc should run");
344
345 let exit_code = output.status.code();
346 if exit_code != Some(0) {
347 let stdout = std::str::from_utf8(&output.stdout).unwrap_or("<invalid utf8>");
348 let stderr = std::str::from_utf8(&output.stderr).unwrap_or("<invalid utf8>");
349 panic!(
350 "'rustc --print=split-debuginfo' exited with non-zero exit code: {exit_code:?}\n\
351 stdout: {stdout}\n\
352 stderr: {stderr}",
353 );
354 }
355
356 let stdout = std::str::from_utf8(&output.stdout).expect("utf8");
357 stdout.lines().map(str::to_owned).collect()
358 });
359
360 SPLIT_DEBUGINFO
361 .iter()
362 .any(|supported| supported == split_debuginfo)
363}
364
365fn check_command(command_path: &Path, args: &[&str]) -> bool {
366 let mut command = Command::new(command_path);
367 let command_name = command.get_program().to_str().unwrap().to_owned();
368 command.args(args);
369 let output = match command.output() {
370 Ok(output) => output,
371 Err(e) => {
372 if is_ci() {
373 panic!("expected command `{command_name}` to be somewhere in PATH: {e}",);
374 }
375 return false;
376 }
377 };
378 if !output.status.success() {
379 panic!(
380 "expected command `{command_name}` to be runnable, got error {}:\n\
381 stderr:{}\n\
382 stdout:{}\n",
383 output.status,
384 String::from_utf8_lossy(&output.stderr),
385 String::from_utf8_lossy(&output.stdout)
386 );
387 }
388 true
389}
390
391fn has_command(command: &str) -> bool {
392 use std::env::consts::EXE_EXTENSION;
393 #[allow(clippy::disallowed_methods)]
395 let Some(paths) = std::env::var_os("PATH") else {
396 return false;
397 };
398 let found = std::env::split_paths(&paths)
399 .flat_map(|path| {
400 let candidate = path.join(&command);
401 let with_exe = if EXE_EXTENSION.is_empty() {
402 None
403 } else {
404 Some(candidate.with_extension(EXE_EXTENSION))
405 };
406 std::iter::once(candidate).chain(with_exe)
407 })
408 .find(|p| is_executable(p))
409 .is_some();
410 if !found && requires_external_tools() && command != "hg" {
414 panic!("expected command `{command}` to be somewhere in PATH");
415 }
416 found
417}
418
419#[cfg(unix)]
420fn is_executable<P: AsRef<Path>>(path: P) -> bool {
421 use std::os::unix::prelude::*;
422 std::fs::metadata(path)
423 .map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
424 .unwrap_or(false)
425}
426
427#[cfg(windows)]
428fn is_executable<P: AsRef<Path>>(path: P) -> bool {
429 path.as_ref().is_file()
430}
431
432fn has_rustup_stable() -> bool {
433 if option_env!("CARGO_TEST_DISABLE_NIGHTLY").is_some() {
434 return false;
436 }
437 let home = match option_env!("CARGO_HOME") {
441 Some(home) => home,
442 None if is_ci() => panic!("expected to run under rustup"),
443 None => return false,
444 };
445 let cargo = Path::new(home).join("bin/cargo");
446 check_command(&cargo, &["+stable", "--version"])
447}
448
449fn is_ci() -> bool {
451 option_env!("CI").is_some() || option_env!("TF_BUILD").is_some()
455}
456
457fn requires_external_tools() -> bool {
459 option_env!("CARGO_TEST_REQUIRE_EXTERNAL_TOOLS").is_some()
460}