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:
osobh
2026-04-05 12:06:07 -05:00
co-authored by Claude Sonnet 4.6
parent ad479324ca
commit b8365a4d41
4 changed files with 215 additions and 4 deletions
+135
View File
@@ -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]
);
}