#
cargo_test_support/
paths.rs

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