mod utils {
use std::sync::atomic::{AtomicI32, Ordering};
use once_cell::sync::Lazy;
static INSTANCE_ID: Lazy<i64> = Lazy::new(|| chrono::Local::now().timestamp_millis());
static ID_OFFSET: AtomicI32 = AtomicI32::new(0);
pub fn get_unique_name() -> String {
let id = ID_OFFSET.fetch_add(1, Ordering::SeqCst);
format!("trash-test-{}-{}", *INSTANCE_ID, id)
}
pub fn init_logging() {
let _ = env_logger::builder().is_test(true).try_init();
}
}
pub use utils::{get_unique_name, init_logging};
#[cfg(any(
target_os = "windows",
all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android"))
))]
mod os_limited {
use super::{get_unique_name, init_logging};
use serial_test::serial;
use std::collections::{hash_map::Entry, HashMap};
use std::ffi::{OsStr, OsString};
use std::fs::File;
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))]
use std::os::unix::ffi::OsStringExt;
use crate as trash;
#[test]
#[serial]
fn list() {
const MAX_SECONDS_DIFFERENCE: i64 = 10;
init_logging();
let deletion_time = chrono::Utc::now();
let actual_unix_deletion_time = deletion_time.naive_utc().timestamp();
assert_eq!(actual_unix_deletion_time, deletion_time.naive_local().timestamp());
let file_name_prefix = get_unique_name();
let batches: usize = 2;
let files_per_batch: usize = 3;
let names: Vec<OsString> = (0..files_per_batch).map(|i| format!("{}#{}", file_name_prefix, i).into()).collect();
for _ in 0..batches {
for path in names.iter() {
File::create_new(path).unwrap();
}
trash::delete_all(&names).unwrap();
}
let items = trash::os_limited::list().unwrap();
let items: HashMap<_, Vec<_>> = items
.into_iter()
.filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))
.fold(HashMap::new(), |mut map, x| {
match map.entry(x.name.clone()) {
Entry::Occupied(mut entry) => {
entry.get_mut().push(x);
}
Entry::Vacant(entry) => {
entry.insert(vec![x]);
}
}
map
});
for name in names {
match items.get(&name) {
Some(items) => {
assert_eq!(items.len(), batches);
for item in items {
if cfg!(feature = "chrono") {
let diff = (item.time_deleted - actual_unix_deletion_time).abs();
if diff > MAX_SECONDS_DIFFERENCE {
panic!(
"The deleted item does not have the timestamp that represents its deletion time. Expected: {}. Got: {}",
actual_unix_deletion_time,
item.time_deleted
);
}
}
}
}
None => panic!("ERROR Could not find '{:?}' in {:#?}", name, items),
}
}
let _ = trash::os_limited::purge_all(items.iter().flat_map(|(_name, item)| item));
}
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))]
#[test]
#[serial]
fn list_invalid_utf8() {
let mut name = OsStr::new(&get_unique_name()).to_os_string().into_encoded_bytes();
name.push(168);
let name = OsString::from_vec(name);
File::create_new(&name).unwrap();
trash::delete(&name).unwrap();
let item = trash::os_limited::list().unwrap().into_iter().find(|item| item.name == name).unwrap();
let _ = trash::os_limited::purge_all([item]);
}
#[test]
fn purge_empty() {
init_logging();
trash::os_limited::purge_all::<Vec<trash::TrashItem>>(vec![]).unwrap();
}
#[test]
fn restore_empty() {
init_logging();
trash::os_limited::restore_all(vec![]).unwrap();
}
#[test]
#[serial]
fn purge() {
init_logging();
let file_name_prefix = get_unique_name();
let batches: usize = 2;
let files_per_batch: usize = 3;
let names: Vec<_> = (0..files_per_batch).map(|i| format!("{}#{}", file_name_prefix, i)).collect();
for _ in 0..batches {
for path in names.iter() {
File::create_new(path).unwrap();
}
trash::delete_all(&names).unwrap();
}
let targets: Vec<_> = trash::os_limited::list()
.unwrap()
.into_iter()
.filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))
.collect();
assert_eq!(targets.len(), batches * files_per_batch);
trash::os_limited::purge_all(targets).unwrap();
let remaining = trash::os_limited::list()
.unwrap()
.into_iter()
.filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))
.count();
assert_eq!(remaining, 0);
}
#[test]
#[serial]
fn restore() {
init_logging();
let file_name_prefix = get_unique_name();
let file_count: usize = 3;
let names: Vec<_> = (0..file_count).map(|i| format!("{}#{}", file_name_prefix, i)).collect();
for path in names.iter() {
File::create_new(path).unwrap();
}
trash::delete_all(&names).unwrap();
let targets: Vec<_> = trash::os_limited::list()
.unwrap()
.into_iter()
.filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))
.collect();
assert_eq!(targets.len(), file_count);
trash::os_limited::restore_all(targets).unwrap();
let remaining = trash::os_limited::list()
.unwrap()
.into_iter()
.filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))
.count();
assert_eq!(remaining, 0);
let mut missing = Vec::new();
for path in names.iter() {
if !std::path::Path::new(&path).is_file() {
missing.push(path);
}
}
for path in names.iter() {
std::fs::remove_file(path).ok();
}
assert_eq!(missing, Vec::<&String>::new());
}
#[test]
#[serial]
fn restore_collision() {
init_logging();
let file_name_prefix = get_unique_name();
let file_count: usize = 3;
let collision_remaining = file_count - 1;
let names: Vec<_> = (0..file_count).map(|i| format!("{}#{}", file_name_prefix, i)).collect();
for path in names.iter() {
File::create_new(path).unwrap();
}
trash::delete_all(&names).unwrap();
for path in names.iter().skip(file_count - collision_remaining) {
File::create_new(path).unwrap();
}
let mut targets: Vec<_> = trash::os_limited::list()
.unwrap()
.into_iter()
.filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))
.collect();
targets.sort_by(|a, b| a.name.cmp(&b.name));
assert_eq!(targets.len(), file_count);
let remaining_count = match trash::os_limited::restore_all(targets) {
Err(trash::Error::RestoreCollision { remaining_items, .. }) => {
let contains = |v: &Vec<trash::TrashItem>, name: &String| {
for curr in v.iter() {
if curr.name.as_encoded_bytes() == name.as_bytes() {
return true;
}
}
false
};
for path in names.iter().filter(|filename| !contains(&remaining_items, filename)) {
assert!(File::open(path).is_ok());
}
remaining_items.len()
}
_ => {
for path in names.iter() {
std::fs::remove_file(path).ok();
}
panic!("restore_all was expected to return `trash::ErrorKind::RestoreCollision` but did not.");
}
};
let remaining = trash::os_limited::list()
.unwrap()
.into_iter()
.filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))
.collect::<Vec<_>>();
assert_eq!(remaining.len(), remaining_count);
trash::os_limited::purge_all(remaining).unwrap();
for path in names.iter() {
std::fs::remove_file(path).ok();
}
}
#[test]
#[serial]
fn restore_twins() {
init_logging();
let file_name_prefix = get_unique_name();
let file_count: usize = 4;
let names: Vec<_> = (0..file_count).map(|i| format!("{}#{}", file_name_prefix, i)).collect();
for path in names.iter() {
File::create_new(path).unwrap();
}
trash::delete_all(&names).unwrap();
let twin_name = &names[1];
File::create_new(twin_name).unwrap();
trash::delete(twin_name).unwrap();
let mut targets: Vec<_> = trash::os_limited::list()
.unwrap()
.into_iter()
.filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))
.collect();
targets.sort_by(|a, b| a.name.cmp(&b.name));
assert_eq!(targets.len(), file_count + 1); match trash::os_limited::restore_all(targets) {
Err(trash::Error::RestoreTwins { path, items }) => {
assert_eq!(path.file_name().unwrap().to_str().unwrap(), twin_name);
trash::os_limited::purge_all(items).unwrap();
}
_ => panic!("restore_all was expected to return `trash::ErrorKind::RestoreTwins` but did not."),
}
}
#[test]
#[serial]
fn is_empty_matches_list() {
init_logging();
let is_empty_list = trash::os_limited::list().unwrap().is_empty();
let is_empty = trash::os_limited::is_empty().unwrap();
assert_eq!(is_empty, is_empty_list, "is_empty() should match empty status from list()");
}
}