diff --git a/crates/cargo-test-macro/src/lib.rs b/crates/cargo-test-macro/src/lib.rs index 5f0eecfeb6c..6926de38c91 100644 --- a/crates/cargo-test-macro/src/lib.rs +++ b/crates/cargo-test-macro/src/lib.rs @@ -200,6 +200,9 @@ pub fn cargo_test(attr: TokenStream, item: TokenStream) -> TokenStream { add_attr(&mut ret, "ignore", reason); } + let mut test_name = None; + let mut num = 0; + // Find where the function body starts, and add the boilerplate at the start. for token in item { let group = match token { @@ -211,18 +214,44 @@ pub fn cargo_test(attr: TokenStream, item: TokenStream) -> TokenStream { continue; } } + TokenTree::Ident(i) => { + // The first time through it will be `fn` the second time is the + // name of the test. + if test_name.is_none() && num == 1 { + test_name = Some(i.to_string()) + } else { + num += 1; + } + ret.extend(Some(TokenTree::Ident(i))); + continue; + } other => { ret.extend(Some(other)); continue; } }; - let mut new_body = to_token_stream( - r#"let _test_guard = { - let tmp_dir = option_env!("CARGO_TARGET_TMPDIR"); - cargo_test_support::paths::init_root(tmp_dir) - };"#, - ); + let name = &test_name + .clone() + .map(|n| n.split("::").next().unwrap().to_string()) + .unwrap(); + + let mut new_body = if cfg!(windows) { + to_token_stream( + r#"let _test_guard = { + let tmp_dir = option_env!("CARGO_TARGET_TMPDIR"); + cargo_test_support::paths::init_root(tmp_dir) + };"#, + ) + } else { + to_token_stream(&format!( + r#"let _test_guard = {{ + let tmp_dir = option_env!("CARGO_TARGET_TMPDIR"); + let test_dir = cargo_test_support::paths::test_dir(std::file!(), "{name}"); + cargo_test_support::paths::init_root(tmp_dir, test_dir) + }};"# + )) + }; new_body.extend(group.stream()); ret.extend(Some(TokenTree::from(Group::new( diff --git a/crates/cargo-test-support/src/paths.rs b/crates/cargo-test-support/src/paths.rs index 742c9b1a9d5..507221041f8 100644 --- a/crates/cargo-test-support/src/paths.rs +++ b/crates/cargo-test-support/src/paths.rs @@ -12,6 +12,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::Mutex; use std::sync::OnceLock; +#[cfg(windows)] use std::sync::atomic::{AtomicUsize, Ordering}; use crate::compare::assert_e2e; @@ -62,16 +63,15 @@ pub fn global_root() -> PathBuf { } } -// We need to give each test a unique id. The test name could serve this -// purpose, but the `test` crate doesn't have a way to obtain the current test -// name.[*] Instead, we used the `cargo-test-macro` crate to automatically -// insert an init function for each test that sets the test name in a thread -// local variable. -// -// [*] It does set the thread name, but only when running concurrently. If not -// running concurrently, all tests are run on the main thread. +// We need to give each test a unique id. The test name serve this +// purpose. We are able to get the test name by having the `cargo-test-macro` +// crate automatically insert an init function for each test that sets the +// test name in a thread local variable. thread_local! { + #[cfg(windows)] static TEST_ID: RefCell> = const { RefCell::new(None) }; + #[cfg(not(windows))] + static TEST_NAME: RefCell> = const { RefCell::new(None) }; } /// See [`init_root`] @@ -80,12 +80,26 @@ pub struct TestIdGuard { } /// For test harnesses like [`crate::cargo_test`] +#[cfg(windows)] pub fn init_root(tmp_dir: Option<&'static str>) -> TestIdGuard { static NEXT_ID: AtomicUsize = AtomicUsize::new(0); let id = NEXT_ID.fetch_add(1, Ordering::SeqCst); TEST_ID.with(|n| *n.borrow_mut() = Some(id)); + let guard = TestIdGuard { _private: () }; + + set_global_root(tmp_dir); + let r = root(); + r.rm_rf(); + r.mkdir_p(); + guard +} + +/// For test harnesses like [`crate::cargo_test`] +#[cfg(not(windows))] +pub fn init_root(tmp_dir: Option<&'static str>, test_name: PathBuf) -> TestIdGuard { + TEST_NAME.with(|n| *n.borrow_mut() = Some(test_name)); let guard = TestIdGuard { _private: () }; set_global_root(tmp_dir); @@ -98,13 +112,17 @@ pub fn init_root(tmp_dir: Option<&'static str>) -> TestIdGuard { impl Drop for TestIdGuard { fn drop(&mut self) { + #[cfg(windows)] TEST_ID.with(|n| *n.borrow_mut() = None); + #[cfg(not(windows))] + TEST_NAME.with(|n| *n.borrow_mut() = None); } } /// Path to the test's filesystem scratchpad /// /// ex: `$CARGO_TARGET_TMPDIR/cit/t0` +#[cfg(windows)] pub fn root() -> PathBuf { let id = TEST_ID.with(|n| { n.borrow().expect( @@ -118,6 +136,23 @@ pub fn root() -> PathBuf { root } +/// Path to the test's filesystem scratchpad +/// +/// ex: `$CARGO_TARGET_TMPDIR/cit/t0` +#[cfg(not(windows))] +pub fn root() -> PathBuf { + let test_name = TEST_NAME.with(|n| { + n.borrow().clone().expect( + "Tests must use the `#[cargo_test]` attribute in \ + order to be able to use the crate root.", + ) + }); + + let mut root = global_root(); + root.push(&test_name); + root +} + /// Path to the current test's `$HOME` /// /// ex: `$CARGO_TARGET_TMPDIR/cit/t0/home` @@ -489,3 +524,26 @@ pub fn windows_reserved_names_are_allowed() -> bool { true } } + +/// This takes the test location (std::file!() should be passed) and the test name +/// and outputs the location the test should be places in, inside of `target/tmp/cit` +/// +/// `path: tests/testsuite/workspaces.rs` +/// `name: `workspace_in_git +/// `output: "testsuite/workspaces/workspace_in_git` +pub fn test_dir(path: &str, name: &str) -> std::path::PathBuf { + let test_dir: std::path::PathBuf = std::path::PathBuf::from(path) + .components() + // Trim .rs from any files + .map(|c| c.as_os_str().to_str().unwrap().trim_end_matches(".rs")) + // We only want to take once we have reached `tests` or `src`. This helps when in a + // workspace: `workspace/more/src/...` would result in `src/...` + .skip_while(|c| c != &"tests" && c != &"src") + // We want to skip "tests" since it is taken in `skip_while`. + // "src" is fine since you could have test in "src" named the same as one in "tests" + // Skip "mod" since `snapbox` tests have a folder per test not a file and the files + // are named "mod.rs" + .filter(|c| c != &"tests" && c != &"mod") + .collect(); + test_dir.join(name) +} diff --git a/src/doc/contrib/src/tests/writing.md b/src/doc/contrib/src/tests/writing.md index 99eef916d3f..58f5407ed03 100644 --- a/src/doc/contrib/src/tests/writing.md +++ b/src/doc/contrib/src/tests/writing.md @@ -200,8 +200,13 @@ Then populate - This is used in place of `#[test]` - This attribute injects code which does some setup before starting the test, creating a filesystem "sandbox" under the "cargo integration test" - directory for each test such as - `/path/to/cargo/target/cit/t123/` + directory for each test similar to the following: + ```toml + # Most platforms + /path/to/cargo/target/tmp/cit/testsuite/bad_config/bad1 + # Windows + /path/to/cargo/target/cit/t123/ + ``` - The sandbox will contain a `home` directory that will be used instead of your normal home directory `Project`: diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index ab4ee7344f5..1ec54c1b035 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -210,3 +210,52 @@ fn aaa_trigger_cross_compile_disabled_check() { // This triggers the cross compile disabled check to run ASAP, see #5141 crate::utils::cross_compile::disabled(); } + +// This is placed here as running tests in `cargo-test-support` would rebuild it +#[cargo_test] +#[cfg(not(windows))] +fn check_test_dir() { + let tests = vec![ + ( + "tests/testsuite/workspaces.rs", + "workspace_in_git", + "testsuite/workspaces/workspace_in_git", + ), + ( + "tests/testsuite/cargo_remove/invalid_arg/mod.rs", + "case", + "testsuite/cargo_remove/invalid_arg/case", + ), + ( + "tests/build-std/main.rs", + "cross_custom", + "build-std/main/cross_custom", + ), + ( + "src/tools/cargo/tests/testsuite/build.rs", + "cargo_compile_simple", + "src/tools/cargo/testsuite/build/cargo_compile_simple", + ), + ( + "src/tools/cargo/tests/testsuite/cargo_add/add_basic/mod.rs", + "case", + "src/tools/cargo/testsuite/cargo_add/add_basic/case", + ), + ( + "src/tools/cargo/tests/build-std/main.rs", + "cross_custom", + "src/tools/cargo/build-std/main/cross_custom", + ), + ( + "workspace/more/src/tools/cargo/tests/testsuite/build.rs", + "cargo_compile_simple", + "src/tools/cargo/testsuite/build/cargo_compile_simple", + ), + ]; + for (path, name, expected) in tests { + assert_eq!( + cargo_test_support::paths::test_dir(path, name), + std::path::PathBuf::from(expected) + ); + } +}