Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de43b08a9f | ||
|
|
e16c46804d |
No files matched your search
@@ -253,8 +253,9 @@ In order:
|
|||||||
`trees` check only that each copy they name still exists, and leave out a
|
`trees` check only that each copy they name still exists, and leave out a
|
||||||
copy that is gone. `scan` is built to run from `cron`. The reports run at any
|
copy that is gone. `scan` is built to run from `cron`. The reports run at any
|
||||||
time, against what the database holds.
|
time, against what the database holds.
|
||||||
4. **Keep the streams apart.** Everything on stdout is machine-readable data.
|
4. **Keep the streams apart.** stdout holds only what was asked for:
|
||||||
Everything for a human reader, such as progress and warnings, goes to stderr.
|
machine-readable data, or help. Everything else for a human reader, such as
|
||||||
|
progress and warnings, goes to stderr.
|
||||||
|
|
||||||
### Subcommands
|
### Subcommands
|
||||||
|
|
||||||
@@ -324,12 +325,13 @@ The reference below is also the program's specification.
|
|||||||
"Operands").
|
"Operands").
|
||||||
- `--version`, `-v` prints `sfdupes VERSION` (see "Install") to stdout and
|
- `--version`, `-v` prints `sfdupes VERSION` (see "Install") to stdout and
|
||||||
exits 0. It writes nothing to stderr.
|
exits 0. It writes nothing to stderr.
|
||||||
- `--help`, `-h`, alone or after a subcommand, prints the help text to stderr
|
- `--help`, `-h`, alone or after a subcommand, prints the help text to stdout
|
||||||
and exits 0. It writes nothing to stdout. The help of `sfdupes` and of `scan`
|
and exits 0. It writes nothing to stderr. `sfdupes help` and
|
||||||
names the database. The help of `report`, `trees`, `info` and `dwim` says what
|
`sfdupes help COMMAND` do the same. The help of `sfdupes` and of `scan` names
|
||||||
each prints. `sfdupes scan --help` and `sfdupes dwim --help` also list the
|
the database. The help of `report`, `trees`, `info` and `dwim` says what each
|
||||||
default excludes of the system they run on, and the filesystem types the walk
|
prints. `sfdupes scan --help` and `sfdupes dwim --help` also list the default
|
||||||
never records anything on.
|
excludes of the system they run on, and the filesystem types the walk never
|
||||||
|
records anything on.
|
||||||
|
|
||||||
### Duplicate detection
|
### Duplicate detection
|
||||||
|
|
||||||
@@ -563,7 +565,7 @@ walk practical on large or busy storage pools.
|
|||||||
`scan` prints a line on stderr naming the path and its filesystem type:
|
`scan` prints a line on stderr naming the path and its filesystem type:
|
||||||
|
|
||||||
```
|
```
|
||||||
scan: skipped /dev/shm (tmpfs, not a persistent filesystem)
|
scan: left out /dev/shm (tmpfs, not a persistent filesystem)
|
||||||
```
|
```
|
||||||
|
|
||||||
The path is not counted as skipped. The records below it are kept, as
|
The path is not counted as skipped. The records below it are kept, as
|
||||||
@@ -573,7 +575,7 @@ walk practical on large or busy storage pools.
|
|||||||
For each one it reaches, `scan` prints a line on stderr naming it:
|
For each one it reaches, `scan` prints a line on stderr naming it:
|
||||||
|
|
||||||
```
|
```
|
||||||
scan: skipped /srv/cache (--exclude)
|
scan: left out /srv/cache (--exclude)
|
||||||
```
|
```
|
||||||
|
|
||||||
The path is not counted as skipped. The scan keeps the records at and below
|
The path is not counted as skipped. The scan keeps the records at and below
|
||||||
@@ -622,7 +624,7 @@ walk practical on large or busy storage pools.
|
|||||||
not enter, `scan` prints a line on stderr naming it:
|
not enter, `scan` prints a line on stderr naming it:
|
||||||
|
|
||||||
```
|
```
|
||||||
scan: skipped /var/tmp (default exclude)
|
scan: left out /var/tmp (default exclude)
|
||||||
```
|
```
|
||||||
|
|
||||||
The directory is not counted as skipped. `scan` prints the filesystem line
|
The directory is not counted as skipped. `scan` prints the filesystem line
|
||||||
@@ -682,6 +684,18 @@ scan: 123400 files seen (1200 added, 34 updated, 56 removed, 122166 unchanged),
|
|||||||
|
|
||||||
`removed` counts deleted records, which are not part of the files seen.
|
`removed` counts deleted records, which are not part of the files seen.
|
||||||
|
|
||||||
|
The warning for a skipped path names the path once and gives the reason:
|
||||||
|
|
||||||
|
```
|
||||||
|
scan: skipped /srv/a/closed (permission denied)
|
||||||
|
scan: skipped /srv/a/pipe (a FIFO)
|
||||||
|
```
|
||||||
|
|
||||||
|
The summary counts each path named in such a warning. A line for a path left out
|
||||||
|
on purpose starts with `scan: left out` instead, and is not counted. A search of
|
||||||
|
the log for `scan: skipped` therefore finds the paths the summary counts. When a
|
||||||
|
file with several hard links cannot be read, each of its paths gets a warning.
|
||||||
|
|
||||||
#### Stopping a scan
|
#### Stopping a scan
|
||||||
|
|
||||||
`scan` stops cleanly on `SIGINT`, the signal Ctrl-C sends, or `SIGTERM`, the
|
`scan` stops cleanly on `SIGINT`, the signal Ctrl-C sends, or `SIGTERM`, the
|
||||||
|
|||||||
+78
-4
@@ -3,6 +3,7 @@
|
|||||||
package sfdupes
|
package sfdupes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -176,9 +177,32 @@ func run(args []string, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
root := newRootCommand(stdout, stderr)
|
root := newRootCommand(stdout, stderr)
|
||||||
|
|
||||||
|
// Help that was asked for, with --help, -h or the help subcommand,
|
||||||
|
// goes to stdout. Execute returns no error after help, so helpErr
|
||||||
|
// keeps the one printHelp returns. Cobra's own help subcommand is
|
||||||
|
// replaced because it answers "sfdupes help scna" with the help of
|
||||||
|
// sfdupes.
|
||||||
|
var helpErr error
|
||||||
|
|
||||||
|
cobraHelp := root.HelpFunc()
|
||||||
|
root.SetHelpFunc(func(cmd *cobra.Command, _ []string) {
|
||||||
|
helpErr = printHelp(cmd, cmd.Flags().Args(), cobraHelp, stdout)
|
||||||
|
})
|
||||||
|
root.SetHelpCommand(&cobra.Command{
|
||||||
|
Use: "help [command]",
|
||||||
|
Short: "Help about any command",
|
||||||
|
Run: func(_ *cobra.Command, names []string) {
|
||||||
|
helpErr = printHelp(root, names, cobraHelp, stdout)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
root.SetArgs(args)
|
root.SetArgs(args)
|
||||||
|
|
||||||
err := root.Execute()
|
err := root.Execute()
|
||||||
|
if err == nil {
|
||||||
|
err = helpErr
|
||||||
|
}
|
||||||
|
|
||||||
var fatal fatalError
|
var fatal fatalError
|
||||||
|
|
||||||
@@ -195,14 +219,62 @@ func run(args []string, stdout, stderr io.Writer) int {
|
|||||||
|
|
||||||
return exitFatal
|
return exitFatal
|
||||||
default:
|
default:
|
||||||
// A usage error, which cobra has already reported on stderr.
|
// A usage error, which cobra or printHelp has already reported
|
||||||
|
// on stderr.
|
||||||
return exitUsage
|
return exitUsage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printHelp prints help that was asked for to stdout. It is the help of
|
||||||
|
// cmd, or, for a command with subcommands, of the subcommand that
|
||||||
|
// operands name: scan in "sfdupes help scan". Operands that name none,
|
||||||
|
// as in "sfdupes help scna" or "sfdupes scna --help", are a usage
|
||||||
|
// error, reported on stderr as for "sfdupes scna".
|
||||||
|
//
|
||||||
|
// cobraHelp is cobra's own help function. It prints to the command's
|
||||||
|
// output writer, which is stderr, and drops a failed write. So the help
|
||||||
|
// goes into a buffer first, and a failed write of the buffer to stdout
|
||||||
|
// is a fatal error.
|
||||||
|
func printHelp(
|
||||||
|
cmd *cobra.Command, operands []string,
|
||||||
|
cobraHelp func(*cobra.Command, []string), stdout io.Writer,
|
||||||
|
) error {
|
||||||
|
if cmd.HasSubCommands() {
|
||||||
|
cmd, operands, _ = cmd.Find(operands)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the help subcommand runs instead of cmd, cobra has not yet
|
||||||
|
// added the --help flag that cmd's help and usage text list.
|
||||||
|
cmd.InitDefaultHelpFlag()
|
||||||
|
|
||||||
|
// Operands left over here name no subcommand. They are reported as
|
||||||
|
// cobra reports "sfdupes scna", which cobra.NoArgs rejects.
|
||||||
|
if cmd.HasSubCommands() && len(operands) > 0 {
|
||||||
|
err := cobra.NoArgs(cmd, operands)
|
||||||
|
cmd.PrintErrln(cmd.ErrPrefix(), err.Error())
|
||||||
|
cmd.Println(cmd.UsageString())
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := cmd.OutOrStderr()
|
||||||
|
help := new(bytes.Buffer)
|
||||||
|
cmd.SetOut(help)
|
||||||
|
cobraHelp(cmd, nil)
|
||||||
|
cmd.SetOut(out)
|
||||||
|
|
||||||
|
_, err := stdout.Write(help.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return fatalError{err: fmt.Errorf("write stdout: %w", err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// newRootCommand builds the command tree. Everything on stdout is
|
// newRootCommand builds the command tree. Everything on stdout is
|
||||||
// machine-readable data, the version line included; all human-facing
|
// machine-readable data, the version line included, or help that was
|
||||||
// output (help, usage, errors) goes to stderr.
|
// asked for, which run prints. The rest of the human-facing output
|
||||||
|
// (usage, errors) goes to stderr.
|
||||||
func newRootCommand(stdout, stderr io.Writer) *cobra.Command {
|
func newRootCommand(stdout, stderr io.Writer) *cobra.Command {
|
||||||
var showVersion bool
|
var showVersion bool
|
||||||
|
|
||||||
@@ -235,11 +307,13 @@ func newRootCommand(stdout, stderr io.Writer) *cobra.Command {
|
|||||||
return errNoSubcommand
|
return errNoSubcommand
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
// Cobra prints the usage text of a usage error to the command's
|
||||||
|
// output writer, so that writer is stderr.
|
||||||
root.SetOut(stderr)
|
root.SetOut(stderr)
|
||||||
root.SetErr(stderr)
|
root.SetErr(stderr)
|
||||||
root.CompletionOptions.DisableDefaultCmd = true
|
root.CompletionOptions.DisableDefaultCmd = true
|
||||||
|
|
||||||
// Cobra's built-in version flag prints through the help writer,
|
// Cobra's built-in version flag prints to the output writer,
|
||||||
// stderr; this one prints to stdout.
|
// stderr; this one prints to stdout.
|
||||||
root.Flags().BoolVarP(&showVersion, "version", "v", false,
|
root.Flags().BoolVarP(&showVersion, "version", "v", false,
|
||||||
"print the version to stdout")
|
"print the version to stdout")
|
||||||
|
|||||||
+110
-28
@@ -26,6 +26,9 @@ const usageMarker = "Usage:"
|
|||||||
// helpFlag asks for the help text.
|
// helpFlag asks for the help text.
|
||||||
const helpFlag = "--help"
|
const helpFlag = "--help"
|
||||||
|
|
||||||
|
// helpCommand is the subcommand that prints the help text.
|
||||||
|
const helpCommand = "help"
|
||||||
|
|
||||||
// workersFlag is the scan and dwim flag that sets the worker count.
|
// workersFlag is the scan and dwim flag that sets the worker count.
|
||||||
const workersFlag = "--workers"
|
const workersFlag = "--workers"
|
||||||
|
|
||||||
@@ -625,6 +628,14 @@ func TestRunNamesDefaultDatabase(t *testing.T) {
|
|||||||
|
|
||||||
func TestRunUsageErrors(t *testing.T) {
|
func TestRunUsageErrors(t *testing.T) {
|
||||||
// Usage errors keep exiting 2 with cobra's own report on stderr.
|
// Usage errors keep exiting 2 with cobra's own report on stderr.
|
||||||
|
// An unknown subcommand is reported the same way with or without a
|
||||||
|
// request for help: the line naming it, then the usage text.
|
||||||
|
const (
|
||||||
|
unknown = "nope"
|
||||||
|
unknownReport = "unknown command \"" + unknown +
|
||||||
|
"\" for \"sfdupes\"\n" + usageMarker
|
||||||
|
)
|
||||||
|
|
||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
args []string
|
args []string
|
||||||
want string
|
want string
|
||||||
@@ -634,7 +645,13 @@ func TestRunUsageErrors(t *testing.T) {
|
|||||||
"info with args": {[]string{cmdInfo, "x"}, usageMarker},
|
"info with args": {[]string{cmdInfo, "x"}, usageMarker},
|
||||||
"dwim with args": {[]string{cmdDwim, "x"}, usageMarker},
|
"dwim with args": {[]string{cmdDwim, "x"}, usageMarker},
|
||||||
"unknown flag": {[]string{cmdScan, "--nope", "/"}, usageMarker},
|
"unknown flag": {[]string{cmdScan, "--nope", "/"}, usageMarker},
|
||||||
"unknown subcommand": {[]string{"nope"}, "unknown command"},
|
"unknown subcommand": {[]string{unknown}, unknownReport},
|
||||||
|
"unknown subcommand with --help": {
|
||||||
|
[]string{unknown, helpFlag}, unknownReport,
|
||||||
|
},
|
||||||
|
"help for an unknown subcommand": {
|
||||||
|
[]string{helpCommand, unknown}, unknownReport,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, tc := range cases {
|
for name, tc := range cases {
|
||||||
@@ -700,9 +717,12 @@ func TestRunScanRejectsWorkersBelowOne(t *testing.T) {
|
|||||||
func TestRunHelp(t *testing.T) {
|
func TestRunHelp(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// README §Settings: help goes to stderr, exits 0, and leaves stdout
|
// README §Settings: help goes to stdout, exits 0, and leaves stderr
|
||||||
// empty.
|
// empty.
|
||||||
cases := [][]string{{helpFlag}, {"-h"}, {cmdScan, helpFlag}}
|
cases := [][]string{
|
||||||
|
{helpFlag}, {"-h"}, {helpCommand},
|
||||||
|
{cmdScan, helpFlag}, {helpCommand, cmdScan},
|
||||||
|
}
|
||||||
|
|
||||||
for _, args := range cases {
|
for _, args := range cases {
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
@@ -712,14 +732,20 @@ func TestRunHelp(t *testing.T) {
|
|||||||
t.Errorf("run(%v) = %d, want %d", args, code, exitOK)
|
t.Errorf("run(%v) = %d, want %d", args, code, exitOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(stderr.String(), usageMarker) {
|
if !strings.Contains(stdout.String(), usageMarker) {
|
||||||
t.Errorf("run(%v) stderr = %q, want the help text",
|
t.Errorf("run(%v) stdout = %q, want the help text",
|
||||||
args, stderr.String())
|
args, stdout.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if got := stdout.String(); got != "" {
|
// The help subcommand prints the same help as --help, which
|
||||||
t.Errorf("run(%v) stdout = %q, want nothing (data only)",
|
// lists --help itself.
|
||||||
args, got)
|
if !strings.Contains(stdout.String(), "-h, "+helpFlag) {
|
||||||
|
t.Errorf("run(%v) stdout = %q, want it to list %s",
|
||||||
|
args, stdout.String(), helpFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := stderr.String(); got != "" {
|
||||||
|
t.Errorf("run(%v) stderr = %q, want nothing", args, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
// README §Location: the help of sfdupes and of scan says where
|
// README §Location: the help of sfdupes and of scan says where
|
||||||
@@ -729,9 +755,9 @@ func TestRunHelp(t *testing.T) {
|
|||||||
"scan and dwim store", databaseEnv, rootDatabasePath,
|
"scan and dwim store", databaseEnv, rootDatabasePath,
|
||||||
stateHomeEnv, "~/.local/state", "not an absolute path is an error",
|
stateHomeEnv, "~/.local/state", "not an absolute path is an error",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(stderr.String(), want) {
|
if !strings.Contains(stdout.String(), want) {
|
||||||
t.Errorf("run(%v) stderr = %q, want it to name %q",
|
t.Errorf("run(%v) stdout = %q, want it to name %q",
|
||||||
args, stderr.String(), want)
|
args, stdout.String(), want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -741,17 +767,48 @@ func TestRunHelp(t *testing.T) {
|
|||||||
|
|
||||||
run([]string{cmdScan, "-h"}, &stdout, &stderr)
|
run([]string{cmdScan, "-h"}, &stdout, &stderr)
|
||||||
|
|
||||||
if !strings.Contains(stderr.String(), excludeFlag+" PATH") {
|
if !strings.Contains(stdout.String(), excludeFlag+" PATH") {
|
||||||
t.Errorf("scan -h stderr = %q, want %s PATH", stderr.String(),
|
t.Errorf("scan -h stdout = %q, want %s PATH", stdout.String(),
|
||||||
excludeFlag)
|
excludeFlag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunHelpScanPrintsScanHelp(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// "sfdupes help scan" and "sfdupes --help scan" print exactly what
|
||||||
|
// "sfdupes scan --help" prints: the help of scan, not of sfdupes.
|
||||||
|
var want bytes.Buffer
|
||||||
|
|
||||||
|
run([]string{cmdScan, helpFlag}, &want, io.Discard)
|
||||||
|
|
||||||
|
for _, args := range [][]string{
|
||||||
|
{helpCommand, cmdScan}, {helpFlag, cmdScan},
|
||||||
|
} {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
||||||
|
code := run(args, &stdout, &stderr)
|
||||||
|
if code != exitOK {
|
||||||
|
t.Errorf("run(%v) = %d, want %d", args, code, exitOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := stdout.String(); got != want.String() {
|
||||||
|
t.Errorf("run(%v) stdout = %q, want what scan %s prints, %q",
|
||||||
|
args, got, helpFlag, want.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := stderr.String(); got != "" {
|
||||||
|
t.Errorf("run(%v) stderr = %q, want nothing", args, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunScanUsageLine(t *testing.T) {
|
func TestRunScanUsageLine(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// README §Subcommands shows this usage line for scan, and the help
|
// README §Subcommands shows this usage line for scan, and the help
|
||||||
// and the usage error both print it.
|
// and the usage error both print it. TestRunHelp and
|
||||||
|
// TestRunUsageErrors check which stream each goes to.
|
||||||
const want = "\n sfdupes scan [--workers N] [-x] [--no-default-excludes] " +
|
const want = "\n sfdupes scan [--workers N] [-x] [--no-default-excludes] " +
|
||||||
"[--exclude PATH]... PATH...\n"
|
"[--exclude PATH]... PATH...\n"
|
||||||
|
|
||||||
@@ -764,13 +821,13 @@ func TestRunScanUsageLine(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
var output bytes.Buffer
|
||||||
|
|
||||||
run(args, &stdout, &stderr)
|
run(args, &output, &output)
|
||||||
|
|
||||||
if !strings.Contains(stderr.String(), want) {
|
if !strings.Contains(output.String(), want) {
|
||||||
t.Errorf("run(%v) stderr = %q, want the usage line %q",
|
t.Errorf("run(%v) output = %q, want the usage line %q",
|
||||||
args, stderr.String(), want)
|
args, output.String(), want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -836,9 +893,9 @@ func TestRunHelpDescribesOutput(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, want := range tc.want {
|
for _, want := range tc.want {
|
||||||
if !strings.Contains(stderr.String(), want) {
|
if !strings.Contains(stdout.String(), want) {
|
||||||
t.Errorf("run(%v) help lacks %q:\n%s",
|
t.Errorf("run(%v) help lacks %q:\n%s",
|
||||||
args, want, stderr.String())
|
args, want, stdout.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -922,6 +979,32 @@ func TestRunVersionWriteFailureIsFatal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunHelpWriteFailureIsFatal(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// README §Error handling: a failed write to stdout is a fatal error,
|
||||||
|
// exit 1, reported on stderr starting with "sfdupes:". Help goes to
|
||||||
|
// stdout, so that holds for help too.
|
||||||
|
cases := [][]string{
|
||||||
|
{helpFlag}, {"-h"}, {helpCommand},
|
||||||
|
{cmdScan, helpFlag}, {helpCommand, cmdScan},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, args := range cases {
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
|
code := run(args, failingWriter{}, &stderr)
|
||||||
|
if code != exitFatal {
|
||||||
|
t.Errorf("run(%v) = %d, want %d", args, code, exitFatal)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := "sfdupes: write stdout: " + errWriteFailed.Error() + "\n"
|
||||||
|
if got := stderr.String(); got != want {
|
||||||
|
t.Errorf("run(%v) stderr = %q, want %q", args, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// scanFixture builds a small tree holding one duplicate pair and one
|
// scanFixture builds a small tree holding one duplicate pair and one
|
||||||
// unreadable file and scans it into the database the caller has
|
// unreadable file and scans it into the database the caller has
|
||||||
// pointed SFDUPES_DATABASE at. The unreadable file makes the scan warn
|
// pointed SFDUPES_DATABASE at. The unreadable file makes the scan warn
|
||||||
@@ -1013,8 +1096,7 @@ func TestRunScanSucceedsDespiteWarnings(t *testing.T) {
|
|||||||
|
|
||||||
stderr := scanOK(t, dir)
|
stderr := scanOK(t, dir)
|
||||||
|
|
||||||
warning := "hash " + unreadable + ": open " + unreadable +
|
warning := "scan: skipped " + unreadable + " (permission denied)\n"
|
||||||
": permission denied\n"
|
|
||||||
if !strings.Contains(stderr, warning) {
|
if !strings.Contains(stderr, warning) {
|
||||||
t.Errorf("stderr = %q, want %q", stderr, warning)
|
t.Errorf("stderr = %q, want %q", stderr, warning)
|
||||||
}
|
}
|
||||||
@@ -1286,7 +1368,7 @@ func TestRunScanSkipsSymlinkToFIFOOperand(t *testing.T) {
|
|||||||
|
|
||||||
stderr := scanOK(t, link)
|
stderr := scanOK(t, link)
|
||||||
|
|
||||||
warning := "walk " + fifo + ": skipping FIFO operand\n"
|
warning := "scan: skipped " + fifo + " (a FIFO)\n"
|
||||||
if !strings.Contains(stderr, warning) {
|
if !strings.Contains(stderr, warning) {
|
||||||
t.Errorf("stderr = %q, want %q", stderr, warning)
|
t.Errorf("stderr = %q, want %q", stderr, warning)
|
||||||
}
|
}
|
||||||
@@ -1321,7 +1403,7 @@ func assertOperandSkipped(t *testing.T, dbPath, operand, kind,
|
|||||||
|
|
||||||
stderr := scanOK(t, operand)
|
stderr := scanOK(t, operand)
|
||||||
|
|
||||||
warning := "walk " + operand + ": skipping " + kind + " operand\n"
|
warning := "scan: skipped " + operand + " (a " + kind + ")\n"
|
||||||
if !strings.Contains(stderr, warning) {
|
if !strings.Contains(stderr, warning) {
|
||||||
t.Errorf("stderr = %q, want %q", stderr, warning)
|
t.Errorf("stderr = %q, want %q", stderr, warning)
|
||||||
}
|
}
|
||||||
@@ -1355,7 +1437,7 @@ func TestRunScanSkipsOperandInsideZFS(t *testing.T) {
|
|||||||
writeFile(t, hourly, "f", pattern(1, 10))
|
writeFile(t, hourly, "f", pattern(1, 10))
|
||||||
f := writeFile(t, dir, "f", pattern(2, 10))
|
f := writeFile(t, dir, "f", pattern(2, 10))
|
||||||
|
|
||||||
warning := "walk " + hourly + ": skipping operand inside a .zfs directory\n"
|
warning := "scan: skipped " + hourly + " (inside a .zfs directory)\n"
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
operands []string
|
operands []string
|
||||||
@@ -1407,7 +1489,7 @@ func TestRunScanSkipsOperandThroughSymlinkIntoZFS(t *testing.T) {
|
|||||||
stderr := scanOK(t, filepath.Join(snaps, "daily"))
|
stderr := scanOK(t, filepath.Join(snaps, "daily"))
|
||||||
|
|
||||||
daily := filepath.Join(dir, "pool", ".zfs", "snapshot", "daily")
|
daily := filepath.Join(dir, "pool", ".zfs", "snapshot", "daily")
|
||||||
warning := "walk " + daily + ": skipping operand inside a .zfs directory\n"
|
warning := "scan: skipped " + daily + " (inside a .zfs directory)\n"
|
||||||
summary := "scan: 0 files seen (0 added, 0 updated, 0 removed, " +
|
summary := "scan: 0 files seen (0 added, 0 updated, 0 removed, " +
|
||||||
"0 unchanged), 1 skipped\n"
|
"0 unchanged), 1 skipped\n"
|
||||||
|
|
||||||
|
|||||||
@@ -121,9 +121,9 @@ func TestRunDwimExcludeLeavesTreeOut(t *testing.T) {
|
|||||||
t.Errorf("group = %q, want it to start %q", lines[1], want)
|
t.Errorf("group = %q, want it to start %q", lines[1], want)
|
||||||
}
|
}
|
||||||
|
|
||||||
skipped := "scan: skipped " + excluded + " (--exclude)\n"
|
leftOut := "scan: left out " + excluded + " (--exclude)\n"
|
||||||
if got := stderr(); !strings.Contains(got, skipped) {
|
if got := stderr(); !strings.Contains(got, leftOut) {
|
||||||
t.Errorf("stderr = %q, want it to contain %q", got, skipped)
|
t.Errorf("stderr = %q, want it to contain %q", got, leftOut)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,21 +190,21 @@ func TestRunDwimHelpListsScanFlags(t *testing.T) {
|
|||||||
|
|
||||||
run([]string{cmdDwim, "-h"}, &stdout, &stderr)
|
run([]string{cmdDwim, "-h"}, &stdout, &stderr)
|
||||||
|
|
||||||
if !strings.Contains(stderr.String(), want) {
|
if !strings.Contains(stdout.String(), want) {
|
||||||
t.Errorf("dwim -h stderr = %q, want the usage line %q",
|
t.Errorf("dwim -h stdout = %q, want the usage line %q",
|
||||||
stderr.String(), want)
|
stdout.String(), want)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(stderr.String(), excludeFlag+" PATH") {
|
if !strings.Contains(stdout.String(), excludeFlag+" PATH") {
|
||||||
t.Errorf("dwim -h stderr = %q, want %s PATH", stderr.String(),
|
t.Errorf("dwim -h stdout = %q, want %s PATH", stdout.String(),
|
||||||
excludeFlag)
|
excludeFlag)
|
||||||
}
|
}
|
||||||
|
|
||||||
// README "Settings": the help lists the default excludes and the
|
// README "Settings": the help lists the default excludes and the
|
||||||
// filesystem types the walk never records anything on.
|
// filesystem types the walk never records anything on.
|
||||||
help := strings.TrimSpace(defaultExcludesHelp(runtime.GOOS))
|
help := strings.TrimSpace(defaultExcludesHelp(runtime.GOOS))
|
||||||
if !strings.Contains(stderr.String(), help) {
|
if !strings.Contains(stdout.String(), help) {
|
||||||
t.Errorf("dwim -h stderr = %q, want it to hold %q", stderr.String(),
|
t.Errorf("dwim -h stdout = %q, want it to hold %q", stdout.String(),
|
||||||
help)
|
help)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,7 +203,7 @@ func (rules walkRules) excludePathLine(path string, fi fs.FileInfo) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("scan: skipped %s (--exclude)", path)
|
return fmt.Sprintf("scan: left out %s (--exclude)", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// nonPersistentFilesystemTypes returns the filesystem types that are
|
// nonPersistentFilesystemTypes returns the filesystem types that are
|
||||||
@@ -233,7 +233,7 @@ func (rules walkRules) nonPersistentLine(path string, fi fs.FileInfo) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("scan: skipped %s (%s, not a persistent filesystem)",
|
return fmt.Sprintf("scan: left out %s (%s, not a persistent filesystem)",
|
||||||
path, fsType)
|
path, fsType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -196,8 +196,8 @@ func TestDefaultExcludesHelp(t *testing.T) {
|
|||||||
run([]string{cmdScan, "-h"}, &stdout, &stderr)
|
run([]string{cmdScan, "-h"}, &stdout, &stderr)
|
||||||
|
|
||||||
help := strings.TrimSpace(defaultExcludesHelp(runtime.GOOS))
|
help := strings.TrimSpace(defaultExcludesHelp(runtime.GOOS))
|
||||||
if !strings.Contains(stderr.String(), help) {
|
if !strings.Contains(stdout.String(), help) {
|
||||||
t.Errorf("scan --help = %q, want it to hold %q", stderr.String(), help)
|
t.Errorf("scan --help = %q, want it to hold %q", stdout.String(), help)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ func TestScanDoesNotEnterDefaultExclude(t *testing.T) {
|
|||||||
t.Errorf("%s recorded: the walk entered %s", unseen, excluded)
|
t.Errorf("%s recorded: the walk entered %s", unseen, excluded)
|
||||||
}
|
}
|
||||||
|
|
||||||
line := "\nscan: skipped " + excluded + " (default exclude)\n"
|
line := "\nscan: left out " + excluded + " (default exclude)\n"
|
||||||
if !strings.Contains(stderr(), line) {
|
if !strings.Contains(stderr(), line) {
|
||||||
t.Errorf("stderr = %q, want %q", stderr(), line)
|
t.Errorf("stderr = %q, want %q", stderr(), line)
|
||||||
}
|
}
|
||||||
@@ -311,7 +311,7 @@ func TestScanNamesNoDefaultExcludeThatIsAnOperand(t *testing.T) {
|
|||||||
|
|
||||||
recordByPath(t, dbRecords(t, db), f)
|
recordByPath(t, dbRecords(t, db), f)
|
||||||
|
|
||||||
if strings.Contains(stderr(), "scan: skipped") {
|
if strings.Contains(stderr(), "scan: left out") {
|
||||||
t.Errorf("stderr = %q, want no line for %s", stderr(), excluded)
|
t.Errorf("stderr = %q, want no line for %s", stderr(), excluded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,8 +332,8 @@ func TestRunScanTempDirOperand(t *testing.T) {
|
|||||||
f := writeFile(t, dir, "sub/f", pattern(1, 10))
|
f := writeFile(t, dir, "sub/f", pattern(1, 10))
|
||||||
|
|
||||||
stderr := scanOK(t, dir)
|
stderr := scanOK(t, dir)
|
||||||
if strings.Contains(stderr, "scan: skipped") {
|
if strings.Contains(stderr, "scan: left out") {
|
||||||
t.Errorf("stderr = %q, want no line saying a path was skipped",
|
t.Errorf("stderr = %q, want no line saying a path was left out",
|
||||||
stderr)
|
stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +377,7 @@ func TestRunScanNoDefaultExcludes(t *testing.T) {
|
|||||||
path := testDBPath(t)
|
path := testDBPath(t)
|
||||||
t.Setenv(databaseEnv, path)
|
t.Setenv(databaseEnv, path)
|
||||||
|
|
||||||
line := "scan: skipped /var/tmp (default exclude)\n"
|
line := "scan: left out /var/tmp (default exclude)\n"
|
||||||
|
|
||||||
if stderr := scanOK(t, "/var"); !strings.Contains(stderr, line) {
|
if stderr := scanOK(t, "/var"); !strings.Contains(stderr, line) {
|
||||||
t.Errorf("scan /var: stderr = %q, want %q", stderr, line)
|
t.Errorf("scan /var: stderr = %q, want %q", stderr, line)
|
||||||
@@ -418,7 +418,7 @@ func TestRunScanExcludeNested(t *testing.T) {
|
|||||||
|
|
||||||
stderr := scanOK(t, excludeFlag, excluded, dir)
|
stderr := scanOK(t, excludeFlag, excluded, dir)
|
||||||
|
|
||||||
line := "scan: skipped " + excluded + " (--exclude)\n"
|
line := "scan: left out " + excluded + " (--exclude)\n"
|
||||||
if !strings.Contains(stderr, line) {
|
if !strings.Contains(stderr, line) {
|
||||||
t.Errorf("stderr = %q, want %q", stderr, line)
|
t.Errorf("stderr = %q, want %q", stderr, line)
|
||||||
}
|
}
|
||||||
@@ -480,8 +480,8 @@ func TestRunScanExcludeReachedByAnotherPath(t *testing.T) {
|
|||||||
stderr := scanOK(t, excludeFlag, filepath.Join(link, "cache"),
|
stderr := scanOK(t, excludeFlag, filepath.Join(link, "cache"),
|
||||||
excludeFlag, dotDot, dir)
|
excludeFlag, dotDot, dir)
|
||||||
|
|
||||||
for _, skipped := range []string{filepath.Dir(inDir), file, hardLink} {
|
for _, leftOut := range []string{filepath.Dir(inDir), file, hardLink} {
|
||||||
line := "scan: skipped " + skipped + " (--exclude)\n"
|
line := "scan: left out " + leftOut + " (--exclude)\n"
|
||||||
if !strings.Contains(stderr, line) {
|
if !strings.Contains(stderr, line) {
|
||||||
t.Errorf("stderr = %q, want %q", stderr, line)
|
t.Errorf("stderr = %q, want %q", stderr, line)
|
||||||
}
|
}
|
||||||
@@ -615,7 +615,7 @@ func TestRunScanSkipsNonPersistentOperand(t *testing.T) {
|
|||||||
|
|
||||||
t.Setenv(databaseEnv, testDBPath(t))
|
t.Setenv(databaseEnv, testDBPath(t))
|
||||||
|
|
||||||
line := "scan: skipped " + shm + " (" + fsType +
|
line := "scan: left out " + shm + " (" + fsType +
|
||||||
", not a persistent filesystem)\n"
|
", not a persistent filesystem)\n"
|
||||||
summary := "scan: 0 files seen"
|
summary := "scan: 0 files seen"
|
||||||
|
|
||||||
@@ -669,7 +669,7 @@ func TestScanKeepsRecordsOnNonPersistent(t *testing.T) {
|
|||||||
t.Errorf("%s recorded", unseen)
|
t.Errorf("%s recorded", unseen)
|
||||||
}
|
}
|
||||||
|
|
||||||
line := "scan: skipped " + shm + " (" + fsType +
|
line := "scan: left out " + shm + " (" + fsType +
|
||||||
", not a persistent filesystem)\n"
|
", not a persistent filesystem)\n"
|
||||||
if !strings.Contains(stderr(), line) {
|
if !strings.Contains(stderr(), line) {
|
||||||
t.Errorf("stderr = %q, want %q", stderr(), line)
|
t.Errorf("stderr = %q, want %q", stderr(), line)
|
||||||
@@ -702,7 +702,7 @@ func TestWalkLeavesOutNonPersistent(t *testing.T) {
|
|||||||
|
|
||||||
leftOut = append(leftOut, ev.leftOut)
|
leftOut = append(leftOut, ev.leftOut)
|
||||||
|
|
||||||
want := "scan: skipped " + ev.leftOut + " (" + fsType +
|
want := "scan: left out " + ev.leftOut + " (" + fsType +
|
||||||
", not a persistent filesystem)"
|
", not a persistent filesystem)"
|
||||||
if ev.warn != want {
|
if ev.warn != want {
|
||||||
t.Errorf("line = %q, want %q", ev.warn, want)
|
t.Errorf("line = %q, want %q", ev.warn, want)
|
||||||
|
|||||||
+28
-12
@@ -664,8 +664,8 @@ func isDatabaseFile(f fileRec, rules walkRules) bool {
|
|||||||
// inode run, committing completed records to the database in batches
|
// inode run, committing completed records to the database in batches
|
||||||
// as results arrive, so a long scan persists its progress as it goes
|
// as results arrive, so a long scan persists its progress as it goes
|
||||||
// (an interrupted scan resumes cheaply: the next run skips everything
|
// (an interrupted scan resumes cheaply: the next run skips everything
|
||||||
// already recorded). A run that fails to hash is warned about and
|
// already recorded). Each path of a run that fails to hash is warned
|
||||||
// counted as skipped; stale records for its paths, if any, are deleted
|
// about and counted as skipped; stale records for them, if any, are deleted
|
||||||
// by the update phase. A run whose file no longer exists gets no record
|
// by the update phase. A run whose file no longer exists gets no record
|
||||||
// and no warning, and is not counted as skipped. The total counts actual
|
// and no warning, and is not counted as skipped. The total counts actual
|
||||||
// reads, so the display's time remaining is meaningful.
|
// reads, so the display's time remaining is meaningful.
|
||||||
@@ -706,9 +706,12 @@ func (s *scanState) hashPhase(ctx context.Context, workers int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if r.err != nil {
|
if r.err != nil {
|
||||||
s.st.skipped += len(r.run)
|
// Every path in the run is counted, so every path gets a line.
|
||||||
|
for _, rec := range r.run {
|
||||||
|
s.st.skipped++
|
||||||
|
|
||||||
prog.warnf("hash %s: %v", r.run[0].path, r.err)
|
prog.warnf("%s", skippedLine(rec.path, r.err))
|
||||||
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -915,8 +918,7 @@ func operandWarning(root string, fi fs.FileInfo) string {
|
|||||||
// The walk never enters a .zfs directory, so it starts from nothing
|
// The walk never enters a .zfs directory, so it starts from nothing
|
||||||
// inside one either.
|
// inside one either.
|
||||||
if slices.Contains(strings.Split(filepath.Dir(root), "/"), ".zfs") {
|
if slices.Contains(strings.Split(filepath.Dir(root), "/"), ".zfs") {
|
||||||
return fmt.Sprintf("walk %s: skipping operand inside a .zfs directory",
|
return fmt.Sprintf("scan: skipped %s (inside a .zfs directory)", root)
|
||||||
root)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var kind string
|
var kind string
|
||||||
@@ -942,7 +944,21 @@ func operandWarning(root string, fi fs.FileInfo) string {
|
|||||||
kind = "non-regular file"
|
kind = "non-regular file"
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("walk %s: skipping %s operand", root, kind)
|
return fmt.Sprintf("scan: skipped %s (a %s)", root, kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
// skippedLine returns the warning for a path the scan skips because of
|
||||||
|
// err. The reason is the error without its path, which the line already
|
||||||
|
// names.
|
||||||
|
func skippedLine(path string, err error) string {
|
||||||
|
reason := err.Error()
|
||||||
|
|
||||||
|
var pathErr *fs.PathError
|
||||||
|
if errors.As(err, &pathErr) {
|
||||||
|
reason = pathErr.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("scan: skipped %s (%s)", path, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
// seedRoot turns one PATH operand into the walk's starting state: a
|
// seedRoot turns one PATH operand into the walk's starting state: a
|
||||||
@@ -965,7 +981,7 @@ func seedRoot(ctx context.Context, root string, rules walkRules,
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendEvent(ctx, events, walkEvent{
|
sendEvent(ctx, events, walkEvent{
|
||||||
warn: fmt.Sprintf("walk %s: %v", root, err),
|
warn: skippedLine(root, err),
|
||||||
fail: true,
|
fail: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1094,7 +1110,7 @@ func walkOneDir(ctx context.Context, job dirJob, oneFS bool,
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendEvent(ctx, events, walkEvent{
|
sendEvent(ctx, events, walkEvent{
|
||||||
warn: fmt.Sprintf("walk %s: %v", job.path, err),
|
warn: skippedLine(job.path, err),
|
||||||
fail: true,
|
fail: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1153,7 +1169,7 @@ func emitFile(ctx context.Context, p string, e fs.DirEntry,
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendEvent(ctx, events, walkEvent{
|
sendEvent(ctx, events, walkEvent{
|
||||||
warn: fmt.Sprintf("stat %s: %v", p, err),
|
warn: skippedLine(p, err),
|
||||||
fail: true,
|
fail: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1214,7 +1230,7 @@ func subdirJob(ctx context.Context, p string, e fs.DirEntry,
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendEvent(ctx, events, walkEvent{
|
sendEvent(ctx, events, walkEvent{
|
||||||
warn: fmt.Sprintf("walk %s: %v", p, err),
|
warn: skippedLine(p, err),
|
||||||
fail: true,
|
fail: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1235,7 +1251,7 @@ func subdirJob(ctx context.Context, p string, e fs.DirEntry,
|
|||||||
if isDefaultExclude(p, rules.defaultExcludes) {
|
if isDefaultExclude(p, rules.defaultExcludes) {
|
||||||
sendEvent(ctx, events, walkEvent{
|
sendEvent(ctx, events, walkEvent{
|
||||||
leftOut: p,
|
leftOut: p,
|
||||||
warn: fmt.Sprintf("scan: skipped %s (default exclude)", p),
|
warn: fmt.Sprintf("scan: left out %s (default exclude)", p),
|
||||||
})
|
})
|
||||||
|
|
||||||
return dirJob{}, false
|
return dirJob{}, false
|
||||||
|
|||||||
@@ -752,8 +752,9 @@ func TestWalkPassesOverRemovedPaths(t *testing.T) {
|
|||||||
|
|
||||||
// TestScanWarnsAboutUnreadablePaths checks that a permission error is
|
// TestScanWarnsAboutUnreadablePaths checks that a permission error is
|
||||||
// still warned about and counted as skipped, unlike a removed path. The
|
// still warned about and counted as skipped, unlike a removed path. The
|
||||||
// walk reaches an unreadable directory, and a file in a directory that
|
// walk reaches an unreadable directory. It also reaches a file and a
|
||||||
// can be read but not searched. Another such file is an operand.
|
// subdirectory in a directory that can be read but not searched. Another
|
||||||
|
// such file is an operand.
|
||||||
//
|
//
|
||||||
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
||||||
func TestScanWarnsAboutUnreadablePaths(t *testing.T) {
|
func TestScanWarnsAboutUnreadablePaths(t *testing.T) {
|
||||||
@@ -765,6 +766,7 @@ func TestScanWarnsAboutUnreadablePaths(t *testing.T) {
|
|||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
tree := filepath.Join(dir, "tree")
|
tree := filepath.Join(dir, "tree")
|
||||||
walked := writeFile(t, tree, "unsearchable/f", []byte("x"))
|
walked := writeFile(t, tree, "unsearchable/f", []byte("x"))
|
||||||
|
subdir := filepath.Join(filepath.Dir(walked), "d")
|
||||||
operand := writeFile(t, dir, "unsearchable/f", []byte("y"))
|
operand := writeFile(t, dir, "unsearchable/f", []byte("y"))
|
||||||
unreadable := filepath.Join(tree, "unreadable")
|
unreadable := filepath.Join(tree, "unreadable")
|
||||||
|
|
||||||
@@ -773,6 +775,11 @@ func TestScanWarnsAboutUnreadablePaths(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = os.Mkdir(subdir, 0o750)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
modes := map[string]fs.FileMode{
|
modes := map[string]fs.FileMode{
|
||||||
unreadable: 0,
|
unreadable: 0,
|
||||||
filepath.Dir(walked): 0o400,
|
filepath.Dir(walked): 0o400,
|
||||||
@@ -793,15 +800,16 @@ func TestScanWarnsAboutUnreadablePaths(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
st := syncTree(t, openTestDB(t), tree, operand)
|
st := syncTree(t, openTestDB(t), tree, operand)
|
||||||
if st != (scanStats{skipped: 3}) {
|
if st != (scanStats{skipped: 4}) {
|
||||||
t.Errorf("stats = %+v, want 3 skipped and nothing else", st)
|
t.Errorf("stats = %+v, want 4 skipped and nothing else", st)
|
||||||
}
|
}
|
||||||
|
|
||||||
got := stderr()
|
got := stderr()
|
||||||
for _, warning := range []string{
|
for _, warning := range []string{
|
||||||
"walk " + unreadable + ": open " + unreadable + ": permission denied\n",
|
"scan: skipped " + unreadable + " (permission denied)\n",
|
||||||
"stat " + walked + ": lstat " + walked + ": permission denied\n",
|
"scan: skipped " + walked + " (permission denied)\n",
|
||||||
"walk " + operand + ": lstat " + operand + ": permission denied\n",
|
"scan: skipped " + subdir + " (permission denied)\n",
|
||||||
|
"scan: skipped " + operand + " (permission denied)\n",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(got, warning) {
|
if !strings.Contains(got, warning) {
|
||||||
t.Errorf("stderr = %q, want %q", got, warning)
|
t.Errorf("stderr = %q, want %q", got, warning)
|
||||||
@@ -1941,14 +1949,19 @@ func TestScanHardlinksReadOnce(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestScanHardlinkRunFailsTogether makes a file with two hard links
|
||||||
|
// unreadable. Both paths are counted as skipped, and each gets its own
|
||||||
|
// warning.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
||||||
func TestScanHardlinkRunFailsTogether(t *testing.T) {
|
func TestScanHardlinkRunFailsTogether(t *testing.T) {
|
||||||
t.Parallel()
|
stderr := captureStderr(t)
|
||||||
|
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
db := openTestDB(t)
|
db := openTestDB(t)
|
||||||
a := writeFile(t, dir, "a.bin", pattern(1, 300))
|
a := writeFile(t, dir, "a.bin", pattern(1, 300))
|
||||||
|
b := filepath.Join(dir, "b.bin")
|
||||||
|
|
||||||
err := os.Link(a, filepath.Join(dir, "b.bin"))
|
err := os.Link(a, b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -1964,6 +1977,15 @@ func TestScanHardlinkRunFailsTogether(t *testing.T) {
|
|||||||
if st.skipped != 2 || st.added != 0 {
|
if st.skipped != 2 || st.added != 0 {
|
||||||
t.Fatalf("stats = %+v, want both hardlink paths skipped", st)
|
t.Fatalf("stats = %+v, want both hardlink paths skipped", st)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
got := stderr()
|
||||||
|
|
||||||
|
for _, p := range []string{a, b} {
|
||||||
|
warning := "scan: skipped " + p + " (permission denied)\n"
|
||||||
|
if !strings.Contains(got, warning) {
|
||||||
|
t.Errorf("stderr = %q, want %q", got, warning)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHashPhaseSkipsFileReplacedByFIFO replaces a walked regular file
|
// TestHashPhaseSkipsFileReplacedByFIFO replaces a walked regular file
|
||||||
@@ -2001,7 +2023,7 @@ func TestHashPhaseSkipsFileReplacedByFIFO(t *testing.T) {
|
|||||||
s.st.skipped, len(s.batch))
|
s.st.skipped, len(s.batch))
|
||||||
}
|
}
|
||||||
|
|
||||||
warning := "hash " + p + ": not a regular file\n"
|
warning := "scan: skipped " + p + " (not a regular file)\n"
|
||||||
if got := stderr(); strings.Count(got, warning) != 1 {
|
if got := stderr(); strings.Count(got, warning) != 1 {
|
||||||
t.Errorf("stderr = %q, want %q once", got, warning)
|
t.Errorf("stderr = %q, want %q once", got, warning)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user