Add sfdupes info: show the database's details (closes #117)
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
This commit was merged in pull request #123.
This commit is contained in:
2026-10-16 22:41:16 +02:00
parent ebe4d4dbeb
commit e9e09effc6
11 changed files with 540 additions and 89 deletions
+24 -14
View File
@@ -284,10 +284,16 @@ func assertFatalOutput(t *testing.T, stderr, stdout string) {
}
func TestRunMissingDatabaseIsFatal(t *testing.T) {
// README §Database: report and trees need an existing database; a
// missing one exits 1 with a message telling the user to run scan,
// after the line naming the database.
for _, name := range []string{cmdReport, cmdTrees} {
// README §Database: report, trees and info need an existing
// database; a missing one exits 1 with a message telling the user to
// run scan, after the line naming the database.
openLines := map[string]string{
cmdReport: reportOpenLine,
cmdTrees: treesOpenLine,
cmdInfo: infoOpenLine,
}
for name, openLine := range openLines {
t.Run(name, func(t *testing.T) {
path := testDBPath(t)
t.Setenv(databaseEnv, path)
@@ -305,13 +311,8 @@ func TestRunMissingDatabaseIsFatal(t *testing.T) {
// Each shows its open phase, as it starts and as it ends,
// before the error.
opening := reportOpenLine + "\n" + reportOpenLine + "\n"
if name == cmdTrees {
opening = treesOpenLine + "\n" + treesOpenLine + "\n"
}
want := name + ": database " + path + " (from " + databaseEnv +
")\n" + opening +
")\n" + openLine + "\n" + openLine + "\n" +
"sfdupes: " + path + ": no database (run \"sfdupes " +
"scan\" first, or set " + databaseEnv + ")\n"
if got := stderr(); got != want {
@@ -512,9 +513,9 @@ func TestRunNamesDefaultDatabase(t *testing.T) {
// exist at the default path on the machine running the test, so
// 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. report and trees show their open phase after
// the line.
// report, trees and info a cancelled context, which stops them
// before they read anything. They show their open phase after the
// line.
t.Setenv(databaseEnv, "")
// Whether scan calls the database new depends on that machine too.
@@ -552,6 +553,13 @@ func TestRunNamesDefaultDatabase(t *testing.T) {
want: "trees: database " + defaultDatabasePath + " (default)\n" +
treesOpenLine + "\n" + treesOpenLine + "\n",
},
cmdInfo: {
run: func() error {
return runInfo(cancelledContext(t), io.Discard)
},
want: "info: database " + defaultDatabasePath + " (default)\n" +
infoOpenLine + "\n" + infoOpenLine + "\n",
},
}
for name, tc := range cases {
@@ -578,6 +586,7 @@ func TestRunUsageErrors(t *testing.T) {
"scan without paths": {[]string{cmdScan}, usageMarker},
"report with args": {[]string{cmdReport, "x"}, usageMarker},
"trees with args": {[]string{cmdTrees, "x"}, usageMarker},
"info with args": {[]string{cmdInfo, "x"}, usageMarker},
"unknown flag": {[]string{cmdScan, "--nope", "/"}, usageMarker},
"unknown subcommand": {[]string{"nope"}, "unknown command"},
}
@@ -1258,7 +1267,7 @@ func TestRunReportsDuringScan(t *testing.T) {
func TestRunStdoutClosedIsFatal(t *testing.T) {
// README §Error handling: a stdout write failure exits 1, reported
// in one line on stderr.
for _, name := range []string{cmdReport, cmdTrees} {
for _, name := range []string{cmdReport, cmdTrees, cmdInfo} {
t.Run(name, func(t *testing.T) {
t.Setenv(databaseEnv, testDBPath(t))
@@ -1308,6 +1317,7 @@ func TestStdoutWriteErrorPropagates(t *testing.T) {
cases := map[string]func(context.Context, io.Writer) error{
cmdReport: runReport,
cmdTrees: runTrees,
cmdInfo: runInfo,
}
for name, fn := range cases {