check / check (push) Canceled after 0s
Every file the walk records now gets three hashes: head and tail, of its first and last 64 KiB, and content. The content hash covers the whole file under 100 MiB, and 1 MiB at each gigabyte from 100 MiB up, so a large file is never read in full. One 100 MiB constant replaces the 10 MiB and 50 MiB thresholds. The size census, the records stored without hashes and the content phase are gone, so a rescan reads only new and changed files. report and trees no longer special-case a missing content hash. README "Duplicate detection" and "Rationale" are rewritten. Judgement call: info drops its two hash-count lines, which now always equal the record count. Model: opus-5-5
164 lines
4.4 KiB
Go
164 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// infoOpenLine is the plain line of info's open phase when it opens the
|
|
// database at once; it prints it as the phase starts and as it ends.
|
|
const infoOpenLine = "[1/2] open: opening the database, elapsed 0s, " +
|
|
"eta unknown"
|
|
|
|
// infoCountLine is the plain line of info's count phase when the count
|
|
// takes under a second; it prints it as the phase starts and as it
|
|
// ends.
|
|
const infoCountLine = "[2/2] count: counting the records, elapsed 0s, " +
|
|
"eta unknown"
|
|
|
|
// infoRecs are four records: a duplicate pair, a file of 20 MiB and
|
|
// another small file.
|
|
func infoRecs() []scanRec {
|
|
return []scanRec{
|
|
{size: 5, head: "h", tail: "t", content: "c", path: "/d/a"},
|
|
{size: 5, head: "h", tail: "t", content: "c", path: "/d/b"},
|
|
{size: 20 << 20, head: "bh", tail: "bt", content: "bc", path: "/d/big"},
|
|
{size: 7, head: "u", tail: "u", content: "u", path: "/d/unique"},
|
|
}
|
|
}
|
|
|
|
// TestRunInfo runs info on an empty and on a filled database, with
|
|
// stdout and stderr the same file that is not a terminal, and checks
|
|
// everything written there: the line naming the database, each phase's
|
|
// line as it starts and as it ends, and then the details, each on a
|
|
// line of its own. No -wal file is beside a database scan has closed.
|
|
func TestRunInfo(t *testing.T) {
|
|
cases := map[string]struct {
|
|
recs []scanRec
|
|
want string // the details after the database file's time
|
|
}{
|
|
"empty": {
|
|
want: "schema version\t1\n" +
|
|
"records\t0\n" +
|
|
"size of recorded files\t0 (0 B)\n",
|
|
},
|
|
"filled": {
|
|
recs: infoRecs(),
|
|
want: "schema version\t1\n" +
|
|
"records\t4\n" +
|
|
"size of recorded files\t20971537 (20.0 MiB)\n",
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
path := seedDatabase(t, tc.recs)
|
|
t.Setenv(databaseEnv, path)
|
|
|
|
file, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stderr := captureStderr(t)
|
|
|
|
code := run([]string{cmdInfo}, os.Stderr, os.Stderr)
|
|
if code != exitOK {
|
|
t.Fatalf("run(info) = %d, want %d; stderr: %s",
|
|
code, exitOK, stderr())
|
|
}
|
|
|
|
named := path + " (from " + databaseEnv + ")\n"
|
|
want := "info: database " + named +
|
|
infoOpenLine + "\n" + infoOpenLine + "\n" +
|
|
infoCountLine + "\n" + infoCountLine + "\n" +
|
|
"database\t" + named +
|
|
fmt.Sprintf("database file size\t%d (%s)\n",
|
|
file.Size(), humanBytes(file.Size())) +
|
|
"modified\t" + file.ModTime().UTC().Format(time.RFC3339) +
|
|
"\n" + tc.want
|
|
|
|
if got := stderr(); got != want {
|
|
t.Errorf("stdout and stderr = %q, want %q", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRunInfoDuringScan runs info while a scan holds the lock and has
|
|
// the database open in WAL mode, with records committed to the -wal
|
|
// file only. info must count those records and show the -wal file's
|
|
// size, also when the database path is a symlink: SQLite keeps the
|
|
// -wal file beside the symlink's target.
|
|
func TestRunInfoDuringScan(t *testing.T) {
|
|
path := testDBPath(t)
|
|
link := filepath.Join(t.TempDir(), "link.sqlite")
|
|
|
|
err := os.Symlink(path, link)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
db, err := openScanDatabase(t.Context(), path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
err = applyChanges(t.Context(), db, infoRecs(), nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
holdScanLock(t, path)
|
|
|
|
wal, err := os.Stat(path + "-wal")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, named := range []string{path, link} {
|
|
t.Run(filepath.Base(named), func(t *testing.T) {
|
|
t.Setenv(databaseEnv, named)
|
|
|
|
stdout := runStdout(t, cmdInfo)
|
|
|
|
for _, want := range []string{
|
|
"database\t" + named + " (from " + databaseEnv + ")\n",
|
|
fmt.Sprintf("\n-wal file size\t%d (%s)\n",
|
|
wal.Size(), humanBytes(wal.Size())),
|
|
"\nrecords\t4\n",
|
|
} {
|
|
if !strings.Contains(stdout, want) {
|
|
t.Errorf("stdout = %q, want it to hold %q", stdout, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunInfoNeedsOnlyReadAccess(t *testing.T) {
|
|
// README "`info` mode": like report, info needs only read access to
|
|
// the database file and none to its directory.
|
|
path := testDBPath(t)
|
|
t.Setenv(databaseEnv, path)
|
|
|
|
scanFixture(t)
|
|
makeReadOnly(t, path)
|
|
|
|
stdout := runStdout(t, cmdInfo)
|
|
|
|
if want := "\nrecords\t2\n"; !strings.Contains(stdout, want) {
|
|
t.Errorf("stdout = %q, want it to hold %q", stdout, want)
|
|
}
|
|
|
|
if strings.Contains(stdout, "-wal") {
|
|
t.Errorf("stdout = %q, want no -wal file between scans", stdout)
|
|
}
|
|
}
|