From 0ffe32ae089533531a92d39e349011c4828b9e0c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 10 Sep 2026 20:03:34 +0200 Subject: [PATCH] mirror: stop per-file lines resetting the progress gauge to 0% parse_rclone_stats still accepted rclone's `Transferring:` entries (`* a.bin: 25% / 60 MiB, 0 B/s, -`) because they contain " / " and "%". rclone writes them right after the byte-weighted line on every tick, and since the last per-file entry is glued to the *next* tick's byte line, the parsed order per second is: good byte line -> per-file 0% -> (one second later) glued byte line. The gauge therefore displayed 0% almost continuously and only ever looked right at the end. Require a real stats body to start with the transferred byte amount. Per-file lines now only feed the "current file" display (they already did, as a fallback) and can no longer touch pct/done/total. Verified with the real engine + rclone (bwlimit): mirror state now goes 0 -> 25 -> 50 -> 75 -> 100 with proper byte counts. The replay test now asserts the exact parse sequence [10%, 40%] so a regression fails when a per-file or file-count line sneaks in as 0%. Bump to 0.3.1. --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/engine.rs | 3 +++ src/progress.rs | 32 ++++++++++++++++++++++++++++---- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21eaecf..121d216 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -253,7 +253,7 @@ dependencies = [ [[package]] name = "dap-tui" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 4e1a5bc..336fa4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dap-tui" -version = "0.3.0" +version = "0.3.1" edition = "2021" description = "Device sync for portable players — TUI edition (ratatui)" license = "MIT" diff --git a/src/engine.rs b/src/engine.rs index 4347daf..5b008e7 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -944,6 +944,9 @@ mod tests { assert!(seen[1].contains("second stats line")); } + + + #[cfg(unix)] fn process_is_alive(pid: i32) -> bool { match std::fs::read_to_string(format!("/proc/{pid}/stat")) { diff --git a/src/progress.rs b/src/progress.rs index 77486d4..dd8f93b 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -193,6 +193,14 @@ pub fn parse_rclone_stats(line: &str) -> Option { return None; } + // A real byte-stats body starts with the transferred amount. This rejects + // per-file `Transferring` entries such as + // `* a.bin: 25% / 60 MiB, 0 B/s, -`, which rclone emits right after the + // byte line on every tick. They used to parse as 0% and reset the gauge. + if !body.starts_with(|c: char| c.is_ascii_digit()) { + return None; + } + let mut parts = body.split(','); let sizes = parts.next()?.trim(); let (done, total) = sizes.split_once('/')?; @@ -348,6 +356,20 @@ mod tests { assert!(parse_rclone_current(" * ").is_none()); } + #[test] + fn rejects_rclone_per_file_lines() { + // Emitted every tick right after the byte line; must never reset the + // gauge to 0%. + assert!(parse_rclone_stats( + " * a.bin: 25% / 60 MiB, 0 B/s, -" + ) + .is_none()); + assert!(parse_rclone_stats("* big.bin: 5% / 40 MiB, 2.059 MiB/s, 18s").is_none()); + // The glued variant still has to yield the trailing stats block. + let glued = "* a.bin: 25% / 60 MiB, 0 B/s, -Transferred: \t 60.086 MiB / 120 MiB, 50%, 30.041 MiB/s, ETA 1s"; + assert_eq!(parse_rclone_stats(glued).unwrap().pct, 50); + } + #[test] fn survives_real_rclone_progress_blocks() { // Captured from `rclone sync --progress --stats 1s` with a pipe: the @@ -366,14 +388,16 @@ Transferring: Checks: 0 / 0, -, Listed 7 Transferred: 2 / 6, 33% "; - let mut last: Option = None; + // Only the two byte lines may parse; every per-file or count line must + // be rejected, so the gauge can never snap back to 0 between ticks. + let mut pcts = Vec::new(); for line in sample.lines() { if let Some(p) = parse_rclone_stats(line) { - last = Some(p); + pcts.push(p.pct); } } - let last = last.expect("byte stats must parse"); - assert_eq!(last.pct, 40); + assert_eq!(pcts, vec![10, 40]); + let last = parse_rclone_stats(sample.lines().nth(7).unwrap()).unwrap(); assert_eq!(last.done, "32.082 MiB"); assert_eq!(last.total, "80 MiB"); }