aboutsummaryrefslogtreecommitdiff
path: root/internal/plan/conflict_test.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-17 13:50:01 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-17 13:50:01 +0200
commit78d8313791f05defc9e0a9f2bad8e9710f741a60 (patch)
tree60d9847eb81313dbd242768a1a06e3e4a175a7c3 /internal/plan/conflict_test.go
parentb596085d2391ce3701fa6820a6728ba634ac453b (diff)
downloadkrino-78d8313791f05defc9e0a9f2bad8e9710f741a60.tar.gz
krino-78d8313791f05defc9e0a9f2bad8e9710f741a60.zip
the suffix search continues instead of starting again at _1
Every file renamed onto one name probed stem_1, stem_2, ... from the beginning, so N files cost N^2/2 Exists calls - a test here counts 1890 of them for 60 files. The search now continues from the highest suffix already tried for that stem. Within one plan that is the same answer: the taken set only grows while a plan is built and the disk is not being written to, so a suffix taken once stays taken. Proved rather than argued - with same_1 and same_3 already on disk and same_2 free, both versions put a file in the gap, and the two plans are byte-identical. 1500 files renamed to one name: 2.63s -> 0.05s
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)
+ }
+}