#
cargo_test_support/
paths.rs

    1//! Access common paths and manipulate the filesystem
    2
    3use filetime::FileTime;
    4use itertools::Itertools;
    5use walkdir::WalkDir;
    6
    7use std::cell::RefCell;
    8use std::fs;
    9use std::io::{self, ErrorKind};
    10use std::path::{Path, PathBuf};
    11use std::process::Command;
    12use std::sync::Mutex;
    13use std::sync::OnceLock;
    14use std::sync::atomic::{AtomicUsize, Ordering};
    15
    16use crate::compare::assert_e2e;
    17use crate::compare::match_contains;
    18
    19static CARGO_INTEGRATION_TEST_DIR: &str = "cit";
    20
    21static GLOBAL_ROOT: OnceLock<Mutex<Option<PathBuf>>> = OnceLock::new();
    22
    23fn set_global_root(tmp_dir: &'static str) {
    24    let mut lock = GLOBAL_ROOT
    25        .get_or_init(|| Default::default())
    26        .lock()
    27        .unwrap();
    28    if lock.is_none() {
    29        let mut root = PathBuf::from(tmp_dir);
    30        root.push(CARGO_INTEGRATION_TEST_DIR);
    31        *lock = Some(root);
    32    }
    33}
    34
    35/// Path to the parent directory of all test [`root`]s
    36///
    37/// ex: `$CARGO_TARGET_TMPDIR/cit`
    38pub fn global_root() -> PathBuf {
    39    let lock = GLOBAL_ROOT
    40        .get_or_init(|| Default::default())
    41        .lock()
    42        .unwrap();
    43    match lock.as_ref() {
    44        Some(p) => p.clone(),
    45        None => unreachable!("GLOBAL_ROOT not set yet"),
    46    }
    47}
    48
    49// We need to give each test a unique id. The test name serve this
    50// purpose. We are able to get the test name by having the `cargo-test-macro`
    51// crate automatically insert an init function for each test that sets the
    52// test name in a thread local variable.
    53thread_local! {
    54    static TEST_ID: RefCell<Option<usize>> = const { RefCell::new(None) };
    55    static TEST_DIR: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
    56}
    57
    58/// See [`init_root`]
    59pub struct TestIdGuard {
    60    _private: (),
    61}
    62
    63/// For test harnesses like [`crate::cargo_test`]
    64pub fn init_root(tmp_dir: &'static str, test_dir: PathBuf) -> TestIdGuard {
    65    static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
    66    let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
    67    TEST_ID.with(|n| *n.borrow_mut() = Some(id));
    68    if cfg!(windows) {
    69        // Due to path-length limits, Windows doesn't use the full test name.
    70        TEST_DIR.with(|n| *n.borrow_mut() = Some(PathBuf::from(format!("t{id}"))));
    71    } else {
    72        TEST_DIR.with(|n| *n.borrow_mut() = Some(test_dir));
    73    }
    74    let guard = TestIdGuard { _private: () };
    75    set_global_root(tmp_dir);
    76    let r = root();
    77    r.rm_rf();
    78    r.mkdir_p();
    79    #[cfg(not(windows))]
    80    if id == 0 {
    81        // Create a symlink from `t0` to the first test to make it easier to
    82        // find and reuse when running a single test.
    83        use crate::SymlinkBuilder;
    84        let mut alias = global_root();
    85        alias.push("t0");
    86        alias.rm_rf();
    87        SymlinkBuilder::new_dir(r, alias).mk();
    88    }
    89    guard
    90}
    91
    92impl Drop for TestIdGuard {
    93    fn drop(&mut self) {
    94        TEST_ID.with(|n| *n.borrow_mut() = None);
    95        TEST_DIR.with(|n| *n.borrow_mut() = None);
    96    }
    97}
    98
    99/// Path to the test's filesystem scratchpad
    100///
    101/// ex: `$CARGO_TARGET_TMPDIR/cit/<integration test>/<module>/<fn name>/`
    102/// or `$CARGO_TARGET_TMPDIR/cit/t0` on Windows
    103pub fn root() -> PathBuf {
    104    let test_dir = TEST_DIR.with(|n| {
    105        n.borrow().clone().expect(
    106            "Tests must use the `#[cargo_test]` attribute in \
    107             order to be able to use the crate root.",
    108        )
    109    });
    110    let mut root = global_root();
    111    root.push(&test_dir);
    112    root
    113}
    114/// Path to the current test's `$HOME`
    115///
    116/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/home`
    117pub fn home() -> PathBuf {
    118    let mut path = root();
    119    path.push("home");
    120    path.mkdir_p();
    121    path
    122}
    123
    124/// Path to the current test's `$CARGO_HOME`
    125///
    126/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/home/.cargo`
    127pub fn cargo_home() -> PathBuf {
    128    home().join(".cargo")
    129}
    130
    131/// Path to the current test's `$CARGO_LOG`
    132///
    133/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/home/.cargo/log`
    134pub fn log_dir() -> PathBuf {
    135    cargo_home().join("log")
    136}
    137
    138/// Path to the current test's `$CARGO_LOG` file
    139///
    140/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/home/.cargo/log/<id>.jsonl`
    141///
    142/// This also asserts the number of log files is exactly the same as `idx + 1`.
    143pub fn log_file(idx: usize) -> PathBuf {
    144    let log_dir = log_dir();
    145
    146    let entries = std::fs::read_dir(&log_dir).unwrap();
    147    let mut log_files: Vec<_> = entries
    148        .filter_map(Result::ok)
    149        .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("jsonl"))
    150        .collect();
    151
    152    // Sort them to get chronological order
    153    log_files.sort_unstable_by(|a, b| a.file_name().to_str().cmp(&b.file_name().to_str()));
    154
    155    assert_eq!(
    156        idx + 1,
    157        log_files.len(),
    158        "unexpected number of log files: {}, expected {}",
    159        log_files.len(),
    160        idx + 1
    161    );
    162
    163    log_files[idx].path()
    164}
    165
    166/// Common path and file operations
    167pub trait CargoPathExt {
    168    fn to_url(&self) -> url::Url;
    169
    170    fn rm_rf(&self);
    171    fn mkdir_p(&self);
    172
    173    /// Returns a list of all files and directories underneath the given
    174    /// directory, recursively, including the starting path.
    175    fn ls_r(&self) -> Vec<PathBuf>;
    176
    177    fn move_into_the_past(&self) {
    178        self.move_in_time(|sec, nsec| (sec - 3600, nsec))
    179    }
    180
    181    fn move_into_the_future(&self) {
    182        self.move_in_time(|sec, nsec| (sec + 3600, nsec))
    183    }
    184
    185    fn move_in_time<F>(&self, travel_amount: F)
    186    where
    187        F: Fn(i64, u32) -> (i64, u32);
    188
    189    fn assert_build_dir_layout(&self, expected: impl snapbox::IntoData);
    190
    191    fn assert_dir_layout(&self, expected: impl snapbox::IntoData, ignored_path_patterns: &[String]);
    192}
    193
    194impl CargoPathExt for Path {
    195    fn to_url(&self) -> url::Url {
    196        url::Url::from_file_path(self).ok().unwrap()
    197    }
    198
    199    fn rm_rf(&self) {
    200        let meta = match self.symlink_metadata() {
    201            Ok(meta) => meta,
    202            Err(e) => {
    203                if e.kind() == ErrorKind::NotFound {
    204                    return;
    205                }
    206                panic!("failed to remove {:?}, could not read: {:?}", self, e);
    207            }
    208        };
    209        // There is a race condition between fetching the metadata and
    210        // actually performing the removal, but we don't care all that much
    211        // for our tests.
    212        if meta.is_dir() {
    213            if let Err(e) = fs::remove_dir_all(self) {
    214                panic!("failed to remove {:?}: {:?}", self, e)
    215            }
    216        } else if let Err(e) = fs::remove_file(self) {
    217            panic!("failed to remove {:?}: {:?}", self, e)
    218        }
    219    }
    220
    221    fn mkdir_p(&self) {
    222        fs::create_dir_all(self)
    223            .unwrap_or_else(|e| panic!("failed to mkdir_p {}: {}", self.display(), e))
    224    }
    225
    226    fn ls_r(&self) -> Vec<PathBuf> {
    227        walkdir::WalkDir::new(self)
    228            .sort_by_file_name()
    229            .into_iter()
    230            .filter_map(|e| e.map(|e| e.path().to_owned()).ok())
    231            .collect()
    232    }
    233
    234    fn move_in_time<F>(&self, travel_amount: F)
    235    where
    236        F: Fn(i64, u32) -> (i64, u32),
    237    {
    238        if self.is_file() {
    239            time_travel(self, &travel_amount);
    240        } else {
    241            recurse(self, &self.join("target"), &travel_amount);
    242        }
    243
    244        fn recurse<F>(p: &Path, bad: &Path, travel_amount: &F)
    245        where
    246            F: Fn(i64, u32) -> (i64, u32),
    247        {
    248            if p.is_file() {
    249                time_travel(p, travel_amount)
    250            } else if !p.starts_with(bad) {
    251                for f in t!(fs::read_dir(p)) {
    252                    let f = t!(f).path();
    253                    recurse(&f, bad, travel_amount);
    254                }
    255            }
    256        }
    257
    258        fn time_travel<F>(path: &Path, travel_amount: &F)
    259        where
    260            F: Fn(i64, u32) -> (i64, u32),
    261        {
    262            let stat = t!(path.symlink_metadata());
    263
    264            let mtime = FileTime::from_last_modification_time(&stat);
    265
    266            let (sec, nsec) = travel_amount(mtime.unix_seconds(), mtime.nanoseconds());
    267            let newtime = FileTime::from_unix_time(sec, nsec);
    268
    269            // Sadly change_file_times has a failure mode where a readonly file
    270            // cannot have its times changed on windows.
    271            do_op(path, "set file times", |path| {
    272                filetime::set_file_times(path, newtime, newtime)
    273            });
    274        }
    275    }
    276
    277    #[track_caller]
    278    fn assert_build_dir_layout(&self, expected: impl snapbox::IntoData) {
    279        // We call `unordered()` here to because the build-dir has some scenarios that make
    280        // consistent ordering not possible.
    281        // Notably:
    282        // 1. Binaries with `.exe` on Windows causing the ordering to change with the dep-info `.d`
    283        //    file.
    284        // 2. Directories with hashes are often reordered differently by platform.
    285        self.assert_dir_layout(expected.unordered(), &build_dir_ignored_path_patterns());
    286    }
    287
    288    #[track_caller]
    289    fn assert_dir_layout(
    290        &self,
    291        expected: impl snapbox::IntoData,
    292        ignored_path_patterns: &[String],
    293    ) {
    294        let assert = assert_e2e();
    295        let actual = WalkDir::new(self)
    296            .sort_by_file_name()
    297            .into_iter()
    298            .filter_map(|e| e.ok())
    299            .filter(|e| e.file_type().is_file())
    300            .map(|e| e.path().to_string_lossy().into_owned())
    301            .filter(|file| {
    302                for ignored in ignored_path_patterns {
    303                    if match_contains(&ignored, file, &assert.redactions()).is_ok() {
    304                        return false;
    305                    }
    306                }
    307                return true;
    308            })
    309            .join("\n");
    310
    311        assert.eq(format!("{actual}\n"), expected);
    312    }
    313}
    314
    315impl CargoPathExt for PathBuf {
    316    fn to_url(&self) -> url::Url {
    317        self.as_path().to_url()
    318    }
    319
    320    fn rm_rf(&self) {
    321        self.as_path().rm_rf()
    322    }
    323    fn mkdir_p(&self) {
    324        self.as_path().mkdir_p()
    325    }
    326
    327    fn ls_r(&self) -> Vec<PathBuf> {
    328        self.as_path().ls_r()
    329    }
    330
    331    fn move_in_time<F>(&self, travel_amount: F)
    332    where
    333        F: Fn(i64, u32) -> (i64, u32),
    334    {
    335        self.as_path().move_in_time(travel_amount)
    336    }
    337
    338    #[track_caller]
    339    fn assert_build_dir_layout(&self, expected: impl snapbox::IntoData) {
    340        self.as_path().assert_build_dir_layout(expected);
    341    }
    342
    343    #[track_caller]
    344    fn assert_dir_layout(
    345        &self,
    346        expected: impl snapbox::IntoData,
    347        ignored_path_patterns: &[String],
    348    ) {
    349        self.as_path()
    350            .assert_dir_layout(expected, ignored_path_patterns);
    351    }
    352}
    353
    354fn do_op<F>(path: &Path, desc: &str, mut f: F)
    355where
    356    F: FnMut(&Path) -> io::Result<()>,
    357{
    358    match f(path) {
    359        Ok(()) => {}
    360        Err(ref e) if e.kind() == ErrorKind::PermissionDenied => {
    361            let mut p = t!(path.metadata()).permissions();
    362            p.set_readonly(false);
    363            t!(fs::set_permissions(path, p));
    364
    365            // Unix also requires the parent to not be readonly for example when
    366            // removing files
    367            let parent = path.parent().unwrap();
    368            let mut p = t!(parent.metadata()).permissions();
    369            p.set_readonly(false);
    370            t!(fs::set_permissions(parent, p));
    371
    372            f(path).unwrap_or_else(|e| {
    373                panic!("failed to {} {}: {}", desc, path.display(), e);
    374            })
    375        }
    376        Err(e) => {
    377            panic!("failed to {} {}: {}", desc, path.display(), e);
    378        }
    379    }
    380}
    381
    382/// The paths to ignore when [`CargoPathExt::assert_build_dir_layout`] is called
    383fn build_dir_ignored_path_patterns() -> Vec<String> {
    384    vec![
    385        // Ignore MacOS debug symbols as there are many files/directories that would clutter up
    386        // tests few not a lot of benefit.
    387        "[..].dSYM/[..]",
    388        // Ignore Windows debug symbols files (.pdb)
    389        "[..].pdb",
    390    ]
    391    .into_iter()
    392    .map(ToString::to_string)
    393    .collect()
    394}
    395
    396/// Get the filename for a library.
    397///
    398/// `kind` should be one of:
    399/// - `lib`
    400/// - `rlib`
    401/// - `staticlib`
    402/// - `dylib`
    403/// - `proc-macro`
    404///
    405/// # Examples
    406/// ```
    407/// # use cargo_test_support::paths::get_lib_filename;
    408/// get_lib_filename("foo", "dylib");
    409/// ```
    410/// would return:
    411/// - macOS: `"libfoo.dylib"`
    412/// - Windows: `"foo.dll"`
    413/// - Unix: `"libfoo.so"`
    414pub fn get_lib_filename(name: &str, kind: &str) -> String {
    415    let prefix = get_lib_prefix(kind);
    416    let extension = get_lib_extension(kind);
    417    format!("{}{}.{}", prefix, name, extension)
    418}
    419
    420/// See [`get_lib_filename`] for more details
    421pub fn get_lib_prefix(kind: &str) -> &str {
    422    match kind {
    423        "lib" | "rlib" => "lib",
    424        "staticlib" | "dylib" | "proc-macro" => {
    425            if cfg!(windows) {
    426                ""
    427            } else {
    428                "lib"
    429            }
    430        }
    431        _ => unreachable!(),
    432    }
    433}
    434
    435/// See [`get_lib_filename`] for more details
    436pub fn get_lib_extension(kind: &str) -> &str {
    437    match kind {
    438        "lib" | "rlib" => "rlib",
    439        "staticlib" => {
    440            if cfg!(windows) {
    441                "lib"
    442            } else {
    443                "a"
    444            }
    445        }
    446        "dylib" | "proc-macro" => {
    447            if cfg!(windows) {
    448                "dll"
    449            } else if cfg!(target_os = "macos") {
    450                "dylib"
    451            } else {
    452                "so"
    453            }
    454        }
    455        _ => unreachable!(),
    456    }
    457}
    458
    459/// Path to `rustc`s sysroot
    460pub fn sysroot() -> String {
    461    let output = Command::new("rustc")
    462        .arg("--print=sysroot")
    463        .output()
    464        .expect("rustc to run");
    465    assert!(output.status.success());
    466    let sysroot = String::from_utf8(output.stdout).unwrap();
    467    sysroot.trim().to_string()
    468}
    469
    470/// Returns true if names such as aux.* are allowed.
    471///
    472/// Traditionally, Windows did not allow a set of file names (see `is_windows_reserved`
    473/// for a list). More recent versions of Windows have relaxed this restriction. This test
    474/// determines whether we are running in a mode that allows Windows reserved names.
    475#[cfg(windows)]
    476pub fn windows_reserved_names_are_allowed() -> bool {
    477    use std::ffi::OsStr;
    478    use std::os::windows::ffi::OsStrExt;
    479    use std::ptr;
    480    use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW;
    481
    482    let test_file_name: Vec<_> = OsStr::new("aux.rs").encode_wide().chain([0]).collect();
    483
    484    let buffer_length =
    485        unsafe { GetFullPathNameW(test_file_name.as_ptr(), 0, ptr::null_mut(), ptr::null_mut()) };
    486
    487    if buffer_length == 0 {
    488        // This means the call failed, so we'll conservatively assume reserved names are not allowed.
    489        return false;
    490    }
    491
    492    let mut buffer = vec![0u16; buffer_length as usize];
    493
    494    let result = unsafe {
    495        GetFullPathNameW(
    496            test_file_name.as_ptr(),
    497            buffer_length,
    498            buffer.as_mut_ptr(),
    499            ptr::null_mut(),
    500        )
    501    };
    502
    503    if result == 0 {
    504        // Once again, conservatively assume reserved names are not allowed if the
    505        // GetFullPathNameW call failed.
    506        return false;
    507    }
    508
    509    // Under the old rules, a file name like aux.rs would get converted into \\.\aux, so
    510    // we detect this case by checking if the string starts with \\.\
    511    //
    512    // Otherwise, the filename will be something like C:\Users\Foo\Documents\aux.rs
    513    let prefix: Vec<_> = OsStr::new("\\\\.\\").encode_wide().collect();
    514    if buffer.starts_with(&prefix) {
    515        false
    516    } else {
    517        true
    518    }
    519}
    520
    521/// This takes the test location (std::file!() should be passed) and the test name
    522/// and outputs the location the test should be places in, inside of `target/tmp/cit`
    523///
    524/// `path: tests/testsuite/workspaces.rs`
    525/// `name: `workspace_in_git
    526/// `output: "testsuite/workspaces/workspace_in_git`
    527pub fn test_dir(path: &str, name: &str) -> std::path::PathBuf {
    528    let test_dir: std::path::PathBuf = std::path::PathBuf::from(path)
    529        .components()
    530        // Trim .rs from any files
    531        .map(|c| c.as_os_str().to_str().unwrap().trim_end_matches(".rs"))
    532        // We only want to take once we have reached `tests` or `src`. This helps when in a
    533        // workspace: `workspace/more/src/...` would result in `src/...`
    534        .skip_while(|c| c != &"tests" && c != &"src")
    535        // We want to skip "tests" since it is taken in `skip_while`.
    536        // "src" is fine since you could have test in "src" named the same as one in "tests"
    537        // Skip "mod" since `snapbox` tests have a folder per test not a file and the files
    538        // are named "mod.rs"
    539        .filter(|c| c != &"tests" && c != &"mod")
    540        .collect();
    541    test_dir.join(name)
    542}