scan: show each phase as N of M, with progress, elapsed time and ETA, also while it waits (closes #112)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Every scan display now starts with its phase and its place among the five, as "[2/5] walk", and always shows the rate, elapsed time and ETA. load counts the records before reading them, and the content phase counts what its search finds before checking it, so both have totals; until then the line says what the phase waits for. The walk's total is estimated from the records under the operands, with the ETA unknown past the estimate or without one. Each display draws from its own goroutine under one mutex: several times a second on a terminal, every 5 seconds in plain mode. scanPhase holds the phase list. Judgement call: on a terminal the line is the plain line behind the library's spinner; the bar graphic is gone. Judgement call: the content phase runs its search query twice, once to count its rows. Model: opus-5-5
This commit is contained in:
@@ -1586,6 +1586,69 @@ func TestScanPipeline(t *testing.T) {
|
||||
assertSmokeTreeGroups(t, dir, db)
|
||||
}
|
||||
|
||||
// TestScanProgressNamesEachPhase checks the plain progress lines of a
|
||||
// first scan, and of a rescan of one operand holding two of the
|
||||
// database's records. Each phase's first line names it with its place
|
||||
// among the five: load counts the records before reading them, the
|
||||
// content phase searches the database, and the walk estimates its total
|
||||
// from the records under its operand, or says its ETA is unknown when
|
||||
// there are none.
|
||||
//
|
||||
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
||||
func TestScanProgressNamesEachPhase(t *testing.T) {
|
||||
stderr := captureStderr(t)
|
||||
dir := buildSmokeTree(t)
|
||||
db := openTestDB(t)
|
||||
|
||||
syncTree(t, db, dir)
|
||||
|
||||
first := stderr()
|
||||
|
||||
syncTree(t, db, filepath.Join(dir, "a"))
|
||||
|
||||
rescan := stderr()[len(first):]
|
||||
load := "[1/5] load: counting the records, elapsed 0s, eta unknown"
|
||||
content := "[5/5] content: searching the database, elapsed 0s, eta unknown"
|
||||
cases := map[string][]string{
|
||||
first: {
|
||||
load, "[2/5] walk: 0 files, 0 files/s, elapsed 0s, eta unknown",
|
||||
"[3/5] hash: [0/", "[4/5] update: [0/", content,
|
||||
},
|
||||
rescan: {
|
||||
load, "[2/5] walk: [0/~2] 0%, 0 files/s, elapsed 0s, eta unknown",
|
||||
"[3/5] hash: [0/", "[4/5] update: [0/", content,
|
||||
},
|
||||
}
|
||||
|
||||
for out, want := range cases {
|
||||
var got []string
|
||||
|
||||
for line := range strings.Lines(out) {
|
||||
label, _, _ := strings.Cut(line, ":")
|
||||
if len(got) == 0 || !strings.HasPrefix(got[len(got)-1], label) {
|
||||
got = append(got, line)
|
||||
}
|
||||
}
|
||||
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("first lines of the phases = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
for i := range want {
|
||||
if !strings.HasPrefix(got[i], want[i]) {
|
||||
t.Errorf("phase %d starts %q, want %q", i+1, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The load phase's total is the count of the records.
|
||||
loaded := fmt.Sprintf("\n[1/5] load: [%d/%d] 100%%, ",
|
||||
smokeTreeFiles, smokeTreeFiles)
|
||||
if !strings.Contains(rescan, loaded) {
|
||||
t.Errorf("rescan stderr = %q, want %q", rescan, loaded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncScanUnchangedReuse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user