summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/krino/matching_test.go4
-rw-r--r--cmd/krino/sort_test.go101
2 files changed, 79 insertions, 26 deletions
diff --git a/cmd/krino/matching_test.go b/cmd/krino/matching_test.go
index 69c0eca..25a2993 100644
--- a/cmd/krino/matching_test.go
+++ b/cmd/krino/matching_test.go
@@ -18,7 +18,7 @@ const dlRules = `
(recursive yes)
(min-age 0s)
(ignore "*.part")
-(rule "dups" (when (duplicate)) (delete) (stop))
+(rule "dups" (when (duplicate)) (move "Dupes") (stop))
(rule "acme" (when (type document) (content "acme ltd")) (move "Work/Acme") (stop))
(rule "images" (when (type image)) (move "Pictures"))
(rule "rest" (when (not (matched)) (type text)) (move "Other"))
@@ -66,7 +66,7 @@ func TestDryRun(t *testing.T) {
"krino: dl ~/dl\n8 scanned · 4 to act on · 2 warnings · ",
"\n 1 inv1.txt move → Work/Acme/ acme type txt, content \"acme ltd\"\n",
"\n 2 notes.txt move → Other/ rest not matched, type txt\n",
- "\n 4 report (1).pdf trash dups duplicate of report.pdf\n",
+ "\n 4 report (1).pdf move → Dupes/ dups duplicate of report.pdf\n",
"\nwarnings\n brochure.doc acme: content unreadable: needs antiword or catdoc, not installed\n",
"\nnot acted on: 1 ignored · 1 busy · 2 unmatched (-v lists them)\n",
} {
diff --git a/cmd/krino/sort_test.go b/cmd/krino/sort_test.go
index fa9a7ca..321c9e8 100644
--- a/cmd/krino/sort_test.go
+++ b/cmd/krino/sort_test.go
@@ -111,14 +111,14 @@ func TestAllSkippedDirectoryReportsZeroAndLogsNothing(t *testing.T) {
}
}
-// TestPermanentDeleteOfDuplicatesUnderOverlappingDirKeepsACopy drives the
-// whole run end to end on the shape where a (duplicate "DIR") overlaps the
-// scanned tree: a recursive root holding Archive/x.pdf and a loose, older
-// x-copy.pdf with the same bytes, and a rule that permanently deletes
-// duplicates of anything under Archive. Whatever the verdicts, applying
-// the plan must leave the content on disk; the expected outcome is that
-// the archived copy stays and only the loose one goes.
-func TestPermanentDeleteOfDuplicatesUnderOverlappingDirKeepsACopy(t *testing.T) {
+// TestDuplicatesUnderOverlappingDirMoveOnlyTheLooseCopy drives the whole run
+// end to end on the shape where a (duplicate "DIR") overlaps the scanned
+// tree: a recursive root holding Archive/x.pdf and a loose, older
+// x-copy.pdf with the same bytes, and a rule that moves duplicates of
+// anything under Archive aside. Every lookup must elect the same original,
+// so the archived copy stays where it is and only the loose one is moved;
+// an inconsistent election would move both.
+func TestDuplicatesUnderOverlappingDirMoveOnlyTheLooseCopy(t *testing.T) {
h := home(t)
dl := filepath.Join(h, "dl")
content := []byte("%PDF acme statement")
@@ -143,33 +143,86 @@ func TestPermanentDeleteOfDuplicatesUnderOverlappingDirKeepsACopy(t *testing.T)
t.Fatal(errOut)
}
rules := "(path \"~/dl\")\n(recursive yes)\n(min-age 0s)\n" +
- "(rule \"dups\" (when (duplicate \"Archive\")) (delete permanent))\n"
+ "(rule \"dups\" (when (duplicate \"Archive\")) (move \"~/dupes\") (stop))\n"
if err := os.WriteFile(filepath.Join(h, ".config", "krino", "dirs", "dl.conf"), []byte(rules), 0o644); err != nil {
t.Fatal(err)
}
code, out, errOut := runCLI(t, "-y")
+ if code != 0 {
+ t.Fatalf("run: exit %d\nstdout:\n%s\nstderr:\n%s", code, out, errOut)
+ }
+ moved := filepath.Join(h, "dupes", "x-copy.pdf")
+ for _, p := range []string{archived, moved} {
+ b, err := os.ReadFile(p)
+ if err != nil || string(b) != string(content) {
+ t.Errorf("%s: want the content there, got err %v", p, err)
+ }
+ }
+ if _, err := os.Stat(loose); !os.IsNotExist(err) {
+ t.Errorf("%s is still in place; want it moved to %s", loose, moved)
+ }
+}
- var copies []string
- err := filepath.WalkDir(dl, func(p string, d os.DirEntry, err error) error {
- if err != nil || !d.Type().IsRegular() {
- return err
+// TestCheckRefusesDuplicateWithDelete: krino check reports the §4.5
+// refusal on stderr and fails, before any run could act on the rule.
+func TestCheckRefusesDuplicateWithDelete(t *testing.T) {
+ h := home(t)
+ dl := filepath.Join(h, "dl")
+ if err := os.MkdirAll(dl, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if code, _, errOut := runCLI(t, "init"); code != 0 {
+ t.Fatal(errOut)
+ }
+ if code, _, errOut := runCLI(t, "new", "dl", dl); code != 0 {
+ t.Fatal(errOut)
+ }
+ rules := "(path \"~/dl\")\n(rule \"d\" (when (duplicate)) (delete))\n"
+ if err := os.WriteFile(filepath.Join(h, ".config", "krino", "dirs", "dl.conf"), []byte(rules), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ code, _, errOut := runCLI(t, "check")
+ want := `rule "d": (duplicate) cannot be combined with (delete)`
+ if code == 0 || !strings.Contains(errOut, want) {
+ t.Errorf("check: exit %d, stderr %q; want non-zero and %q", code, errOut, want)
+ }
+}
+
+// TestDryRunShowsNeverDeletedSkip pins the plan line spec §5.5 names.
+func TestDryRunShowsNeverDeletedSkip(t *testing.T) {
+ h := home(t)
+ dl := filepath.Join(h, "dl")
+ old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
+ for name, mt := range map[string]time.Time{"a.pdf": old, "b.pdf": old.Add(time.Hour)} {
+ p := filepath.Join(dl, name)
+ if err := os.MkdirAll(dl, 0o755); err != nil {
+ t.Fatal(err)
}
- if b, err := os.ReadFile(p); err == nil && string(b) == string(content) {
- copies = append(copies, p)
+ if err := os.WriteFile(p, []byte("%PDF same"), 0o644); err != nil {
+ t.Fatal(err)
}
- return nil
- })
- if err != nil {
- t.Fatal(err)
+ if err := os.Chtimes(p, mt, mt); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if code, _, errOut := runCLI(t, "init"); code != 0 {
+ t.Fatal(errOut)
+ }
+ if code, _, errOut := runCLI(t, "new", "dl", dl); code != 0 {
+ t.Fatal(errOut)
}
- if len(copies) == 0 {
- t.Fatalf("every copy was deleted (exit %d)\nstdout:\n%s\nstderr:\n%s", code, out, errOut)
+ rules := "(path \"~/dl\")\n(min-age 0s)\n" +
+ "(rule \"dupes\" (when (duplicate)) (move \"Dupes\"))\n" +
+ "(rule \"cleanup\" (when (matched)) (delete))\n"
+ if err := os.WriteFile(filepath.Join(h, ".config", "krino", "dirs", "dl.conf"), []byte(rules), 0o644); err != nil {
+ t.Fatal(err)
}
+ code, out, errOut := runCLI(t, "-n")
if code != 0 {
- t.Fatalf("run: %d %s", code, errOut)
+ t.Fatalf("exit %d: %s", code, errOut)
}
- if len(copies) != 1 || copies[0] != archived {
- t.Errorf("copies left = %v, want only %s", copies, archived)
+ if !strings.Contains(out, "skipped: a duplicate is never deleted") {
+ t.Errorf("plan lacks the skipped delete:\n%s", out)
}
}