diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-16 14:13:26 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-16 14:13:26 +0200 |
| commit | 85d65ebe0adf1b156324a3a4c220e415a79ba9ce (patch) | |
| tree | 4e648edce13cbc370e2685475944cafc2236f44a /gui/internal/model/rules_test.go | |
| parent | 6ef83d6bdfb6120f9e1fbd145e0bc463196103d1 (diff) | |
| download | krino-85d65ebe0adf1b156324a3a4c220e415a79ba9ce.tar.gz krino-85d65ebe0adf1b156324a3a4c220e415a79ba9ce.zip | |
gui: Rules tab - the file as text, checked as you type, tested and saved
Diffstat (limited to 'gui/internal/model/rules_test.go')
| -rw-r--r-- | gui/internal/model/rules_test.go | 241 |
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 +} |
