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}