aboutsummaryrefslogtreecommitdiff
path: root/gui/internal/model/plan_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'gui/internal/model/plan_test.go')
-rw-r--r--gui/internal/model/plan_test.go118
1 files changed, 118 insertions, 0 deletions
diff --git a/gui/internal/model/plan_test.go b/gui/internal/model/plan_test.go
index c157b1d..8b7e32f 100644
--- a/gui/internal/model/plan_test.go
+++ b/gui/internal/model/plan_test.go
@@ -4,13 +4,16 @@ package model
import (
"context"
+ "errors"
"os"
"path/filepath"
+ "sort"
"strings"
"testing"
"time"
"krino/internal/engine"
+ "krino/internal/lock"
"krino/internal/plan"
)
@@ -185,3 +188,118 @@ func TestPlanLeavesTheDirectoryAlone(t *testing.T) {
t.Errorf("planning created the destination: %v", err)
}
}
+
+// TestPlanHoldsTheLock: while a plan is open nothing else may work in that
+// directory - what the window shows stays the truth while the user chooses -
+// and closing the tab lets the next run in (GUI design §3).
+func TestPlanHoldsTheLock(t *testing.T) {
+ conf := "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n"
+ e, _ := sandboxDir(t, conf, map[string]string{"a.pdf": "one"})
+ tab, _ := planTab(t, e)
+ if _, err := lock.Acquire(context.Background(), e.Config.LockFile("dl"), false); !errors.Is(err, lock.ErrHeld) {
+ t.Fatalf("an open plan does not hold the lock: %v", err)
+ }
+ if err := tab.Close(); err != nil {
+ t.Fatal(err)
+ }
+ l, err := lock.Acquire(context.Background(), e.Config.LockFile("dl"), false)
+ if err != nil {
+ t.Fatalf("closing the tab did not release the lock: %v", err)
+ }
+ l.Release()
+ if err := tab.Close(); err != nil {
+ t.Errorf("closing twice: %v", err)
+ }
+}
+
+// TestPlanRefusesAHeldDirectory: a directory another krino is working in is
+// not planned at all, and the message names it.
+func TestPlanRefusesAHeldDirectory(t *testing.T) {
+ conf := "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n"
+ e, _ := sandboxDir(t, conf, map[string]string{"a.pdf": "one"})
+ held, err := lock.Acquire(context.Background(), e.Config.LockFile("dl"), false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer held.Release()
+ s, err := e.NewSession(false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer s.Close()
+ if _, err := Plan(context.Background(), s, e.Dirs[0]); !errors.Is(err, lock.ErrHeld) {
+ t.Fatalf("Plan = %v, want lock.ErrHeld", err)
+ } else if !strings.Contains(err.Error(), "dl") {
+ t.Errorf("the message does not name the directory: %v", err)
+ }
+}
+
+// TestRescanForgetsUnusedNames: a destination the last plan claimed but
+// never used is free again on the next scan, so looking twice at the same
+// directory does not creep up through name-1, name-2 (spec §7.4).
+func TestRescanForgetsUnusedNames(t *testing.T) {
+ conf := "(path \"~/dl\")\n(rule \"all\" (move \"Out\") (rename \"same.pdf\"))\n"
+ e, _ := sandboxDir(t, conf, map[string]string{"a.pdf": "one", "b.pdf": "two"})
+ tab, s := planTab(t, e)
+ // Each file moves into Out and is then renamed; the second file's name
+ // is taken, so the plan reserves a suffixed one for it.
+ want := destinations(t, tab)
+ if len(want) != 4 || filepath.Base(want[2]) != "same.pdf" || filepath.Base(want[3]) != "same_1.pdf" {
+ t.Fatalf("first plan = %v", want)
+ }
+ // Looking again without applying anything: the names the closed plan
+ // reserved are free, so the second look reads the same as the first.
+ if err := tab.Close(); err != nil {
+ t.Fatal(err)
+ }
+ again, err := Plan(context.Background(), s, e.Dirs[0])
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got := destinations(t, again); !equal(got, want) {
+ t.Errorf("second plan = %v, want %v", got, want)
+ }
+ // And again after an apply that applied nothing.
+ again.SelectNone()
+ if _, err := again.Apply(context.Background()); err != nil {
+ t.Fatal(err)
+ }
+ if err := again.Close(); err != nil {
+ t.Fatal(err)
+ }
+ third, err := Plan(context.Background(), s, e.Dirs[0])
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer third.Close()
+ if got := destinations(t, third); !equal(got, want) {
+ t.Errorf("third plan = %v, want %v", got, want)
+ }
+}
+
+// destinations is every step's destination in the tab, sorted.
+func destinations(t *testing.T, tab *PlanTab) []string {
+ t.Helper()
+ var out []string
+ for _, r := range tab.Rows {
+ for _, st := range r.Steps {
+ if st.Dst != "" {
+ out = append(out, st.Dst)
+ }
+ }
+ }
+ sort.Strings(out)
+ return out
+}
+
+func equal(a, b []string) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := range a {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+ return true
+}