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