| 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 |
| } |
| } |