summaryrefslogtreecommitdiff
path: root/internal/engine/plan_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/engine/plan_test.go')
-rw-r--r--internal/engine/plan_test.go146
1 files changed, 146 insertions, 0 deletions
diff --git a/internal/engine/plan_test.go b/internal/engine/plan_test.go
new file mode 100644
index 0000000..a169168
--- /dev/null
+++ b/internal/engine/plan_test.go
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package engine
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "krino/internal/plan"
+)
+
+func TestPlanBuildsChainsFromMatchedFiles(t *testing.T) {
+ h := sandbox(t)
+ os.Mkdir(filepath.Join(h, "dl"), 0o755)
+ old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
+ for _, n := range []string{"z.pdf", "a.pdf", "notes.txt"} {
+ p := filepath.Join(h, "dl", n)
+ if err := os.WriteFile(p, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Chtimes(p, old, old); err != nil {
+ t.Fatal(err)
+ }
+ }
+ main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": `
+(path "~/dl")
+(min-age 0s)
+(rule "pdfs" (when (type pdf)) (move "PDF"))
+`})
+ e, errs := Load(main, "dl")
+ if len(errs) > 0 {
+ t.Fatal(errs)
+ }
+
+ dp, err := e.Plan(context.Background(), e.Dirs[0], plan.NewClaims())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if dp.Dir != e.Dirs[0] {
+ t.Errorf("Dir = %v, want %v", dp.Dir, e.Dirs[0])
+ }
+ if dp.Result == nil || len(dp.Result.Matched) != 2 || len(dp.Result.Unmatched) != 1 {
+ t.Fatalf("Result = %+v", dp.Result)
+ }
+
+ // Chains keep Match's Rel order: "a.pdf" before "z.pdf", regardless of
+ // walk or map-iteration order.
+ if len(dp.Chains) != 2 {
+ t.Fatalf("got %d chains, want 2 (unmatched notes.txt must not appear)", len(dp.Chains))
+ }
+ if dp.Chains[0].File.Rel != "a.pdf" || dp.Chains[1].File.Rel != "z.pdf" {
+ t.Fatalf("chains not in Rel order: %s, %s", dp.Chains[0].File.Rel, dp.Chains[1].File.Rel)
+ }
+
+ c := dp.Chains[0]
+ if len(c.Steps) != 1 {
+ t.Fatalf("a.pdf steps = %+v", c.Steps)
+ }
+ s := c.Steps[0]
+ want := filepath.Join(h, "dl", "PDF", "a.pdf")
+ if s.Kind != plan.Move || s.Rule != "pdfs" || s.Dst != want || s.Skip != "" {
+ t.Errorf("step = %+v, want Move to %s", s, want)
+ }
+}
+
+// TestPlanSharesClaimsAcrossDirectories: A3 - plan.Claims is created once
+// by the caller and threaded through every Engine.Plan call of a run, so
+// two configured directories cannot plan the same final name. Two
+// directories each hold a file named "x.txt" and each carry a rule moving
+// it to one shared destination outside both roots; calling Plan for both
+// with the *same* claims must resolve the second directory's step onto
+// "x_1.txt" rather than let it collide on "x.txt" too.
+func TestPlanSharesClaimsAcrossDirectories(t *testing.T) {
+ h := sandbox(t)
+ shared := filepath.Join(h, "elsewhere")
+ for _, d := range []string{"d1", "d2"} {
+ if err := os.Mkdir(filepath.Join(h, d), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(h, d, "x.txt"), []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ }
+ dirConf := func(path string) string {
+ return fmt.Sprintf(`(path %q)
+(min-age 0s)
+(rule "r" (when (type txt)) (move %q))
+`, path, shared)
+ }
+ main := writeConfig(t, h, `(include "d1" "d2")`, map[string]string{
+ "d1": dirConf("~/d1"),
+ "d2": dirConf("~/d2"),
+ })
+ e, errs := Load(main, "d1", "d2")
+ if len(errs) > 0 {
+ t.Fatal(errs)
+ }
+
+ claims := plan.NewClaims()
+ dp1, err := e.Plan(context.Background(), e.Dirs[0], claims)
+ if err != nil {
+ t.Fatal(err)
+ }
+ dp2, err := e.Plan(context.Background(), e.Dirs[1], claims)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(dp1.Chains) != 1 || len(dp1.Chains[0].Steps) != 1 {
+ t.Fatalf("d1 chains = %+v", dp1.Chains)
+ }
+ if len(dp2.Chains) != 1 || len(dp2.Chains[0].Steps) != 1 {
+ t.Fatalf("d2 chains = %+v", dp2.Chains)
+ }
+ got1 := dp1.Chains[0].Steps[0].Dst
+ got2 := dp2.Chains[0].Steps[0].Dst
+ want1 := filepath.Join(shared, "x.txt")
+ want2 := filepath.Join(shared, "x_1.txt")
+ if got1 != want1 {
+ t.Errorf("d1 dst = %s, want %s", got1, want1)
+ }
+ if got2 != want2 {
+ t.Errorf("d2 dst = %s, want %s (claims must be shared with d1's plan)", got2, want2)
+ }
+}
+
+func TestPlanReturnsMatchError(t *testing.T) {
+ h := sandbox(t)
+ main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": `
+(path "~/dl")
+(rule "r" (when (type pdf)) (move "PDF"))
+`})
+ e, errs := Load(main, "dl")
+ if len(errs) > 0 {
+ t.Fatal(errs)
+ }
+ // dl's root ~/dl does not exist: scan.Walk fails, and Plan must
+ // propagate that error rather than paper over it with an empty plan.
+ if _, err := e.Plan(context.Background(), e.Dirs[0], plan.NewClaims()); err == nil {
+ t.Error("want an error when the root does not exist, got nil")
+ }
+}