v0.147.1 — 4c follow-up: the bar must move on an INCREMENTAL run
Found by watching the v0.147.0 card during a real manual run on the demo box, which is the only way this was going to surface: a 430MB immich push reported 0% for 40+ seconds and then completed. The parser was not broken. restic was genuinely reporting no transferred bytes — on an incremental run where nothing changed, bytes_done is omitempty on restic's side so it is not even in the JSON, and percent_done stays 0 for the whole run. Confirmed against the real schema by capturing backup --dry-run --json from restic 0.14.0 in the controller image rather than guessing; those captured lines are now quoted verbatim in the type's doc comment. Why it mattered: a byte-only bar is indistinguishable from a hang in the COMMON case, which is precisely the silence 4c set out to remove. Shipping it would have traded "no feedback" for "feedback that says 0% and looks stuck". files_done/total_files are now parsed and published alongside the bytes; the card prefers bytes when bytes move, otherwise drives the bar from files and says "N / M fájl ellenőrizve". parseResticStatus returns a struct instead of four positional values, and a new test pins the real incremental line shape (bytes absent, files climbing) so a refactor cannot quietly restore the stuck bar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
This commit is contained in:
@@ -181,29 +181,65 @@ func TestParseResticStatusIgnoresNonStatus(t *testing.T) {
|
||||
``,
|
||||
`{}`,
|
||||
} {
|
||||
if _, _, _, ok := parseResticStatus(line); ok {
|
||||
if _, ok := parseResticStatus(line); ok {
|
||||
t.Errorf("parsed a non-status line as progress: %q", line)
|
||||
}
|
||||
}
|
||||
pct, done, total, ok := parseResticStatus(jsonStatus(0.42, 4200, 10000))
|
||||
r, ok := parseResticStatus(jsonStatus(0.42, 4200, 10000))
|
||||
if !ok {
|
||||
t.Fatal("a real status line was not parsed")
|
||||
}
|
||||
if pct != 42 || done != 4200 || total != 10000 {
|
||||
t.Errorf("got %v %d %d, want 42 4200 10000", pct, done, total)
|
||||
if r.Percent != 42 || r.BytesDone != 4200 || r.TotalBytes != 10000 {
|
||||
t.Errorf("got %v %d %d, want 42 4200 10000", r.Percent, r.BytesDone, r.TotalBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseResticStatusIncrementalRun is the case that MATTERS and the one a synthetic test suite
|
||||
// would never think to write. It is a real status line shape from restic 0.14 on the demo box:
|
||||
// an incremental push where nothing changed transfers no new bytes, so restic omits bytes_done
|
||||
// entirely (`omitempty`) and percent_done stays 0 — while files_done climbs steadily.
|
||||
//
|
||||
// Measured live: a 430MB immich push reported 0% for 40+ seconds and then completed. A byte-only
|
||||
// progress bar is therefore indistinguishable from a hang in the COMMON case, which is the exact
|
||||
// failure 4c exists to remove. The file counters must survive parsing so the page can fall back to
|
||||
// them.
|
||||
func TestParseResticStatusIncrementalRun(t *testing.T) {
|
||||
// bytes_done and files_done absent — restic's scan-start state.
|
||||
r, ok := parseResticStatus(`{"message_type":"status","percent_done":0,"total_files":1,"total_bytes":112}`)
|
||||
if !ok {
|
||||
t.Fatal("scan-start status line was not parsed")
|
||||
}
|
||||
if r.BytesDone != 0 || r.TotalBytes != 112 || r.TotalFiles != 1 {
|
||||
t.Errorf("scan-start: got %+v", r)
|
||||
}
|
||||
|
||||
// The incremental steady state: no bytes moving, files moving.
|
||||
r, ok = parseResticStatus(`{"message_type":"status","percent_done":0,"total_files":8123,"files_done":4110,"total_bytes":451130451}`)
|
||||
if !ok {
|
||||
t.Fatal("incremental status line was not parsed")
|
||||
}
|
||||
if r.BytesDone != 0 {
|
||||
t.Errorf("bytes_done = %d, want 0 (absent in the JSON)", r.BytesDone)
|
||||
}
|
||||
if r.FilesDone != 4110 || r.TotalFiles != 8123 {
|
||||
t.Errorf("file counters lost: got %d/%d, want 4110/8123 — the page has nothing left to move",
|
||||
r.FilesDone, r.TotalFiles)
|
||||
}
|
||||
if r.TotalBytes != 451130451 {
|
||||
t.Errorf("total_bytes = %d, want 451130451", r.TotalBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseResticStatusClampsPercent — restic has been seen to report percent_done slightly above 1
|
||||
// near completion. A bar wider than its track is a visible bug.
|
||||
func TestParseResticStatusClampsPercent(t *testing.T) {
|
||||
pct, _, _, ok := parseResticStatus(`{"message_type":"status","percent_done":1.04}`)
|
||||
if !ok || pct != 100 {
|
||||
t.Errorf("percent = %v (ok=%v), want clamped to 100", pct, ok)
|
||||
r, ok := parseResticStatus(`{"message_type":"status","percent_done":1.04}`)
|
||||
if !ok || r.Percent != 100 {
|
||||
t.Errorf("percent = %v (ok=%v), want clamped to 100", r.Percent, ok)
|
||||
}
|
||||
pct, _, _, ok = parseResticStatus(`{"message_type":"status","percent_done":-0.2}`)
|
||||
if !ok || pct != 0 {
|
||||
t.Errorf("percent = %v (ok=%v), want clamped to 0", pct, ok)
|
||||
r, ok = parseResticStatus(`{"message_type":"status","percent_done":-0.2}`)
|
||||
if !ok || r.Percent != 0 {
|
||||
t.Errorf("percent = %v (ok=%v), want clamped to 0", r.Percent, ok)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user