check / check (push) Canceled after 0s
info prints the database's path and source, the sizes of the database file and its -wal file, when the database file last changed, the schema version and the record counts, one per line as a name, a tab and the value. It opens the database as report does and shows an open and a count phase on stderr. The schema version and the counts come from one read-only transaction, the counts from one query. seedDatabase now closes test databases out of WAL mode, as scan does; otherwise info's own connection creates an empty -wal file. The open phase's name and text became constants for the linter, and the scan command moved to newScanCommand to keep newRootCommand short enough. Judgement call: the lines are written after the last display ends, so they skip the display's stdout writer. Model: opus-5-5
169 lines
4.6 KiB
Go
169 lines
4.6 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 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)
|
|
}
|
|
}
|