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