summaryrefslogtreecommitdiffstats
path: root/crates/cargo-test-support/src/paths.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/cargo-test-support/src/paths.rs')
-rw-r--r--crates/cargo-test-support/src/paths.rs347
1 files changed, 347 insertions, 0 deletions
diff --git a/crates/cargo-test-support/src/paths.rs b/crates/cargo-test-support/src/paths.rs
new file mode 100644
index 0000000..ef1fddb
--- /dev/null
+++ b/crates/cargo-test-support/src/paths.rs
@@ -0,0 +1,347 @@
+use filetime::{self, FileTime};
+use lazy_static::lazy_static;
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::env;
+use std::fs;
+use std::io::{self, ErrorKind};
+use std::path::{Path, PathBuf};
+use std::process::Command;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::Mutex;
+
+static CARGO_INTEGRATION_TEST_DIR: &str = "cit";
+
+lazy_static! {
+ // TODO: Use `SyncOnceCell` when stable
+ static ref GLOBAL_ROOT: Mutex<Option<PathBuf>> = Mutex::new(None);
+
+ static ref TEST_ROOTS: Mutex<HashMap<String, PathBuf>> = Default::default();
+}
+
+/// This is used when running cargo is pre-CARGO_TARGET_TMPDIR
+/// TODO: Remove when CARGO_TARGET_TMPDIR grows old enough.
+fn global_root_legacy() -> PathBuf {
+ let mut path = t!(env::current_exe());
+ path.pop(); // chop off exe name
+ path.pop(); // chop off "deps"
+ path.push("tmp");
+ path.mkdir_p();
+ path
+}
+
+fn set_global_root(tmp_dir: Option<&'static str>) {
+ let mut lock = GLOBAL_ROOT.lock().unwrap();
+ if lock.is_none() {
+ let mut root = match tmp_dir {
+ Some(tmp_dir) => PathBuf::from(tmp_dir),
+ None => global_root_legacy(),
+ };
+
+ root.push(CARGO_INTEGRATION_TEST_DIR);
+ *lock = Some(root);
+ }
+}
+
+pub fn global_root() -> PathBuf {
+ let lock = GLOBAL_ROOT.lock().unwrap();
+ match lock.as_ref() {
+ Some(p) => p.clone(),
+ None => unreachable!("GLOBAL_ROOT not set yet"),
+ }
+}
+
+// We need to give each test a unique id. The test name could serve this
+// purpose, but the `test` crate doesn't have a way to obtain the current test
+// name.[*] Instead, we used the `cargo-test-macro` crate to automatically
+// insert an init function for each test that sets the test name in a thread
+// local variable.
+//
+// [*] It does set the thread name, but only when running concurrently. If not
+// running concurrently, all tests are run on the main thread.
+thread_local! {
+ static TEST_ID: RefCell<Option<usize>> = RefCell::new(None);
+}
+
+pub struct TestIdGuard {
+ _private: (),
+}
+
+pub fn init_root(tmp_dir: Option<&'static str>) -> TestIdGuard {
+ static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
+
+ let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
+ TEST_ID.with(|n| *n.borrow_mut() = Some(id));
+
+ let guard = TestIdGuard { _private: () };
+
+ set_global_root(tmp_dir);
+ let r = root();
+ r.rm_rf();
+ r.mkdir_p();
+
+ guard
+}
+
+impl Drop for TestIdGuard {
+ fn drop(&mut self) {
+ TEST_ID.with(|n| *n.borrow_mut() = None);
+ }
+}
+
+pub fn root() -> PathBuf {
+ let id = TEST_ID.with(|n| {
+ n.borrow().expect(
+ "Tests must use the `#[cargo_test]` attribute in \
+ order to be able to use the crate root.",
+ )
+ });
+
+ let mut root = global_root();
+ root.push(&format!("t{}", id));
+ root
+}
+
+pub fn home() -> PathBuf {
+ let mut path = root();
+ path.push("home");
+ path.mkdir_p();
+ path
+}
+
+pub trait CargoPathExt {
+ fn rm_rf(&self);
+ fn mkdir_p(&self);
+
+ fn move_into_the_past(&self) {
+ self.move_in_time(|sec, nsec| (sec - 3600, nsec))
+ }
+
+ fn move_into_the_future(&self) {
+ self.move_in_time(|sec, nsec| (sec + 3600, nsec))
+ }
+
+ fn move_in_time<F>(&self, travel_amount: F)
+ where
+ F: Fn(i64, u32) -> (i64, u32);
+}
+
+impl CargoPathExt for Path {
+ fn rm_rf(&self) {
+ let meta = match self.symlink_metadata() {
+ Ok(meta) => meta,
+ Err(e) => {
+ if e.kind() == ErrorKind::NotFound {
+ return;
+ }
+ panic!("failed to remove {:?}, could not read: {:?}", self, e);
+ }
+ };
+ // There is a race condition between fetching the metadata and
+ // actually performing the removal, but we don't care all that much
+ // for our tests.
+ if meta.is_dir() {
+ if let Err(e) = fs::remove_dir_all(self) {
+ panic!("failed to remove {:?}: {:?}", self, e)
+ }
+ } else if let Err(e) = fs::remove_file(self) {
+ panic!("failed to remove {:?}: {:?}", self, e)
+ }
+ }
+
+ fn mkdir_p(&self) {
+ fs::create_dir_all(self)
+ .unwrap_or_else(|e| panic!("failed to mkdir_p {}: {}", self.display(), e))
+ }
+
+ fn move_in_time<F>(&self, travel_amount: F)
+ where
+ F: Fn(i64, u32) -> (i64, u32),
+ {
+ if self.is_file() {
+ time_travel(self, &travel_amount);
+ } else {
+ recurse(self, &self.join("target"), &travel_amount);
+ }
+
+ fn recurse<F>(p: &Path, bad: &Path, travel_amount: &F)
+ where
+ F: Fn(i64, u32) -> (i64, u32),
+ {
+ if p.is_file() {
+ time_travel(p, travel_amount)
+ } else if !p.starts_with(bad) {
+ for f in t!(fs::read_dir(p)) {
+ let f = t!(f).path();
+ recurse(&f, bad, travel_amount);
+ }
+ }
+ }
+
+ fn time_travel<F>(path: &Path, travel_amount: &F)
+ where
+ F: Fn(i64, u32) -> (i64, u32),
+ {
+ let stat = t!(path.symlink_metadata());
+
+ let mtime = FileTime::from_last_modification_time(&stat);
+
+ let (sec, nsec) = travel_amount(mtime.unix_seconds(), mtime.nanoseconds());
+ let newtime = FileTime::from_unix_time(sec, nsec);
+
+ // Sadly change_file_times has a failure mode where a readonly file
+ // cannot have its times changed on windows.
+ do_op(path, "set file times", |path| {
+ filetime::set_file_times(path, newtime, newtime)
+ });
+ }
+ }
+}
+
+fn do_op<F>(path: &Path, desc: &str, mut f: F)
+where
+ F: FnMut(&Path) -> io::Result<()>,
+{
+ match f(path) {
+ Ok(()) => {}
+ Err(ref e) if e.kind() == ErrorKind::PermissionDenied => {
+ let mut p = t!(path.metadata()).permissions();
+ p.set_readonly(false);
+ t!(fs::set_permissions(path, p));
+
+ // Unix also requires the parent to not be readonly for example when
+ // removing files
+ let parent = path.parent().unwrap();
+ let mut p = t!(parent.metadata()).permissions();
+ p.set_readonly(false);
+ t!(fs::set_permissions(parent, p));
+
+ f(path).unwrap_or_else(|e| {
+ panic!("failed to {} {}: {}", desc, path.display(), e);
+ })
+ }
+ Err(e) => {
+ panic!("failed to {} {}: {}", desc, path.display(), e);
+ }
+ }
+}
+
+/// Get the filename for a library.
+///
+/// `kind` should be one of: "lib", "rlib", "staticlib", "dylib", "proc-macro"
+///
+/// For example, dynamic library named "foo" would return:
+/// - macOS: "libfoo.dylib"
+/// - Windows: "foo.dll"
+/// - Unix: "libfoo.so"
+pub fn get_lib_filename(name: &str, kind: &str) -> String {
+ let prefix = get_lib_prefix(kind);
+ let extension = get_lib_extension(kind);
+ format!("{}{}.{}", prefix, name, extension)
+}
+
+pub fn get_lib_prefix(kind: &str) -> &str {
+ match kind {
+ "lib" | "rlib" => "lib",
+ "staticlib" | "dylib" | "proc-macro" => {
+ if cfg!(windows) {
+ ""
+ } else {
+ "lib"
+ }
+ }
+ _ => unreachable!(),
+ }
+}
+
+pub fn get_lib_extension(kind: &str) -> &str {
+ match kind {
+ "lib" | "rlib" => "rlib",
+ "staticlib" => {
+ if cfg!(windows) {
+ "lib"
+ } else {
+ "a"
+ }
+ }
+ "dylib" | "proc-macro" => {
+ if cfg!(windows) {
+ "dll"
+ } else if cfg!(target_os = "macos") {
+ "dylib"
+ } else {
+ "so"
+ }
+ }
+ _ => unreachable!(),
+ }
+}
+
+/// Returns the sysroot as queried from rustc.
+pub fn sysroot() -> String {
+ let output = Command::new("rustc")
+ .arg("--print=sysroot")
+ .output()
+ .expect("rustc to run");
+ assert!(output.status.success());
+ let sysroot = String::from_utf8(output.stdout).unwrap();
+ sysroot.trim().to_string()
+}
+
+/// Returns true if names such as aux.* are allowed.
+///
+/// Traditionally, Windows did not allow a set of file names (see `is_windows_reserved`
+/// for a list). More recent versions of Windows have relaxed this restriction. This test
+/// determines whether we are running in a mode that allows Windows reserved names.
+#[cfg(windows)]
+pub fn windows_reserved_names_are_allowed() -> bool {
+ use cargo_util::is_ci;
+
+ // Ensure tests still run in CI until we need to migrate.
+ if is_ci() {
+ return false;
+ }
+
+ use std::ffi::OsStr;
+ use std::os::windows::ffi::OsStrExt;
+ use std::ptr;
+ use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW;
+
+ let test_file_name: Vec<_> = OsStr::new("aux.rs").encode_wide().collect();
+
+ let buffer_length =
+ unsafe { GetFullPathNameW(test_file_name.as_ptr(), 0, ptr::null_mut(), ptr::null_mut()) };
+
+ if buffer_length == 0 {
+ // This means the call failed, so we'll conservatively assume reserved names are not allowed.
+ return false;
+ }
+
+ let mut buffer = vec![0u16; buffer_length as usize];
+
+ let result = unsafe {
+ GetFullPathNameW(
+ test_file_name.as_ptr(),
+ buffer_length,
+ buffer.as_mut_ptr(),
+ ptr::null_mut(),
+ )
+ };
+
+ if result == 0 {
+ // Once again, conservatively assume reserved names are not allowed if the
+ // GetFullPathNameW call failed.
+ return false;
+ }
+
+ // Under the old rules, a file name like aux.rs would get converted into \\.\aux, so
+ // we detect this case by checking if the string starts with \\.\
+ //
+ // Otherwise, the filename will be something like C:\Users\Foo\Documents\aux.rs
+ let prefix: Vec<_> = OsStr::new("\\\\.\\").encode_wide().collect();
+ if buffer.starts_with(&prefix) {
+ false
+ } else {
+ true
+ }
+}