aboutsummaryrefslogtreecommitdiff
path: root/gui/internal/model/history_test.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-16 09:39:59 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-16 09:39:59 +0200
commit6ef83d6bdfb6120f9e1fbd145e0bc463196103d1 (patch)
tree8eee1f2c29dcbef4ed96b23b08273d42ba0b5d49 /gui/internal/model/history_test.go
parent27eb6353030953e91a45b0104bd0567e53622090 (diff)
downloadkrino-6ef83d6bdfb6120f9e1fbd145e0bc463196103d1.tar.gz
krino-6ef83d6bdfb6120f9e1fbd145e0bc463196103d1.zip
gui: History and undo tab; each plan and undo is its own run
Diffstat (limited to 'gui/internal/model/history_test.go')
-rw-r--r--gui/internal/model/history_test.go207
1 files changed, 207 insertions, 0 deletions
diff --git a/gui/internal/model/history_test.go b/gui/internal/model/history_test.go
new file mode 100644
index 0000000..a486c2e
--- /dev/null
+++ b/gui/internal/model/history_test.go
@@ -0,0 +1,207 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package model
+
+import (
+ "context"
+ "errors"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "krino/internal/engine"
+ "krino/internal/lock"
+)
+
+// applyOneRun sorts the sandbox directory and returns the run id it wrote,
+// so the history tests have something to look back at.
+func applyOneRun(t *testing.T, e *engine.Engine) string {
+ t.Helper()
+ tab, err := Plan(context.Background(), e, e.Dirs[0])
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := tab.Apply(context.Background()); err != nil {
+ t.Fatal(err)
+ }
+ return tab.Run()
+}
+
+// TestRunsListsWhatHappened: the run list shows the run just applied, with
+// its directory and its counts in krino log's words, and says so once the
+// run has been undone.
+func TestRunsListsWhatHappened(t *testing.T) {
+ conf := "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n"
+ e, _ := sandboxDir(t, conf, map[string]string{"a.pdf": "one", "b.pdf": "two"})
+ id := applyOneRun(t, e)
+
+ runs, err := Runs(e, 50)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(runs) != 1 {
+ t.Fatalf("runs = %+v, want the one run", runs)
+ }
+ r := runs[0]
+ if r.ID != id || len(r.Dirs) != 1 || r.Dirs[0] != "dl" {
+ t.Errorf("run = %+v, want %s in dl", r, id)
+ }
+ if r.Summary != "2 moved" {
+ t.Errorf("summary = %q, want %q", r.Summary, "2 moved")
+ }
+ if r.Note != "" || r.UndoOf != "" {
+ t.Errorf("a fresh run is not marked: %+v", r)
+ }
+
+ // Undo it, and the list says so - and carries the undo run itself.
+ tab, err := PlanUndo(context.Background(), e, id)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := tab.Apply(context.Background()); err != nil {
+ t.Fatal(err)
+ }
+ tab.Close()
+ runs, err = Runs(e, 50)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(runs) != 2 {
+ t.Fatalf("runs = %+v, want the run and its undo", runs)
+ }
+ if runs[0].UndoOf != id {
+ t.Errorf("newest run = %+v, want the undo of %s", runs[0], id)
+ }
+ if runs[1].Note != "(undone)" {
+ t.Errorf("undone run = %+v, want (undone)", runs[1])
+ }
+}
+
+// TestUndoRowsAndSelection: every file of the run is a row, unchecked files
+// are logged as declined rather than reversed, and the counts read as the
+// terminal's header does.
+func TestUndoRowsAndSelection(t *testing.T) {
+ conf := "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n"
+ e, h := sandboxDir(t, conf, map[string]string{"a.pdf": "one", "b.pdf": "two"})
+ id := applyOneRun(t, e)
+
+ tab, err := PlanUndo(context.Background(), e, id)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer tab.Close()
+ if tab.Counts.Files != 2 || tab.Counts.ToReverse != 2 || tab.Counts.Refused != 0 {
+ t.Fatalf("counts = %+v", tab.Counts)
+ }
+ if tab.SelectedCount() != 2 {
+ t.Fatalf("rows do not start selected: %+v", tab.Rows)
+ }
+ for i, r := range tab.Rows {
+ if r.File == "b.pdf" {
+ tab.Toggle(i)
+ }
+ }
+ res, err := tab.Apply(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.Applied != 1 || res.Declined != 1 {
+ t.Errorf("result = %+v", res)
+ }
+ if _, err := os.Stat(filepath.Join(h, "dl", "a.pdf")); err != nil {
+ t.Errorf("the checked file was not put back: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(h, "dl", "Out", "b.pdf")); err != nil {
+ t.Errorf("the unchecked file was put back anyway: %v", err)
+ }
+ for _, r := range tab.Rows {
+ want := "done"
+ if r.File == "b.pdf" {
+ want = "declined"
+ }
+ if r.Outcome != want {
+ t.Errorf("%s: outcome %q, want %q", r.File, r.Outcome, want)
+ }
+ }
+}
+
+// TestUndoRefusedRowIsNotSelectable: a file undo will not touch - here its
+// destination is gone - is shown with the reason and can never be checked
+// (GUI design §4).
+func TestUndoRefusedRowIsNotSelectable(t *testing.T) {
+ conf := "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n"
+ e, h := sandboxDir(t, conf, map[string]string{"a.pdf": "one", "b.pdf": "two"})
+ id := applyOneRun(t, e)
+ if err := os.Remove(filepath.Join(h, "dl", "Out", "a.pdf")); err != nil {
+ t.Fatal(err)
+ }
+
+ tab, err := PlanUndo(context.Background(), e, id)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer tab.Close()
+ if tab.Counts.Refused != 1 || tab.Counts.ToReverse != 1 {
+ t.Fatalf("counts = %+v", tab.Counts)
+ }
+ tab.SelectAll()
+ for i, r := range tab.Rows {
+ if r.File != "a.pdf" {
+ continue
+ }
+ if r.Refused == "" {
+ t.Errorf("the missing file carries no reason: %+v", r)
+ }
+ if r.Selected || r.Actable {
+ t.Errorf("a refused row is selectable: %+v", r)
+ }
+ tab.Toggle(i)
+ if tab.Rows[i].Selected {
+ t.Error("Toggle checked a refused row")
+ }
+ }
+}
+
+// TestUndoHoldsTheDirectoryLocks: while an undo plan is open nothing else
+// may work in the directories it covers, and Close - like Apply - frees
+// them (GUI design §4).
+func TestUndoHoldsTheDirectoryLocks(t *testing.T) {
+ conf := "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n"
+ e, _ := sandboxDir(t, conf, map[string]string{"a.pdf": "one"})
+ id := applyOneRun(t, e)
+
+ tab, err := PlanUndo(context.Background(), e, id)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := lock.Acquire(context.Background(), e.Config.LockFile("dl"), false); !errors.Is(err, lock.ErrHeld) {
+ t.Fatalf("an open undo 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()
+}
+
+// TestPlanUndoRefusesAHeldDirectory: a directory another krino is working
+// in stops the undo before anything is shown, and the message names it.
+func TestPlanUndoRefusesAHeldDirectory(t *testing.T) {
+ conf := "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n"
+ e, _ := sandboxDir(t, conf, map[string]string{"a.pdf": "one"})
+ id := applyOneRun(t, e)
+ held, err := lock.Acquire(context.Background(), e.Config.LockFile("dl"), false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer held.Release()
+ if _, err := PlanUndo(context.Background(), e, id); !errors.Is(err, lock.ErrHeld) {
+ t.Fatalf("PlanUndo = %v, want lock.ErrHeld", err)
+ } else if !strings.Contains(err.Error(), "dl") {
+ t.Errorf("the message does not name the directory: %v", err)
+ }
+}