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