// SPDX-License-Identifier: GPL-3.0-or-later package model import ( "context" "errors" "os" "path/filepath" "strings" "testing" "git.labunix.xyz/krino/internal/engine" "git.labunix.xyz/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) } }