From 1bb5097986558ee7dd3aeaf8e7defefdf91ae4b6 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Mon, 14 Sep 2026 23:48:23 +0200 Subject: duplicate warnings share one format --- internal/engine/facts.go | 21 +++++++++++++++-- internal/engine/match_test.go | 55 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 5 deletions(-) (limited to 'internal/engine') 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) + } + } +} -- cgit v1.3