aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/dup/dup.go8
-rw-r--r--internal/engine/facts.go21
-rw-r--r--internal/engine/match_test.go55
3 files changed, 75 insertions, 9 deletions
diff --git a/internal/dup/dup.go b/internal/dup/dup.go
index 32bcfb0..042aea8 100644
--- a/internal/dup/dup.go
+++ b/internal/dup/dup.go
@@ -108,14 +108,14 @@ func (x *Index) addExtraDir(dir string) []error {
if path == dir {
return err
}
- errs = append(errs, fmt.Errorf("%s: %w", path, err))
+ errs = append(errs, CandidateError{Path: path, Err: err})
return nil
}
if path == dir && d.Type()&fs.ModeSymlink != 0 {
// filepath.WalkDir Lstats its root: a symlinked extra directory
// (A4) would otherwise be silently treated as empty rather than
// followed, with no sign anything was wrong.
- errs = append(errs, fmt.Errorf("%s is a symlink; not followed", path))
+ errs = append(errs, CandidateError{Path: path, Err: errors.New("a symlink; not followed")})
return nil
}
if d.Type()&fs.ModeSymlink != 0 || d.IsDir() || !d.Type().IsRegular() {
@@ -125,7 +125,7 @@ func (x *Index) addExtraDir(dir string) []error {
return nil
})
if err != nil {
- errs = append(errs, fmt.Errorf("%s: %w", dir, err))
+ errs = append(errs, CandidateError{Path: dir, Err: err})
}
return errs
}
@@ -141,7 +141,7 @@ func (x *Index) addEntry(path string, d fs.DirEntry, errs *[]error) {
info, err := d.Info()
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
- *errs = append(*errs, fmt.Errorf("%s: %w", path, err))
+ *errs = append(*errs, CandidateError{Path: path, Err: err})
}
return
}
diff --git a/internal/engine/facts.go b/internal/engine/facts.go
index c510b20..f8fde89 100644
--- a/internal/engine/facts.go
+++ b/internal/engine/facts.go
@@ -5,6 +5,7 @@ package engine
import (
"context"
"errors"
+ "io/fs"
"path/filepath"
"sort"
"strings"
@@ -73,11 +74,27 @@ func (run *matchRun) drainDupErrors() {
defer run.mu.Unlock()
for _, idx := range run.dupIdx {
for _, ce := range idx.Errors() {
- run.warn = append(run.warn, "duplicate: "+xdg.Abbrev(ce.Path)+": "+ce.Err.Error())
+ run.warn = append(run.warn, dupWarning(ce))
}
}
}
+// dupWarning is the one form of a duplicate warning (triage 6): "duplicate:
+// PATH: cause", the path abbreviated like every other one shown, and an OS
+// error's cause without the raw path it would repeat.
+func dupWarning(err error) string {
+ var ce dup.CandidateError
+ if !errors.As(err, &ce) {
+ return "duplicate: " + err.Error()
+ }
+ cause := ce.Err.Error()
+ var pe *fs.PathError
+ if errors.As(ce.Err, &pe) {
+ cause = pe.Op + ": " + pe.Err.Error()
+ }
+ return "duplicate: " + xdg.Abbrev(ce.Path) + ": " + cause
+}
+
// dupIndex returns the shared *dup.Index for the resolved, sorted extra
// directories named by key, building it exactly once across every
// concurrent caller that asks for the same key.
@@ -95,7 +112,7 @@ func (run *matchRun) dupIndex(key string, dirs []string) *dup.Index {
run.mu.Lock()
run.dupIdx[key] = idx
for _, err := range errs {
- run.warn = append(run.warn, "duplicate: "+err.Error())
+ run.warn = append(run.warn, dupWarning(err))
}
run.mu.Unlock()
})
diff --git a/internal/engine/match_test.go b/internal/engine/match_test.go
index f2a01bc..93311df 100644
--- a/internal/engine/match_test.go
+++ b/internal/engine/match_test.go
@@ -232,12 +232,10 @@ func TestMatchWarningsSorted(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- aaDir := filepath.Join(h, "aa-missing")
- zzDir := filepath.Join(h, "zz-missing")
if len(r.Warnings) != 2 {
t.Fatalf("got %d warnings, want 2 (the shared aa-missing dir should fold into one):\n%s", len(r.Warnings), strings.Join(r.Warnings, "\n"))
}
- wantPrefix := []string{"duplicate: " + aaDir + ":", "duplicate: " + zzDir + ":"}
+ wantPrefix := []string{"duplicate: ~/aa-missing:", "duplicate: ~/zz-missing:"}
for i, want := range wantPrefix {
if !strings.HasPrefix(r.Warnings[i], want) {
t.Errorf("Warnings[%d] = %q, want prefix %q", i, r.Warnings[i], want)
@@ -367,3 +365,54 @@ func TestMatchDrainsDupCandidateErrors(t *testing.T) {
t.Errorf("got %d warnings with prefix %q, want 1; warnings: %v", found, want, r.Warnings)
}
}
+
+// TestDuplicateWarningsShareOneFormat: a missing extra directory and a
+// candidate that cannot be hashed are both reported as "duplicate: PATH:
+// cause", the path abbreviated and not repeated raw inside the cause
+// (triage 6).
+func TestDuplicateWarningsShareOneFormat(t *testing.T) {
+ if os.Getuid() == 0 {
+ t.Skip("root reads a chmod 000 file")
+ }
+ h := sandbox(t)
+ dl := filepath.Join(h, "dl")
+ extra := filepath.Join(h, "extra")
+ os.MkdirAll(dl, 0o755)
+ os.MkdirAll(extra, 0o755)
+ old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
+ for _, p := range []string{filepath.Join(dl, "a.txt"), filepath.Join(extra, "c.txt")} {
+ os.WriteFile(p, []byte("same size"), 0o644)
+ os.Chtimes(p, old, old)
+ }
+ os.Chmod(filepath.Join(extra, "c.txt"), 0)
+ t.Cleanup(func() { os.Chmod(filepath.Join(extra, "c.txt"), 0o644) })
+ main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": `
+(path "~/dl")
+(rule "d" (when (duplicate "~/extra" "~/nonexistent")) (move "Dupes"))
+`})
+ e, errs := Load(main)
+ if len(errs) > 0 {
+ t.Fatal(errs)
+ }
+ r, err := e.Match(context.Background(), e.Dirs[0])
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := map[string]bool{
+ "duplicate: ~/extra/c.txt: open: permission denied": false,
+ "duplicate: ~/nonexistent: lstat: no such file or directory": false,
+ }
+ for _, w := range r.Warnings {
+ if strings.Contains(w, h) {
+ t.Errorf("warning repeats the raw path: %q", w)
+ }
+ if _, ok := want[w]; ok {
+ want[w] = true
+ }
+ }
+ for w, seen := range want {
+ if !seen {
+ t.Errorf("missing warning %q in %q", w, r.Warnings)
+ }
+ }
+}