aboutsummaryrefslogtreecommitdiff
path: root/gui/internal/model/rules_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'gui/internal/model/rules_test.go')
-rw-r--r--gui/internal/model/rules_test.go241
1 files changed, 241 insertions, 0 deletions
diff --git a/gui/internal/model/rules_test.go b/gui/internal/model/rules_test.go
new file mode 100644
index 0000000..c7fa384
--- /dev/null
+++ b/gui/internal/model/rules_test.go
@@ -0,0 +1,241 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package model
+
+import (
+ "context"
+ "errors"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "krino/internal/engine"
+)
+
+// TestOpenAndCheck: the editor opens a directory's own file, reports the
+// mistakes in unsaved text with their positions, and says so when there are
+// none (GUI design §5.3).
+func TestOpenAndCheck(t *testing.T) {
+ conf := "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n"
+ e, _ := sandboxDir(t, conf, map[string]string{"a.pdf": "one"})
+
+ r, err := OpenRules(e, "dl")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if r.Text != conf {
+ t.Fatalf("text = %q, want the file as written", r.Text)
+ }
+ if diags := r.Check(); len(diags) != 0 {
+ t.Fatalf("a good file reports %v", diags)
+ }
+ if r.Modified() {
+ t.Error("an untouched file counts as modified")
+ }
+
+ // An unknown placeholder is refused at check time (plan 12 task 3), and
+ // the position is the action's, inside this file.
+ r.SetText("(path \"~/dl\")\n(rule \"all\" (move \"Out/{nope}\"))\n")
+ if !r.Modified() {
+ t.Error("edited text does not count as modified")
+ }
+ diags := r.Check()
+ if len(diags) != 1 {
+ t.Fatalf("diags = %v, want one", diags)
+ }
+ if diags[0].File != r.File || diags[0].Pos.Line != 2 {
+ t.Errorf("diag = %s, want it in %s at line 2", diags[0], r.File)
+ }
+ if !strings.Contains(diags[0].Msg, "nope") {
+ t.Errorf("message does not name the placeholder: %s", diags[0].Msg)
+ }
+ // Checking never writes: the file on disk still holds the good text.
+ on, _ := os.ReadFile(r.File)
+ if string(on) != conf {
+ t.Errorf("check wrote to the file: %q", on)
+ }
+}
+
+// TestSaveKeepsABackup: saving writes the text, keeps what was there as
+// NAME.conf.bak, and the engine then loads the new rules (GUI design §5.3).
+func TestSaveKeepsABackup(t *testing.T) {
+ conf := "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n"
+ e, h := sandboxDir(t, conf, map[string]string{"a.pdf": "one"})
+ r, err := OpenRules(e, "dl")
+ if err != nil {
+ t.Fatal(err)
+ }
+ next := "(path \"~/dl\")\n(rule \"all\" (move \"Elsewhere\"))\n"
+ r.SetText(next)
+ if err := r.Save(); err != nil {
+ t.Fatal(err)
+ }
+ if on, _ := os.ReadFile(r.File); string(on) != next {
+ t.Errorf("file = %q, want the new text", on)
+ }
+ bak := filepath.Join(filepath.Dir(r.File), "dl.conf.bak")
+ if on, _ := os.ReadFile(bak); string(on) != conf {
+ t.Errorf("backup = %q, want the previous text", on)
+ }
+ if r.Modified() {
+ t.Error("a saved file still counts as modified")
+ }
+ // The saved rules are what krino now reads.
+ fresh := reload(t, h)
+ tab, err := Plan(context.Background(), fresh, fresh.Dirs[0])
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer tab.Close()
+ if len(tab.Rows) != 1 || len(tab.Rows[0].Steps) == 0 ||
+ !strings.Contains(tab.Rows[0].Steps[0].Dst, "Elsewhere") {
+ t.Errorf("the saved rule is not in effect: %+v", tab.Rows)
+ }
+}
+
+// TestSaveRefusesBrokenRules: a file that will not load is not written, so
+// a window cannot leave krino unable to run (GUI design §5.3).
+func TestSaveRefusesBrokenRules(t *testing.T) {
+ conf := "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n"
+ e, _ := sandboxDir(t, conf, map[string]string{"a.pdf": "one"})
+ r, err := OpenRules(e, "dl")
+ if err != nil {
+ t.Fatal(err)
+ }
+ r.SetText("(path \"~/dl\")\n(rule \"all\" (move \"Out/{nope}\"))\n")
+ err = r.Save()
+ if err == nil {
+ t.Fatal("Save accepted a file with errors")
+ }
+ if !strings.Contains(err.Error(), "nope") {
+ t.Errorf("the refusal does not say what is wrong: %v", err)
+ }
+ if on, _ := os.ReadFile(r.File); string(on) != conf {
+ t.Errorf("the refused save wrote anyway: %q", on)
+ }
+ if _, err := os.Stat(filepath.Join(filepath.Dir(r.File), "dl.conf.bak")); !os.IsNotExist(err) {
+ t.Error("the refused save left a backup")
+ }
+}
+
+// TestSaveRefusesAFileChangedOnDisk: something else edited the file while
+// the window had it open, so saving would lose that edit; the caller is
+// told, and can Reload (GUI design §5.3).
+func TestSaveRefusesAFileChangedOnDisk(t *testing.T) {
+ conf := "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n"
+ e, _ := sandboxDir(t, conf, map[string]string{"a.pdf": "one"})
+ r, err := OpenRules(e, "dl")
+ if err != nil {
+ t.Fatal(err)
+ }
+ elsewhere := "(path \"~/dl\")\n(rule \"all\" (move \"Elsewhere\"))\n"
+ if err := os.WriteFile(r.File, []byte(elsewhere), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ r.SetText("(path \"~/dl\")\n(rule \"all\" (move \"Mine\"))\n")
+ if err := r.Save(); !errors.Is(err, ErrChangedOnDisk) {
+ t.Fatalf("Save = %v, want ErrChangedOnDisk", err)
+ }
+ if on, _ := os.ReadFile(r.File); string(on) != elsewhere {
+ t.Errorf("the refused save overwrote the other edit: %q", on)
+ }
+ // Overwrite keeps the other edit as the backup rather than losing it.
+ if err := r.SaveOverwriting(); err != nil {
+ t.Fatal(err)
+ }
+ if on, _ := os.ReadFile(r.File); !strings.Contains(string(on), "Mine") {
+ t.Errorf("overwrite did not save: %q", on)
+ }
+ if on, _ := os.ReadFile(r.File + ".bak"); string(on) != elsewhere {
+ t.Errorf("backup = %q, want the edit that was overwritten", on)
+ }
+
+ // And from the other direction: reload takes the other edit and drops
+ // ours; saving then works.
+ if err := os.WriteFile(r.File, []byte(elsewhere), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := r.Reload(); err != nil {
+ t.Fatal(err)
+ }
+ if r.Text != elsewhere {
+ t.Errorf("reload = %q, want what is on disk", r.Text)
+ }
+ r.SetText("(path \"~/dl\")\n(rule \"all\" (move \"Mine\"))\n")
+ if err := r.Save(); err != nil {
+ t.Errorf("save after reload: %v", err)
+ }
+}
+
+// TestExplainWithUnsavedText: Test on file answers for the text in the
+// editor, not for what is on disk (GUI design §5.3).
+func TestExplainWithUnsavedText(t *testing.T) {
+ conf := "(path \"~/dl\")\n(rule \"pdfs\" (when (type pdf)) (move \"Out\"))\n"
+ e, h := sandboxDir(t, conf, map[string]string{"a.pdf": "one"})
+ r, err := OpenRules(e, "dl")
+ if err != nil {
+ t.Fatal(err)
+ }
+ file := filepath.Join(h, "dl", "a.pdf")
+
+ text, err := r.Explain(context.Background(), file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(text, "rule pdfs: MATCH") || !strings.Contains(text, "Out") {
+ t.Errorf("explanation of the saved rules = %q", text)
+ }
+
+ r.SetText("(path \"~/dl\")\n(rule \"pdfs\" (when (type image)) (move \"Out\"))\n")
+ text, err = r.Explain(context.Background(), file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if strings.Contains(text, "MATCH") {
+ t.Errorf("the unsaved rule was ignored: %q", text)
+ }
+ // The unsaved text is never written by a test run.
+ if on, _ := os.ReadFile(r.File); string(on) != conf {
+ t.Errorf("Test on file wrote to the config: %q", on)
+ }
+}
+
+// TestExplainReportsBrokenRules: testing a file against text that will not
+// load says so instead of answering from the saved rules.
+func TestExplainReportsBrokenRules(t *testing.T) {
+ conf := "(path \"~/dl\")\n(rule \"pdfs\" (when (type pdf)) (move \"Out\"))\n"
+ e, h := sandboxDir(t, conf, map[string]string{"a.pdf": "one"})
+ r, err := OpenRules(e, "dl")
+ if err != nil {
+ t.Fatal(err)
+ }
+ r.SetText("(path \"~/dl\")\n(rule \"pdfs\" (when (type pdf)) (move \"Out/{nope}\"))\n")
+ if _, err := r.Explain(context.Background(), filepath.Join(h, "dl", "a.pdf")); err == nil {
+ t.Fatal("Test on file answered from a file that will not load")
+ }
+}
+
+// TestRuleFilesLister: the picker offers every included directory that has
+// a file of its own.
+func TestRuleFilesLister(t *testing.T) {
+ conf := "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n"
+ e, _ := sandboxDir(t, conf, map[string]string{"a.pdf": "one"})
+ if got := RuleFiles(e); len(got) != 1 || got[0] != "dl" {
+ t.Errorf("RuleFiles = %v, want [dl]", got)
+ }
+ if _, err := OpenRules(e, "nosuch"); err == nil {
+ t.Error("OpenRules accepted a directory that is not included")
+ }
+}
+
+// reload builds a fresh engine from the sandbox's config, the way the next
+// krino run would read it.
+func reload(t *testing.T, home string) *engine.Engine {
+ t.Helper()
+ e, errs := engine.Load(filepath.Join(home, ".config", "krino", "krino.conf"))
+ if len(errs) > 0 {
+ t.Fatal(errs)
+ }
+ return e
+}