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 with every hash, a file // of 20 MiB with head and tail hashes but no content hash yet, and a // file never hashed. 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", path: "/d/big"}, {size: 7, 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" + "records with head and tail hashes\t0\n" + "records with content hash\t0\n" + "size of recorded files\t0 (0 B)\n", }, "filled": { recs: infoRecs(), want: "schema version\t1\n" + "records\t4\n" + "records with head and tail hashes\t3\n" + "records with content hash\t2\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) } }