feat: --verbose per-file progress for sync + serve-all QUIC integration tests
- Add ProgressEvent enum (TotalFiles / FileAcked) to clawsync-fs - Add FsSyncClient::with_progress() builder accepting an UnboundedSender<ProgressEvent>; emits one TotalFiles before RTT2 and one FileAcked per completed file - Add --verbose / -v flag to `clawsync sync`: spawns a background printer that reports each synced file and byte count to stderr as acks arrive - Add ServeAllServerQuic harness to serve_all integration tests - Add 3 QUIC integration tests for serve-all: cold-copy, warm-noop, incremental; exercises the wait_for_peer_close drain path in handle_any_client Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
ad479324ca
commit
b8365a4d41
@@ -324,3 +324,138 @@ fn serve_all_push_pull_round_trip() {
|
||||
"round-tripped file must have same revision count"
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// QUIC transport tests for serve-all
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Harness for `clawsync serve-all --quic`.
|
||||
struct ServeAllServerQuic {
|
||||
child: Child,
|
||||
pub addr: SocketAddr,
|
||||
pub dir: TempDir,
|
||||
}
|
||||
|
||||
impl ServeAllServerQuic {
|
||||
fn start(allow_delete: bool) -> Self {
|
||||
let dir = TempDir::new().expect("failed to create temp dir");
|
||||
let mut cmd = Command::new(BIN);
|
||||
cmd.arg("serve-all")
|
||||
.arg(dir.path())
|
||||
.arg("--bind")
|
||||
.arg("127.0.0.1:0")
|
||||
.arg("--quic");
|
||||
if allow_delete {
|
||||
cmd.arg("--allow-delete");
|
||||
}
|
||||
cmd.stdout(Stdio::piped()).stderr(Stdio::inherit());
|
||||
|
||||
let mut child = cmd.spawn().expect("failed to spawn clawsync serve-all --quic");
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
let mut reader = BufReader::new(stdout);
|
||||
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(10);
|
||||
let mut line = String::new();
|
||||
loop {
|
||||
line.clear();
|
||||
reader.read_line(&mut line).expect("failed to read startup line");
|
||||
if line.contains("ClawSync server listening on") {
|
||||
break;
|
||||
}
|
||||
if std::time::Instant::now() > deadline {
|
||||
panic!("serve-all --quic did not print startup line in time; got: {line:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let addr_str = line.trim().rsplit("on ").next().unwrap().trim();
|
||||
let addr: SocketAddr = addr_str
|
||||
.parse()
|
||||
.expect(&format!("could not parse bound addr from: {addr_str}"));
|
||||
|
||||
std::thread::spawn(move || for _ in reader.lines() {});
|
||||
|
||||
Self { child, addr, dir }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ServeAllServerQuic {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.child.kill();
|
||||
let _ = self.child.wait();
|
||||
}
|
||||
}
|
||||
|
||||
fn run_fs_sync_quic(src: &Path, addr: SocketAddr) {
|
||||
let remote = format!("{addr}/");
|
||||
let status = Command::new(BIN)
|
||||
.args(["sync", src.to_str().unwrap(), &remote, "--quic"])
|
||||
.status()
|
||||
.expect("failed to run sync --quic");
|
||||
assert!(status.success(), "sync --quic exited with {status}");
|
||||
}
|
||||
|
||||
/// Cold copy via QUIC through the universal serve-all server.
|
||||
#[test]
|
||||
fn serve_all_quic_fs_cold_copy() {
|
||||
let server = ServeAllServerQuic::start(false);
|
||||
|
||||
let src = TempDir::new().unwrap();
|
||||
fs::write(src.path().join("quic_file.txt"), b"quic cold copy test").unwrap();
|
||||
fs::write(src.path().join("binary.bin"), vec![0x42u8; 1024]).unwrap();
|
||||
|
||||
run_fs_sync_quic(src.path(), server.addr);
|
||||
|
||||
let dst_txt = server.dir.path().join("quic_file.txt");
|
||||
let dst_bin = server.dir.path().join("binary.bin");
|
||||
assert!(dst_txt.exists(), "quic_file.txt must be synced via QUIC");
|
||||
assert!(dst_bin.exists(), "binary.bin must be synced via QUIC");
|
||||
assert_eq!(fs::read(&dst_txt).unwrap(), b"quic cold copy test");
|
||||
assert_eq!(fs::read(&dst_bin).unwrap(), vec![0x42u8; 1024]);
|
||||
}
|
||||
|
||||
/// Warm no-op via QUIC: re-syncing identical content transfers nothing
|
||||
/// and exits 0.
|
||||
#[test]
|
||||
fn serve_all_quic_fs_warm_noop() {
|
||||
let server = ServeAllServerQuic::start(false);
|
||||
|
||||
// Seed server directory with the same content as the source.
|
||||
let src = TempDir::new().unwrap();
|
||||
let content = b"identical content for noop";
|
||||
fs::write(src.path().join("noop.txt"), content).unwrap();
|
||||
fs::write(server.dir.path().join("noop.txt"), content).unwrap();
|
||||
|
||||
// Second sync should be a warm no-op.
|
||||
run_fs_sync_quic(src.path(), server.addr);
|
||||
|
||||
assert_eq!(
|
||||
fs::read(server.dir.path().join("noop.txt")).unwrap(),
|
||||
content
|
||||
);
|
||||
}
|
||||
|
||||
/// Incremental sync via QUIC: modifying one file transfers only that file.
|
||||
#[test]
|
||||
fn serve_all_quic_fs_incremental() {
|
||||
let server = ServeAllServerQuic::start(false);
|
||||
|
||||
let src = TempDir::new().unwrap();
|
||||
|
||||
// Put both files on server already.
|
||||
fs::write(src.path().join("unchanged.bin"), vec![0x11u8; 512]).unwrap();
|
||||
fs::write(src.path().join("changed.bin"), vec![0xAAu8; 512]).unwrap();
|
||||
run_fs_sync_quic(src.path(), server.addr);
|
||||
|
||||
// Modify one file; re-sync.
|
||||
fs::write(src.path().join("changed.bin"), vec![0xBBu8; 512]).unwrap();
|
||||
run_fs_sync_quic(src.path(), server.addr);
|
||||
|
||||
assert_eq!(
|
||||
fs::read(server.dir.path().join("changed.bin")).unwrap(),
|
||||
vec![0xBBu8; 512]
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(server.dir.path().join("unchanged.bin")).unwrap(),
|
||||
vec![0x11u8; 512]
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user