From 6ef83d6bdfb6120f9e1fbd145e0bc463196103d1 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Wed, 16 Sep 2026 09:39:59 +0200 Subject: gui: History and undo tab; each plan and undo is its own run --- gui/internal/model/history_test.go | 207 +++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 gui/internal/model/history_test.go (limited to 'gui/internal/model/history_test.go') 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) + } +} -- cgit v1.3