aboutsummaryrefslogtreecommitdiff
path: root/internal/plan/conflict_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/plan/conflict_test.go')
-rw-r--r--internal/plan/conflict_test.go54
1 files changed, 53 insertions, 1 deletions
diff --git a/internal/plan/conflict_test.go b/internal/plan/conflict_test.go
index ba415d5..ba111f8 100644
--- a/internal/plan/conflict_test.go
+++ b/internal/plan/conflict_test.go
@@ -3,6 +3,7 @@
package plan
import (
+ "fmt"
"os"
"path/filepath"
"testing"
@@ -232,7 +233,7 @@ func TestSuffixedCapsAttempts(t *testing.T) {
func TestOverwriteNeverDisplacesADirectory(t *testing.T) {
dir := "/home/x/Documents/Invoices"
d := fakeDisk{exists: map[string]bool{dir: true}, dirs: map[string]bool{dir: true}}
- _, skip, displaces := resolveConflict(Move, config.ConflictOverwrite, "/r/Invoices", dir, d, claimed{})
+ _, skip, displaces := resolveConflict(Move, config.ConflictOverwrite, "/r/Invoices", dir, d, newClaimed())
if displaces != "" || skip != "target is not a regular file" {
t.Errorf("skip %q displaces %q; want skipped, nothing displaced", skip, displaces)
}
@@ -252,3 +253,54 @@ func TestOverwriteNeverDisplacesAnotherScannedFile(t *testing.T) {
t.Errorf("Displaces %q Dst %q Skip %q; want no displacement and /r/b_1.pdf", s.Displaces, s.Dst, s.Skip)
}
}
+
+// countingDisk counts Exists calls, so a test can pin how much probing a
+// plan does rather than only what it produces.
+type countingDisk struct {
+ fakeDisk
+ calls int
+}
+
+func (d *countingDisk) Exists(p string) bool {
+ d.calls++
+ return d.fakeDisk.Exists(p)
+}
+
+// TestSuffixedDoesNotRescanFromOne: every file renamed to one name probes
+// stem_1, stem_2, ... for a free suffix. Starting each file's search at 1
+// makes N files into N^2/2 Exists calls - 4000 files were eight million of
+// them - so the search continues from the highest suffix already tried for
+// that name. Within one plan that is the same answer: the taken set only
+// grows while a plan is built, so a suffix taken once stays taken.
+func TestSuffixedDoesNotRescanFromOne(t *testing.T) {
+ const n = 60
+ in := make([]Input, 0, n)
+ for i := 0; i < n; i++ {
+ in = append(in, Input{File: file("/r", fmt.Sprintf("f%02d.pdf", i)), Rules: []RuleMatch{
+ {Name: "one", Actions: []config.Action{act(config.Move, "Work"), act(config.Rename, "same.pdf")}}}})
+ }
+ d := &countingDisk{fakeDisk: fakeDisk{exists: map[string]bool{}}}
+ chains := Build("/r", in, time.Now(), d, NewClaims())
+
+ // Every file must still land on its own name, the lowest free one.
+ seen := map[string]bool{}
+ for _, c := range chains {
+ last := c.Steps[len(c.Steps)-1]
+ if last.Dst == "" {
+ t.Fatalf("%s was skipped: %q", c.File.Rel, last.Skip)
+ }
+ if seen[last.Dst] {
+ t.Errorf("two files planned onto %s", last.Dst)
+ }
+ seen[last.Dst] = true
+ }
+ if len(seen) != n {
+ t.Errorf("%d distinct destinations for %d files", len(seen), n)
+ }
+ // Quadratic probing would be about n*n/2 = 1800 here; linear is a few
+ // per file. The bound is loose on purpose - it must fail on n^2 and
+ // pass on anything sane.
+ if d.calls > 6*n {
+ t.Errorf("%d Exists calls for %d files: the suffix search is rescanning from _1", d.calls, n)
+ }
+}