use super::*;
#[cfg(unix)]
#[test]
fn a_job_asks_about_a_failure_and_goes_on_when_told_to_skip() {
let dir = std::env::temp_dir().join(format!("fm84-ask-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let tree = dir.join("tree");
std::fs::create_dir_all(&tree).unwrap();
std::fs::write(tree.join("a.txt"), "a").unwrap();
let pipe = std::ffi::CString::new(tree.join("pipe").to_str().unwrap()).unwrap();
assert_eq!(unsafe { libc::mkfifo(pipe.as_ptr(), 0o600) }, 0);
std::fs::write(tree.join("z.txt"), "z").unwrap();
let mut app_state = AppState::new();
app_state.options = Options::default();
app_state.start_transfer(vec![(tree.clone(), dir.join("copy"), true)], true, crate::app::Clashes::Ask);
let started = std::time::Instant::now();
let wait = |app_state: &mut AppState, done: &dyn Fn(&AppState) -> bool| {
while !done(app_state) {
assert!(started.elapsed() < std::time::Duration::from_secs(10), "the job never got there");
std::thread::sleep(std::time::Duration::from_millis(5));
app_state.poll_transfer();
}
};
wait(&mut app_state, &|app_state| app_state.job.as_ref().is_some_and(|job| job.problem.is_some()));
assert_eq!(app_state.job.as_ref().unwrap().problem.as_ref().unwrap().path, tree.join("pipe"));
app_state.job.as_mut().unwrap().answer(crate::app::Answer::Skip);
wait(&mut app_state, &|app_state| app_state.job.is_none());
assert_eq!(app_state.error.as_deref(), Some("Copy finished, 1 entry skipped"));
assert!(dir.join("copy").join("a.txt").exists());
assert!(dir.join("copy").join("z.txt").exists());
app_state.start_transfer(vec![(tree.clone(), dir.join("again"), true)], true, crate::app::Clashes::Ask);
wait(&mut app_state, &|app_state| app_state.job.as_ref().is_some_and(|job| job.problem.is_some()));
app_state.cancel_transfer();
wait(&mut app_state, &|app_state| app_state.job.is_none());
assert!(app_state.error.as_deref().unwrap_or_default().starts_with("Copy cancelled"), "{:?}", app_state.error);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn esc_answers_a_question_still_on_its_way() {
let dir = std::env::temp_dir().join(format!("fm84-esc-race-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("onto")).unwrap();
std::fs::write(dir.join("a.txt"), "new").unwrap();
std::fs::write(dir.join("onto").join("a.txt"), "old").unwrap();
let mut app_state = AppState::new();
app_state.options = Options::default();
app_state.start_transfer(vec![(dir.join("a.txt"), dir.join("onto").join("a.txt"), false)], true, crate::app::Clashes::Ask);
std::thread::sleep(std::time::Duration::from_millis(200));
app_state.cancel_transfer();
let started = std::time::Instant::now();
while app_state.job.is_some() {
assert!(app_state.job.as_ref().is_some_and(|job| job.clash.is_none()), "the question came up after Esc");
assert!(started.elapsed() < std::time::Duration::from_secs(10), "the job never ended");
std::thread::sleep(std::time::Duration::from_millis(5));
app_state.poll_transfer();
}
assert_eq!(std::fs::read_to_string(dir.join("onto").join("a.txt")).unwrap(), "old");
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn files_in_the_way_are_asked_about_one_at_a_time() {
use crate::app::{Answer, Clashes};
let dir = std::env::temp_dir().join(format!("fm84-clashes-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let then = |seconds: u64| std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(seconds);
let write = |path: &std::path::Path, text: &str, at: u64| {
std::fs::write(path, text).unwrap();
std::fs::File::options().write(true).open(path).unwrap().set_modified(then(at)).unwrap();
};
let fresh = |name: &str| {
let (from, onto) = (dir.join(name).join("from"), dir.join(name).join("onto"));
std::fs::create_dir_all(&from).unwrap();
std::fs::create_dir_all(&onto).unwrap();
for file in ["a", "b", "c"] {
write(&from.join(file), "new", 2_000_000_000);
write(&onto.join(file), "old", if file == "c" { 2_100_000_000 } else { 1_000_000_000 });
}
write(&from.join("only-new"), "new", 2_000_000_000);
(from, onto)
};
let run = |clashes: Clashes, answer: &dyn Fn(&str) -> Answer, from: &std::path::Path, onto: &std::path::Path| {
let mut app_state = AppState::new();
app_state.start_transfer(vec![(from.to_path_buf(), onto.to_path_buf(), true)], true, clashes);
let (mut asked, started) = (0, std::time::Instant::now());
while let Some(job) = app_state.job.as_mut() {
assert!(started.elapsed() < std::time::Duration::from_secs(10), "the job never finished");
if let Some(clash) = &job.clash {
let name = clash.dest.file_name().unwrap().to_string_lossy().into_owned();
asked += 1;
job.answer(answer(&name));
}
std::thread::sleep(std::time::Duration::from_millis(2));
app_state.poll_transfer();
}
let held: Vec<String> = ["a", "b", "c", "only-new"].iter().map(|file| std::fs::read_to_string(onto.join(file)).unwrap()).collect();
(asked, held, app_state.error)
};
let (from, onto) = fresh("each");
let (asked, held, said) = run(Clashes::Ask, &|name| if name == "b" { Answer::Skip } else { Answer::Overwrite }, &from, &onto);
assert_eq!(asked, 3);
assert_eq!(held, ["new", "old", "new", "new"]);
assert_eq!(said.as_deref(), Some("Copy finished, 1 entry skipped"));
let (from, onto) = fresh("all");
let (asked, held, _) = run(Clashes::Ask, &|_| Answer::OverwriteAll, &from, &onto);
assert_eq!((asked, held), (1, vec!["new".to_string(); 4]));
let (from, onto) = fresh("none");
let (asked, held, said) = run(Clashes::Ask, &|_| Answer::SkipAll, &from, &onto);
assert_eq!((asked, held), (1, vec!["old".into(), "old".into(), "old".into(), "new".into()]));
assert_eq!(said.as_deref(), Some("Copy finished, 3 entries skipped"));
let (from, onto) = fresh("older");
let (asked, held, _) = run(Clashes::Ask, &|_| Answer::OverwriteOlder, &from, &onto);
assert_eq!((asked, held), (1, vec!["new".into(), "new".into(), "old".into(), "new".into()]));
let (from, onto) = fresh("overwrite");
let (asked, held, _) = run(Clashes::ReplaceAll, &|_| Answer::Abort, &from, &onto);
assert_eq!((asked, held), (0, vec!["new".to_string(); 4]));
let beside = |onto: &std::path::Path| ["a (2)", "b (2)", "c (2)"].map(|file| std::fs::read_to_string(onto.join(file)).ok());
let (from, onto) = fresh("keep-one");
let (asked, held, said) = run(Clashes::Ask, &|name| if name == "b" { Answer::KeepBoth } else { Answer::Skip }, &from, &onto);
assert_eq!((asked, held), (3, vec!["old".into(), "old".into(), "old".into(), "new".into()]));
assert_eq!(beside(&onto), [None, Some("new".to_string()), None]);
assert_eq!(said.as_deref(), Some("Copy finished, 2 entries skipped"));
let (from, onto) = fresh("keep-all");
let (asked, held, said) = run(Clashes::Ask, &|_| Answer::KeepBothAll, &from, &onto);
assert_eq!((asked, held, said), (1, vec!["old".into(), "old".into(), "old".into(), "new".into()], None));
assert_eq!(beside(&onto), [Some("new".to_string()), Some("new".to_string()), Some("new".to_string())]);
let (from, onto) = fresh("keep-f11");
let (asked, _, _) = run(Clashes::KeepBothAll, &|_| Answer::Abort, &from, &onto);
assert_eq!((asked, beside(&onto).iter().flatten().count()), (0, 3));
std::fs::remove_dir_all(&dir).unwrap();
}