use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use std::thread;
use ripsync_core::Filter;
use ripsync_core::RunControl;
use ripsync_core::net::proto::{NetOptions, Role};
use ripsync_core::net::{duplex_pair, run_initiator, run_responder};
use ripsync_core::report::NullReporter;
fn snapshot(root: &Path) -> BTreeMap<String, String> {
let mut out = BTreeMap::new();
fn walk(root: &Path, dir: &Path, out: &mut BTreeMap<String, String>) {
for ent in fs::read_dir(dir).unwrap() {
let ent = ent.unwrap();
let path = ent.path();
let rel = path
.strip_prefix(root)
.unwrap()
.to_string_lossy()
.replace('\\', "/");
let meta = fs::symlink_metadata(&path).unwrap();
if meta.file_type().is_symlink() {
let target = fs::read_link(&path).unwrap();
out.insert(rel, format!("<sym:{}>", target.to_string_lossy()));
} else if meta.is_dir() {
out.insert(rel, "<dir>".to_string());
walk(root, &path, out);
} else {
out.insert(
rel,
String::from_utf8_lossy(&fs::read(&path).unwrap()).into_owned(),
);
}
}
}
walk(root, root, &mut out);
out
}
fn run_transfer(role: Role, local_root: &Path, remote_root: &Path, options: NetOptions) {
let (mut end_a, mut end_b) = duplex_pair();
let local = local_root.to_path_buf();
let remote = remote_root.to_path_buf();
let server = thread::spawn(move || {
let ctrl = RunControl::default();
let excludes = Filter::none();
run_responder(&mut end_b, &excludes, 0, &ctrl, &NullReporter)
});
let ctrl = RunControl::default();
let excludes = Filter::none();
let stats = run_initiator(
&mut end_a,
role,
&local,
&remote,
options,
&excludes,
0,
&ctrl,
&NullReporter,
)
.expect("initiator");
server.join().unwrap().expect("responder");
let _ = stats;
}
fn build_source(src: &Path) {
fs::create_dir_all(src.join("sub")).unwrap();
fs::write(src.join("keep.txt"), "identical contents\n").unwrap();
fs::write(
src.join("changed.txt"),
"the new improved contents, longer than before\n",
)
.unwrap();
fs::write(src.join("brand_new.txt"), "freshly created file\n").unwrap();
fs::write(src.join("sub/nested.bin"), vec![7u8; 4096]).unwrap();
#[cfg(unix)]
std::os::unix::fs::symlink("keep.txt", src.join("link")).unwrap();
}
fn build_stale_dest(dst: &Path) {
fs::create_dir_all(dst).unwrap();
fs::write(dst.join("keep.txt"), "identical contents\n").unwrap();
fs::write(dst.join("changed.txt"), "old contents\n").unwrap();
fs::write(dst.join("obsolete.txt"), "remove me\n").unwrap();
}
#[test]
fn push_syncs_source_into_dest() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
let dst = tmp.path().join("dst");
build_source(&src);
build_stale_dest(&dst);
let options = NetOptions {
delete: true,
preserve_mtime: true,
..NetOptions::default()
};
run_transfer(Role::Push, &src, &dst, options);
let want = snapshot(&src);
let got = snapshot(&dst);
assert_eq!(want, got, "push: dst must match src exactly");
assert!(
!dst.join("obsolete.txt").exists(),
"stale file must be deleted"
);
assert_eq!(
fs::read_to_string(dst.join("changed.txt")).unwrap(),
"the new improved contents, longer than before\n"
);
}
#[test]
fn pull_syncs_remote_source_into_local_dest() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("remote_src");
let dst = tmp.path().join("local_dst");
build_source(&src);
build_stale_dest(&dst);
let options = NetOptions {
delete: true,
preserve_mtime: true,
..NetOptions::default()
};
run_transfer(Role::Pull, &dst, &src, options);
let want = snapshot(&src);
let got = snapshot(&dst);
assert_eq!(want, got, "pull: dst must match src exactly");
assert!(
!dst.join("obsolete.txt").exists(),
"stale file must be deleted"
);
}
#[test]
fn push_with_compression_roundtrips() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
let dst = tmp.path().join("dst");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("big.log"), "A".repeat(200_000)).unwrap();
fs::write(src.join("note.txt"), "small\n").unwrap();
let options = NetOptions {
whole_file: true,
compress: true,
compress_level: 6,
..NetOptions::default()
};
run_transfer(Role::Push, &src, &dst, options);
assert_eq!(
snapshot(&src),
snapshot(&dst),
"compressed push must match src"
);
assert_eq!(
fs::read_to_string(dst.join("big.log")).unwrap(),
"A".repeat(200_000)
);
}
#[test]
fn push_into_empty_dest_copies_everything() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
let dst = tmp.path().join("dst");
build_source(&src);
run_transfer(Role::Push, &src, &dst, NetOptions::default());
assert_eq!(
snapshot(&src),
snapshot(&dst),
"fresh push must clone the tree"
);
}