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