cargo/tests/cargotest/support/paths.rs
2016-07-27 19:33:39 -04:00

163 lines
4.8 KiB
Rust

use std::env;
use std::cell::Cell;
use std::fs;
use std::io::{self, ErrorKind};
use std::path::{Path, PathBuf};
use std::sync::{Once, ONCE_INIT};
use std::sync::atomic::{AtomicUsize, ATOMIC_USIZE_INIT, Ordering};
use filetime::{self, FileTime};
static CARGO_INTEGRATION_TEST_DIR : &'static str = "cit";
static NEXT_ID: AtomicUsize = ATOMIC_USIZE_INIT;
thread_local!(static TASK_ID: usize = NEXT_ID.fetch_add(1, Ordering::SeqCst));
fn init() {
static GLOBAL_INIT: Once = ONCE_INIT;
thread_local!(static LOCAL_INIT: Cell<bool> = Cell::new(false));
GLOBAL_INIT.call_once(|| {
global_root().mkdir_p();
});
LOCAL_INIT.with(|i| {
if i.get() {
return
}
i.set(true);
root().rm_rf();
home().mkdir_p();
})
}
fn global_root() -> PathBuf {
let mut path = t!(env::current_exe());
path.pop(); // chop off exe name
path.pop(); // chop off 'debug'
// If `cargo test` is run manually then our path looks like
// `target/debug/foo`, in which case our `path` is already pointing at
// `target`. If, however, `cargo test --target $target` is used then the
// output is `target/$target/debug/foo`, so our path is pointing at
// `target/$target`. Here we conditionally pop the `$target` name.
if path.file_name().and_then(|s| s.to_str()) != Some("target") {
path.pop();
}
path.join(CARGO_INTEGRATION_TEST_DIR)
}
pub fn root() -> PathBuf {
init();
global_root().join(&TASK_ID.with(|my_id| format!("t{}", my_id)))
}
pub fn home() -> PathBuf {
root().join("home")
}
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, F)
where F: Fn(u64, u32) -> (u64, u32);
}
impl CargoPathExt for Path {
/* Technically there is a potential race condition, but we don't
* care all that much for our tests
*/
fn rm_rf(&self) {
if !self.exists() {
return
}
for file in t!(fs::read_dir(self)) {
let file = t!(file).path();
if file.is_dir() {
file.rm_rf();
} else {
// On windows we can't remove a readonly file, and git will
// often clone files as readonly. As a result, we have some
// special logic to remove readonly files on windows.
do_op(&file, "remove file", |p| fs::remove_file(p));
}
}
do_op(self, "remove dir", |p| fs::remove_dir(p));
}
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(u64, u32) -> ((u64, 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(u64, u32) -> ((u64, 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(u64, u32) -> ((u64, u32)),
{
let stat = t!(path.metadata());
let mtime = FileTime::from_last_modification_time(&stat);
let (sec, nsec) = travel_amount(mtime.seconds_relative_to_1970(), mtime.nanoseconds());
let newtime = FileTime::from_seconds_since_1970(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 cfg!(windows) &&
e.kind() == ErrorKind::PermissionDenied => {
let mut p = t!(path.metadata()).permissions();
p.set_readonly(false);
t!(fs::set_permissions(path, p));
f(path).unwrap_or_else(|e| {
panic!("failed to {} {}: {}", desc, path.display(), e);
})
}
Err(e) => {
panic!("failed to {} {}: {}", desc, path.display(), e);
}
}
}