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}