diff options
Diffstat (limited to 'gui/internal/model')
| -rw-r--r-- | gui/internal/model/testrule.go | 75 | ||||
| -rw-r--r-- | gui/internal/model/testrule_test.go | 76 |
2 files changed, 151 insertions, 0 deletions
diff --git a/gui/internal/model/testrule.go b/gui/internal/model/testrule.go new file mode 100644 index 0000000..3cf0437 --- /dev/null +++ b/gui/internal/model/testrule.go @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package model + +import ( + "context" + "fmt" + + "krino/internal/engine" + "krino/internal/plan" +) + +// RuleHit is one file a rule would act on, and what it would do to it. +type RuleHit struct { + Rel string + Steps []plan.Step +} + +// RuleHits is the answer to "which files would this rule take?". +type RuleHits struct { + Rule string + Files []RuleHit + Scanned int +} + +// TestRule plans the directory with the unsaved text and reports the files +// the named rule would act on. It reads only: no lock is taken - the file +// the Plan tab is looking at is none of its business - and nothing is +// written or moved. Content extraction makes it as slow as a scan, so +// callers run it off the main loop. +func (r *Rules) TestRule(ctx context.Context, name string) (*RuleHits, error) { + e, diags := r.load() + if len(diags) > 0 { + return nil, fmt.Errorf("%s", diags[0]) + } + var dir *engine.Dir + for _, d := range e.Dirs { + if d.Name == r.Name { + dir = d + break + } + } + if dir == nil { + return nil, fmt.Errorf("model: %s is not in the configuration", r.Name) + } + found := false + for _, rule := range dir.Rules { + if rule.Conf.Name == name { + found = true + break + } + } + if !found { + return nil, fmt.Errorf("model: no rule called %q in %s", name, r.Name) + } + dp, err := e.Plan(ctx, dir, plan.NewClaims()) + if err != nil { + return nil, err + } + hits := &RuleHits{Rule: name} + res := dp.Result + hits.Scanned = len(res.Matched) + len(res.Unmatched) + len(res.Skipped) + for _, c := range dp.Chains { + var steps []plan.Step + for _, s := range c.Steps { + if s.Rule == name { + steps = append(steps, s) + } + } + if len(steps) > 0 { + hits.Files = append(hits.Files, RuleHit{Rel: c.File.Rel, Steps: steps}) + } + } + return hits, nil +} diff --git a/gui/internal/model/testrule_test.go b/gui/internal/model/testrule_test.go new file mode 100644 index 0000000..99989f4 --- /dev/null +++ b/gui/internal/model/testrule_test.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package model + +import ( + "context" + "strings" + "testing" +) + +// TestTestRule: testing one rule says which of the directory's files it +// would act on, and what would happen to each - answered from the unsaved +// text, and changing nothing (GUI design ยง5.3, his request 2026-09-16). +func TestTestRule(t *testing.T) { + conf := "(path \"~/dl\")\n" + + "(rule \"pdfs\" (when (type pdf)) (move \"Docs\") (stop))\n" + + "(rule \"rest\" (move \"Other\"))\n" + e, _ := sandboxDir(t, conf, map[string]string{ + "a.pdf": "one", "b.pdf": "two", "c.txt": "three", + }) + r, err := OpenRules(e, "dl") + if err != nil { + t.Fatal(err) + } + + hits, err := r.TestRule(context.Background(), "pdfs") + if err != nil { + t.Fatal(err) + } + if hits.Scanned != 3 || len(hits.Files) != 2 { + t.Fatalf("hits = %+v", hits) + } + for _, h := range hits.Files { + if !strings.HasSuffix(h.Rel, ".pdf") { + t.Errorf("a file the rule does not match: %+v", h) + } + if len(h.Steps) == 0 || !strings.Contains(h.Steps[0].Dst, "Docs") { + t.Errorf("%s: steps = %+v", h.Rel, h.Steps) + } + } + + // The unsaved text is what is tested, not what is on disk. + r.SetText("(path \"~/dl\")\n(rule \"pdfs\" (when (type text)) (move \"Docs\") (stop))\n") + hits, err = r.TestRule(context.Background(), "pdfs") + if err != nil { + t.Fatal(err) + } + if len(hits.Files) != 1 || hits.Files[0].Rel != "c.txt" { + t.Errorf("the unsaved rule was not the one tested: %+v", hits) + } + + // A rule that matches nothing says so rather than failing. + r.SetText("(path \"~/dl\")\n(rule \"pdfs\" (when (type iso)) (move \"Docs\") (stop))\n") + hits, err = r.TestRule(context.Background(), "pdfs") + if err != nil || len(hits.Files) != 0 || hits.Scanned != 3 { + t.Errorf("hits = %+v, err = %v", hits, err) + } +} + +// TestTestRuleRefusals: a rule that is not in the text, and text that does +// not load, are reported rather than answered. +func TestTestRuleRefusals(t *testing.T) { + conf := "(path \"~/dl\")\n(rule \"pdfs\" (when (type pdf)) (move \"Docs\"))\n" + e, _ := sandboxDir(t, conf, map[string]string{"a.pdf": "one"}) + r, err := OpenRules(e, "dl") + if err != nil { + t.Fatal(err) + } + if _, err := r.TestRule(context.Background(), "nosuch"); err == nil { + t.Error("a rule that is not in the file was tested") + } + r.SetText("(path \"~/dl\")\n(rule \"pdfs\" (when (type pdf)) (move \"Docs/{nope}\"))\n") + if _, err := r.TestRule(context.Background(), "pdfs"); err == nil { + t.Error("text that does not load was tested") + } +} |
