diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-16 09:17:08 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-16 09:17:08 +0200 |
| commit | bcfeb773afcc28641ef2fb47749c91fd0767054a (patch) | |
| tree | f8593c29bad0b204f1ecc9cae650b55f3cc755d6 /gui/internal/model/plan_test.go | |
| parent | 4f98392c9a062e9d4dd8363a2ede9a919ad14006 (diff) | |
| download | krino-bcfeb773afcc28641ef2fb47749c91fd0767054a.tar.gz krino-bcfeb773afcc28641ef2fb47749c91fd0767054a.zip | |
gui: the Plan tab - scan, review, apply, with the directory locked
Diffstat (limited to 'gui/internal/model/plan_test.go')
| -rw-r--r-- | gui/internal/model/plan_test.go | 118 |
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 +} |
