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.
This commit is contained in:
Generated
+1
-1
@@ -253,7 +253,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dap-tui"
|
name = "dap-tui"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dap-tui"
|
name = "dap-tui"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Device sync for portable players — TUI edition (ratatui)"
|
description = "Device sync for portable players — TUI edition (ratatui)"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -944,6 +944,9 @@ mod tests {
|
|||||||
assert!(seen[1].contains("second stats line"));
|
assert!(seen[1].contains("second stats line"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn process_is_alive(pid: i32) -> bool {
|
fn process_is_alive(pid: i32) -> bool {
|
||||||
match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
|
match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
|
||||||
|
|||||||
+28
-4
@@ -193,6 +193,14 @@ pub fn parse_rclone_stats(line: &str) -> Option<RcloneStats> {
|
|||||||
return None;
|
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 mut parts = body.split(',');
|
||||||
let sizes = parts.next()?.trim();
|
let sizes = parts.next()?.trim();
|
||||||
let (done, total) = sizes.split_once('/')?;
|
let (done, total) = sizes.split_once('/')?;
|
||||||
@@ -348,6 +356,20 @@ mod tests {
|
|||||||
assert!(parse_rclone_current(" * ").is_none());
|
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]
|
#[test]
|
||||||
fn survives_real_rclone_progress_blocks() {
|
fn survives_real_rclone_progress_blocks() {
|
||||||
// Captured from `rclone sync --progress --stats 1s` with a pipe: the
|
// Captured from `rclone sync --progress --stats 1s` with a pipe: the
|
||||||
@@ -366,14 +388,16 @@ Transferring:
|
|||||||
Checks: 0 / 0, -, Listed 7
|
Checks: 0 / 0, -, Listed 7
|
||||||
Transferred: 2 / 6, 33%
|
Transferred: 2 / 6, 33%
|
||||||
";
|
";
|
||||||
let mut last: Option<RcloneStats> = 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() {
|
for line in sample.lines() {
|
||||||
if let Some(p) = parse_rclone_stats(line) {
|
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!(pcts, vec![10, 40]);
|
||||||
assert_eq!(last.pct, 40);
|
let last = parse_rclone_stats(sample.lines().nth(7).unwrap()).unwrap();
|
||||||
assert_eq!(last.done, "32.082 MiB");
|
assert_eq!(last.done, "32.082 MiB");
|
||||||
assert_eq!(last.total, "80 MiB");
|
assert_eq!(last.total, "80 MiB");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user