Skip to main content

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 serve this
50// purpose. We are able to get the test name by having the `cargo-test-macro`
51// crate automatically insert an init function for each test that sets the
52// test name in a thread local variable.
53thread_local! {
54    static TEST_ID: RefCell<Option<usize>> = const { RefCell::new(None) };
55    static TEST_DIR: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
56}
57
58/// See [`init_root`]
59pub struct TestIdGuard {
60    _private: (),
61}
62
63/// For test harnesses like [`crate::cargo_test`]
64pub fn init_root(tmp_dir: &'static str, test_dir: PathBuf) -> TestIdGuard {
65    static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
66    let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
67    TEST_ID.with(|n| *n.borrow_mut() = Some(id));
68    if cfg!(windows) {
69        // Due to path-length limits, Windows doesn't use the full test name.
70        TEST_DIR.with(|n| *n.borrow_mut() = Some(PathBuf::from(format!("t{id}"))));
71    } else {
72        TEST_DIR.with(|n| *n.borrow_mut() = Some(test_dir));
73    }
74    let guard = TestIdGuard { _private: () };
75    set_global_root(tmp_dir);
76    let r = root();
77    r.rm_rf();
78    r.mkdir_p();
79    #[cfg(not(windows))]
80    if id == 0 {
81        // Create a symlink from `t0` to the first test to make it easier to
82        // find and reuse when running a single test.
83        use crate::SymlinkBuilder;
84        let mut alias = global_root();
85        alias.push("t0");
86        alias.rm_rf();
87        SymlinkBuilder::new_dir(r, alias).mk();
88    }
89    guard
90}
91
92impl Drop for TestIdGuard {
93    fn drop(&mut self) {
94        TEST_ID.with(|n| *n.borrow_mut() = None);
95        TEST_DIR.with(|n| *n.borrow_mut() = None);
96    }
97}
98
99/// Path to the test's filesystem scratchpad
100///
101/// ex: `$CARGO_TARGET_TMPDIR/cit/<integration test>/<module>/<fn name>/`
102/// or `$CARGO_TARGET_TMPDIR/cit/t0` on Windows
103pub fn root() -> PathBuf {
104    let test_dir = TEST_DIR.with(|n| {
105        n.borrow().clone().expect(
106            "Tests must use the `#[cargo_test]` attribute in \
107             order to be able to use the crate root.",
108        )
109    });
110    let mut root = global_root();
111    root.push(&test_dir);
112    root
113}
114/// Path to the current test's `$HOME`
115///
116/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/home`
117pub fn home() -> PathBuf {
118    let mut path = root();
119    path.push("home");
120    path.mkdir_p();
121    path
122}
123
124/// Path to the current test's `$CARGO_HOME`
125///
126/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/home/.cargo`
127pub fn cargo_home() -> PathBuf {
128    home().join(".cargo")
129}
130
131/// Path to the current test's `$CARGO_LOG`
132///
133/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/home/.cargo/log`
134pub fn log_dir() -> PathBuf {
135    cargo_home().join("log")
136}
137
138/// Path to the current test's `$CARGO_LOG` file
139///
140/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/home/.cargo/log/<id>.jsonl`
141///
142/// This also asserts the number of log files is exactly the same as `idx + 1`.
143pub fn log_file(idx: usize) -> PathBuf {
144    let log_dir = log_dir();
145
146    let entries = std::fs::read_dir(&log_dir).unwrap();
147    let mut log_files: Vec<_> = entries
148        .filter_map(Result::ok)
149        .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("jsonl"))
150        .collect();
151
152    // Sort them to get chronological order
153    log_files.sort_unstable_by(|a, b| a.file_name().to_str().cmp(&b.file_name().to_str()));
154
155    assert_eq!(
156        idx + 1,
157        log_files.len(),
158        "unexpected number of log files: {}, expected {}",
159        log_files.len(),
160        idx + 1
161    );
162
163    log_files[idx].path()
164}
165
166/// Common path and file operations
167pub trait CargoPathExt {
168    fn to_url(&self) -> url::Url;
169
170    fn rm_rf(&self);
171    fn mkdir_p(&self);
172
173    /// Returns a list of all files and directories underneath the given
174    /// directory, recursively, including the starting path.
175    fn ls_r(&self) -> Vec<PathBuf>;
176
177    fn move_into_the_past(&self) {
178        self.move_in_time(|sec, nsec| (sec - 3600, nsec))
179    }
180
181    fn move_into_the_future(&self) {
182        self.move_in_time(|sec, nsec| (sec + 3600, nsec))
183    }
184
185    fn move_in_time<F>(&self, travel_amount: F)
186    where
187        F: Fn(i64, u32) -> (i64, u32);
188
189    fn assert_build_dir_layout(&self, expected: impl snapbox::IntoData);
190
191    fn assert_dir_layout(&self, expected: impl snapbox::IntoData, ignored_path_patterns: &[String]);
192}
193
194impl CargoPathExt for Path {
195    fn to_url(&self) -> url::Url {
196        url::Url::from_file_path(self).ok().unwrap()
197    }
198
199    fn rm_rf(&self) {
200        let meta = match self.symlink_metadata() {
201            Ok(meta) => meta,
202            Err(e) => {
203                if e.kind() == ErrorKind::NotFound {
204                    return;
205                }
206                panic!("failed to remove {:?}, could not read: {:?}", self, e);
207            }
208        };
209        // There is a race condition between fetching the metadata and
210        // actually performing the removal, but we don't care all that much
211        // for our tests.
212        if meta.is_dir() {
213            if let Err(e) = fs::remove_dir_all(self) {
214                panic!("failed to remove {:?}: {:?}", self, e)
215            }
216        } else if let Err(e) = fs::remove_file(self) {
217            panic!("failed to remove {:?}: {:?}", self, e)
218        }
219    }
220
221    fn mkdir_p(&self) {
222        fs::create_dir_all(self)
223            .unwrap_or_else(|e| panic!("failed to mkdir_p {}: {}", self.display(), e))
224    }
225
226    fn ls_r(&self) -> Vec<PathBuf> {
227        walkdir::WalkDir::new(self)
228            .sort_by_file_name()
229            .into_iter()
230            .filter_map(|e| e.map(|e| e.path().to_owned()).ok())
231            .collect()
232    }
233
234    fn move_in_time<F>(&self, travel_amount: F)
235    where
236        F: Fn(i64, u32) -> (i64, u32),
237    {
238        if self.is_file() {
239            time_travel(self, &travel_amount);
240        } else {
241            recurse(self, &self.join("target"), &travel_amount);
242        }
243
244        fn recurse<F>(p: &Path, bad: &Path, travel_amount: &F)
245        where
246            F: Fn(i64, u32) -> (i64, u32),
247        {
248            if p.is_file() {
249                time_travel(p, travel_amount)
250            } else if !p.starts_with(bad) {
251                for f in t!(fs::read_dir(p)) {
252                    let f = t!(f).path();
253                    recurse(&f, bad, travel_amount);
254                }
255            }
256        }
257
258        fn time_travel<F>(path: &Path, travel_amount: &F)
259        where
260            F: Fn(i64, u32) -> (i64, u32),
261        {
262            let stat = t!(path.symlink_metadata());
263
264            let mtime = FileTime::from_last_modification_time(&stat);
265
266            let (sec, nsec) = travel_amount(mtime.unix_seconds(), mtime.nanoseconds());
267            let newtime = FileTime::from_unix_time(sec, nsec);
268
269            // Sadly change_file_times has a failure mode where a readonly file
270            // cannot have its times changed on windows.
271            do_op(path, "set file times", |path| {
272                let times = fs::FileTimes::new()
273                    .set_accessed(newtime.into())
274                    .set_modified(newtime.into());
275                fs::set_times(path, times)
276            });
277        }
278    }
279
280    #[track_caller]
281    fn assert_build_dir_layout(&self, expected: impl snapbox::IntoData) {
282        // We call `unordered()` here to because the build-dir has some scenarios that make
283        // consistent ordering not possible.
284        // Notably:
285        // 1. Binaries with `.exe` on Windows causing the ordering to change with the dep-info `.d`
286        //    file.
287        // 2. Directories with hashes are often reordered differently by platform.
288        self.assert_dir_layout(expected.unordered(), &build_dir_ignored_path_patterns());
289    }
290
291    #[track_caller]
292    fn assert_dir_layout(
293        &self,
294        expected: impl snapbox::IntoData,
295        ignored_path_patterns: &[String],
296    ) {
297        let assert = assert_e2e();
298        let actual = WalkDir::new(self)
299            .sort_by_file_name()
300            .into_iter()
301            .filter_map(|e| e.ok())
302            .filter(|e| e.file_type().is_file())
303            .map(|e| e.path().to_string_lossy().into_owned())
304            .filter(|file| {
305                for ignored in ignored_path_patterns {
306                    if match_contains(&ignored, file, &assert.redactions()).is_ok() {
307                        return false;
308                    }
309                }
310                return true;
311            })
312            .join("\n");
313
314        assert.eq(format!("{actual}\n"), expected);
315    }
316}
317
318impl CargoPathExt for PathBuf {
319    fn to_url(&self) -> url::Url {
320        self.as_path().to_url()
321    }
322
323    fn rm_rf(&self) {
324        self.as_path().rm_rf()
325    }
326    fn mkdir_p(&self) {
327        self.as_path().mkdir_p()
328    }
329
330    fn ls_r(&self) -> Vec<PathBuf> {
331        self.as_path().ls_r()
332    }
333
334    fn move_in_time<F>(&self, travel_amount: F)
335    where
336        F: Fn(i64, u32) -> (i64, u32),
337    {
338        self.as_path().move_in_time(travel_amount)
339    }
340
341    #[track_caller]
342    fn assert_build_dir_layout(&self, expected: impl snapbox::IntoData) {
343        self.as_path().assert_build_dir_layout(expected);
344    }
345
346    #[track_caller]
347    fn assert_dir_layout(
348        &self,
349        expected: impl snapbox::IntoData,
350        ignored_path_patterns: &[String],
351    ) {
352        self.as_path()
353            .assert_dir_layout(expected, ignored_path_patterns);
354    }
355}
356
357fn do_op<F>(path: &Path, desc: &str, mut f: F)
358where
359    F: FnMut(&Path) -> io::Result<()>,
360{
361    match f(path) {
362        Ok(()) => {}
363        Err(ref e) if e.kind() == ErrorKind::PermissionDenied => {
364            let mut p = t!(path.metadata()).permissions();
365            p.set_readonly(false);
366            t!(fs::set_permissions(path, p));
367
368            // Unix also requires the parent to not be readonly for example when
369            // removing files
370            let parent = path.parent().unwrap();
371            let mut p = t!(parent.metadata()).permissions();
372            p.set_readonly(false);
373            t!(fs::set_permissions(parent, p));
374
375            f(path).unwrap_or_else(|e| {
376                panic!("failed to {} {}: {}", desc, path.display(), e);
377            })
378        }
379        Err(e) => {
380            panic!("failed to {} {}: {}", desc, path.display(), e);
381        }
382    }
383}
384
385/// The paths to ignore when [`CargoPathExt::assert_build_dir_layout`] is called
386fn build_dir_ignored_path_patterns() -> Vec<String> {
387    vec![
388        // Ignore MacOS debug symbols as there are many files/directories that would clutter up
389        // tests few not a lot of benefit.
390        "[..].dSYM/[..]",
391        // Ignore Windows debug symbols files (.pdb)
392        "[..].pdb",
393    ]
394    .into_iter()
395    .map(ToString::to_string)
396    .collect()
397}
398
399/// Get the filename for a library.
400///
401/// `kind` should be one of:
402/// - `lib`
403/// - `rlib`
404/// - `staticlib`
405/// - `dylib`
406/// - `proc-macro`
407///
408/// # Examples
409/// ```
410/// # use cargo_test_support::paths::get_lib_filename;
411/// get_lib_filename("foo", "dylib");
412/// ```
413/// would return:
414/// - macOS: `"libfoo.dylib"`
415/// - Windows: `"foo.dll"`
416/// - Unix: `"libfoo.so"`
417pub fn get_lib_filename(name: &str, kind: &str) -> String {
418    let prefix = get_lib_prefix(kind);
419    let extension = get_lib_extension(kind);
420    format!("{}{}.{}", prefix, name, extension)
421}
422
423/// See [`get_lib_filename`] for more details
424pub fn get_lib_prefix(kind: &str) -> &str {
425    match kind {
426        "lib" | "rlib" => "lib",
427        "staticlib" | "dylib" | "proc-macro" => {
428            if cfg!(windows) {
429                ""
430            } else {
431                "lib"
432            }
433        }
434        _ => unreachable!(),
435    }
436}
437
438/// See [`get_lib_filename`] for more details
439pub fn get_lib_extension(kind: &str) -> &str {
440    match kind {
441        "lib" | "rlib" => "rlib",
442        "staticlib" => {
443            if cfg!(windows) {
444                "lib"
445            } else {
446                "a"
447            }
448        }
449        "dylib" | "proc-macro" => {
450            if cfg!(windows) {
451                "dll"
452            } else if cfg!(target_os = "macos") {
453                "dylib"
454            } else {
455                "so"
456            }
457        }
458        _ => unreachable!(),
459    }
460}
461
462/// Path to `rustc`s sysroot
463pub fn sysroot() -> String {
464    let output = Command::new("rustc")
465        .arg("--print=sysroot")
466        .output()
467        .expect("rustc to run");
468    assert!(output.status.success());
469    let sysroot = String::from_utf8(output.stdout).unwrap();
470    sysroot.trim().to_string()
471}
472
473/// Returns true if names such as aux.* are allowed.
474///
475/// Traditionally, Windows did not allow a set of file names (see `is_windows_reserved`
476/// for a list). More recent versions of Windows have relaxed this restriction. This test
477/// determines whether we are running in a mode that allows Windows reserved names.
478#[cfg(windows)]
479pub fn windows_reserved_names_are_allowed() -> bool {
480    use std::ffi::OsStr;
481    use std::os::windows::ffi::OsStrExt;
482    use std::ptr;
483    use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW;
484
485    let test_file_name: Vec<_> = OsStr::new("aux.rs").encode_wide().chain([0]).collect();
486
487    let buffer_length =
488        unsafe { GetFullPathNameW(test_file_name.as_ptr(), 0, ptr::null_mut(), ptr::null_mut()) };
489
490    if buffer_length == 0 {
491        // This means the call failed, so we'll conservatively assume reserved names are not allowed.
492        return false;
493    }
494
495    let mut buffer = vec![0u16; buffer_length as usize];
496
497    let result = unsafe {
498        GetFullPathNameW(
499            test_file_name.as_ptr(),
500            buffer_length,
501            buffer.as_mut_ptr(),
502            ptr::null_mut(),
503        )
504    };
505
506    if result == 0 {
507        // Once again, conservatively assume reserved names are not allowed if the
508        // GetFullPathNameW call failed.
509        return false;
510    }
511
512    // Under the old rules, a file name like aux.rs would get converted into \\.\aux, so
513    // we detect this case by checking if the string starts with \\.\
514    //
515    // Otherwise, the filename will be something like C:\Users\Foo\Documents\aux.rs
516    let prefix: Vec<_> = OsStr::new("\\\\.\\").encode_wide().collect();
517    if buffer.starts_with(&prefix) {
518        false
519    } else {
520        true
521    }
522}
523
524/// This takes the test location (std::file!() should be passed) and the test name
525/// and outputs the location the test should be places in, inside of `target/tmp/cit`
526///
527/// `path: tests/testsuite/workspaces.rs`
528/// `name: `workspace_in_git
529/// `output: "testsuite/workspaces/workspace_in_git`
530pub fn test_dir(path: &str, name: &str) -> std::path::PathBuf {
531    let test_dir: std::path::PathBuf = std::path::PathBuf::from(path)
532        .components()
533        // Trim .rs from any files
534        .map(|c| c.as_os_str().to_str().unwrap().trim_end_matches(".rs"))
535        // We only want to take once we have reached `tests` or `src`. This helps when in a
536        // workspace: `workspace/more/src/...` would result in `src/...`
537        .skip_while(|c| c != &"tests" && c != &"src")
538        // We want to skip "tests" since it is taken in `skip_while`.
539        // "src" is fine since you could have test in "src" named the same as one in "tests"
540        // Skip "mod" since `snapbox` tests have a folder per test not a file and the files
541        // are named "mod.rs"
542        .filter(|c| c != &"tests" && c != &"mod")
543        .collect();
544    test_dir.join(name)
545}
546
547/// Temporarily sets a file or directory as read only until
548/// this struct is dropped.
549/// Useful to make sure tests reset permissions if they fail.
550pub struct ReadOnly(PathBuf);
551
552impl ReadOnly {
553    pub fn new(path: impl Into<PathBuf>) -> Self {
554        let path = path.into();
555        chmod_readonly(&path, true);
556
557        Self(path)
558    }
559}
560
561impl Drop for ReadOnly {
562    fn drop(&mut self) {
563        chmod_readonly(&self.0, false);
564    }
565}
566
567fn chmod_readonly(path: &Path, readonly: bool) {
568    if path.is_file() {
569        set_readonly(path, readonly);
570        return;
571    }
572
573    for entry in t!(path.read_dir()) {
574        let entry = t!(entry);
575        let path = entry.path();
576        if t!(entry.file_type()).is_dir() {
577            chmod_readonly(&path, readonly);
578        } else {
579            set_readonly(&path, readonly);
580        }
581    }
582    set_readonly(path, readonly);
583}
584
585fn set_readonly(path: &Path, readonly: bool) {
586    let mut perms = t!(path.metadata()).permissions();
587    perms.set_readonly(readonly);
588    t!(fs::set_permissions(path, perms));
589}