aboutsummaryrefslogtreecommitdiff
path: root/internal/engine/engine_test.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-12 01:22:12 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-12 01:22:12 +0200
commit3b36a48b7ce5a53a9366f3b31f94311f178e2553 (patch)
treeecbb277ff916b719f2ee45fba017792b85d5faf9 /internal/engine/engine_test.go
parent42b02c47be9b285099203e44a2570636d4ca6f03 (diff)
downloadkrino-3b36a48b7ce5a53a9366f3b31f94311f178e2553.tar.gz
krino-3b36a48b7ce5a53a9366f3b31f94311f178e2553.zip
krino: matching — scan, ignore, conditions, extraction, duplicates, explain, dry run
Diffstat (limited to 'internal/engine/engine_test.go')
-rw-r--r--internal/engine/engine_test.go166
1 files changed, 166 insertions, 0 deletions
diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go
new file mode 100644
index 0000000..5c0536f
--- /dev/null
+++ b/internal/engine/engine_test.go
@@ -0,0 +1,166 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package engine
+
+import (
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+ "time"
+
+ "krino/internal/cond"
+)
+
+// sandbox gives a test its own HOME with no XDG overrides and returns it.
+func sandbox(t *testing.T) string {
+ t.Helper()
+ h := t.TempDir()
+ t.Setenv("HOME", h)
+ for _, v := range []string{"XDG_CONFIG_HOME", "XDG_STATE_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME"} {
+ t.Setenv(v, "")
+ }
+ return h
+}
+
+// writeConfig writes krino.conf and dirs/<name>.conf files under home/.config/krino.
+func writeConfig(t *testing.T, home, main string, dirs map[string]string) string {
+ t.Helper()
+ cdir := filepath.Join(home, ".config", "krino")
+ if err := os.MkdirAll(filepath.Join(cdir, "dirs"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ mainFile := filepath.Join(cdir, "krino.conf")
+ if err := os.WriteFile(mainFile, []byte(main), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ for n, body := range dirs {
+ if err := os.WriteFile(filepath.Join(cdir, "dirs", n+".conf"), []byte(body), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ }
+ return mainFile
+}
+
+// fakeFacts is a minimal cond.Facts for checking compiled rules.
+type fakeFacts struct{ name string }
+
+func (f fakeFacts) Name() string { return f.name }
+func (f fakeFacts) Rel() string { return f.name }
+func (f fakeFacts) Size() int64 { return 1 }
+func (f fakeFacts) ModTime() time.Time { return time.Time{} }
+func (f fakeFacts) Now() time.Time { return time.Time{} }
+func (f fakeFacts) Content(bool, bool) (string, error) { return "", nil }
+func (f fakeFacts) Duplicate([]string) (string, bool, error) { return "", false, nil }
+func (f fakeFacts) Matched() bool { return false }
+
+var _ cond.Facts = fakeFacts{}
+
+func TestLoadCompilesWithRuleSettings(t *testing.T) {
+ h := sandbox(t)
+ os.Mkdir(filepath.Join(h, "dl"), 0o755)
+ main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": `
+(path "~/dl")
+(case strict)
+(ignore "*.part")
+(rule "strict" (when (name "^img")) (stop))
+(rule "loose" (case ignore) (when (name "^img")) (stop))
+`})
+ e, errs := Load(main, "dl", "dl")
+ if len(errs) > 0 {
+ t.Fatal(errs)
+ }
+ if len(e.Dirs) != 1 {
+ t.Fatalf("got %d dirs, want 1 (duplicate name ignored)", len(e.Dirs))
+ }
+ d := e.Dirs[0]
+ if d.Root != filepath.Join(h, "dl") || d.Ignore == nil || !d.Ignore.Match("x.part", false) {
+ t.Fatalf("dir = %+v", d)
+ }
+ img := fakeFacts{name: "IMG_1.jpg"}
+ if d.Rules[0].Cond.Eval(img).Match {
+ t.Error("rule under (case strict) matched IMG against ^img")
+ }
+ if !d.Rules[1].Cond.Eval(img).Match {
+ t.Error("rule-level (case ignore) did not apply at compile time")
+ }
+}
+
+func TestLoadReportsEveryProblem(t *testing.T) {
+ h := sandbox(t)
+ main := writeConfig(t, h, `(include "a" "b")`, map[string]string{
+ "a": `(path "/tmp") (rule "x" (when (type "pdf")) (stop))`,
+ "b": `(path "/tmp") (ignore "[abc") (rule "y" (when (size 1)) (stop))`,
+ })
+ e, errs := Load(main)
+ if e != nil {
+ t.Fatal("engine returned despite errors")
+ }
+ joined := ""
+ for _, d := range errs {
+ joined += d.Error() + "\n"
+ }
+ for _, want := range []string{
+ "a.conf:1:37: type names are bare words: write (type pdf)",
+ `b.conf: bad ignore pattern "[abc": unterminated [`,
+ "b.conf:1:47: size takes an operator and a size, like (size > 10M)",
+ } {
+ if !strings.Contains(joined, want) {
+ t.Errorf("missing %q in:\n%s", want, joined)
+ }
+ }
+}
+
+func TestCheck(t *testing.T) {
+ h := sandbox(t)
+ bin := t.TempDir()
+ os.WriteFile(filepath.Join(bin, "pdftotext"), []byte("#!/bin/sh\n"), 0o755)
+ t.Setenv("PATH", bin)
+ os.Mkdir(filepath.Join(h, "here"), 0o755)
+ main := writeConfig(t, h, `(include "here" "gone")`, map[string]string{
+ "here": `(path "~/here") (rule "r" (stop))`,
+ "gone": `(path "~/gone") (rule "r" (stop))`,
+ })
+ e, errs := Load(main)
+ if len(errs) > 0 {
+ t.Fatal(errs)
+ }
+ r := e.Check()
+ if r.MainFile != main || r.LogFile != filepath.Join(h, ".local", "state", "krino", "krino.log") {
+ t.Errorf("report files = %q %q", r.MainFile, r.LogFile)
+ }
+ if len(r.Dirs) != 2 || r.Dirs[0].Missing || !r.Dirs[1].Missing {
+ t.Errorf("dirs = %+v", r.Dirs)
+ }
+ if r.Tools[0].Name != "pdftotext" || r.Tools[0].Path != filepath.Join(bin, "pdftotext") || r.Tools[1].Path != "" {
+ t.Errorf("tools = %+v", r.Tools)
+ }
+}
+
+// TestContentVariantsComputedAtLoad: B2 plumbing. Load computes each
+// directory's distinct (ignoreCase, fold) content-test variants from its
+// rules' resolved settings: a rule with no content test contributes
+// nothing; two rules sharing a variant fold into one; a rule-level (case
+// ignore) override adds a second.
+func TestContentVariantsComputedAtLoad(t *testing.T) {
+ h := sandbox(t)
+ os.Mkdir(filepath.Join(h, "dl"), 0o755)
+ main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": `
+(path "~/dl")
+(case strict)
+(rule "no-content" (when (type pdf)) (stop))
+(rule "strict-content" (when (content "acme")) (stop))
+(rule "also-strict-content" (when (content "other")) (stop))
+(rule "loose-content" (case ignore) (when (content "acme")) (stop))
+`})
+ e, errs := Load(main, "dl")
+ if len(errs) > 0 {
+ t.Fatal(errs)
+ }
+ got := e.Dirs[0].ContentVariants
+ want := []cond.Options{{IgnoreCase: false, Fold: true}, {IgnoreCase: true, Fold: true}}
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("ContentVariants = %+v, want %+v", got, want)
+ }
+}