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}