report: show its phases as N of M, with progress, elapsed time and ETA (closes #114)
check / check (push) Waiting to run

report now runs in two phases, open and read, under the progress
display scan and trees use, until the summary line. open covers a wait
on a busy database. read counts the records, then reads them in the
same read-only transaction in signature order, the files_signature
index read backwards, so SQLite sorts nothing and the read ends at
exactly its count. The duplicate groups are formed in Go from runs of
equal signatures; only those of the size being read are held, and they
are written, ordered by first path, before the next size. The rows go
through the display's stdout writer. stdout and the summary line are
unchanged.

Judgement call: writing is part of read, not a phase of its own, since each size's rows are written while the read goes on.

Model: opus-5-5
This commit is contained in:
2026-10-16 19:13:20 +00:00
parent 8e0facdcd3
commit 3864071c3f
11 changed files with 590 additions and 180 deletions
+15 -8
View File
@@ -303,9 +303,9 @@ func TestRunMissingDatabaseIsFatal(t *testing.T) {
t.Errorf("run(%s) = %d, want %d", name, code, exitFatal)
}
// trees shows its open phase, as it starts and as it ends,
// Each shows its open phase, as it starts and as it ends,
// before the error.
opening := ""
opening := reportOpenLine + "\n" + reportOpenLine + "\n"
if name == cmdTrees {
opening = treesOpenLine + "\n" + treesOpenLine + "\n"
}
@@ -513,7 +513,8 @@ func TestRunNamesDefaultDatabase(t *testing.T) {
// nothing here may create or read one: scan gets an operand that
// does not exist, which stops it before it takes the lock, and
// report and trees a cancelled context, which stops them before
// they read anything. trees shows its open phase after the line.
// they read anything. report and trees show their open phase after
// the line.
t.Setenv(databaseEnv, "")
// Whether scan calls the database new depends on that machine too.
@@ -541,7 +542,8 @@ func TestRunNamesDefaultDatabase(t *testing.T) {
run: func() error {
return runReport(cancelledContext(t), io.Discard)
},
want: "report: database " + defaultDatabasePath + " (default)\n",
want: "report: database " + defaultDatabasePath + " (default)\n" +
reportOpenLine + "\n" + reportOpenLine + "\n",
},
cmdTrees: {
run: func() error {
@@ -916,11 +918,16 @@ func TestRunReportSucceeds(t *testing.T) {
t.Errorf("stdout = %q, want %q", got, want)
}
want = "report: database " + path + " (from " + databaseEnv + ")\n" +
"report: 2 records read, 1 duplicate groups, 1 dupe files, " +
// Its phases come between the two lines; see
// TestRunReportProgressNamesEachPhase.
first := "report: database " + path + " (from " + databaseEnv + ")\n"
last := "\nreport: 2 records read, 1 duplicate groups, 1 dupe files, " +
"300 B reclaimable\n"
if got := stderr(); got != want {
t.Errorf("stderr = %q, want %q", got, want)
if got := stderr(); !strings.HasPrefix(got, first) ||
!strings.HasSuffix(got, last) {
t.Errorf("stderr = %q, want it to start %q and end %q",
got, first, last)
}
assertNoSidecars(t, path)