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/// Path to the current test's `$CARGO_LOG`
123///
124/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/home/.cargo/log`
125pub fn log_dir() -> PathBuf {
126    cargo_home().join("log")
127}
128
129/// Path to the current test's `$CARGO_LOG` file
130///
131/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/home/.cargo/log/<id>.jsonl`
132///
133/// This also asserts the number of log files is exactly the same as `idx + 1`.
134pub fn log_file(idx: usize) -> PathBuf {
135    let log_dir = log_dir();
136
137    let entries = std::fs::read_dir(&log_dir).unwrap();
138    let mut log_files: Vec<_> = entries
139        .filter_map(Result::ok)
140        .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("jsonl"))
141        .collect();
142
143    // Sort them to get chronological order
144    log_files.sort_unstable_by(|a, b| a.file_name().to_str().cmp(&b.file_name().to_str()));
145
146    assert_eq!(
147        idx + 1,
148        log_files.len(),
149        "unexpected number of log files: {}, expected {}",
150        log_files.len(),
151        idx + 1
152    );
153
154    log_files[idx].path()
155}
156
157/// Common path and file operations
158pub trait CargoPathExt {
159    fn to_url(&self) -> url::Url;
160
161    fn rm_rf(&self);
162    fn mkdir_p(&self);
163
164    /// Returns a list of all files and directories underneath the given
165    /// directory, recursively, including the starting path.
166    fn ls_r(&self) -> Vec<PathBuf>;
167
168    fn move_into_the_past(&self) {
169        self.move_in_time(|sec, nsec| (sec - 3600, nsec))
170    }
171
172    fn move_into_the_future(&self) {
173        self.move_in_time(|sec, nsec| (sec + 3600, nsec))
174    }
175
176    fn move_in_time<F>(&self, travel_amount: F)
177    where
178        F: Fn(i64, u32) -> (i64, u32);
179
180    fn assert_build_dir_layout(&self, expected: impl snapbox::IntoData);
181
182    fn assert_dir_layout(&self, expected: impl snapbox::IntoData, ignored_path_patterns: &[String]);
183}
184
185impl CargoPathExt for Path {
186    fn to_url(&self) -> url::Url {
187        url::Url::from_file_path(self).ok().unwrap()
188    }
189
190    fn rm_rf(&self) {
191        let meta = match self.symlink_metadata() {
192            Ok(meta) => meta,
193            Err(e) => {
194                if e.kind() == ErrorKind::NotFound {
195                    return;
196                }
197                panic!("failed to remove {:?}, could not read: {:?}", self, e);
198            }
199        };
200        // There is a race condition between fetching the metadata and
201        // actually performing the removal, but we don't care all that much
202        // for our tests.
203        if meta.is_dir() {
204            if let Err(e) = fs::remove_dir_all(self) {
205                panic!("failed to remove {:?}: {:?}", self, e)
206            }
207        } else if let Err(e) = fs::remove_file(self) {
208            panic!("failed to remove {:?}: {:?}", self, e)
209        }
210    }
211
212    fn mkdir_p(&self) {
213        fs::create_dir_all(self)
214            .unwrap_or_else(|e| panic!("failed to mkdir_p {}: {}", self.display(), e))
215    }
216
217    fn ls_r(&self) -> Vec<PathBuf> {
218        walkdir::WalkDir::new(self)
219            .sort_by_file_name()
220            .into_iter()
221            .filter_map(|e| e.map(|e| e.path().to_owned()).ok())
222            .collect()
223    }
224
225    fn move_in_time<F>(&self, travel_amount: F)
226    where
227        F: Fn(i64, u32) -> (i64, u32),
228    {
229        if self.is_file() {
230            time_travel(self, &travel_amount);
231        } else {
232            recurse(self, &self.join("target"), &travel_amount);
233        }
234
235        fn recurse<F>(p: &Path, bad: &Path, travel_amount: &F)
236        where
237            F: Fn(i64, u32) -> (i64, u32),
238        {
239            if p.is_file() {
240                time_travel(p, travel_amount)
241            } else if !p.starts_with(bad) {
242                for f in t!(fs::read_dir(p)) {
243                    let f = t!(f).path();
244                    recurse(&f, bad, travel_amount);
245                }
246            }
247        }
248
249        fn time_travel<F>(path: &Path, travel_amount: &F)
250        where
251            F: Fn(i64, u32) -> (i64, u32),
252        {
253            let stat = t!(path.symlink_metadata());
254
255            let mtime = FileTime::from_last_modification_time(&stat);
256
257            let (sec, nsec) = travel_amount(mtime.unix_seconds(), mtime.nanoseconds());
258            let newtime = FileTime::from_unix_time(sec, nsec);
259
260            // Sadly change_file_times has a failure mode where a readonly file
261            // cannot have its times changed on windows.
262            do_op(path, "set file times", |path| {
263                filetime::set_file_times(path, newtime, newtime)
264            });
265        }
266    }
267
268    #[track_caller]
269    fn assert_build_dir_layout(&self, expected: impl snapbox::IntoData) {
270        // We call `unordered()` here to because the build-dir has some scenarios that make
271        // consistent ordering not possible.
272        // Notably:
273        // 1. Binaries with `.exe` on Windows causing the ordering to change with the dep-info `.d`
274        //    file.
275        // 2. Directories with hashes are often reordered differently by platform.
276        self.assert_dir_layout(expected.unordered(), &build_dir_ignored_path_patterns());
277    }
278
279    #[track_caller]
280    fn assert_dir_layout(
281        &self,
282        expected: impl snapbox::IntoData,
283        ignored_path_patterns: &[String],
284    ) {
285        let assert = assert_e2e();
286        let actual = WalkDir::new(self)
287            .sort_by_file_name()
288            .into_iter()
289            .filter_map(|e| e.ok())
290            .filter(|e| e.file_type().is_file())
291            .map(|e| e.path().to_string_lossy().into_owned())
292            .filter(|file| {
293                for ignored in ignored_path_patterns {
294                    if match_contains(&ignored, file, &assert.redactions()).is_ok() {
295                        return false;
296                    }
297                }
298                return true;
299            })
300            .join("\n");
301
302        assert.eq(format!("{actual}\n"), expected);
303    }
304}
305
306impl CargoPathExt for PathBuf {
307    fn to_url(&self) -> url::Url {
308        self.as_path().to_url()
309    }
310
311    fn rm_rf(&self) {
312        self.as_path().rm_rf()
313    }
314    fn mkdir_p(&self) {
315        self.as_path().mkdir_p()
316    }
317
318    fn ls_r(&self) -> Vec<PathBuf> {
319        self.as_path().ls_r()
320    }
321
322    fn move_in_time<F>(&self, travel_amount: F)
323    where
324        F: Fn(i64, u32) -> (i64, u32),
325    {
326        self.as_path().move_in_time(travel_amount)
327    }
328
329    #[track_caller]
330    fn assert_build_dir_layout(&self, expected: impl snapbox::IntoData) {
331        self.as_path().assert_build_dir_layout(expected);
332    }
333
334    #[track_caller]
335    fn assert_dir_layout(
336        &self,
337        expected: impl snapbox::IntoData,
338        ignored_path_patterns: &[String],
339    ) {
340        self.as_path()
341            .assert_dir_layout(expected, ignored_path_patterns);
342    }
343}
344
345fn do_op<F>(path: &Path, desc: &str, mut f: F)
346where
347    F: FnMut(&Path) -> io::Result<()>,
348{
349    match f(path) {
350        Ok(()) => {}
351        Err(ref e) if e.kind() == ErrorKind::PermissionDenied => {
352            let mut p = t!(path.metadata()).permissions();
353            p.set_readonly(false);
354            t!(fs::set_permissions(path, p));
355
356            // Unix also requires the parent to not be readonly for example when
357            // removing files
358            let parent = path.parent().unwrap();
359            let mut p = t!(parent.metadata()).permissions();
360            p.set_readonly(false);
361            t!(fs::set_permissions(parent, p));
362
363            f(path).unwrap_or_else(|e| {
364                panic!("failed to {} {}: {}", desc, path.display(), e);
365            })
366        }
367        Err(e) => {
368            panic!("failed to {} {}: {}", desc, path.display(), e);
369        }
370    }
371}
372
373/// The paths to ignore when [`CargoPathExt::assert_build_dir_layout`] is called
374fn build_dir_ignored_path_patterns() -> Vec<String> {
375    vec![
376        // Ignore MacOS debug symbols as there are many files/directories that would clutter up
377        // tests few not a lot of benefit.
378        "[..].dSYM/[..]",
379        // Ignore Windows debug symbols files (.pdb)
380        "[..].pdb",
381    ]
382    .into_iter()
383    .map(ToString::to_string)
384    .collect()
385}
386
387/// Get the filename for a library.
388///
389/// `kind` should be one of:
390/// - `lib`
391/// - `rlib`
392/// - `staticlib`
393/// - `dylib`
394/// - `proc-macro`
395///
396/// # Examples
397/// ```
398/// # use cargo_test_support::paths::get_lib_filename;
399/// get_lib_filename("foo", "dylib");
400/// ```
401/// would return:
402/// - macOS: `"libfoo.dylib"`
403/// - Windows: `"foo.dll"`
404/// - Unix: `"libfoo.so"`
405pub fn get_lib_filename(name: &str, kind: &str) -> String {
406    let prefix = get_lib_prefix(kind);
407    let extension = get_lib_extension(kind);
408    format!("{}{}.{}", prefix, name, extension)
409}
410
411/// See [`get_lib_filename`] for more details
412pub fn get_lib_prefix(kind: &str) -> &str {
413    match kind {
414        "lib" | "rlib" => "lib",
415        "staticlib" | "dylib" | "proc-macro" => {
416            if cfg!(windows) {
417                ""
418            } else {
419                "lib"
420            }
421        }
422        _ => unreachable!(),
423    }
424}
425
426/// See [`get_lib_filename`] for more details
427pub fn get_lib_extension(kind: &str) -> &str {
428    match kind {
429        "lib" | "rlib" => "rlib",
430        "staticlib" => {
431            if cfg!(windows) {
432                "lib"
433            } else {
434                "a"
435            }
436        }
437        "dylib" | "proc-macro" => {
438            if cfg!(windows) {
439                "dll"
440            } else if cfg!(target_os = "macos") {
441                "dylib"
442            } else {
443                "so"
444            }
445        }
446        _ => unreachable!(),
447    }
448}
449
450/// Path to `rustc`s sysroot
451pub fn sysroot() -> String {
452    let output = Command::new("rustc")
453        .arg("--print=sysroot")
454        .output()
455        .expect("rustc to run");
456    assert!(output.status.success());
457    let sysroot = String::from_utf8(output.stdout).unwrap();
458    sysroot.trim().to_string()
459}
460
461/// Returns true if names such as aux.* are allowed.
462///
463/// Traditionally, Windows did not allow a set of file names (see `is_windows_reserved`
464/// for a list). More recent versions of Windows have relaxed this restriction. This test
465/// determines whether we are running in a mode that allows Windows reserved names.
466#[cfg(windows)]
467pub fn windows_reserved_names_are_allowed() -> bool {
468    use std::ffi::OsStr;
469    use std::os::windows::ffi::OsStrExt;
470    use std::ptr;
471    use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW;
472
473    let test_file_name: Vec<_> = OsStr::new("aux.rs").encode_wide().chain([0]).collect();
474
475    let buffer_length =
476        unsafe { GetFullPathNameW(test_file_name.as_ptr(), 0, ptr::null_mut(), ptr::null_mut()) };
477
478    if buffer_length == 0 {
479        // This means the call failed, so we'll conservatively assume reserved names are not allowed.
480        return false;
481    }
482
483    let mut buffer = vec![0u16; buffer_length as usize];
484
485    let result = unsafe {
486        GetFullPathNameW(
487            test_file_name.as_ptr(),
488            buffer_length,
489            buffer.as_mut_ptr(),
490            ptr::null_mut(),
491        )
492    };
493
494    if result == 0 {
495        // Once again, conservatively assume reserved names are not allowed if the
496        // GetFullPathNameW call failed.
497        return false;
498    }
499
500    // Under the old rules, a file name like aux.rs would get converted into \\.\aux, so
501    // we detect this case by checking if the string starts with \\.\
502    //
503    // Otherwise, the filename will be something like C:\Users\Foo\Documents\aux.rs
504    let prefix: Vec<_> = OsStr::new("\\\\.\\").encode_wide().collect();
505    if buffer.starts_with(&prefix) {
506        false
507    } else {
508        true
509    }
510}